Commit b8c87a11 authored by Adam Langley's avatar Adam Langley

encoding/pem: be more permissive about decoding empty blocks.

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/10516Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Adam Langley <agl@golang.org>
parent 8cd191b6
...@@ -10,8 +10,10 @@ package pem ...@@ -10,8 +10,10 @@ package pem
import ( import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"errors"
"io" "io"
"sort" "sort"
"strings"
) )
// A Block represents a PEM encoded structure. // A Block represents a PEM encoded structure.
...@@ -110,27 +112,37 @@ func Decode(data []byte) (p *Block, rest []byte) { ...@@ -110,27 +112,37 @@ func Decode(data []byte) (p *Block, rest []byte) {
} }
// TODO(agl): need to cope with values that spread across lines. // 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) key = bytes.TrimSpace(key)
val = bytes.TrimSpace(val) val = bytes.TrimSpace(val)
p.Headers[string(key)] = string(val) p.Headers[string(key)] = string(val)
rest = next rest = next
} }
i := bytes.Index(rest, pemEnd) var endIndex int
if i < 0 { // 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) return decodeError(data, rest)
} }
base64Data := removeWhitespace(rest[0:i])
base64Data := removeWhitespace(rest[:endIndex])
p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data))) p.Bytes = make([]byte, base64.StdEncoding.DecodedLen(len(base64Data)))
n, err := base64.StdEncoding.Decode(p.Bytes, base64Data) n, err := base64.StdEncoding.Decode(p.Bytes, base64Data)
if err != nil { if err != nil {
return decodeError(data, rest) 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 return
} }
...@@ -246,6 +258,9 @@ func Encode(out io.Writer, b *Block) error { ...@@ -246,6 +258,9 @@ func Encode(out io.Writer, b *Block) error {
// For consistency of output, write other headers sorted by key. // For consistency of output, write other headers sorted by key.
sort.Strings(h) sort.Strings(h)
for _, k := range 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 { if err := writeHeader(out, k, b.Headers[k]); err != nil {
return err return err
} }
......
...@@ -8,7 +8,9 @@ import ( ...@@ -8,7 +8,9 @@ import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"reflect" "reflect"
"strings"
"testing" "testing"
"testing/quick"
) )
type GetLineTest struct { type GetLineTest struct {
...@@ -44,6 +46,32 @@ func TestDecode(t *testing.T) { ...@@ -44,6 +46,32 @@ func TestDecode(t *testing.T) {
if !reflect.DeepEqual(result, privateKey) { if !reflect.DeepEqual(result, privateKey) {
t.Errorf("#1 got:%#v want:%#v", 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)) result, _ = Decode([]byte(pemPrivateKey2))
if !reflect.DeepEqual(result, privateKey2) { if !reflect.DeepEqual(result, privateKey2) {
t.Errorf("#2 got:%#v want:%#v", result, privateKey2) t.Errorf("#2 got:%#v want:%#v", result, privateKey2)
...@@ -117,6 +145,44 @@ func TestLineBreaker(t *testing.T) { ...@@ -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) { func BenchmarkEncode(b *testing.B) {
data := &Block{Bytes: make([]byte, 65536)} data := &Block{Bytes: make([]byte, 65536)}
b.SetBytes(int64(len(data.Bytes))) b.SetBytes(int64(len(data.Bytes)))
...@@ -188,7 +254,32 @@ BTiHcL3s3KrJu1vDVrshvxfnz71KTeNnZH8UbOqT5i7fPGyXtY1XJddcbI/Q6tXf ...@@ -188,7 +254,32 @@ BTiHcL3s3KrJu1vDVrshvxfnz71KTeNnZH8UbOqT5i7fPGyXtY1XJddcbI/Q6tXf
wHFsZc20TzSdsVLBtwksUacpbDogcEVMctnNrB8FIrB3vZEv9Q0Z1VeY7nmTpF+6 wHFsZc20TzSdsVLBtwksUacpbDogcEVMctnNrB8FIrB3vZEv9Q0Z1VeY7nmTpF+6
a+z2P7acL7j6A6Pr3+q8P9CPiPC7zFonVzuVPyB8GchGR2hytyiOVpuD9+k8hcuw a+z2P7acL7j6A6Pr3+q8P9CPiPC7zFonVzuVPyB8GchGR2hytyiOVpuD9+k8hcuw
ZWAaUoVtWIQ52aKS0p19G99hhb+IVANC4akkdHV4SP8i7MVNZhfUmg== 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", var certificate = &Block{Type: "CERTIFICATE",
Headers: map[string]string{}, Headers: map[string]string{},
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment