diff --git a/smx509/oid.go b/smx509/oid.go new file mode 100644 index 0000000..4288a1e --- /dev/null +++ b/smx509/oid.go @@ -0,0 +1,87 @@ +package smx509 + +import ( + "crypto/x509" + "encoding/asn1" + "reflect" + "unsafe" +) + +func setDer(oid *x509.OID, der []byte) { + oidValue := reflect.ValueOf(oid).Elem() + derField := oidValue.FieldByName("der") + + derPointer := (*[]byte)(unsafe.Pointer(derField.UnsafeAddr())) + *derPointer = der +} + +func getDer(oid *x509.OID) []byte { + oidValue := reflect.ValueOf(oid).Elem() + derField := oidValue.FieldByName("der") + + derPointer := (*[]byte)(unsafe.Pointer(derField.UnsafeAddr())) + return *derPointer +} + +func newOIDFromDER(der []byte) (x509.OID, bool) { + if len(der) == 0 || der[len(der)-1]&0x80 != 0 { + return x509.OID{}, false + } + + start := 0 + for i, v := range der { + // ITU-T X.690, section 8.19.2: + // The subidentifier shall be encoded in the fewest possible octets, + // that is, the leading octet of the subidentifier shall not have the value 0x80. + if i == start && v == 0x80 { + return x509.OID{}, false + } + if v&0x80 == 0 { + start = i + 1 + } + } + + oid := x509.OID{} + setDer(&oid, der) + return oid, true +} + +func toASN1OID(oid x509.OID) (asn1.ObjectIdentifier, bool) { + der := getDer(&oid) + out := make([]int, 0, len(der)+1) + + const ( + valSize = 31 // amount of usable bits of val for OIDs. + bitsPerByte = 7 + maxValSafeShift = (1 << (valSize - bitsPerByte)) - 1 + ) + + val := 0 + + for _, v := range der { + if val > maxValSafeShift { + return nil, false + } + + val <<= bitsPerByte + val |= int(v & 0x7F) + + if v&0x80 == 0 { + if len(out) == 0 { + if val < 80 { + out = append(out, val/40) + out = append(out, val%40) + } else { + out = append(out, 2) + out = append(out, val-80) + } + val = 0 + continue + } + out = append(out, val) + val = 0 + } + } + + return out, true +} diff --git a/smx509/oid_test.go b/smx509/oid_test.go new file mode 100644 index 0000000..5b762fa --- /dev/null +++ b/smx509/oid_test.go @@ -0,0 +1,107 @@ +package smx509 + +import ( + "crypto/x509" + "encoding/asn1" + "math" + "testing" +) + +func TestOID(t *testing.T) { + var tests = []struct { + raw []byte + valid bool + str string + ints []uint64 + }{ + {[]byte{}, false, "", nil}, + {[]byte{0x80, 0x01}, false, "", nil}, + {[]byte{0x01, 0x80, 0x01}, false, "", nil}, + + {[]byte{1, 2, 3}, true, "0.1.2.3", []uint64{0, 1, 2, 3}}, + {[]byte{41, 2, 3}, true, "1.1.2.3", []uint64{1, 1, 2, 3}}, + {[]byte{86, 2, 3}, true, "2.6.2.3", []uint64{2, 6, 2, 3}}, + + {[]byte{41, 255, 255, 255, 127}, true, "1.1.268435455", []uint64{1, 1, 268435455}}, + {[]byte{41, 0x87, 255, 255, 255, 127}, true, "1.1.2147483647", []uint64{1, 1, 2147483647}}, + {[]byte{41, 255, 255, 255, 255, 127}, true, "1.1.34359738367", []uint64{1, 1, 34359738367}}, + {[]byte{42, 255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "1.2.9223372036854775807", []uint64{1, 2, 9223372036854775807}}, + {[]byte{43, 0x81, 255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "1.3.18446744073709551615", []uint64{1, 3, 18446744073709551615}}, + {[]byte{44, 0x83, 255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "1.4.36893488147419103231", nil}, + {[]byte{85, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "2.5.1180591620717411303423", nil}, + {[]byte{85, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "2.5.19342813113834066795298815", nil}, + + {[]byte{255, 255, 255, 127}, true, "2.268435375", []uint64{2, 268435375}}, + {[]byte{0x87, 255, 255, 255, 127}, true, "2.2147483567", []uint64{2, 2147483567}}, + {[]byte{255, 127}, true, "2.16303", []uint64{2, 16303}}, + {[]byte{255, 255, 255, 255, 127}, true, "2.34359738287", []uint64{2, 34359738287}}, + {[]byte{255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "2.9223372036854775727", []uint64{2, 9223372036854775727}}, + {[]byte{0x81, 255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "2.18446744073709551535", []uint64{2, 18446744073709551535}}, + {[]byte{0x83, 255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "2.36893488147419103151", nil}, + {[]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "2.1180591620717411303343", nil}, + {[]byte{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 127}, true, "2.19342813113834066795298735", nil}, + } + + for _, v := range tests { + oid, ok := newOIDFromDER(v.raw) + if ok != v.valid { + if ok { + t.Errorf("%v: unexpected success while parsing: %v", v.raw, oid) + } else { + t.Errorf("%v: unexpected failure while parsing", v.raw) + } + continue + } + + if !ok { + continue + } + + if str := oid.String(); str != v.str { + t.Errorf("%v: oid.String() = %v, want; %v", v.raw, str, v.str) + } + + var asn1OID asn1.ObjectIdentifier + for _, v := range v.ints { + if v > math.MaxInt32 { + asn1OID = nil + break + } + asn1OID = append(asn1OID, int(v)) + } + + o, ok := toASN1OID(oid) + if shouldOk := asn1OID != nil; shouldOk != ok { + if ok { + t.Errorf("%v: oid.toASN1OID() unexpected success", v.raw) + } else { + t.Errorf("%v: oid.toASN1OID() unexpected fauilure", v.raw) + } + continue + } + + if asn1OID != nil { + if !o.Equal(asn1OID) { + t.Errorf("%v: oid.toASN1OID(asn1OID).Equal(oid) = false, want: true", v.raw) + } + } + + if v.ints != nil { + oid2, err := x509.OIDFromInts(v.ints) + if err != nil { + t.Errorf("%v: OIDFromInts() unexpected error: %v", v.raw, err) + } + if !oid2.Equal(oid) { + t.Errorf("%v: %#v.Equal(%#v) = false, want: true", v.raw, oid2, oid) + } + } + } +} + +func mustNewOIDFromInts(t *testing.T, ints []uint64) x509.OID { + oid, err := x509.OIDFromInts(ints) + if err != nil { + t.Fatalf("OIDFromInts(%v) unexpected error: %v", ints, err) + } + return oid +} diff --git a/smx509/parser.go b/smx509/parser.go index bbae447..a34f2f4 100644 --- a/smx509/parser.go +++ b/smx509/parser.go @@ -549,18 +549,19 @@ func parseExtKeyUsageExtension(der cryptobyte.String) ([]ExtKeyUsage, []asn1.Obj return extKeyUsages, unknownUsages, nil } -func parseCertificatePoliciesExtension(der cryptobyte.String) ([]asn1.ObjectIdentifier, error) { - var oids []asn1.ObjectIdentifier +func parseCertificatePoliciesExtension(der cryptobyte.String) ([]x509.OID, error) { + var oids []x509.OID if !der.ReadASN1(&der, cryptobyte_asn1.SEQUENCE) { return nil, errors.New("x509: invalid certificate policies") } for !der.Empty() { var cp cryptobyte.String - if !der.ReadASN1(&cp, cryptobyte_asn1.SEQUENCE) { + var OIDBytes cryptobyte.String + if !der.ReadASN1(&cp, cryptobyte_asn1.SEQUENCE) || !cp.ReadASN1(&OIDBytes, cryptobyte_asn1.OBJECT_IDENTIFIER) { return nil, errors.New("x509: invalid certificate policies") } - var oid asn1.ObjectIdentifier - if !cp.ReadASN1ObjectIdentifier(&oid) { + oid, ok := newOIDFromDER(OIDBytes) + if !ok { return nil, errors.New("x509: invalid certificate policies") } oids = append(oids, oid) @@ -870,10 +871,16 @@ func processExtensions(out *Certificate) error { } out.SubjectKeyId = skid case 32: - out.PolicyIdentifiers, err = parseCertificatePoliciesExtension(e.Value) + out.Policies, err = parseCertificatePoliciesExtension(e.Value) if err != nil { return err } + out.PolicyIdentifiers = make([]asn1.ObjectIdentifier, 0, len(out.Policies)) + for _, oid := range out.Policies { + if oid, ok := toASN1OID(oid); ok { + out.PolicyIdentifiers = append(out.PolicyIdentifiers, oid) + } + } default: // Unknown extensions are recorded if critical. unhandled = true diff --git a/smx509/x509.go b/smx509/x509.go index fd48a68..fbf1b4d 100644 --- a/smx509/x509.go +++ b/smx509/x509.go @@ -1149,7 +1149,7 @@ func buildCertExtensions(template *x509.Certificate, subjectIsEmpty bool, author if len(template.PolicyIdentifiers) > 0 && !oidInExtensions(oidExtensionCertificatePolicies, template.ExtraExtensions) { - ret[n], err = marshalCertificatePolicies(template.PolicyIdentifiers) + ret[n], err = marshalCertificatePolicies(template.Policies, template.PolicyIdentifiers) if err != nil { return nil, err } @@ -1334,14 +1334,27 @@ func marshalBasicConstraints(isCA bool, maxPathLen int, maxPathLenZero bool) (pk return ext, err } -func marshalCertificatePolicies(policyIdentifiers []asn1.ObjectIdentifier) (pkix.Extension, error) { +func marshalCertificatePolicies(policies []x509.OID, policyIdentifiers []asn1.ObjectIdentifier) (pkix.Extension, error) { ext := pkix.Extension{Id: oidExtensionCertificatePolicies} - policies := make([]policyInformation, len(policyIdentifiers)) - for i, policy := range policyIdentifiers { - policies[i].Policy = policy - } + + b := cryptobyte.NewBuilder(make([]byte, 0, 128)) + b.AddASN1(cryptobyte_asn1.SEQUENCE, func(child *cryptobyte.Builder) { + for _, v := range policies { + child.AddASN1(cryptobyte_asn1.SEQUENCE, func(child *cryptobyte.Builder) { + child.AddASN1(cryptobyte_asn1.OBJECT_IDENTIFIER, func(child *cryptobyte.Builder) { + child.AddBytes(getDer(&v)) + }) + }) + } + for _, v := range policyIdentifiers { + child.AddASN1(cryptobyte_asn1.SEQUENCE, func(child *cryptobyte.Builder) { + child.AddASN1ObjectIdentifier(v) + }) + } + }) + var err error - ext.Value, err = asn1.Marshal(policies) + ext.Value, err = b.Bytes() return ext, err } diff --git a/smx509/x509_test.go b/smx509/x509_test.go index 810537c..c542201 100644 --- a/smx509/x509_test.go +++ b/smx509/x509_test.go @@ -17,11 +17,13 @@ import ( "encoding/hex" "encoding/pem" "io" + "math" "math/big" "net" "net/url" "reflect" "runtime" + "slices" "strings" "testing" "time" @@ -490,6 +492,7 @@ func TestCreateSelfSignedCertificate(t *testing.T) { URIs: []*url.URL{parseURI("https://foo.com/wibble#foo")}, PolicyIdentifiers: []asn1.ObjectIdentifier{[]int{1, 2, 3}}, + Policies: []x509.OID{mustNewOIDFromInts(t, []uint64{1, 2, 3, math.MaxUint32, math.MaxUint64})}, PermittedDNSDomains: []string{".example.com", "example.com"}, ExcludedDNSDomains: []string{"bar.example.com"}, PermittedIPRanges: []*net.IPNet{parseCIDR("192.168.1.1/16"), parseCIDR("1.2.3.4/8")}, @@ -3679,3 +3682,49 @@ func TestCreateCertificateNegativeMaxPathLength(t *testing.T) { t.Fatalf(`CreateCertificate() = %v; want = "x509: invalid MaxPathLen, must be greater or equal to -1"`, err) } } + +func TestCertificateOIDPolicies(t *testing.T) { + template := Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Cert"}, + NotBefore: time.Unix(1000, 0), + NotAfter: time.Unix(100000, 0), + PolicyIdentifiers: []asn1.ObjectIdentifier{[]int{1, 2, 3}}, + Policies: []x509.OID{ + mustNewOIDFromInts(t, []uint64{1, 2, 3, 4, 5}), + mustNewOIDFromInts(t, []uint64{1, 2, 3, math.MaxInt32}), + mustNewOIDFromInts(t, []uint64{1, 2, 3, math.MaxUint32, math.MaxUint64}), + }, + } + + var expectPolicyIdentifiers = []asn1.ObjectIdentifier{ + []int{1, 2, 3, 4, 5}, + []int{1, 2, 3, math.MaxInt32}, + []int{1, 2, 3}, + } + + var expectPolicies = []x509.OID{ + mustNewOIDFromInts(t, []uint64{1, 2, 3, 4, 5}), + mustNewOIDFromInts(t, []uint64{1, 2, 3, math.MaxInt32}), + mustNewOIDFromInts(t, []uint64{1, 2, 3, math.MaxUint32, math.MaxUint64}), + mustNewOIDFromInts(t, []uint64{1, 2, 3}), + } + + certDER, err := CreateCertificate(rand.Reader, &template, &template, rsaPrivateKey.Public(), rsaPrivateKey) + if err != nil { + t.Fatalf("CreateCertificate() unexpected error: %v", err) + } + + cert, err := ParseCertificate(certDER) + if err != nil { + t.Fatalf("ParseCertificate() unexpected error: %v", err) + } + + if !slices.EqualFunc(cert.PolicyIdentifiers, expectPolicyIdentifiers, slices.Equal) { + t.Errorf("cert.PolicyIdentifiers = %v, want: %v", cert.PolicyIdentifiers, expectPolicyIdentifiers) + } + + if !slices.EqualFunc(cert.Policies, expectPolicies, x509.OID.Equal) { + t.Errorf("cert.Policies = %v, want: %v", cert.Policies, expectPolicies) + } +}