From aaaf27a65e73aa7b9ae2c28c3ef06ef7a6db653f Mon Sep 17 00:00:00 2001 From: Mateusz Poliwczak Date: Thu, 17 Aug 2023 16:34:28 +0200 Subject: [PATCH] crypto/x509 add new OID type and use it in Certificate --- api/next/60665.txt | 6 + src/crypto/x509/oid.go | 253 ++++++++++++++++++++++++++++++++++++ src/crypto/x509/oid_test.go | 98 ++++++++++++++ src/crypto/x509/parser.go | 20 ++- src/crypto/x509/x509.go | 4 + 5 files changed, 374 insertions(+), 7 deletions(-) create mode 100644 api/next/60665.txt create mode 100644 src/crypto/x509/oid.go create mode 100644 src/crypto/x509/oid_test.go diff --git a/api/next/60665.txt b/api/next/60665.txt new file mode 100644 index 00000000000..10e50e18328 --- /dev/null +++ b/api/next/60665.txt @@ -0,0 +1,6 @@ +pkg crypto/x509, type Certificate struct, Policies []OID #60665 +pkg crypto/x509, type OID struct #60665 +pkg crypto/x509, method (OID) Equal(OID) bool #60665 +pkg crypto/x509, method (OID) EqualASN1OID(asn1.ObjectIdentifier) bool #60665 +pkg crypto/x509, method (OID) String() string #60665 +pkg crypto/x509, func OIDFromInts([]uint64) (OID, error) #60665 diff --git a/src/crypto/x509/oid.go b/src/crypto/x509/oid.go new file mode 100644 index 00000000000..ff95b928619 --- /dev/null +++ b/src/crypto/x509/oid.go @@ -0,0 +1,253 @@ +package x509 + +import ( + "bytes" + "encoding/asn1" + "errors" + "math/big" + "strconv" + "strings" +) + +var ( + errInvalidOID = errors.New("invalid oid") +) + +// An OID represents an ASN.1 OBJECT IDENTIFIER. +type OID struct { + der []byte +} + +func newOIDFromDER(der []byte) (OID, bool) { + if len(der) == 0 || der[len(der)-1]&0x80 != 0 { + return 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 OID{}, false + } + if v&0x80 == 0 { + start = i + 1 + } + } + + return OID{der}, true +} + +// OIDFromInts creates a new OID using ints, each integer is a separate component. +func OIDFromInts(oid []uint64) (OID, error) { + if len(oid) < 2 || oid[0] > 2 || (oid[0] < 2 && oid[1] >= 40) { + return OID{}, errInvalidOID + } + + length := base128IntLength(oid[0]*40 + oid[1]) + for _, v := range oid[2:] { + length += base128IntLength(v) + } + + der := make([]byte, 0, length) + der = appendBase128Int(der, oid[0]*40+oid[1]) + for _, v := range oid[2:] { + der = appendBase128Int(der, v) + } + return OID{der}, nil +} + +func base128IntLength(n uint64) int { + if n == 0 { + return 1 + } + l := 0 + for i := n; i > 0; i >>= 7 { + l++ + } + return l +} + +func appendBase128Int(dst []byte, n uint64) []byte { + for i := base128IntLength(n) - 1; i >= 0; i-- { + o := byte(n >> uint(i*7)) + o &= 0x7f + if i != 0 { + o |= 0x80 + } + dst = append(dst, o) + } + return dst +} + +// Equal returns true when oid and other represents the same Object Identifier. +func (oid OID) Equal(other OID) bool { + // There is only one possible DER encoding of + // each unique Object Identifier. + return bytes.Equal(oid.der, other.der) +} + +// EqualASN1OID returns whether an OID equals an asn1.ObjectIdentifier. If +// asn1.ObjectIdentifier cannot represent the OID specified by oid, because +// a component of OID requires more than 31 bits, it returns false. +func (oid OID) EqualASN1OID(other asn1.ObjectIdentifier) bool { + const ( + valSize = 31 // amount of usable bits of val for OIDs. + bitsPerByte = 7 + maxValSafeShift = (1 << (valSize - bitsPerByte)) - 1 + ) + var ( + val = 0 + first = true + ) + for _, v := range oid.der { + if val > maxValSafeShift { + return false + } + val <<= bitsPerByte + val |= int(v & 0x7F) + if v&0x80 == 0 { + if first { + if len(other) < 2 { + return false + } + var val1, val2 int + if val < 80 { + val1 = val / 40 + val2 = val % 40 + } else { + val1 = 2 + val2 = val - 80 + } + if val1 != other[0] || val2 != other[1] { + return false + } + val = 0 + first = false + other = other[2:] + continue + } + if len(other) == 0 { + return false + } + if val != other[0] { + return false + } + val = 0 + other = other[1:] + } + } + return true +} + +// Strings returns the string representation of the Object Identifier. +func (oid OID) String() string { + var b strings.Builder + b.Grow(32) + const ( + valSize = 64 // size in bits of val. + bitsPerByte = 7 + maxValSafeShift = (1 << (valSize - bitsPerByte)) - 1 + ) + var ( + start = 0 + val = uint64(0) + numBuf = make([]byte, 0, 21) + bigVal *big.Int + overflow bool + ) + for i, v := range oid.der { + curVal := v & 0x7F + valEnd := v&0x80 == 0 + if valEnd { + if start != 0 { + b.WriteByte('.') + } + } + if !overflow && val > maxValSafeShift { + if bigVal == nil { + bigVal = new(big.Int) + } + bigVal = bigVal.SetUint64(val) + overflow = true + } + if overflow { + bigVal = bigVal.Lsh(bigVal, bitsPerByte).Or(bigVal, big.NewInt(int64(curVal))) + if valEnd { + if start == 0 { + b.WriteString("2.") + bigVal = bigVal.Sub(bigVal, big.NewInt(80)) + } + numBuf = bigVal.Append(numBuf, 10) + b.Write(numBuf) + numBuf = numBuf[:0] + val = 0 + start = i + 1 + overflow = false + } + continue + } + val <<= bitsPerByte + val |= uint64(curVal) + if valEnd { + if start == 0 { + var val1, val2 uint64 + if val < 80 { + val1 = val / 40 + val2 = val % 40 + } else { + val1 = 2 + val2 = val - 80 + } + b.Write(strconv.AppendUint(numBuf, val1, 10)) + b.WriteByte('.') + b.Write(strconv.AppendUint(numBuf, val2, 10)) + } else { + b.Write(strconv.AppendUint(numBuf, val, 10)) + } + val = 0 + start = i + 1 + } + } + return b.String() +} + +func (oid OID) toASN1OID() (asn1.ObjectIdentifier, bool) { + out := make([]int, 0, len(oid.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 oid.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/src/crypto/x509/oid_test.go b/src/crypto/x509/oid_test.go new file mode 100644 index 00000000000..aa29657868d --- /dev/null +++ b/src/crypto/x509/oid_test.go @@ -0,0 +1,98 @@ +package x509 + +import ( + "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 := oid.toASN1OID() + 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 := 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) + } + } + } +} diff --git a/src/crypto/x509/parser.go b/src/crypto/x509/parser.go index 019a53b5dcf..812b0d2d285 100644 --- a/src/crypto/x509/parser.go +++ b/src/crypto/x509/parser.go @@ -435,23 +435,23 @@ 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) ([]OID, error) { + var oids []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) } - return oids, nil } @@ -748,10 +748,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 := oid.toASN1OID(); ok { + out.PolicyIdentifiers = append(out.PolicyIdentifiers, oid) + } + } default: // Unknown extensions are recorded if critical. unhandled = true diff --git a/src/crypto/x509/x509.go b/src/crypto/x509/x509.go index dfc5092b307..825e7cea912 100644 --- a/src/crypto/x509/x509.go +++ b/src/crypto/x509/x509.go @@ -773,6 +773,10 @@ type Certificate struct { CRLDistributionPoints []string PolicyIdentifiers []asn1.ObjectIdentifier + + // Policies contains policy OIDs included in any certificatePolicies extensions + // in the certificate. + Policies []OID } // ErrUnsupportedAlgorithm results from attempting to perform an operation that