issuance: split linting and issuing (#6788)

It's useful to be able to get a copy of the linting certificate out of
the process, so we can store it in the database for use by processes
that want a certificate-shaped object (for instance, scanning
possibly-issued public keys for a newly discovered weakness in key
generation).

To do this, while still ensuring that it's impossible to issue a
certificate without linting it, return an IssuanceToken from the linting
process. The IssuanceToken has a private field containing the template
that was used for linting. To issue a final certificate, the
IssuanceToken has to be redeemed, and only the template stored in it can
be used.
This commit is contained in:
Jacob Hoffman-Andrews 2023-04-06 13:24:19 -07:00 committed by GitHub
parent d6cd589795
commit b4bdd035ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 158 additions and 37 deletions

View File

@ -319,12 +319,19 @@ func (ca *certificateAuthorityImpl) IssueCertificateForPrecertificate(ctx contex
ca.log.AuditInfof("Signing cert: serial=[%s] regID=[%d] names=[%s] precert=[%s]", ca.log.AuditInfof("Signing cert: serial=[%s] regID=[%d] names=[%s] precert=[%s]",
serialHex, req.RegistrationID, names, hex.EncodeToString(precert.Raw)) serialHex, req.RegistrationID, names, hex.EncodeToString(precert.Raw))
certDER, err := issuer.Issue(issuanceReq) _, issuanceToken, err := issuer.Prepare(issuanceReq)
if err != nil {
ca.log.AuditErrf("Preparing cert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, req.RegistrationID, names, err)
return nil, berrors.InternalServerError("failed to prepare certificate signing: %s", err)
}
certDER, err := issuer.Issue(issuanceToken)
if err != nil { if err != nil {
ca.noteSignError(err) ca.noteSignError(err)
ca.log.AuditErrf("Signing cert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]", ca.log.AuditErrf("Signing cert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, req.RegistrationID, names, err) serialHex, req.RegistrationID, names, err)
return nil, berrors.InternalServerError("failed to sign precertificate: %s", err) return nil, berrors.InternalServerError("failed to sign certificate: %s", err)
} }
ca.signatureCount.With(prometheus.Labels{"purpose": string(certType), "issuer": issuer.Name()}).Inc() ca.signatureCount.With(prometheus.Labels{"purpose": string(certType), "issuer": issuer.Name()}).Inc()
@ -448,7 +455,14 @@ func (ca *certificateAuthorityImpl) issuePrecertificateInner(ctx context.Context
NotAfter: validity.NotAfter, NotAfter: validity.NotAfter,
} }
certDER, err := issuer.Issue(req) _, issuanceToken, err := issuer.Prepare(req)
if err != nil {
ca.log.AuditErrf("Preparing precert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",
serialHex, issueReq.RegistrationID, strings.Join(csr.DNSNames, ", "), err)
return nil, nil, nil, berrors.InternalServerError("failed to prepare precertificate signing: %s", err)
}
certDER, err := issuer.Issue(issuanceToken)
if err != nil { if err != nil {
ca.noteSignError(err) ca.noteSignError(err)
ca.log.AuditErrf("Signing precert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]", ca.log.AuditErrf("Signing precert failed: serial=[%s] regID=[%d] names=[%s] err=[%v]",

View File

@ -19,6 +19,7 @@ import (
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
ct "github.com/google/certificate-transparency-go" ct "github.com/google/certificate-transparency-go"
@ -598,16 +599,30 @@ type IssuanceRequest struct {
SCTList []ct.SignedCertificateTimestamp SCTList []ct.SignedCertificateTimestamp
} }
// Issue generates a certificate from the provided issuance request and // An issuanceToken represents an assertion that Issuer.Lint has generated
// signs it. Before signing the certificate with the issuer's private // a linting certificate for a given input and run the linter over it with no
// key, it is signed using a throwaway key so that it can be linted using // errors. The token may be redeemed (at most once) to sign a certificate or
// zlint. If the linting fails, an error is returned and the certificate // precertificate with the same Issuer's private key, containing the same
// is not signed using the issuer's key. // contents that were linted.
func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) { type issuanceToken struct {
mu sync.Mutex
template *x509.Certificate
pubKey any
// A pointer to the issuer that created this token. This token may only
// be redeemed by the same issuer.
issuer *Issuer
}
// Prepare applies this Issuer's profile to create a template certificate. It
// then generates a linting certificate from that template and runs the linter
// over it. If successful, returns both the linting certificate (which can be
// stored) and an issuanceToken. The issuanceToken can be used to sign a
// matching certificate with this Issuer's private key.
func (i *Issuer) Prepare(req *IssuanceRequest) ([]byte, *issuanceToken, error) {
// check request is valid according to the issuance profile // check request is valid according to the issuance profile
err := i.Profile.requestValid(i.Clk, req) err := i.Profile.requestValid(i.Clk, req)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
// generate template from the issuance profile // generate template from the issuance profile
@ -623,7 +638,7 @@ func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
template.AuthorityKeyId = i.Cert.SubjectKeyId template.AuthorityKeyId = i.Cert.SubjectKeyId
skid, err := generateSKID(req.PublicKey) skid, err := generateSKID(req.PublicKey)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
template.SubjectKeyId = skid template.SubjectKeyId = skid
switch req.PublicKey.(type) { switch req.PublicKey.(type) {
@ -638,7 +653,7 @@ func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
} else if req.SCTList != nil { } else if req.SCTList != nil {
sctListExt, err := generateSCTListExt(req.SCTList) sctListExt, err := generateSCTListExt(req.SCTList)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
template.ExtraExtensions = append(template.ExtraExtensions, sctListExt) template.ExtraExtensions = append(template.ExtraExtensions, sctListExt)
} }
@ -649,12 +664,35 @@ func (i *Issuer) Issue(req *IssuanceRequest) ([]byte, error) {
// check that the tbsCertificate is properly formed by signing it // check that the tbsCertificate is properly formed by signing it
// with a throwaway key and then linting it using zlint // with a throwaway key and then linting it using zlint
err = i.Linter.Check(template, req.PublicKey) lintCertBytes, err := i.Linter.Check(template, req.PublicKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("tbsCertificate linting failed: %w", err) return nil, nil, fmt.Errorf("tbsCertificate linting failed: %w", err)
} }
return x509.CreateCertificate(rand.Reader, template, i.Cert.Certificate, req.PublicKey, i.Signer) token := &issuanceToken{sync.Mutex{}, template, req.PublicKey, i}
return lintCertBytes, token, nil
}
// Issue performs a real issuance using an issuanceToken resulting from a
// previous call to Prepare(). Call this at most once per token. Calls after
// the first will receive an error.
func (i *Issuer) Issue(token *issuanceToken) ([]byte, error) {
if token == nil {
return nil, errors.New("nil issuanceToken")
}
token.mu.Lock()
defer token.mu.Unlock()
if token.template == nil {
return nil, errors.New("issuance token already redeemed")
}
template := token.template
token.template = nil
if token.issuer != i {
return nil, errors.New("tried to redeem issuance token with the wrong issuer")
}
return x509.CreateCertificate(rand.Reader, template, i.Cert.Certificate, token.pubKey, i.Signer)
} }
func ContainsMustStaple(extensions []pkix.Extension) bool { func ContainsMustStaple(extensions []pkix.Extension) bool {

View File

@ -552,7 +552,7 @@ func TestIssue(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed") test.AssertNotError(t, err, "NewIssuer failed")
pk, err := tc.generateFunc() pk, err := tc.generateFunc()
test.AssertNotError(t, err, "failed to generate test key") test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{ lintCertBytes, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(), PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
CommonName: "example.com", CommonName: "example.com",
@ -560,6 +560,10 @@ func TestIssue(t *testing.T) {
NotBefore: fc.Now(), NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second), NotAfter: fc.Now().Add(time.Hour - time.Second),
}) })
test.AssertNotError(t, err, "Prepare failed")
_, err = x509.ParseCertificate(lintCertBytes)
test.AssertNotError(t, err, "failed to parse certificate")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed") test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes) cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate") test.AssertNotError(t, err, "failed to parse certificate")
@ -591,14 +595,16 @@ func TestIssueRSA(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed") test.AssertNotError(t, err, "NewIssuer failed")
pk, err := rsa.GenerateKey(rand.Reader, 2048) pk, err := rsa.GenerateKey(rand.Reader, 2048)
test.AssertNotError(t, err, "failed to generate test key") test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{ _, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(), PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"}, DNSNames: []string{"example.com"},
NotBefore: fc.Now(), NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second), NotAfter: fc.Now().Add(time.Hour - time.Second),
}) })
test.AssertNotError(t, err, "Issue failed") test.AssertNotError(t, err, "failed to parse lint certificate")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "failed to parse certificate")
cert, err := x509.ParseCertificate(certBytes) cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate") test.AssertNotError(t, err, "failed to parse certificate")
err = cert.CheckSignatureFrom(issuerCert.Certificate) err = cert.CheckSignatureFrom(issuerCert.Certificate)
@ -635,18 +641,22 @@ func TestIssueCommonName(t *testing.T) {
NotAfter: fc.Now().Add(time.Hour - time.Second), NotAfter: fc.Now().Add(time.Hour - time.Second),
} }
certBytes, err := signer.Issue(ir) _, issuanceToken, err := signer.Prepare(ir)
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed") test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes) cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate") test.AssertNotError(t, err, "failed to parse certificate")
test.AssertEquals(t, cert.Subject.CommonName, "example.com") test.AssertEquals(t, cert.Subject.CommonName, "example.com")
signer.Profile.allowCommonName = false signer.Profile.allowCommonName = false
_, err = signer.Issue(ir) _, _, err = signer.Prepare(ir)
test.AssertError(t, err, "Issue should have failed") test.AssertError(t, err, "Prepare should have failed")
ir.CommonName = "" ir.CommonName = ""
certBytes, err = signer.Issue(ir) _, issuanceToken, err = signer.Prepare(ir)
test.AssertNotError(t, err, "Prepare failed")
certBytes, err = signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed") test.AssertNotError(t, err, "Issue failed")
cert, err = x509.ParseCertificate(certBytes) cert, err = x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate") test.AssertNotError(t, err, "failed to parse certificate")
@ -670,7 +680,7 @@ func TestIssueCTPoison(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed") test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key") test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{ _, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(), PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"}, DNSNames: []string{"example.com"},
@ -678,6 +688,8 @@ func TestIssueCTPoison(t *testing.T) {
NotBefore: fc.Now(), NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second), NotAfter: fc.Now().Add(time.Hour - time.Second),
}) })
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed") test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes) cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate") test.AssertNotError(t, err, "failed to parse certificate")
@ -708,7 +720,7 @@ func TestIssueSCTList(t *testing.T) {
test.AssertNotError(t, err, "failed to decode ct log ID") test.AssertNotError(t, err, "failed to decode ct log ID")
logID2, err := base64.StdEncoding.DecodeString("UtToynGEyMkkXDMQei8Ll54oMwWHI0IieDEKs12/Td4=") logID2, err := base64.StdEncoding.DecodeString("UtToynGEyMkkXDMQei8Ll54oMwWHI0IieDEKs12/Td4=")
test.AssertNotError(t, err, "failed to decode ct log ID") test.AssertNotError(t, err, "failed to decode ct log ID")
certBytes, err := signer.Issue(&IssuanceRequest{ _, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(), PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"}, DNSNames: []string{"example.com"},
@ -725,6 +737,8 @@ func TestIssueSCTList(t *testing.T) {
NotBefore: fc.Now(), NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second), NotAfter: fc.Now().Add(time.Hour - time.Second),
}) })
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed") test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes) cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate") test.AssertNotError(t, err, "failed to parse certificate")
@ -762,7 +776,7 @@ func TestIssueMustStaple(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed") test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key") test.AssertNotError(t, err, "failed to generate test key")
certBytes, err := signer.Issue(&IssuanceRequest{ _, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(), PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"}, DNSNames: []string{"example.com"},
@ -770,6 +784,8 @@ func TestIssueMustStaple(t *testing.T) {
NotBefore: fc.Now(), NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second), NotAfter: fc.Now().Add(time.Hour - time.Second),
}) })
test.AssertNotError(t, err, "Prepare failed")
certBytes, err := signer.Issue(issuanceToken)
test.AssertNotError(t, err, "Issue failed") test.AssertNotError(t, err, "Issue failed")
cert, err := x509.ParseCertificate(certBytes) cert, err := x509.ParseCertificate(certBytes)
test.AssertNotError(t, err, "failed to parse certificate") test.AssertNotError(t, err, "failed to parse certificate")
@ -790,14 +806,14 @@ func TestIssueBadLint(t *testing.T) {
test.AssertNotError(t, err, "NewIssuer failed") test.AssertNotError(t, err, "NewIssuer failed")
pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "failed to generate test key") test.AssertNotError(t, err, "failed to generate test key")
_, err = signer.Issue(&IssuanceRequest{ _, _, err = signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(), PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"}, DNSNames: []string{"example.com"},
NotBefore: fc.Now(), NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second), NotAfter: fc.Now().Add(time.Hour - time.Second),
}) })
test.AssertError(t, err, "Issue didn't fail") test.AssertError(t, err, "Prepare didn't fail")
test.AssertContains(t, err.Error(), "tbsCertificate linting failed: failed lints") test.AssertContains(t, err.Error(), "tbsCertificate linting failed: failed lints")
} }
@ -856,3 +872,50 @@ func TestLoadChain_InvalidSig(t *testing.T) {
test.Assert(t, strings.Contains(err.Error(), "signature from \"CN=happy hacker fake CA\""), test.Assert(t, strings.Contains(err.Error(), "signature from \"CN=happy hacker fake CA\""),
fmt.Sprintf("Expected error to mention subject, got: %s", err)) fmt.Sprintf("Expected error to mention subject, got: %s", err))
} }
func TestIssuanceToken(t *testing.T) {
fc := clock.NewFake()
linter, err := linter.New(issuerCert.Certificate, issuerSigner, []string{})
test.AssertNotError(t, err, "failed to create linter")
signer, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
test.AssertNotError(t, err, "NewIssuer failed")
_, err = signer.Issue(&issuanceToken{})
test.AssertError(t, err, "expected issuance with a zero token to fail")
_, err = signer.Issue(nil)
test.AssertError(t, err, "expected issuance with a nil token to fail")
pk, err := rsa.GenerateKey(rand.Reader, 2048)
test.AssertNotError(t, err, "failed to generate test key")
_, issuanceToken, err := signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "expected Prepare to succeed")
_, err = signer.Issue(issuanceToken)
test.AssertNotError(t, err, "expected first issuance to succeed")
_, err = signer.Issue(issuanceToken)
test.AssertError(t, err, "expected second issuance with the same issuance token to fail")
test.AssertContains(t, err.Error(), "issuance token already redeemed")
_, issuanceToken, err = signer.Prepare(&IssuanceRequest{
PublicKey: pk.Public(),
Serial: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9},
DNSNames: []string{"example.com"},
NotBefore: fc.Now(),
NotAfter: fc.Now().Add(time.Hour - time.Second),
})
test.AssertNotError(t, err, "expected Prepare to succeed")
signer2, err := NewIssuer(issuerCert, issuerSigner, defaultProfile(), linter, fc)
test.AssertNotError(t, err, "NewIssuer failed")
_, err = signer2.Issue(issuanceToken)
test.AssertError(t, err, "expected redeeming an issuance token with the wrong issuer to fail")
test.AssertContains(t, err.Error(), "wrong issuer")
}

