You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
343 lines
12 KiB
343 lines
12 KiB
package der
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCheck(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, hex string
|
|
ok bool
|
|
}{
|
|
{"sequence of an integer and a null", "3005020101" + "0500", true},
|
|
{"a long length", "04" + "8180" + zeros(128), true},
|
|
{"a long form under 128", "0481" + "01" + "00", false},
|
|
{"a length with a leading zero", "04820001" + "00", false},
|
|
{"a length of 128 with a leading zero", "04820080" + zeros(128), false},
|
|
{"an indefinite length", "30800000", false},
|
|
{"a length of 0xff", "04ff" + zeros(127), false},
|
|
{"a length of five bytes", "0485" + "0100000000" + "00", false},
|
|
{"a length of nine bytes that wraps around to 128", "0489" + "010000000000000080" + zeros(128), false},
|
|
{"a length whose bytes are missing", "0482" + "01", false},
|
|
{"a lone identifier octet", "04", false},
|
|
{"nothing", "", false},
|
|
{"a high tag number", "1f0100", false},
|
|
{"truncated", "0402aa", false},
|
|
{"trailing bytes", "0500" + "00", false},
|
|
{"BOOLEAN 01", "010101", false},
|
|
{"BOOLEAN 00", "010100", true},
|
|
{"BOOLEAN FF", "0101ff", true},
|
|
{"BOOLEAN of two bytes", "0102ffff", false},
|
|
{"INTEGER with a leading zero", "02020001", false},
|
|
{"INTEGER 0x80 with its zero", "02020080", true},
|
|
{"INTEGER with a leading FF", "0202ff80", false},
|
|
{"INTEGER -1 in two bytes", "0202ffff", false},
|
|
{"INTEGER -129", "0202ff7f", true},
|
|
{"empty INTEGER", "0200", false},
|
|
{"ENUMERATED with a leading zero", "0a020001", false},
|
|
{"NULL with content", "050100", false},
|
|
{"constructed OCTET STRING", "2404" + "0402aabb", false},
|
|
{"constructed INTEGER", "2203" + "020101", false},
|
|
{"BIT STRING with unused bits set", "03020701", false},
|
|
{"BIT STRING with unused bits clear", "03020780", true},
|
|
{"BIT STRING of 4 bits", "030204f0", true},
|
|
{"an empty BIT STRING", "030100", true},
|
|
{"a BIT STRING without its first octet", "0300", false},
|
|
{"a BIT STRING of eight unused bits", "03020800", false},
|
|
{"a BIT STRING of no bits with unused bits", "030101", false},
|
|
{"OID", "06032a0304", true},
|
|
{"OID with 0x80 inside a subidentifier", "0604" + "2a818001", true},
|
|
{"OID with a leading 0x80", "06038001" + "02", false},
|
|
{"OID that does not end", "06022a83", false},
|
|
{"an empty OID", "0600", false},
|
|
{"context tag, constructed", "a003020101", true},
|
|
{"context tag, primitive, any content", "8003000000", true},
|
|
{"SET in primitive form", "1100", false},
|
|
{"SEQUENCE in primitive form", "1000", false},
|
|
{"the end of contents", "0000", false},
|
|
{"a reserved universal tag", "0e0141", false},
|
|
{"a SEQUENCE of the end of contents", "30020000", false},
|
|
{"a SEQUENCE with a bad child", "3003" + "020000", false},
|
|
{"a SEQUENCE whose child does not fit", "3003" + "040500", false},
|
|
{"UTF8String", "0c026162", true},
|
|
{"UTF8String that is not UTF-8", "0c01ff", true},
|
|
{"PrintableString with an underscore", "13015f", true},
|
|
{"TeletexString", "1401e9", true},
|
|
{"IA5String", "160161", true},
|
|
{"VisibleString", "1a0161", true},
|
|
{"UniversalString", "1c0400000061", true},
|
|
{"BMPString", "1e020061", true},
|
|
// The other string types are DER too, whatever their content: a name
|
|
// may hold a NumericString, as the INN of a Russian certificate.
|
|
{"NumericString", "1204" + hexOf("1234"), true},
|
|
{"NumericString with a letter", "1201" + hexOf("A"), true},
|
|
{"VideotexString", "150141", true},
|
|
{"GraphicString", "190141", true},
|
|
{"GeneralString", "1b0141", true},
|
|
{"ObjectDescriptor", "070141", true},
|
|
{"NumericString in constructed form", "3203" + "120131", false},
|
|
{"REAL", "0900", false},
|
|
{"RELATIVE-OID", "0d0101", false},
|
|
// Times in the forms of DER (X.690 11.7, 11.8).
|
|
{"UTCTime", "170d" + hexOf("250101120000Z"), true},
|
|
{"an empty UTCTime", "1700", false},
|
|
{"UTCTime without seconds", "170b" + hexOf("2501011200Z"), false},
|
|
{"UTCTime with a digit more", "170e" + hexOf("2501011200001Z"), false},
|
|
{"UTCTime with an offset", "1711" + hexOf("250101120000+0100"), false},
|
|
{"UTCTime of 30 February", "170d" + hexOf("250230120000Z"), false},
|
|
{"UTCTime with second 60", "170d" + hexOf("250101235960Z"), false},
|
|
{"a UTCTime that is not a time", "170a" + hexOf("not a time"), false},
|
|
{"UTCTime with a slash in its seconds", "170d" + hexOf("2501011200/0Z"), false},
|
|
{"UTCTime with a colon in its day", "170d" + hexOf("25010:120000Z"), false},
|
|
{"GeneralizedTime", "180f" + hexOf("20250101120000Z"), true},
|
|
{"GeneralizedTime with a fraction", "1812" + hexOf("20250101120000.25Z"), true},
|
|
{"GeneralizedTime with a trailing zero", "1813" + hexOf("20250101120000.250Z"), false},
|
|
{"GeneralizedTime with an empty fraction", "1810" + hexOf("20250101120000.Z"), false},
|
|
{"GeneralizedTime with a letter in its fraction", "1812" + hexOf("20250101120000.2aZ"), false},
|
|
{"GeneralizedTime without Z", "180e" + hexOf("20250101120000"), false},
|
|
{"GeneralizedTime with a comma", "1812" + hexOf("20250101120000,25Z"), false},
|
|
{"GeneralizedTime without seconds", "180d" + hexOf("202501011200Z"), false},
|
|
{"GeneralizedTime with a slash in its seconds", "180f" + hexOf("202501011200/0Z"), false},
|
|
{"GeneralizedTime of month 0", "180f" + hexOf("20250001120000Z"), false},
|
|
{"GeneralizedTime of month 13", "180f" + hexOf("20251301120000Z"), false},
|
|
{"GeneralizedTime of day 0", "180f" + hexOf("20250100120000Z"), false},
|
|
{"GeneralizedTime of 31 April", "180f" + hexOf("20250431120000Z"), false},
|
|
{"GeneralizedTime of hour 24", "180f" + hexOf("20250101240000Z"), false},
|
|
{"GeneralizedTime of minute 60", "180f" + hexOf("20250101126000Z"), false},
|
|
{"GeneralizedTime of 29 February 2024", "180f" + hexOf("20240229235959Z"), true},
|
|
// What a check that is missing would let through: the last byte read
|
|
// as Z, a colon read as the digit 10, a slash read as a year, a tag of
|
|
// a high number read as one byte, and an indefinite length read as a
|
|
// long form.
|
|
{"UTCTime that ends in another letter", "170d" + hexOf("250101120000X"), false},
|
|
{"GeneralizedTime with a digit in the place of Z", "180f" + hexOf("202501011200000"), false},
|
|
{"GeneralizedTime with a colon in its day", "180f" + hexOf("2025010:120000Z"), false},
|
|
{"GeneralizedTime with a slash in its year", "180f" + hexOf("/0250101120000Z"), false},
|
|
{"a context tag of a high number", "9f0100", false},
|
|
{"an indefinite length and nothing after it", "3080", false},
|
|
} {
|
|
b, err := hex.DecodeString(tc.hex)
|
|
if err != nil {
|
|
t.Fatal(tc.name, err)
|
|
}
|
|
if err := Check(b); (err == nil) != tc.ok {
|
|
t.Errorf("%s: %v", tc.name, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func zeros(n int) string {
|
|
b := make([]byte, n*2)
|
|
for i := range b {
|
|
b[i] = '0'
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func hexOf(s string) string { return hex.EncodeToString([]byte(s)) }
|
|
|
|
// The elements of a SET OF go in ascending order, and equal ones may repeat
|
|
// (X.690 11.6).
|
|
func TestSetOfSorted(t *testing.T) {
|
|
a, b := []byte{0x02, 0x01, 0x01}, []byte{0x02, 0x01, 0x02}
|
|
if !SetOfSorted([][]byte{a, b}) || SetOfSorted([][]byte{b, a}) || !SetOfSorted([][]byte{a, a, b}) || SetOfSorted([][]byte{a, b, a}) || !SetOfSorted(nil) {
|
|
t.Error("SetOfSorted")
|
|
}
|
|
}
|
|
|
|
// Elements nested 32 levels below the outer one are DER; one more is too deep.
|
|
func TestDepth(t *testing.T) {
|
|
nested := func(n int) []byte {
|
|
b := []byte{0x05, 0x00}
|
|
for range n {
|
|
b = append([]byte{0x30, byte(len(b))}, b...)
|
|
}
|
|
return b
|
|
}
|
|
if err := Check(nested(maxDepth)); err != nil {
|
|
t.Errorf("%d levels: %v", maxDepth, err)
|
|
}
|
|
if err := Check(nested(maxDepth + 1)); err == nil {
|
|
t.Errorf("%d levels: accepted", maxDepth+1)
|
|
}
|
|
}
|
|
|
|
func TestParseTime(t *testing.T) {
|
|
for _, c := range []struct {
|
|
el string
|
|
want time.Time
|
|
frac bool
|
|
}{
|
|
{"\x17\x0d" + "491231235959Z", time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC), false},
|
|
{"\x17\x0d" + "500101000000Z", time.Date(1950, 1, 1, 0, 0, 0, 0, time.UTC), false},
|
|
{"\x18\x13" + "20240229120000.125Z", time.Date(2024, 2, 29, 12, 0, 0, 125e6, time.UTC), true},
|
|
{"\x18\x19" + "20240229120000.123456789Z", time.Date(2024, 2, 29, 12, 0, 0, 123456789, time.UTC), true},
|
|
{"\x18\x0f" + "19490101000000Z", time.Date(1949, 1, 1, 0, 0, 0, 0, time.UTC), false},
|
|
} {
|
|
got, frac, err := ParseTime([]byte(c.el))
|
|
if err != nil || !got.Equal(c.want) || frac != c.frac {
|
|
t.Errorf("%q: %v %v %v", c.el, got, frac, err)
|
|
}
|
|
}
|
|
for _, bad := range []string{
|
|
"\x18\x0f" + "20230229120000Z",
|
|
"\x04\x0d" + "491231235959Z",
|
|
"\x04\x0f" + "20230228120000Z", // a GeneralizedTime in an OCTET STRING
|
|
"\x0c\x0d" + "491231235959Z", // a UTCTime in a UTF8String
|
|
"\x17",
|
|
"\x17\x0d" + "4912", // its content does not fit
|
|
"",
|
|
} {
|
|
if _, _, err := ParseTime([]byte(bad)); err == nil {
|
|
t.Errorf("%q: accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSplit(t *testing.T) {
|
|
b, _ := hex.DecodeString("30050201010500")
|
|
if err := Check(b); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, kids, err := Split(b)
|
|
if err != nil || id != 0x30 || len(kids) != 2 || len(kids[0]) != 3 || len(kids[1]) != 2 {
|
|
t.Errorf("%x %v %v", id, kids, err)
|
|
}
|
|
if c, err := Content(b); err != nil || !bytes.Equal(c, b[2:]) {
|
|
t.Errorf("Content: %x %v", c, err)
|
|
}
|
|
// Nothing, a primitive element, and constructed ones whose content does
|
|
// not fit.
|
|
for _, bad := range []string{"", "0400", "3005", "a005"} {
|
|
x, _ := hex.DecodeString(bad)
|
|
if _, _, err := Split(x); err == nil {
|
|
t.Errorf("Split(%q): no error", bad)
|
|
}
|
|
}
|
|
if _, err := Content([]byte{0x04, 0x05, 0x00}); err == nil {
|
|
t.Error("Content of a truncated element: no error")
|
|
}
|
|
if _, err := Content([]byte{0x04}); err == nil {
|
|
t.Error("Content of a lone identifier: no error")
|
|
}
|
|
}
|
|
|
|
// A child that does not fit in its parent is an error of Split, which must
|
|
// return, and quickly: the loop over the children advances by each header.
|
|
func TestSplitChildDoesNotFit(t *testing.T) {
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
_, _, err := Split([]byte{0x30, 0x03, 0x04, 0x05, 0x00})
|
|
done <- err
|
|
}()
|
|
select {
|
|
case err := <-done:
|
|
if err == nil {
|
|
t.Error("a child that does not fit: no error")
|
|
}
|
|
case <-time.After(time.Second):
|
|
// The loop would never end, and fill the memory: stop here.
|
|
t.Error("Split does not return")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// FuzzDERCheck checks that Check never panics, and that what it accepts
|
|
// Split and Content read without error, element by element: the children of
|
|
// a constructed element are its content exactly, a primitive one is not
|
|
// split, and a time of the universal class is read by ParseTime.
|
|
func FuzzDERCheck(f *testing.F) {
|
|
for _, s := range []string{
|
|
"30050201010500", "a003020101", "0603" + "2a0304", "170d" + hexOf("250101120000Z"),
|
|
"1812" + hexOf("20250101120000.25Z"), "30800000", "0489" + "010000000000000080",
|
|
} {
|
|
b, _ := hex.DecodeString(s)
|
|
f.Add(b)
|
|
}
|
|
for _, b := range vectorSeeds(f) {
|
|
f.Add(b)
|
|
}
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
if Check(b) != nil {
|
|
return
|
|
}
|
|
walk(t, b)
|
|
})
|
|
}
|
|
|
|
func walk(t *testing.T, b []byte) {
|
|
c, err := Content(b)
|
|
if err != nil {
|
|
t.Fatalf("Content of %x: %v", b, err)
|
|
}
|
|
if b[0]&0x20 == 0 {
|
|
if _, _, err := Split(b); err == nil {
|
|
t.Fatalf("Split of the primitive %x", b)
|
|
}
|
|
if b[0] == 0x17 || b[0] == 0x18 {
|
|
if _, _, err := ParseTime(b); err != nil {
|
|
t.Fatalf("ParseTime of %x: %v", b, err)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
id, kids, err := Split(b)
|
|
if err != nil || id != b[0] || !bytes.Equal(bytes.Join(kids, nil), c) {
|
|
t.Fatalf("Split of %x: %x %x %v", b, id, kids, err)
|
|
}
|
|
for _, k := range kids {
|
|
walk(t, k)
|
|
}
|
|
}
|
|
|
|
// vectorSeeds returns the DER elements of the areas of security_cms.json: the
|
|
// outermost runs of bytes that Check accepts, which are the signatures and
|
|
// the tokens.
|
|
func vectorSeeds(f *testing.F) [][]byte {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "testdata", "vectors", "security_cms.json"))
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
var file struct {
|
|
Cases []struct {
|
|
Area string `json:"security_cbor"`
|
|
} `json:"cases"`
|
|
}
|
|
if err := json.Unmarshal(raw, &file); err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
var out [][]byte
|
|
for _, c := range file.Cases {
|
|
area, err := hex.DecodeString(c.Area)
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
out = append(out, derElements(area)...)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// derElements returns the outermost runs of bytes of b that are one DER
|
|
// SEQUENCE of more than 127 bytes each.
|
|
func derElements(b []byte) [][]byte {
|
|
var out [][]byte
|
|
for i := 0; i+2 < len(b); i++ {
|
|
if b[i] != 0x30 || b[i+1] < 0x81 || b[i+1] > 0x83 {
|
|
continue
|
|
}
|
|
hl, cl, err := header(b[i:])
|
|
if err != nil || Check(b[i:i+hl+cl]) != nil {
|
|
continue
|
|
}
|
|
out = append(out, b[i:i+hl+cl])
|
|
i += hl + cl - 1
|
|
}
|
|
return out
|
|
}
|