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: 4 additions & 2 deletions cmd/ceremony/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ceremony --config path/to/config.yml
- `root`: generates a signing key on HSM and creates a self-signed root certificate that uses the generated key, outputting a PEM public key, and a PEM certificate. After generating such a root for public trust purposes, it should be submitted to [as many root programs as is possible/practical](https://github.com/daknob/root-programs).
- `intermediate`: creates a intermediate certificate and signs it using a signing key already on a HSM, outputting a PEM certificate
- `cross-csr`: creates a CSR for signing by a third party, outputting a PEM CSR.
- `cross-certificate`: issues a certificate for one root, signed by another root. This is distinct from an intermediate because there is no path length constraint and there are no EKUs.
- `cross-certificate`: issues a certificate for one CA, signed by another CA. Although this does produce a Subordinate CA Certificate (an "intermediate"), this is a distinct ceremony because the resulting certificate must reflect the existing certificate in certain critical ways.
- `key`: generates a signing key on HSM, outputting a PEM public key
- `crl`: creates a CRL with the IDP extension and `onlyContainsCACerts = true` from the provided profile and signs it using a signing key already on a HSM, outputting a PEM CRL

Expand Down Expand Up @@ -200,9 +200,10 @@ certificate-profile:
- Digital Signature
- Cert Sign
- CRL Sign
ekus: server
```

This config generates a cross-sign of the already-created "CA root 2", issued from the similarly-already-created "CA root". The subject key used is taken from `/home/user/root-signing-pub-2.pem`. The EKUs and Subject Key Identifier are taken from `/home/user/root-cert-2-cross.pem`. The issuer is `/home/user/root-cert.pem`, and the Issuer and Authority Key Identifier fields are taken from that cert. The resulting certificate is written to `/home/user/root-cert-2-cross.pem`.
This config generates a cross-sign of the already-created "CA root 2", issued from the similarly-already-created "CA root". The subject key used is taken from `/home/user/root-signing-pub-2.pem`. The Subject Key Identifier is taken from `/home/user/root-cert-2-cross.pem`. The issuer is `/home/user/root-cert.pem`, and the Issuer and Authority Key Identifier fields are taken from that cert. The resulting certificate is written to `/home/user/root-cert-2-cross.pem`.

### Cross-CSR ceremony

Expand Down Expand Up @@ -365,3 +366,4 @@ The certificate profile defines a restricted set of fields that are used to gene
| `issuer-url` | Specifies the AIA caIssuer URL |
| `policies` | Specifies contents of a certificatePolicies extension. Should contain a list of policies with the field `oid`, indicating the policy OID. |
| `key-usages` | Specifies list of key usage bits should be set, list can contain `Digital Signature`, `CRL Sign`, and `Cert Sign` |
| `ekus` | Must be `none`, `server`, or `both`. |
69 changes: 57 additions & 12 deletions cmd/ceremony/cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"io"
"math/big"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -56,6 +57,14 @@ type certProfile struct {

// KeyUsages should contain the set of key usage bits to set
KeyUsages []string `yaml:"key-usages"`

// EKUs must be either "none" (used for self-signed roots), "server" (used
// for modern single-purpose hierarchies), or "both" (used for legacy
// hierarchies with both id-kp-tlsClientAuth and id-kp-tlsServerAuth). If
// empty, defaults to "none" for root ceremonies and to "server" for others.
//
// TODO: Remove this when we no longer issue any tlsClientAuth CA certs.
EKUs string `yaml:"ekus"`
}

// AllowedSigAlgs contains the allowed signature algorithms
Expand Down Expand Up @@ -231,6 +240,49 @@ func makeTemplate(randReader io.Reader, profile *certProfile, pubKey []byte, tbc
return nil, errors.New("at least one key usage must be set")
}

var ekus []x509.ExtKeyUsage
if ct == rootCert {
// rootCert does not get EKU or MaxPathZero.
// BR 7.1.2.1.2 Root CA Extensions
// Extension Presence Critical Description
// extKeyUsage MUST NOT N -
if profile.EKUs != "" && profile.EKUs != "none" {
return nil, fmt.Errorf("root certificates MUST NOT have an EKU extension; profile configured %q", profile.EKUs)
}
} else {
switch profile.EKUs {
case "", "server":
// By default, only include id-kp-tlsServerAuth. This reflects the move
// towards single-purpose hierarchies, as required by the Chrome Root
// Program, among others.
ekus = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
case "both":
// Until June 15, 2026, including both EKUs is acceptable.
Comment thread
aarongable marked this conversation as resolved.
// https://googlechrome.github.io/chromerootprogram/#132-promote-use-of-dedicated-tls-server-authentication-pki-hierarchies
// 1.3.2 Promote use of Dedicated TLS Server Authentication PKI Hierarchies
// ...
// All corresponding unexpired and unrevoked subordinate CA certificates operated beneath an existing root included in the Chrome Root Store MUST:
// if disclosed to the CCADB before June 15, 2026: include the extendedKeyUsage extension and (a) only assert an extendedKeyUsage purpose of id-kp-serverAuth or (b) only assert extendedKeyUsage purposes of id-kp-serverAuth and id-kp-clientAuth.
// ...
//
// Note: this safety check uses on notBefore rather than a disclosure date, so it's imperfect but still useful.
notBefore, err := time.Parse(time.DateTime, profile.NotBefore)
if err != nil {
return nil, fmt.Errorf("parsing notBefore: %s", err)
}
if notBefore.Before(time.Date(2026, 6, 15, 0, 0, 0, 0, time.UTC)) {
ekus = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
} else {
return nil, fmt.Errorf("notBefore of %s is too late for including clientAuth EKU", tbcs.NotAfter.Format(time.RFC3339))
}
default:
return nil, fmt.Errorf("unrecognized EKUs %q; must be 'none', 'server', or 'both'", profile.EKUs)
}
}
if ct == crossCert && len(tbcs.ExtKeyUsage) != 0 && !slices.Equal(ekus, tbcs.ExtKeyUsage) {
return nil, fmt.Errorf("existing cert has EKUs %v, but cross-certificate profile has EKUs %v", tbcs.ExtKeyUsage, ekus)
}

cert := &x509.Certificate{
SerialNumber: big.NewInt(0).SetBytes(serial),
BasicConstraintsValid: true,
Expand All @@ -239,6 +291,7 @@ func makeTemplate(randReader io.Reader, profile *certProfile, pubKey []byte, tbc
CRLDistributionPoints: crlDistributionPoints,
IssuingCertificateURL: issuingCertificateURL,
KeyUsage: ku,
ExtKeyUsage: ekus,
SubjectKeyId: subjectKeyID,
}

Expand All @@ -250,11 +303,11 @@ func makeTemplate(randReader io.Reader, profile *certProfile, pubKey []byte, tbc
cert.SignatureAlgorithm = sigAlg
notBefore, err := time.Parse(time.DateTime, profile.NotBefore)
if err != nil {
return nil, err
return nil, fmt.Errorf("parsing notBefore: %s", err)
}
notAfter, err := time.Parse(time.DateTime, profile.NotAfter)
if err != nil {
return nil, err
return nil, fmt.Errorf("parsing notAfter: %s", err)
}
validity := notAfter.Add(time.Second).Sub(notBefore)
if ct == rootCert && validity >= 9132*24*time.Hour {
Expand All @@ -273,19 +326,11 @@ func makeTemplate(randReader io.Reader, profile *certProfile, pubKey []byte, tbc
}

switch ct {
// rootCert does not get EKU or MaxPathZero.
// BR 7.1.2.1.2 Root CA Extensions
// Extension Presence Critical Description
// extKeyUsage MUST NOT N -
case requestCert, intermediateCert:
// id-kp-serverAuth is included in intermediate certificates, as required by
// Section 7.1.2.10.6 of the CA/BF Baseline Requirements.
// id-kp-clientAuth is excluded, as required by section 3.2.1 of the Chrome
// Root Program Requirements.
cert.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
// Issuing intermediates must always have MaxPathLen 0.
cert.MaxPathLenZero = true
case crossCert:
cert.ExtKeyUsage = tbcs.ExtKeyUsage
// Cross-signs should have the same MaxPathLen as the existing cert.
cert.MaxPathLenZero = tbcs.MaxPathLenZero
// The SKID needs to match the previous SKID, no matter how it was computed.
cert.SubjectKeyId = tbcs.SubjectKeyId
Expand Down
169 changes: 169 additions & 0 deletions cmd/ceremony/cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"fmt"
"io/fs"
"math/big"
"reflect"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -63,6 +65,173 @@ func TestMakeSubject(t *testing.T) {
test.AssertDeepEquals(t, profile.Subject(), expectedSubject)
}

func TestMakeTemplateEnforcesRootNoEKUs(t *testing.T) {
s, ctx := pkcs11helpers.NewSessionWithMock()
randReader := newRandReader(s)
pubKey := samplePubkey()
ctx.GenerateRandomFunc = realRand

workingRootProfile := &certProfile{
EKUs: "",
KeyUsages: []string{"Digital Signature", "CRL Sign"},
SignatureAlgorithm: "ECDSAWithSHA256",
NotBefore: "2026-05-11 00:00:00",
NotAfter: "2026-05-12 00:00:00",
}
_, err := makeTemplate(randReader, workingRootProfile, pubKey, nil, rootCert)
if err != nil {
t.Fatalf("makeTemplate with workingRootProfile: %s", err)
}

workingRootProfile.EKUs = "none"
_, err = makeTemplate(randReader, workingRootProfile, pubKey, nil, rootCert)
if err != nil {
t.Fatalf("makeTemplate with workingRootProfile: %s", err)
}

brokenRootProfile := *workingRootProfile
brokenRootProfile.EKUs = "both"
_, err = makeTemplate(randReader, &brokenRootProfile, pubKey, nil, rootCert)
if err == nil {
t.Errorf("makeTemplate with brokenRootProfile: got nil error, want error")
}

brokenRootProfile = *workingRootProfile
brokenRootProfile.EKUs = "unintelligible"
_, err = makeTemplate(randReader, &brokenRootProfile, pubKey, nil, rootCert)
if err == nil {
t.Errorf("makeTemplate with brokenRootProfile: got nil error, want error")
}
}

func TestMakeTemplateEnforcesCrossCertEKUs(t *testing.T) {
s, ctx := pkcs11helpers.NewSessionWithMock()
randReader := newRandReader(s)
pubKey := samplePubkey()
ctx.GenerateRandomFunc = realRand

tbcsCert := &x509.Certificate{
SerialNumber: big.NewInt(666),
Subject: pkix.Name{
Organization: []string{"While Eek Ayote"},
},
NotBefore: time.Date(2026, 5, 11, 0, 0, 0, 0, time.UTC),
NotAfter: time.Date(2026, 5, 12, 0, 0, 0, 0, time.UTC),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}

crossCertProfile := &certProfile{
EKUs: "",
KeyUsages: []string{"Digital Signature", "CRL Sign"},
SignatureAlgorithm: "ECDSAWithSHA256",
NotBefore: "2026-05-11 00:00:00",
NotAfter: "2026-05-12 00:00:00",
}

template, err := makeTemplate(randReader, crossCertProfile, pubKey, tbcsCert, crossCert)
if err != nil {
t.Fatalf("makeTemplate with crossCertProfile: %s", err)
}
expected := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
if !reflect.DeepEqual(template.ExtKeyUsage, expected) {
t.Errorf("makeTemplate with crossCertProfile: got %v, want %v",
template.ExtKeyUsage, expected)
}

crossCertProfile.EKUs = "server"
_, err = makeTemplate(randReader, crossCertProfile, pubKey, tbcsCert, crossCert)
if err != nil {
t.Fatalf("makeTemplate with crossCertProfile: %s", err)
}
if !reflect.DeepEqual(template.ExtKeyUsage, expected) {
t.Errorf("makeTemplate with crossCertProfile: got %v, want %v",
template.ExtKeyUsage, expected)
}

// This will error because the tbcsCert has [serverAuth], but "both" means [serverAuth, clientAuth] on the cross sign
crossCertProfile.EKUs = "both"
_, err = makeTemplate(randReader, crossCertProfile, pubKey, tbcsCert, crossCert)
if err == nil {
t.Fatalf("makeTemplate with \"both\" and to-be-cross-signed certificate that has \"serverAuth\": got nil error, want error")
}

// Now simulate a to-be-cross-signed certificate that has no EKU constraints.
liberalTBCS := *tbcsCert
liberalTBCS.ExtKeyUsage = nil
crossCertProfile.EKUs = "both"
_, err = makeTemplate(randReader, crossCertProfile, pubKey, &liberalTBCS, crossCert)
if err != nil {
t.Errorf("makeTemplate with \"both\" and liberal to-be-cross-signed certificate: %s", err)
}

// Now check that issuing a "both" cross-sign in 2027 fails.
crossCertProfile.EKUs = "both"
crossCertProfile.NotBefore = "2027-05-11 00:00:00"
crossCertProfile.NotAfter = "2027-05-12 00:00:00"
_, err = makeTemplate(randReader, crossCertProfile, pubKey, &liberalTBCS, crossCert)
if err == nil {
t.Fatalf("makeTemplate with \"both\" and late notBefore: go nil error, want error")
}
if !strings.Contains(err.Error(), "late for including clientAuth EKU") {
t.Errorf("makeTemplate with \"both\" and late notBefore: got error %q, want error with \"late for including clientAuth EKU\"", err)
}
}

func TestMakeTemplateIntermediateEKUs(t *testing.T) {
s, ctx := pkcs11helpers.NewSessionWithMock()
randReader := newRandReader(s)
pubKey := samplePubkey()
ctx.GenerateRandomFunc = realRand

intermediateProfile := &certProfile{
EKUs: "",
KeyUsages: []string{"Digital Signature", "CRL Sign"},
SignatureAlgorithm: "ECDSAWithSHA256",
NotBefore: "2026-05-11 00:00:00",
NotAfter: "2026-05-12 00:00:00",
}

template, err := makeTemplate(randReader, intermediateProfile, pubKey, nil, intermediateCert)
if err != nil {
t.Fatalf("makeTemplate with intermediateProfile: %s", err)
}
expected := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
if !reflect.DeepEqual(template.ExtKeyUsage, expected) {
t.Errorf("makeTemplate with intermediateProfile: got %v, want %v",
template.ExtKeyUsage, expected)
}

intermediateProfile.EKUs = "server"
template, err = makeTemplate(randReader, intermediateProfile, pubKey, nil, intermediateCert)
if err != nil {
t.Fatalf("makeTemplate with intermediateProfile and EKUs: \"server\": %s", err)
}
expected = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
if !reflect.DeepEqual(template.ExtKeyUsage, expected) {
t.Errorf("makeTemplate with intermediateProfile and EKUs: \"server\": got %v, want %v",
template.ExtKeyUsage, expected)
}

intermediateProfile.EKUs = "both"
template, err = makeTemplate(randReader, intermediateProfile, pubKey, nil, intermediateCert)
if err != nil {
t.Fatalf("makeTemplate with intermediateProfile and EKUs: \"both\": %s", err)
}
expected = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}
if !reflect.DeepEqual(template.ExtKeyUsage, expected) {
t.Errorf("makeTemplate with intermediateProfile and EKUs: \"both\": got %v, want %v",
template.ExtKeyUsage, expected)
}

intermediateProfile.EKUs = "unintelligible"
_, err = makeTemplate(randReader, intermediateProfile, pubKey, nil, intermediateCert)
if err == nil {
t.Fatalf("makeTemplate with intermediateProfile and EKUs: \"unintelligible\": got nil error, want error")
}
}

func TestMakeTemplateRoot(t *testing.T) {
s, ctx := pkcs11helpers.NewSessionWithMock()
profile := &certProfile{}
Expand Down
39 changes: 21 additions & 18 deletions cmd/ceremony/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -714,27 +714,30 @@ func crossCertCeremony(configBytes []byte) error {
return fmt.Errorf("cross-signed subordinate CA's NotBefore predates the existing CA's NotBefore")
}
// BR 7.1.2.2.3 Cross-Certified Subordinate CA Extensions
// We want the Extended Key Usages of our cross-signs to be identical to those
// in the cert being cross-signed, for the sake of consistency. However, our
// Root CA Certificates do not contain any EKUs, as required by BR 7.1.2.1.2.
// Therefore, cross-signs of our roots count as "unrestricted" cross-signs per
// the definition in BR 7.1.2.2.3, and are subject to the requirement that
// the cross-sign's Issuer and Subject fields must either:
// - have identical organizationNames; or
// - have orgnaizationNames which are affiliates of each other.
// Therefore, we enforce that cross-signs with empty EKUs have identical
// Subject Organization Name fields... or allow one special case where the
// issuer is "Internet Security Research Group" and the subject is "ISRG" to
// allow us to migrate from the longer string to the shorter one.
if !slices.Equal(lintCert.ExtKeyUsage, toBeCrossSigned.ExtKeyUsage) {
// If the existing cert has Extended Key Usages (i.e. it is an issuing
// intermediate), then we need the EKUs of the cross-sign to be identical.
// However, if the existing cert has no EKUs (i.e. it is a self-signed root),
// then we need the EKUs to be exactly [serverAuth] (a dedicated
// hierarchy) or exactly [serverAuth, clientAuth] (allowed by the Chrome
// Root Program only until 2026-06-15).
Comment thread
aarongable marked this conversation as resolved.
if len(toBeCrossSigned.ExtKeyUsage) == 0 {
if !slices.Equal(lintCert.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}) &&
!slices.Equal(lintCert.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}) {
return fmt.Errorf("lint cert has unacceptable EKUs for cross-signing a root")
}
} else if !slices.Equal(lintCert.ExtKeyUsage, toBeCrossSigned.ExtKeyUsage) {
return fmt.Errorf("lint cert and toBeCrossSigned cert EKUs differ")
}
if len(lintCert.ExtKeyUsage) == 0 {
if !slices.Equal(lintCert.Subject.Organization, issuer.Subject.Organization) &&
!(slices.Equal(issuer.Subject.Organization, []string{"Internet Security Research Group"}) && slices.Equal(lintCert.Subject.Organization, []string{"ISRG"})) {
return fmt.Errorf("attempted unrestricted cross-sign of certificate operated by a different organization")
}

// A lot of additional requirements come into play if we issue a cross-sign to
// an external operator. Ensure that we only ever issue cross-signs to certs
// with the same Subject Organization field, with one exception for migrating
// from the longer "Internet Security Research Group" to the shorter "ISRG".
if !slices.Equal(lintCert.Subject.Organization, issuer.Subject.Organization) &&
!(slices.Equal(issuer.Subject.Organization, []string{"Internet Security Research Group"}) && slices.Equal(lintCert.Subject.Organization, []string{"ISRG"})) {
return fmt.Errorf("attempted unrestricted cross-sign of certificate operated by a different organization")
}

// Issue the cross-signed certificate.
finalCert, err := signAndWriteCert(template, issuer, lintCert, pub, signer, config.Outputs.CertificatePath)
if err != nil {
Expand Down