Skip to content

Commit

Permalink
encoding/pem: be more permissive about decoding empty blocks.
Browse files Browse the repository at this point in the history
As noted in bug #10980, an empty PEM block is encoded as
-----BEGIN foo-----
-----END foo-----

However, Decode failed to process this.

RFC 1421 doesn't answer what the encoding of the empty block should be
because PEM messages always contain at least one header. However, PEM
these days is just the encoding format – nobody uses the rest of PEM any
longer.

Having the empty block not contain a newline seems most correct because
https://tools.ietf.org/html/rfc1421#section-9 clearly says that the
optional “pemtext” carries the leading new-line with it. So if omitted,
the new-line should be omitted too.

None the less, this changes makes encoding/pem permissive, accepting any
number of blank lines in an empty PEM block.

Fixes #10980

Change-Id: If36bdfbf991ee281eccd50b56ddc95f24c6debb2
Reviewed-on: https://go-review.googlesource.com/10516
Reviewed-by: Brad Fitzpatrick <[email protected]>
Run-TryBot: Adam Langley <[email protected]>
  • Loading branch information
agl committed May 31, 2015
1 parent 8cd191b commit b8c87a1
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 7 deletions.
27 changes: 21 additions & 6 deletions src/encoding/pem/pem.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ package pem
import (
"bytes"
"encoding/base64"
"errors"
"io"
"sort"
"strings"
)

// A Block represents a PEM encoded structure.
Expand Down Expand Up @@ -110,27 +112,37 @@ func Decode(data []byte) (p *Block, rest []byte) {
}

// TODO(agl): need to cope with values that spread across lines.
key, val := line[0:i], line[i+1:]
key, val := line[:i], line[i+1:]
key = bytes.TrimSpace(key)
val = bytes.TrimSpace(val)
p.Headers[string(key)] = string(val)
rest = next
}

i := bytes.Index(rest, pemEnd)
if i < 0 {
var endIndex int
// If there were no headers, the END line might occur
// immediately, without a leading newline.
if len(p.Headers) == 0 && bytes.HasPrefix(rest, pemEnd[1:]) {
endIndex = 0
} else {
endIndex = bytes.Index(rest, pemEnd)
}

if endIndex < 0 {
return decodeError(data, rest)
}
base64Data := removeWhitespace(rest[0:i])

base64Data := removeWhitespace(rest[:endIndex])
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
if err != nil {
return decodeError(data, rest)
}
p.Bytes = p.Bytes[0:n]
p.Bytes = p.Bytes[:n]

_, rest = getLine(rest[i+len(pemEnd):])
// the -1 is because we might have only matched pemEnd without the
// leading newline if the PEM block was empty.
_, rest = getLine(rest[endIndex+len(pemEnd)-1:])

return
}
Expand Down Expand Up @@ -246,6 +258,9 @@ func Encode(out io.Writer, b *Block) error {
// For consistency of output, write other headers sorted by key.
sort.Strings(h)
for _, k := range h {
if strings.Contains(k, ":") {
return errors.New("pem: cannot encode a header key that contains a colon")
}
if err := writeHeader(out, k, b.Headers[k]); err != nil {
return err
}
Expand Down
93 changes: 92 additions & 1 deletion src/encoding/pem/pem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"bytes"
"io/ioutil"
"reflect"
"strings"
"testing"
"testing/quick"
)

type GetLineTest struct {
Expand Down Expand Up @@ -44,6 +46,32 @@ func TestDecode(t *testing.T) {
if !reflect.DeepEqual(result, privateKey) {
t.Errorf("#1 got:%#v want:%#v", result, privateKey)
}

isEmpty := func(block *Block) bool {
return block != nil && block.Type == "EMPTY" && len(block.Headers) == 0 && len(block.Bytes) == 0
}
result, remainder = Decode(remainder)
if !isEmpty(result) {
t.Errorf("#2 should be empty but got:%#v", result)
}
result, remainder = Decode(remainder)
if !isEmpty(result) {
t.Errorf("#3 should be empty but got:%#v", result)
}
result, remainder = Decode(remainder)
if !isEmpty(result) {
t.Errorf("#4 should be empty but got:%#v", result)
}

result, remainder = Decode(remainder)
if result == nil || result.Type != "HEADERS" || len(result.Headers) != 1 {
t.Errorf("#5 expected single header block but got :%v", result)
}

if len(remainder) != 0 {
t.Errorf("expected nothing remaining of pemData, but found %s", string(remainder))
}

result, _ = Decode([]byte(pemPrivateKey2))
if !reflect.DeepEqual(result, privateKey2) {
t.Errorf("#2 got:%#v want:%#v", result, privateKey2)
Expand Down Expand Up @@ -117,6 +145,44 @@ func TestLineBreaker(t *testing.T) {
}
}

func TestFuzz(t *testing.T) {
testRoundtrip := func(block *Block) bool {
for key := range block.Headers {
if strings.Contains(key, ":") {
// Keys with colons cannot be encoded.
return true
}
}

var buf bytes.Buffer
err := Encode(&buf, block)
decoded, rest := Decode(buf.Bytes())

switch {
case err != nil:
t.Errorf("Encode of %#v resulted in error: %s", block, err)
case !reflect.DeepEqual(block, decoded):
t.Errorf("Encode of %#v decoded as %#v", block, decoded)
case len(rest) != 0:
t.Errorf("Encode of %#v decoded correctly, but with %x left over", block, rest)
default:
return true
}
return false
}

// Explicitly test the empty block.
if !testRoundtrip(&Block{
Type: "EMPTY",
Headers: make(map[string]string),
Bytes: []byte{},
}) {
return
}

quick.Check(testRoundtrip, nil)
}

func BenchmarkEncode(b *testing.B) {
data := &Block{Bytes: make([]byte, 65536)}
b.SetBytes(int64(len(data.Bytes)))
Expand Down Expand Up @@ -188,7 +254,32 @@ BTiHcL3s3KrJu1vDVrshvxfnz71KTeNnZH8UbOqT5i7fPGyXtY1XJddcbI/Q6tXf
wHFsZc20TzSdsVLBtwksUacpbDogcEVMctnNrB8FIrB3vZEv9Q0Z1VeY7nmTpF+6
a+z2P7acL7j6A6Pr3+q8P9CPiPC7zFonVzuVPyB8GchGR2hytyiOVpuD9+k8hcuw
ZWAaUoVtWIQ52aKS0p19G99hhb+IVANC4akkdHV4SP8i7MVNZhfUmg==
-----END RSA PRIVATE KEY-----`
-----END RSA PRIVATE KEY-----
-----BEGIN EMPTY-----
-----END EMPTY-----
-----BEGIN EMPTY-----
-----END EMPTY-----
-----BEGIN EMPTY-----
-----END EMPTY-----
# This shouldn't be recognised because of the missing newline after the
headers.
-----BEGIN HEADERS-----
Header: 1
-----END HEADERS-----
# This should be valid, however.
-----BEGIN HEADERS-----
Header: 1
-----END HEADERS-----`

var certificate = &Block{Type: "CERTIFICATE",
Headers: map[string]string{},
Expand Down

0 comments on commit b8c87a1

Please sign in to comment.