From cb976082a354e314aa3a65a2e8dd0a0631852704 Mon Sep 17 00:00:00 2001 From: Brandon Weeks Date: Thu, 2 Jun 2022 15:05:51 -0700 Subject: [PATCH] x509ext: initial version of package (#279) --- x509/x509ext.go | 174 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 x509/x509ext.go diff --git a/x509/x509ext.go b/x509/x509ext.go new file mode 100644 index 0000000..0a8f8bf --- /dev/null +++ b/x509/x509ext.go @@ -0,0 +1,174 @@ +// Package x509ext provides functions for (un)marshalling X.509 extensions not +// supported by the crypto/x509 package. +package x509ext + +import ( + "crypto/x509/pkix" + "encoding/asn1" + "errors" + "fmt" + + "github.com/google/go-attestation/oid" +) + +// RFC 4043 +// +// https://tools.ietf.org/html/rfc4043 +var ( + oidPermanentIdentifier = []int{1, 3, 6, 1, 5, 5, 7, 8, 3} +) + +// OtherName ::= SEQUENCE { +// type-id OBJECT IDENTIFIER, +// value [0] EXPLICIT ANY DEFINED BY type-id } +type otherName struct { + TypeID asn1.ObjectIdentifier + Value asn1.RawValue +} + +func marshalOtherName(typeID asn1.ObjectIdentifier, value interface{}) (asn1.RawValue, error) { + valueBytes, err := asn1.MarshalWithParams(value, "explicit,tag:0") + if err != nil { + return asn1.RawValue{}, err + } + otherName := otherName{ + TypeID: typeID, + Value: asn1.RawValue{FullBytes: valueBytes}, + } + bytes, err := asn1.MarshalWithParams(otherName, "tag:0") + if err != nil { + return asn1.RawValue{}, err + } + return asn1.RawValue{FullBytes: bytes}, nil +} + +// PermanentIdentifier ::= SEQUENCE { +// identifierValue UTF8String OPTIONAL, +// assigner OBJECT IDENTIFIER OPTIONAL +// } +// +// https://datatracker.ietf.org/doc/html/rfc4043 +type PermanentIdentifier struct { + IdentifierValue string `asn1:"utf8,optional"` + Assigner asn1.ObjectIdentifier `asn1:"optional"` +} + +func parsePermanentIdentifier(der []byte) (PermanentIdentifier, error) { + var permID PermanentIdentifier + if _, err := asn1.UnmarshalWithParams(der, &permID, "explicit,tag:0"); err != nil { + return PermanentIdentifier{}, err + } + return permID, nil +} + +// SubjectAltName contains GeneralName variations not supported by the +// crypto/x509 package. +// +// https://datatracker.ietf.org/doc/html/rfc5280 +type SubjectAltName struct { + DirectoryNames []pkix.Name + PermanentIdentifiers []PermanentIdentifier +} + +// ParseSubjectAltName parses a pkix.Extension into a SubjectAltName struct. +func ParseSubjectAltName(ext pkix.Extension) (*SubjectAltName, error) { + var out SubjectAltName + dirNames, otherNames, err := parseSubjectAltName(ext) + if err != nil { + return nil, fmt.Errorf("parseSubjectAltName: %v", err) + } + out.DirectoryNames = dirNames + + for _, otherName := range otherNames { + if otherName.TypeID.Equal(oidPermanentIdentifier) { + permID, err := parsePermanentIdentifier(otherName.Value.FullBytes) + if err != nil { + return nil, fmt.Errorf("parsePermanentIdentifier: %v", err) + } + out.PermanentIdentifiers = append(out.PermanentIdentifiers, permID) + } + } + return &out, nil +} + +// https://datatracker.ietf.org/doc/html/rfc5280#page-35 +func parseSubjectAltName(ext pkix.Extension) (dirNames []pkix.Name, otherNames []otherName, err error) { + err = forEachSAN(ext.Value, func(generalName asn1.RawValue) error { + switch generalName.Tag { + case 0: // otherName + var otherName otherName + if _, err := asn1.UnmarshalWithParams(generalName.FullBytes, &otherName, "tag:0"); err != nil { + return fmt.Errorf("OtherName: asn1.UnmarshalWithParams: %v", err) + } + otherNames = append(otherNames, otherName) + case 4: // directoryName + var rdns pkix.RDNSequence + if _, err := asn1.Unmarshal(generalName.Bytes, &rdns); err != nil { + return fmt.Errorf("DirectoryName: asn1.Unmarshal: %v", err) + } + var dirName pkix.Name + dirName.FillFromRDNSequence(&rdns) + dirNames = append(dirNames, dirName) + default: + return fmt.Errorf("expected tag %d", generalName.Tag) + } + return nil + }) + return +} + +// Borrowed from the x509 package. +func forEachSAN(extension []byte, callback func(ext asn1.RawValue) error) error { + var seq asn1.RawValue + rest, err := asn1.Unmarshal(extension, &seq) + if err != nil { + return err + } else if len(rest) != 0 { + return errors.New("x509: trailing data after X.509 extension") + } + if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 { + return asn1.StructuralError{Msg: "bad SAN sequence"} + } + + rest = seq.Bytes + for len(rest) > 0 { + var v asn1.RawValue + rest, err = asn1.Unmarshal(rest, &v) + if err != nil { + return err + } + + if err := callback(v); err != nil { + return err + } + } + + return nil +} + +// MarshalSubjectAltName converts a SubjectAltName struct into a pkix.Extension. +func MarshalSubjectAltName(san *SubjectAltName) (pkix.Extension, error) { + var generalNames []asn1.RawValue + for _, permID := range san.PermanentIdentifiers { + val, err := marshalOtherName(oidPermanentIdentifier, permID) + if err != nil { + return pkix.Extension{}, err + } + generalNames = append(generalNames, val) + } + for _, dirName := range san.DirectoryNames { + bytes, err := asn1.MarshalWithParams(dirName.ToRDNSequence(), "explicit,tag:4") + if err != nil { + return pkix.Extension{}, err + } + generalNames = append(generalNames, asn1.RawValue{FullBytes: bytes}) + } + val, err := asn1.Marshal(generalNames) + if err != nil { + return pkix.Extension{}, err + } + return pkix.Extension{ + Id: oid.SubjectAltName, + Value: val, + }, nil +}