package ca import ( "bytes" "context" "crypto" "crypto/rand" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/gob" "encoding/hex" "errors" "fmt" "math/big" mrand "math/rand/v2" "strings" "time" ct "github.com/google/certificate-transparency-go" cttls "github.com/google/certificate-transparency-go/tls" "github.com/jmhodges/clock" "github.com/miekg/pkcs11" "github.com/prometheus/client_golang/prometheus" "golang.org/x/crypto/cryptobyte" cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" "golang.org/x/crypto/ocsp" "google.golang.org/protobuf/types/known/timestamppb" capb "github.com/letsencrypt/boulder/ca/proto" "github.com/letsencrypt/boulder/core" corepb "github.com/letsencrypt/boulder/core/proto" csrlib "github.com/letsencrypt/boulder/csr" berrors "github.com/letsencrypt/boulder/errors" "github.com/letsencrypt/boulder/goodkey" "github.com/letsencrypt/boulder/issuance" "github.com/letsencrypt/boulder/linter" blog "github.com/letsencrypt/boulder/log" sapb "github.com/letsencrypt/boulder/sa/proto" ) type certificateType string const ( precertType = certificateType("precertificate") certType = certificateType("certificate") ) // Two maps of keys to Issuers. Lookup by PublicKeyAlgorithm is useful for // determining the set of issuers which can sign a given (pre)cert, based on its // PublicKeyAlgorithm. Lookup by NameID is useful for looking up a specific // issuer based on the issuer of a given (pre)certificate. type issuerMaps struct { byAlg map[x509.PublicKeyAlgorithm][]*issuance.Issuer byNameID map[issuance.NameID]*issuance.Issuer } type certProfileWithID struct { // name is a human readable name used to refer to the certificate profile. name string // hash is SHA256 sum over every exported field of an issuance.ProfileConfig // used to generate the embedded *issuance.Profile. hash [32]byte profile *issuance.Profile } // certProfilesMaps allows looking up the human-readable name of a certificate // profile to retrieve the actual profile. The default profile to be used is // stored alongside the maps. type certProfilesMaps struct { // The name of the profile that will be selected if no explicit profile name // is provided via gRPC. defaultName string profileByHash map[[32]byte]*certProfileWithID profileByName map[string]*certProfileWithID } // caMetrics holds various metrics which are shared between caImpl, ocspImpl, // and crlImpl. type caMetrics struct { signatureCount *prometheus.CounterVec signErrorCount *prometheus.CounterVec lintErrorCount prometheus.Counter } func NewCAMetrics(stats prometheus.Registerer) *caMetrics { signatureCount := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "signatures", Help: "Number of signatures", }, []string{"purpose", "issuer"}) stats.MustRegister(signatureCount) signErrorCount := prometheus.NewCounterVec(prometheus.CounterOpts{ Name: "signature_errors", Help: "A counter of signature errors labelled by error type", }, []string{"type"}) stats.MustRegister(signErrorCount) lintErrorCount := prometheus.NewCounter( prometheus.CounterOpts{ Name: "lint_errors", Help: "Number of issuances that were halted by linting errors", }) stats.MustRegister(lintErrorCount) return &caMetrics{signatureCount, signErrorCount, lintErrorCount} } func (m *caMetrics) noteSignError(err error) { var pkcs11Error pkcs11.Error if errors.As(err, &pkcs11Error) { m.signErrorCount.WithLabelValues("HSM").Inc() } } // certificateAuthorityImpl represents a CA that signs certificates. // It can sign OCSP responses as well, but only via delegation to an ocspImpl. type certificateAuthorityImpl struct { capb.UnsafeCertificateAuthorityServer sa sapb.StorageAuthorityCertificateClient pa core.PolicyAuthority issuers issuerMaps certProfiles certProfilesMaps prefix int // Prepended to the serial number maxNames int keyPolicy goodkey.KeyPolicy clk clock.Clock log blog.Logger metrics *caMetrics } var _ capb.CertificateAuthorityServer = (*certificateAuthorityImpl)(nil) // makeIssuerMaps processes a list of issuers into a set of maps for easy // lookup either by key algorithm (useful for picking an issuer for a precert) // or by unique ID (useful for final certs, OCSP, and CRLs). If two issuers with // the same unique ID are encountered, an error is returned. func makeIssuerMaps(issuers []*issuance.Issuer) (issuerMaps, error) { issuersByAlg := make(map[x509.PublicKeyAlgorithm][]*issuance.Issuer, 2) issuersByNameID := make(map[issuance.NameID]*issuance.Issuer, len(issuers)) for _, issuer := range issuers { if _, found := issuersByNameID[issuer.NameID()]; found { return issuerMaps{}, fmt.Errorf("two issuers with same NameID %d (%s) configured", issuer.NameID(), issuer.Name()) } issuersByNameID[issuer.NameID()] = issuer if issuer.IsActive() { issuersByAlg[issuer.KeyType()] = append(issuersByAlg[issuer.KeyType()], issuer) } } if i, ok := issuersByAlg[x509.ECDSA]; !ok || len(i) == 0 { return issuerMaps{}, errors.New("no ECDSA issuers configured") } if i, ok := issuersByAlg[x509.RSA]; !ok || len(i) == 0 { return issuerMaps{}, errors.New("no RSA issuers configured") } return issuerMaps{issuersByAlg, issuersByNameID}, nil } // makeCertificateProfilesMap processes a set of named certificate issuance // profile configs into a two pre-computed maps: 1) a human-readable name to the // profile and 2) a unique hash over contents of the profile to the profile // itself. It returns the maps or an error if a duplicate name or hash is found. // // The unique hash is used in the case of // - RA instructs CA1 to issue a precertificate // - CA1 returns the precertificate DER bytes and profile hash to the RA // - RA instructs CA2 to issue a final certificate, but CA2 does not contain a // profile corresponding to that hash and an issuance is prevented. func makeCertificateProfilesMap(defaultName string, profiles map[string]*issuance.ProfileConfig) (certProfilesMaps, error) { if len(profiles) <= 0 { return certProfilesMaps{}, fmt.Errorf("must pass at least one certificate profile") } // Check that a profile exists with the configured default profile name. _, ok := profiles[defaultName] if !ok { return certProfilesMaps{}, fmt.Errorf("defaultCertificateProfileName:\"%s\" was configured, but a profile object was not found for that name", defaultName) } profilesByName := make(map[string]*certProfileWithID, len(profiles)) profilesByHash := make(map[[32]byte]*certProfileWithID, len(profiles)) for name, profileConfig := range profiles { profile, err := issuance.NewProfile(profileConfig) if err != nil { return certProfilesMaps{}, err } // gob can only encode exported fields, of which an issuance.Profile has // none. However, since we're already in a loop iteration having access // to the issuance.ProfileConfig used to generate the issuance.Profile, // we'll generate the hash from that. var encodedProfile bytes.Buffer enc := gob.NewEncoder(&encodedProfile) err = enc.Encode(profileConfig) if err != nil { return certProfilesMaps{}, err } if len(encodedProfile.Bytes()) <= 0 { return certProfilesMaps{}, fmt.Errorf("certificate profile encoding returned 0 bytes") } hash := sha256.Sum256(encodedProfile.Bytes()) withID := certProfileWithID{ name: name, hash: hash, profile: profile, } profilesByName[name] = &withID _, found := profilesByHash[hash] if found { return certProfilesMaps{}, fmt.Errorf("duplicate certificate profile hash %d", hash) } profilesByHash[hash] = &withID } return certProfilesMaps{defaultName, profilesByHash, profilesByName}, nil } // NewCertificateAuthorityImpl creates a CA instance that can sign certificates // from any number of issuance.Issuers according to their profiles, and can sign // OCSP (via delegation to an ocspImpl and its issuers). func NewCertificateAuthorityImpl( sa sapb.StorageAuthorityCertificateClient, pa core.PolicyAuthority, boulderIssuers []*issuance.Issuer, defaultCertProfileName string, certificateProfiles map[string]*issuance.ProfileConfig, serialPrefix int, maxNames int, keyPolicy goodkey.KeyPolicy, logger blog.Logger, metrics *caMetrics, clk clock.Clock, ) (*certificateAuthorityImpl, error) { var ca *certificateAuthorityImpl var err error if serialPrefix < 1 || serialPrefix > 127 { err = errors.New("serial prefix must be between 1 and 127") return nil, err } if len(boulderIssuers) == 0 { return nil, errors.New("must have at least one issuer") } certProfiles, err := makeCertificateProfilesMap(defaultCertProfileName, certificateProfiles) if err != nil { return nil, err } issuers, err := makeIssuerMaps(boulderIssuers) if err != nil { return nil, err } ca = &certificateAuthorityImpl{ sa: sa, pa: pa, issuers: issuers, certProfiles: certProfiles, prefix: serialPrefix, maxNames: maxNames, keyPolicy: keyPolicy, log: logger, metrics: metrics, clk: clk, } return ca, nil } var ocspStatusToCode = map[string]int{ "good": ocsp.Good, "revoked": ocsp.Revoked, "unknown": ocsp.Unknown, } // IssuePrecertificate is the first step in the [issuance cycle]. It allocates and stores a serial number, // selects a certificate profile, generates and stores a linting certificate, sets the serial's status to // "wait", signs and stores a precertificate, updates the serial's status to "good", then returns the // precertificate. // // Subsequent final issuance based on this precertificate must happen at most once, and must use the same // certificate profile. The certificate profile is identified by a hash to ensure an exact match even if // the configuration for a specific profile _name_ changes. // // [issuance cycle]: https://github.com/letsencrypt/boulder/blob/main/docs/ISSUANCE-CYCLE.md func (ca *certificateAuthorityImpl) IssuePrecertificate(ctx context.Context, issueReq *capb.IssueCertificateRequest) (*capb.IssuePrecertificateResponse, error) { // issueReq.orderID may be zero, for ACMEv1 requests. if core.IsAnyNilOrZero(issueReq, issueReq.Csr, issueReq.RegistrationID) { return nil, berrors.InternalServerError("Incomplete issue certificate request") } // The CA must check if it is capable of issuing for the given certificate // profile name. The name is checked here instead of the hash because the RA // is unaware of what certificate profiles exist. Pre-existing orders stored // in the database may not have an associated certificate profile name and // will take the default name stored alongside the map. if issueReq.CertProfileName == "" { issueReq.CertProfileName = ca.certProfiles.defaultName } certProfile, ok := ca.certProfiles.profileByName[issueReq.CertProfileName] if !ok { return nil, fmt.Errorf("the CA is incapable of using a profile named %s", issueReq.CertProfileName) } serialBigInt, err := ca.generateSerialNumber() if err != nil { return nil, err } notBefore, notAfter := certProfile.profile.GenerateValidity(ca.clk.Now()) serialHex := core.SerialToString(serialBigInt) regID := issueReq.RegistrationID _, err = ca.sa.AddSerial(ctx, &sapb.AddSerialRequest{ Serial: serialHex, RegID: regID, Created: timestamppb.New(ca.clk.Now()), Expires: timestamppb.New(notAfter), }) if err != nil { return nil, err } precertDER, cpwid, err := ca.issuePrecertificateInner(ctx, issueReq, certProfile, serialBigInt, notBefore, notAfter) if err != nil { return nil, err } _, err = ca.sa.SetCertificateStatusReady(ctx, &sapb.Serial{Serial: serialHex}) if err != nil { return nil, err } return &capb.IssuePrecertificateResponse{ DER: precertDER, CertProfileName: cpwid.name, CertProfileHash: cpwid.hash[:], }, nil } // IssueCertificateForPrecertificate final step in the [issuance cycle]. // // Given a precertificate and a set of SCTs for that precertificate, it generates // a linting final certificate, then signs a final certificate using a real issuer. // The poison extension is removed from the precertificate and a // SCT list extension is inserted in its place. Except for this and the // signature the final certificate exactly matches the precertificate. // // It's critical not to sign two different final certificates for the same // precertificate. This can happen, for instance, if the caller provides a // different set of SCTs on subsequent calls to IssueCertificateForPrecertificate. // We rely on the RA not to call IssueCertificateForPrecertificate twice for the // same serial. This is accomplished by the fact that // IssueCertificateForPrecertificate is only ever called in a straight-through // RPC path without retries. If there is any error, including a networking // error, the whole certificate issuance attempt fails and any subsequent // issuance will use a different serial number. // // We also check that the provided serial number does not already exist as a // final certificate, but this is just a belt-and-suspenders measure, since // there could be race conditions where two goroutines are issuing for the same // serial number at the same time. // // [issuance cycle]: https://github.com/letsencrypt/boulder/blob/main/docs/ISSUANCE-CYCLE.md func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx context.Context, req *capb.IssueCertificateForPrecertificateRequest) (*corepb.Certificate, error) { // issueReq.orderID may be zero, for ACMEv1 requests. if core.IsAnyNilOrZero(req, req.DER, req.SCTs, req.RegistrationID, req.CertProfileHash) { return nil, berrors.InternalServerError("Incomplete cert for precertificate request") } // The certificate profile hash is checked here instead of the name because // the hash is over the entire contents of a *ProfileConfig giving assurance // that the certificate profile has remained unchanged during the roundtrip // from a CA, to the RA, then back to a (potentially different) CA node. certProfile, ok := ca.certProfiles.profileByHash[[32]byte(req.CertProfileHash)] if !ok { return nil, fmt.Errorf("the CA is incapable of using a profile with hash %d", req.CertProfileHash) } precert, err := x509.ParseCertificate(req.DER) if err != nil { return nil, err } serialHex := core.SerialToString(precert.SerialNumber) if _, err = ca.sa.GetCertificate(ctx, &sapb.Serial{Serial: serialHex}); err == nil { err = berrors.InternalServerError("issuance of duplicate final certificate requested: %s", serialHex) ca.log.AuditErr(err.Error()) return nil, err } else if !errors.Is(err, berrors.NotFound) { return nil, fmt.Errorf("error checking for duplicate issuance of %s: %s", serialHex, err) } var scts []ct.SignedCertificateTimestamp for _, sctBytes := range req.SCTs { var sct ct.SignedCertificateTimestamp _, err = cttls.Unmarshal(sctBytes, &sct) if err != nil { return nil, err } scts = append(scts, sct) } issuer, ok := ca.issuers.byNameID[issuance.IssuerNameID(precert)] if !ok { return nil, berrors.InternalServerError("no issuer found for Issuer Name %s", precert.Issuer) } issuanceReq, err := issuance.RequestFromPrecert(precert, scts) if err != nil { return nil, err } names := strings.Join(issuanceReq.DNSNames, ", ") ca.log.AuditInfof("Signing cert: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] precert=[%s]", issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, hex.EncodeToString(precert.Raw)) lintCertBytes, issuanceToken, err := issuer.Prepare(certProfile.profile, issuanceReq) if err != nil { ca.log.AuditErrf("Preparing cert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]", issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err) return nil, berrors.InternalServerError("failed to prepare certificate signing: %s", err) } certDER, err := issuer.Issue(issuanceToken) if err != nil { ca.metrics.noteSignError(err) ca.log.AuditErrf("Signing cert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]", issuer.Name(), serialHex, req.RegistrationID, names, certProfile.name, certProfile.hash, err) return nil, berrors.InternalServerError("failed to sign certificate: %s", err) } err = tbsCertIsDeterministic(lintCertBytes, certDER) if err != nil { return nil, err } ca.metrics.signatureCount.With(prometheus.Labels{"purpose": string(certType), "issuer": issuer.Name()}).Inc() ca.log.AuditInfof("Signing cert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certificate=[%s] certProfileName=[%s] certProfileHash=[%x]", issuer.Name(), serialHex, req.RegistrationID, names, hex.EncodeToString(certDER), certProfile.name, certProfile.hash) _, err = ca.sa.AddCertificate(ctx, &sapb.AddCertificateRequest{ Der: certDER, RegID: req.RegistrationID, Issued: timestamppb.New(ca.clk.Now()), }) if err != nil { ca.log.AuditErrf("Failed RPC to store at SA: issuer=[%s] serial=[%s] cert=[%s] regID=[%d] orderID=[%d] certProfileName=[%s] certProfileHash=[%x] err=[%v]", issuer.Name(), serialHex, hex.EncodeToString(certDER), req.RegistrationID, req.OrderID, certProfile.name, certProfile.hash, err) return nil, err } return &corepb.Certificate{ RegistrationID: req.RegistrationID, Serial: core.SerialToString(precert.SerialNumber), Der: certDER, Digest: core.Fingerprint256(certDER), Issued: timestamppb.New(precert.NotBefore), Expires: timestamppb.New(precert.NotAfter), }, nil } // generateSerialNumber produces a big.Int which has more than 64 bits of // entropy and has the CA's configured one-byte prefix. func (ca *certificateAuthorityImpl) generateSerialNumber() (*big.Int, error) { // We want 136 bits of random number, plus an 8-bit instance id prefix. const randBits = 136 serialBytes := make([]byte, randBits/8+1) serialBytes[0] = byte(ca.prefix) _, err := rand.Read(serialBytes[1:]) if err != nil { err = berrors.InternalServerError("failed to generate serial: %s", err) ca.log.AuditErrf("Serial randomness failed, err=[%v]", err) return nil, err } serialBigInt := big.NewInt(0) serialBigInt = serialBigInt.SetBytes(serialBytes) return serialBigInt, nil } // generateSKID computes the Subject Key Identifier using one of the methods in // RFC 7093 Section 2 Additional Methods for Generating Key Identifiers: // The keyIdentifier [may be] composed of the leftmost 160-bits of the // SHA-256 hash of the value of the BIT STRING subjectPublicKey // (excluding the tag, length, and number of unused bits). func generateSKID(pk crypto.PublicKey) ([]byte, error) { pkBytes, err := x509.MarshalPKIXPublicKey(pk) if err != nil { return nil, err } var pkixPublicKey struct { Algo pkix.AlgorithmIdentifier BitString asn1.BitString } if _, err := asn1.Unmarshal(pkBytes, &pkixPublicKey); err != nil { return nil, err } skid := sha256.Sum256(pkixPublicKey.BitString.Bytes) return skid[0:20:20], nil } func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context, issueReq *capb.IssueCertificateRequest, certProfile *certProfileWithID, serialBigInt *big.Int, notBefore time.Time, notAfter time.Time) ([]byte, *certProfileWithID, error) { csr, err := x509.ParseCertificateRequest(issueReq.Csr) if err != nil { return nil, nil, err } err = csrlib.VerifyCSR(ctx, csr, ca.maxNames, &ca.keyPolicy, ca.pa) if err != nil { ca.log.AuditErr(err.Error()) // VerifyCSR returns berror instances that can be passed through as-is // without wrapping. return nil, nil, err } // Select which pool of issuers to use, based on the to-be-issued cert's key // type. alg := csr.PublicKeyAlgorithm // Select a random issuer from among the active issuers of this key type. issuerPool, ok := ca.issuers.byAlg[alg] if !ok || len(issuerPool) == 0 { return nil, nil, berrors.InternalServerError("no issuers found for public key algorithm %s", csr.PublicKeyAlgorithm) } issuer := issuerPool[mrand.IntN(len(issuerPool))] if issuer.Cert.NotAfter.Before(notAfter) { err = berrors.InternalServerError("cannot issue a certificate that expires after the issuer certificate") ca.log.AuditErr(err.Error()) return nil, nil, err } subjectKeyId, err := generateSKID(csr.PublicKey) if err != nil { return nil, nil, fmt.Errorf("computing subject key ID: %w", err) } serialHex := core.SerialToString(serialBigInt) ca.log.AuditInfof("Signing precert: serial=[%s] regID=[%d] names=[%s] csr=[%s]", serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(csr.Raw)) names := csrlib.NamesFromCSR(csr) req := &issuance.IssuanceRequest{ PublicKey: csr.PublicKey, SubjectKeyId: subjectKeyId, Serial: serialBigInt.Bytes(), DNSNames: names.SANs, CommonName: names.CN, IncludeCTPoison: true, IncludeMustStaple: issuance.ContainsMustStaple(csr.Extensions), NotBefore: notBefore, NotAfter: notAfter, } lintCertBytes, issuanceToken, err := issuer.Prepare(certProfile.profile, req) if err != nil { ca.log.AuditErrf("Preparing precert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]", issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err) if errors.Is(err, linter.ErrLinting) { ca.metrics.lintErrorCount.Inc() } return nil, nil, berrors.InternalServerError("failed to prepare precertificate signing: %s", err) } _, err = ca.sa.AddPrecertificate(context.Background(), &sapb.AddCertificateRequest{ Der: lintCertBytes, RegID: issueReq.RegistrationID, Issued: timestamppb.New(ca.clk.Now()), IssuerNameID: int64(issuer.NameID()), OcspNotReady: true, }) if err != nil { return nil, nil, err } certDER, err := issuer.Issue(issuanceToken) if err != nil { ca.metrics.noteSignError(err) ca.log.AuditErrf("Signing precert failed: issuer=[%s] serial=[%s] regID=[%d] names=[%s] certProfileName=[%s] certProfileHash=[%x] err=[%v]", issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), certProfile.name, certProfile.hash, err) return nil, nil, berrors.InternalServerError("failed to sign precertificate: %s", err) } err = tbsCertIsDeterministic(lintCertBytes, certDER) if err != nil { return nil, nil, err } ca.metrics.signatureCount.With(prometheus.Labels{"purpose": string(precertType), "issuer": issuer.Name()}).Inc() ca.log.AuditInfof("Signing precert success: issuer=[%s] serial=[%s] regID=[%d] names=[%s] precertificate=[%s] certProfileName=[%s] certProfileHash=[%x]", issuer.Name(), serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), hex.EncodeToString(certDER), certProfile.name, certProfile.hash) return certDER, &certProfileWithID{certProfile.name, certProfile.hash, nil}, nil } // verifyTBSCertIsDeterministic verifies that x509.CreateCertificate signing // operation is deterministic and produced identical DER bytes between the given // lint certificate and leaf certificate. If the DER byte equality check fails // it's mississuance, but it's better to know about the problem sooner than // later. The caller is responsible for passing the appropriate valid // certificate bytes in the correct position. func tbsCertIsDeterministic(lintCertBytes []byte, leafCertBytes []byte) error { if core.IsAnyNilOrZero(lintCertBytes, leafCertBytes) { return fmt.Errorf("lintCertBytes of leafCertBytes were nil") } // extractTBSCertBytes is a partial copy of //crypto/x509/parser.go to // extract the RawTBSCertificate field from given DER bytes. It the // RawTBSCertificate field bytes or an error if the given bytes cannot be // parsed. This is far more performant than parsing the entire *Certificate // structure with x509.ParseCertificate(). // // RFC 5280, Section 4.1 // Certificate ::= SEQUENCE { // tbsCertificate TBSCertificate, // signatureAlgorithm AlgorithmIdentifier, // signatureValue BIT STRING } // // TBSCertificate ::= SEQUENCE { // .. extractTBSCertBytes := func(inputDERBytes *[]byte) ([]byte, error) { input := cryptobyte.String(*inputDERBytes) // Extract the Certificate bytes if !input.ReadASN1(&input, cryptobyte_asn1.SEQUENCE) { return nil, errors.New("malformed certificate") } var tbs cryptobyte.String // Extract the TBSCertificate bytes from the Certificate bytes if !input.ReadASN1(&tbs, cryptobyte_asn1.SEQUENCE) { return nil, errors.New("malformed tbs certificate") } if tbs.Empty() { return nil, errors.New("parsed RawTBSCertificate field was empty") } return tbs, nil } lintRawTBSCert, err := extractTBSCertBytes(&lintCertBytes) if err != nil { return fmt.Errorf("while extracting lint TBS cert: %w", err) } leafRawTBSCert, err := extractTBSCertBytes(&leafCertBytes) if err != nil { return fmt.Errorf("while extracting leaf TBS cert: %w", err) } if !bytes.Equal(lintRawTBSCert, leafRawTBSCert) { return fmt.Errorf("mismatch between lintCert and leafCert RawTBSCertificate DER bytes: \"%x\" != \"%x\"", lintRawTBSCert, leafRawTBSCert) } return nil }