crypto/x509: accept non-string pkix.Name attributes

Renamed parseTime to readASN1Time to make it clear it consumes the
cryptobyte.String, like the String methods.

Fixes #75260

Change-Id: I707b70e65fb627904f997d2e7cf122f96a6a6964
Reviewed-on: https://go-review.googlesource.com/c/go/+/778681
Reviewed-by: Roland Shoemaker <roland@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
Reviewed-by: David Chase <drchase@google.com>
This commit is contained in:
Filippo Valsorda 2026-05-16 23:23:48 -04:00 committed by Gopher Robot
parent d2095798a1
commit caa4c72fee
5 changed files with 156 additions and 18 deletions

View file

@ -0,0 +1,3 @@
When parsing into [pkix.Name] fields, a wider range of
[pkix.AttributeTypeAndValue.Value] types is now supported, and unknown types are
parsed into [asn1.RawValue].

View file

@ -138,6 +138,70 @@ func parseASN1String(tag cryptobyte_asn1.Tag, value []byte) (string, error) {
return "", fmt.Errorf("unsupported string type: %v", tag)
}
// readASN1Any parses types documented at [pkix.AttributeTypeAndValue].
func readASN1Any(der *cryptobyte.String) (any, error) {
var fullValue cryptobyte.String
var valueTag cryptobyte_asn1.Tag
if !der.ReadAnyASN1Element(&fullValue, &valueTag) {
return nil, errors.New("invalid ASN.1 element")
}
switch valueTag {
case cryptobyte_asn1.T61String, cryptobyte_asn1.PrintableString,
cryptobyte_asn1.UTF8String, cryptobyte_asn1.Tag(asn1.TagBMPString),
cryptobyte_asn1.IA5String, cryptobyte_asn1.Tag(asn1.TagNumericString):
var rawValue []byte
if !fullValue.ReadASN1((*cryptobyte.String)(&rawValue), valueTag) {
return nil, errors.New("invalid ASN.1 element")
}
return parseASN1String(valueTag, rawValue)
case cryptobyte_asn1.INTEGER:
var i int64
if !fullValue.ReadASN1Integer(&i) {
return nil, errors.New("invalid ASN.1 integer")
}
return i, nil
case cryptobyte_asn1.BIT_STRING:
var bs asn1.BitString
if !fullValue.ReadASN1BitString(&bs) {
return nil, errors.New("invalid ASN.1 BIT STRING")
}
return bs, nil
case cryptobyte_asn1.OCTET_STRING:
var s []byte
if !fullValue.ReadASN1((*cryptobyte.String)(&s), cryptobyte_asn1.OCTET_STRING) {
return nil, errors.New("invalid ASN.1 OCTET STRING")
}
return s, nil
case cryptobyte_asn1.OBJECT_IDENTIFIER:
var oid asn1.ObjectIdentifier
if !fullValue.ReadASN1ObjectIdentifier(&oid) {
return nil, errors.New("invalid ASN.1 OBJECT IDENTIFIER")
}
return oid, nil
case cryptobyte_asn1.UTCTime, cryptobyte_asn1.GeneralizedTime:
out, err := readASN1Time(&fullValue)
return out, err
case cryptobyte_asn1.BOOLEAN:
var b bool
if !fullValue.ReadASN1Boolean(&b) {
return nil, errors.New("invalid ASN.1 BOOLEAN")
}
return b, nil
case cryptobyte_asn1.NULL:
return nil, nil
default:
var v asn1.RawValue
v.Class = int(valueTag >> 6)
v.IsCompound = valueTag&0x20 == 0x20
v.Tag = int(valueTag & 0x1f)
v.FullBytes = fullValue
if !fullValue.ReadAnyASN1((*cryptobyte.String)(&v.Bytes), &valueTag) {
return nil, errors.New("invalid ASN.1 element")
}
return v, nil
}
}
// parseName parses a DER encoded Name as defined in RFC 5280. We may
// want to export this function in the future for use in crypto/tls.
func parseName(raw cryptobyte.String) (*pkix.RDNSequence, error) {
@ -161,13 +225,8 @@ func parseName(raw cryptobyte.String) (*pkix.RDNSequence, error) {
if !atav.ReadASN1ObjectIdentifier(&attr.Type) {
return nil, errors.New("x509: invalid RDNSequence: invalid attribute type")
}
var rawValue cryptobyte.String
var valueTag cryptobyte_asn1.Tag
if !atav.ReadAnyASN1(&rawValue, &valueTag) {
return nil, errors.New("x509: invalid RDNSequence: invalid attribute value")
}
var err error
attr.Value, err = parseASN1String(valueTag, rawValue)
attr.Value, err = readASN1Any(&atav)
if err != nil {
return nil, fmt.Errorf("x509: invalid RDNSequence: invalid attribute value: %s", err)
}
@ -198,7 +257,7 @@ func parseAI(der cryptobyte.String) (pkix.AlgorithmIdentifier, error) {
return ai, nil
}
func parseTime(der *cryptobyte.String) (time.Time, error) {
func readASN1Time(der *cryptobyte.String) (time.Time, error) {
var t time.Time
switch {
case der.PeekASN1Tag(cryptobyte_asn1.UTCTime):
@ -216,11 +275,11 @@ func parseTime(der *cryptobyte.String) (time.Time, error) {
}
func parseValidity(der cryptobyte.String) (time.Time, time.Time, error) {
notBefore, err := parseTime(&der)
notBefore, err := readASN1Time(&der)
if err != nil {
return time.Time{}, time.Time{}, err
}
notAfter, err := parseTime(&der)
notAfter, err := readASN1Time(&der)
if err != nil {
return time.Time{}, time.Time{}, err
}
@ -1187,12 +1246,12 @@ func ParseRevocationList(der []byte) (*RevocationList, error) {
}
rl.Issuer.FillFromRDNSequence(issuerRDNs)
rl.ThisUpdate, err = parseTime(&tbs)
rl.ThisUpdate, err = readASN1Time(&tbs)
if err != nil {
return nil, err
}
if tbs.PeekASN1Tag(cryptobyte_asn1.GeneralizedTime) || tbs.PeekASN1Tag(cryptobyte_asn1.UTCTime) {
rl.NextUpdate, err = parseTime(&tbs)
rl.NextUpdate, err = readASN1Time(&tbs)
if err != nil {
return nil, err
}
@ -1219,7 +1278,7 @@ func ParseRevocationList(der []byte) (*RevocationList, error) {
if !certSeq.ReadASN1Integer(rce.SerialNumber) {
return nil, errors.New("x509: malformed serial number")
}
rce.RevocationTime, err = parseTime(&certSeq)
rce.RevocationTime, err = readASN1Time(&certSeq)
if err != nil {
return nil, err
}

View file

@ -5,14 +5,18 @@
package x509
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"os"
"reflect"
"strings"
"testing"
"time"
cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1"
)
@ -358,3 +362,64 @@ func FuzzDomainNameValid(f *testing.F) {
domainNameValid(data, true)
})
}
func TestParseNameTypes(t *testing.T) {
block, _ := pem.Decode([]byte(`
-----BEGIN CERTIFICATE-----
MIICGzCCAcGgAwIBAgIJAIZft7jy3RcpMAoGCCqGSM49BAMCMA8xDTALBgNVBAMM
BFRlc3QwHhcNMjUwOTAyMTg0MzE3WhcNMjUxMDAyMTg0MzE3WjCCARUxIjAgBg0q
hkiG9xIEAYS3CQIBDA91dGY4LXN0cmluZ/CfpooxHzAdBg0qhkiG9xIEAYS3CQIC
BAxvY3RldC1zdHJpbmcxGDAWBg0qhkiG9xIEAYS3CQIDDQUBAgMEBTETMBEGDSqG
SIb3EgQBhLcJAgQFADEVMBMGDSqGSIb3EgQBhLcJAgUwAgECMRQwEgYNKoZIhvcS
BAGEtwkCBgIBKjEgMB4GDSqGSIb3EgQBhLcJAgcGDSqGSIb3EgQBhLcJAgcxGDAW
Bg0qhkiG9xIEAYS3CQIIAwUAqrvM3TEgMB4GDSqGSIb3EgQBhLcJAgkXDTI1MDkw
MjE4NDMxN1oxFDASBg0qhkiG9xIEAYS3CQIKAQH/MFkwEwYHKoZIzj0CAQYIKoZI
zj0DAQcDQgAE7M4zoqQtXbvGsudKaM5gd8emxyk68AFTjIYU4PO1AtiYX3wyL89k
wbHjxgvmh/9aBg1LOj6kfsJIxULUmUpdzTAKBggqhkjOPQQDAgNIADBFAiBvKz3o
ALhCqKrRFLUbax6+tI1s1B14IPVk2ZHbBEou5gIhAOpvJRNj5qluPXKLXmZvIK8u
OjUhiZoowYvborSS1EBK
-----END CERTIFICATE-----
`))
expected := map[string]any{
"1.2.840.113554.4.1.72585.2.1": "utf8-string🦊",
"1.2.840.113554.4.1.72585.2.2": []byte("octet-string"),
"1.2.840.113554.4.1.72585.2.3": asn1.RawValue{Tag: 13,
Bytes: []byte{1, 2, 3, 4, 5}, FullBytes: []byte{13, 5, 1, 2, 3, 4, 5}},
"1.2.840.113554.4.1.72585.2.4": nil,
"1.2.840.113554.4.1.72585.2.5": asn1.RawValue{Tag: asn1.TagSequence, IsCompound: true,
Bytes: []byte{1, 2}, FullBytes: []byte{0x30, 2, 1, 2}},
"1.2.840.113554.4.1.72585.2.6": int64(42),
"1.2.840.113554.4.1.72585.2.7": asn1.ObjectIdentifier{1, 2, 840, 113554, 4, 1, 72585, 2, 7},
"1.2.840.113554.4.1.72585.2.8": asn1.BitString{BitLength: 32,
Bytes: []byte{0xaa, 0xbb, 0xcc, 0xdd}},
"1.2.840.113554.4.1.72585.2.9": time.Date(2025, 9, 2, 18, 43, 17, 0, time.UTC),
"1.2.840.113554.4.1.72585.2.10": true,
}
cert, err := ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("ParseCertificate failed: %v", err)
}
for _, attr := range cert.Subject.Names {
if val, ok := expected[attr.Type.String()]; ok {
if !reflect.DeepEqual(attr.Value, val) {
t.Errorf("unexpected value for %s: got %v (%T), want %v (%T)", attr.Type, attr.Value, attr.Value, val, val)
}
} else {
t.Errorf("unexpected attribute type: %s", attr.Type)
}
}
extra := pkix.Name{ExtraNames: cert.Subject.Names}
// asn1.Marshal does not encode NULL.
for i := range extra.ExtraNames {
if extra.ExtraNames[i].Value == nil {
extra.ExtraNames[i].Value = asn1.NullRawValue
}
}
got, err := asn1.Marshal(extra.ToRDNSequence())
if err != nil {
t.Fatalf("asn1.Marshal failed: %v", err)
}
if !bytes.Equal(got, cert.RawSubject) {
t.Errorf("unexpected marshaled RDNSequence: got %x, want %x", got, cert.RawSubject)
}
}

View file

@ -106,6 +106,20 @@ type RelativeDistinguishedNameSET []AttributeTypeAndValue
// AttributeTypeAndValue mirrors the ASN.1 structure of the same name in
// RFC 5280, Section 4.1.2.4.
//
// When parsed as part of a pkix.Name structure in a crypto/x509 type,
// the Value will be
//
// - a string if the ASN.1 type is PrintableString, IA5String,
// NumericString, BMPString, T61String, or UTF8String;
// - an int64 if the ASN.1 type is INTEGER;
// - an asn1.BitString if the ASN.1 type is BIT STRING;
// - a []byte if the ASN.1 type is OCTET STRING;
// - an asn1.ObjectIdentifier if the ASN.1 type is OBJECT IDENTIFIER;
// - a time.Time if the ASN.1 type is UTCTIME or GENERALIZEDTIME;
// - a bool if the ASN.1 type is BOOLEAN;
// - nil if the ASN.1 type is NULL;
// - an asn1.RawValue otherwise.
type AttributeTypeAndValue struct {
Type asn1.ObjectIdentifier
Value any

View file

@ -2259,14 +2259,11 @@ func parseCertificateRequest(in *certificateRequest) (*CertificateRequest, error
}
}
var subject pkix.RDNSequence
if rest, err := asn1.Unmarshal(in.TBSCSR.Subject.FullBytes, &subject); err != nil {
subject, err := parseName(in.TBSCSR.Subject.FullBytes)
if err != nil {
return nil, err
} else if len(rest) != 0 {
return nil, errors.New("x509: trailing data after X.509 Subject")
}
out.Subject.FillFromRDNSequence(&subject)
out.Subject.FillFromRDNSequence(subject)
if out.Extensions, err = parseCSRExtensions(in.TBSCSR.RawAttributes); err != nil {
return nil, err