Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ type Config struct {
// unnecessary work due to parallel validations, but requires a database
// change to work.
SetAuthzProcessing bool

// UnsignLintCerts controls whether the linting package returns RFC 9925
// Unsigned versions of the linting precerts it checks. This in turn controls
// the the contents of the precertificates table (which actually stores
// linting precerts), saving on storage volume by dropping fake signatures.
UnsignLintCerts bool
}

var fMu = new(sync.RWMutex)
Expand Down
8 changes: 8 additions & 0 deletions linter/linter.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"github.com/zmap/zlint/v3/lint"

"github.com/letsencrypt/boulder/core"
"github.com/letsencrypt/boulder/features"
"github.com/letsencrypt/boulder/unsigned"

_ "github.com/letsencrypt/boulder/linter/lints/cabf_br"
_ "github.com/letsencrypt/boulder/linter/lints/chrome"
Expand Down Expand Up @@ -51,6 +53,9 @@ func Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, realIssuer *x5
return nil, err
}

if features.Get().UnsignLintCerts {
return unsigned.Design(lintCertBytes, false)
}
return lintCertBytes, nil
}

Expand Down Expand Up @@ -135,6 +140,9 @@ func (l *Linter) Check(tbs *x509.Certificate, subjectPubKey crypto.PublicKey, re
return nil, err
}

if features.Get().UnsignLintCerts {
return unsigned.Design(lintCertBytes, false)
}
return lintCertBytes, nil
}

Expand Down
11 changes: 9 additions & 2 deletions test/certs/genmtpki/genmtpki.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"os"
"path"
"time"

"github.com/letsencrypt/boulder/unsigned"
)

