223 lines
7.3 KiB
Go
223 lines
7.3 KiB
Go
package precert
|
|
|
|
import (
|
|
"bytes"
|
|
encoding_asn1 "encoding/asn1"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"golang.org/x/crypto/cryptobyte"
|
|
"golang.org/x/crypto/cryptobyte/asn1"
|
|
)
|
|
|
|
// Correspond returns nil if the two certificates are a valid precertificate/final certificate pair.
|
|
// Order of the arguments matters: the precertificate is first and the final certificate is second.
|
|
// Note that RFC 6962 allows the precertificate and final certificate to have different Issuers, but
|
|
// this function rejects such pairs.
|
|
func Correspond(precertDER, finalDER []byte) error {
|
|
preTBS, err := tbsDERFromCertDER(precertDER)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing precert: %w", err)
|
|
}
|
|
|
|
finalTBS, err := tbsDERFromCertDER(finalDER)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing final cert: %w", err)
|
|
}
|
|
|
|
// The first 7 fields of TBSCertificate must be byte-for-byte identical.
|
|
// The next 2 fields (issuerUniqueID and subjectUniqueID) are forbidden
|
|
// by the Baseline Requirements so we assume they are not present (if they
|
|
// are, they will fail the next check, for extensions).
|
|
// https://datatracker.ietf.org/doc/html/rfc5280#page-117
|
|
// TBSCertificate ::= SEQUENCE {
|
|
// version [0] Version DEFAULT v1,
|
|
// serialNumber CertificateSerialNumber,
|
|
// signature AlgorithmIdentifier,
|
|
// issuer Name,
|
|
// validity Validity,
|
|
// subject Name,
|
|
// subjectPublicKeyInfo SubjectPublicKeyInfo,
|
|
// issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
|
|
// -- If present, version MUST be v2 or v3
|
|
// subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
|
|
// -- If present, version MUST be v2 or v3
|
|
// extensions [3] Extensions OPTIONAL
|
|
// -- If present, version MUST be v3 -- }
|
|
for i := range 7 {
|
|
if err := readIdenticalElement(&preTBS, &finalTBS); err != nil {
|
|
return fmt.Errorf("checking for identical field %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
// The extensions should be mostly the same, with these exceptions:
|
|
// - The precertificate should have exactly one precertificate poison extension
|
|
// not present in the final certificate.
|
|
// - The final certificate should have exactly one SCTList extension not present
|
|
// in the precertificate.
|
|
// - As a consequence, the byte lengths of the extensions fields will not be the
|
|
// same, so we ignore the lengths (so long as they parse)
|
|
precertExtensionBytes, err := unwrapExtensions(preTBS)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing precert extensions: %w", err)
|
|
}
|
|
|
|
finalCertExtensionBytes, err := unwrapExtensions(finalTBS)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing final cert extensions: %w", err)
|
|
}
|
|
|
|
precertParser := extensionParser{bytes: precertExtensionBytes, skippableOID: poisonOID}
|
|
finalCertParser := extensionParser{bytes: finalCertExtensionBytes, skippableOID: sctListOID}
|
|
|
|
for i := 0; ; i++ {
|
|
precertExtn, err := precertParser.Next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
finalCertExtn, err := finalCertParser.Next()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !bytes.Equal(precertExtn, finalCertExtn) {
|
|
return fmt.Errorf("precert extension %d (%x) not equal to final cert extension %d (%x)",
|
|
i+precertParser.skipped, precertExtn, i+finalCertParser.skipped, finalCertExtn)
|
|
}
|
|
|
|
if precertExtn == nil && finalCertExtn == nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
if precertParser.skipped == 0 {
|
|
return fmt.Errorf("no poison extension found in precert")
|
|
}
|
|
if precertParser.skipped > 1 {
|
|
return fmt.Errorf("multiple poison extensions found in precert")
|
|
}
|
|
if finalCertParser.skipped == 0 {
|
|
return fmt.Errorf("no SCTList extension found in final cert")
|
|
}
|
|
if finalCertParser.skipped > 1 {
|
|
return fmt.Errorf("multiple SCTList extensions found in final cert")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var poisonOID = []int{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
|
|
var sctListOID = []int{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}
|
|
|
|
// extensionParser takes a sequence of bytes representing the inner bytes of the
|
|
// `extensions` field. Repeated calls to Next() will return all the extensions
|
|
// except those that match the skippableOID. The skipped extensions will be
|
|
// counted in `skipped`.
|
|
type extensionParser struct {
|
|
skippableOID encoding_asn1.ObjectIdentifier
|
|
bytes cryptobyte.String
|
|
skipped int
|
|
}
|
|
|
|
// Next returns the next extension in the sequence, skipping (and counting)
|
|
// any extension that matches the skippableOID.
|
|
// Returns nil, nil when there are no more extensions.
|
|
func (e *extensionParser) Next() (cryptobyte.String, error) {
|
|
if e.bytes.Empty() {
|
|
return nil, nil
|
|
}
|
|
|
|
var next cryptobyte.String
|
|
if !e.bytes.ReadASN1(&next, asn1.SEQUENCE) {
|
|
return nil, fmt.Errorf("failed to parse extension")
|
|
}
|
|
|
|
var oid encoding_asn1.ObjectIdentifier
|
|
nextCopy := next
|
|
if !nextCopy.ReadASN1ObjectIdentifier(&oid) {
|
|
return nil, fmt.Errorf("failed to parse extension OID")
|
|
}
|
|
|
|
if oid.Equal(e.skippableOID) {
|
|
e.skipped++
|
|
return e.Next()
|
|
}
|
|
|
|
return next, nil
|
|
}
|
|
|
|
// unwrapExtensions takes a given a sequence of bytes representing the `extensions` field
|
|
// of a TBSCertificate and parses away the outermost two layers, returning the inner bytes
|
|
// of the Extensions SEQUENCE.
|
|
//
|
|
// https://datatracker.ietf.org/doc/html/rfc5280#page-117
|
|
//
|
|
// TBSCertificate ::= SEQUENCE {
|
|
// ...
|
|
// extensions [3] Extensions OPTIONAL
|
|
// }
|
|
//
|
|
// Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension
|
|
func unwrapExtensions(field cryptobyte.String) (cryptobyte.String, error) {
|
|
var extensions cryptobyte.String
|
|
if !field.ReadASN1(&extensions, asn1.Tag(3).Constructed().ContextSpecific()) {
|
|
return nil, errors.New("error reading extensions")
|
|
}
|
|
|
|
var extensionsInner cryptobyte.String
|
|
if !extensions.ReadASN1(&extensionsInner, asn1.SEQUENCE) {
|
|
return nil, errors.New("error reading extensions inner")
|
|
}
|
|
|
|
return extensionsInner, nil
|
|
}
|
|
|
|
// readIdenticalElement parses a single ASN1 element and returns an error if
|
|
// their tags are different or their contents are different.
|
|
func readIdenticalElement(a, b *cryptobyte.String) error {
|
|
var aInner, bInner cryptobyte.String
|
|
var aTag, bTag asn1.Tag
|
|
if !a.ReadAnyASN1Element(&aInner, &aTag) {
|
|
return fmt.Errorf("failed to read element from first input")
|
|
}
|
|
if !b.ReadAnyASN1Element(&bInner, &bTag) {
|
|
return fmt.Errorf("failed to read element from first input")
|
|
}
|
|
if aTag != bTag {
|
|
return fmt.Errorf("tags differ: %d != %d", aTag, bTag)
|
|
}
|
|
if !bytes.Equal([]byte(aInner), []byte(bInner)) {
|
|
return fmt.Errorf("elements differ: %x != %x", aInner, bInner)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// tbsDERFromCertDER takes a Certificate object encoded as DER, and parses
|
|
// away the outermost two SEQUENCEs to get the inner bytes of the TBSCertificate.
|
|
//
|
|
// https://datatracker.ietf.org/doc/html/rfc5280#page-116
|
|
//
|
|
// Certificate ::= SEQUENCE {
|
|
// tbsCertificate TBSCertificate,
|
|
// ...
|
|
//
|
|
// TBSCertificate ::= SEQUENCE {
|
|
// version [0] Version DEFAULT v1,
|
|
// serialNumber CertificateSerialNumber,
|
|
// ...
|
|
func tbsDERFromCertDER(certDER []byte) (cryptobyte.String, error) {
|
|
var inner cryptobyte.String
|
|
input := cryptobyte.String(certDER)
|
|
|
|
if !input.ReadASN1(&inner, asn1.SEQUENCE) {
|
|
return nil, fmt.Errorf("failed to read outer sequence")
|
|
}
|
|
|
|
var tbsCertificate cryptobyte.String
|
|
if !inner.ReadASN1(&tbsCertificate, asn1.SEQUENCE) {
|
|
return nil, fmt.Errorf("failed to read tbsCertificate")
|
|
}
|
|
|
|
return tbsCertificate, nil
|
|
}
|