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.
DateKeys/internal/der/der_test.go

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
}

Powered by TurnKey Linux.