func main() {
Expand Down Expand Up @@ -85,7 +87,12 @@ func main2() error {
return err
}

_, err = x509.ParseCertificate(certBytes)
unsignedBytes, err := unsigned.Design(certBytes, true)
if err != nil {
return err
}

_, err = x509.ParseCertificate(unsignedBytes)
if err != nil {
return err
}
Expand All @@ -96,7 +103,7 @@ func main2() error {
}
defer certFile.Close()

err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
err = pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: unsignedBytes})
if err != nil {
return err
}
Expand Down
4 changes: 3 additions & 1 deletion test/config-next/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,9 @@
"goodkey": {},
"ocspLogMaxLength": 4000,
"ctLogListFile": "test/ct-test-srv/log_list.json",
"features": {}
"features": {
"UnsignLintCerts": true
}
},
"pa": {
"challenges": {
Expand Down
109 changes: 109 additions & 0 deletions unsigned/unsigned.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Package unsigned implements RFC 9925 Unsigned X.509 Certificates.
//
// https://datatracker.ietf.org/doc/html/rfc9925
package unsigned

import (
encoding_asn1 "encoding/asn1"
"fmt"

"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
)

// Design parses the input as an X.509 certificate and returns an unsigned certificate
// with the same contents.
//
// RFC 9925 says "Senders SHOULD omit the authority key identifier and issuer alternative
// name extensions", but since we use this for returning lint certificate bytes we leave
// all fields unchanged, including those fields.
//
// If replaceSigAlg is true, Design emits a proper RFC 9925 certificate with the signature
// algorithm id-alg-unsigned in both slots. If false, it retains the original signature
// algorithm and simply truncates the signature. This is convenient for efficiently storing
// lint certificates where we don't care about the signature bytes.
func Design(cert []byte, replaceSigAlg bool) ([]byte, error) {
certificate := cryptobyte.String(cert)

// https://datatracker.ietf.org/doc/html/rfc5280#page-116
//
// Certificate ::= SEQUENCE {
// tbsCertificate TBSCertificate,
// signatureAlgorithm AlgorithmIdentifier,
// signature BIT STRING }
//
// TBSCertificate ::= SEQUENCE {
// version [0] Version DEFAULT v1,
// serialNumber CertificateSerialNumber,
// signature AlgorithmIdentifier,
// ...
var certificateInner cryptobyte.String
if !certificate.ReadASN1(&certificateInner, asn1.SEQUENCE) {
return nil, fmt.Errorf("failed to read outer sequence")
}

var tbsCertificate cryptobyte.String
if !certificateInner.ReadASN1(&tbsCertificate, asn1.SEQUENCE) {
return nil, fmt.Errorf("failed to read tbsCertificate")
}

var versionElement cryptobyte.String
if !tbsCertificate.ReadASN1Element(&versionElement, asn1.Tag(0).Constructed().ContextSpecific()) {
return nil, fmt.Errorf("failed to read version")
}

var serialNumberElement cryptobyte.String
if !tbsCertificate.ReadASN1Element(&serialNumberElement, asn1.INTEGER) {
return nil, fmt.Errorf("failed to read serial number")
}

var innerSignatureAlgorithm cryptobyte.String
if !tbsCertificate.ReadASN1(&innerSignatureAlgorithm, asn1.SEQUENCE) {
return nil, fmt.Errorf("failed to read inner signature field")
}

// back out to the certificate
var signatureAlgorithm cryptobyte.String
if !certificateInner.ReadASN1Element(&signatureAlgorithm, asn1.SEQUENCE) {
return nil, fmt.Errorf("failed to read signatureAlgorithm")
}

var signature cryptobyte.String
if !certificateInner.ReadASN1(&signature, asn1.BIT_STRING) {
return nil, fmt.Errorf("failed to read signature")
}

if !certificate.Empty() {
return nil, fmt.Errorf("extra bytes at end")
}

var b cryptobyte.Builder

idAlgUnsigned := encoding_asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 6, 36}

b.AddASN1(asn1.SEQUENCE, func(outTBS *cryptobyte.Builder) {
outTBS.AddASN1(asn1.SEQUENCE, func(outBytes *cryptobyte.Builder) {
outBytes.AddBytes(versionElement)
outBytes.AddBytes(serialNumberElement)
outBytes.AddASN1(asn1.SEQUENCE, func(algorithmIdentifier *cryptobyte.Builder) {
if replaceSigAlg {
algorithmIdentifier.AddASN1ObjectIdentifier(idAlgUnsigned)
} else {
algorithmIdentifier.AddBytes(innerSignatureAlgorithm)
}
})
outBytes.AddBytes([]byte(tbsCertificate))
})
outTBS.AddASN1(asn1.SEQUENCE, func(algorithmIdentifier *cryptobyte.Builder) {
if replaceSigAlg {
algorithmIdentifier.AddASN1ObjectIdentifier(idAlgUnsigned)
} else {
algorithmIdentifier.AddBytes(innerSignatureAlgorithm)
}
})
// The Certificate's signatureValue field MUST be a BIT STRING of length zero.
outTBS.AddASN1BitString(nil)
})

return b.Bytes()
}
79 changes: 79 additions & 0 deletions unsigned/unsigned_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package unsigned

import (
"bytes"
"crypto/x509"
"encoding/base64"
"strings"
"testing"
)

func TestDesign(t *testing.T) {
input, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(`
MIIB4TCCAWegAwIBAgIIJuDkMteShO8wCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
bWluaWNhIHJvb3QgY2EgNGU0YjFkMB4XDTI2MDYyOTA1NDUyNloXDTI4MDcyOTA1
NDUyNlowFjEUMBIGA1UEAxMLd2ZlLmJvdWxkZXIwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAATLkY1wBrGl9+jhR4+HSycRv5kvVV8LUO3xY1styu8+q9kaSi03wrdH7LUf
rJkRE6S60XzVXkeqL9N//jOXFsaM9JbsbeHFRoAx+mBEV68Vu69dblxtXIAKNlMM
5dav5XOjeDB2MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBSA+ZfinkHdxJDZuRo1
zJ7mHOmaCDAWBgNVHREEDzANggt3ZmUuYm91bGRlcjAKBggqhkjOPQQDAwNoADBl
AjEApE8cwaAQ6hnGtUM/TWAb54E5/29ZVy5E/UY8mEzoE021pl3tq1fEof5qz5n/
KrL4AjAuEpVOjRrRWWMnRJxd05Pfxq7gZmxgwppjnE9JZ9P6WRP7ZWqZcc9p8YLM
YhKuXQo=`, "\n", ""))
if err != nil {
t.Fatal(err)
}

out, err := Design(input, true)
if err != nil {
t.Fatal(err)
}
expected, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(`
MIIBejCCAWegAwIBAgIIJuDkMteShO8wCgYIKwYBBQUHBiQwIDEeMBwGA1UEAxMV
bWluaWNhIHJvb3QgY2EgNGU0YjFkMB4XDTI2MDYyOTA1NDUyNloXDTI4MDcyOTA1
NDUyNlowFjEUMBIGA1UEAxMLd2ZlLmJvdWxkZXIwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAATLkY1wBrGl9+jhR4+HSycRv5kvVV8LUO3xY1styu8+q9kaSi03wrdH7LUf
rJkRE6S60XzVXkeqL9N//jOXFsaM9JbsbeHFRoAx+mBEV68Vu69dblxtXIAKNlMM
5dav5XOjeDB2MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBSA+ZfinkHdxJDZuRo1
zJ7mHOmaCDAWBgNVHREEDzANggt3ZmUuYm91bGRlcjAKBggrBgEFBQcGJAMBAA==
`, "\n", ""))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(out, expected) {
t.Errorf("got %x, want %x", out, expected)
}

_, err = x509.ParseCertificate(out)
if err != nil {
t.Fatal(err)
}

out, err = Design(input, false)
if err != nil {
t.Fatal(err)
}
expected, err = base64.StdEncoding.DecodeString(strings.ReplaceAll(`
MIIBejCCAWegAwIBAgIIJuDkMteShO8wCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
bWluaWNhIHJvb3QgY2EgNGU0YjFkMB4XDTI2MDYyOTA1NDUyNloXDTI4MDcyOTA1
NDUyNlowFjEUMBIGA1UEAxMLd2ZlLmJvdWxkZXIwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAATLkY1wBrGl9+jhR4+HSycRv5kvVV8LUO3xY1styu8+q9kaSi03wrdH7LUf
rJkRE6S60XzVXkeqL9N//jOXFsaM9JbsbeHFRoAx+mBEV68Vu69dblxtXIAKNlMM
5dav5XOjeDB2MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYI
KwYBBQUHAwIwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBSA+ZfinkHdxJDZuRo1
zJ7mHOmaCDAWBgNVHREEDzANggt3ZmUuYm91bGRlcjAKBggqhkjOPQQDAwMBAA==
`, "\n", ""))
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(out, expected) {
t.Errorf("got %x, want %x", out, expected)
}

_, err = x509.ParseCertificate(out)
if err != nil {
t.Fatal(err)
}
}