View File

@ -33,7 +33,8 @@ func Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, realIssuer *x5
if err != nil { if err != nil {
return err return err
} }
return linter.Check(tbs, subjectPubKey) _, err = linter.Check(tbs, subjectPubKey)
return err
} }
// Linter is capable of linting a to-be-signed (TBS) certificate. It does so by // Linter is capable of linting a to-be-signed (TBS) certificate. It does so by
@ -69,14 +70,19 @@ func New(realIssuer *x509.Certificate, realSigner crypto.Signer, skipLints []str
// Check signs the given TBS certificate using the Linter's fake issuer cert and // Check signs the given TBS certificate using the Linter's fake issuer cert and
// private key, then runs the resulting certificate through all non-filtered // private key, then runs the resulting certificate through all non-filtered
// lints. It returns an error if any lint fails. // lints. It returns an error if any lint fails. On success it also returns the
func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey) error { // DER bytes of the linting certificate.
cert, err := makeLintCert(tbs, subjectPubKey, l.issuer, l.signer) func (l Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey) ([]byte, error) {
lintCertBytes, cert, err := makeLintCert(tbs, subjectPubKey, l.issuer, l.signer)
if err != nil { if err != nil {
return err return nil, err
} }
lintRes := zlint.LintCertificateEx(cert, l.registry) lintRes := zlint.LintCertificateEx(cert, l.registry)
return ProcessResultSet(lintRes) err = ProcessResultSet(lintRes)
if err != nil {
return nil, err
}
return lintCertBytes, nil
} }
// CheckCRL signs the given RevocationList template using the Linter's fake // CheckCRL signs the given RevocationList template using the Linter's fake
@ -178,16 +184,16 @@ func makeRegistry(skipLints []string) (lint.Registry, error) {
return reg, nil return reg, nil
} }
func makeLintCert(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, issuer *x509.Certificate, signer crypto.Signer) (*zlintx509.Certificate, error) { func makeLintCert(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, issuer *x509.Certificate, signer crypto.Signer) ([]byte, *zlintx509.Certificate, error) {
lintCertBytes, err := x509.CreateCertificate(rand.Reader, tbs, issuer, subjectPubKey, signer) lintCertBytes, err := x509.CreateCertificate(rand.Reader, tbs, issuer, subjectPubKey, signer)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create lint certificate: %w", err) return nil, nil, fmt.Errorf("failed to create lint certificate: %w", err)
} }
lintCert, err := zlintx509.ParseCertificate(lintCertBytes) lintCert, err := zlintx509.ParseCertificate(lintCertBytes)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse lint certificate: %w", err) return nil, nil, fmt.Errorf("failed to parse lint certificate: %w", err)
} }
return lintCert, nil return lintCertBytes, lintCert, nil
} }
func ProcessResultSet(lintRes *zlint.ResultSet) error { func ProcessResultSet(lintRes *zlint.ResultSet) error {