diff --git a/cmd/ceremony/README.md b/cmd/ceremony/README.md index a31cecc6f09..97238b76602 100644 --- a/cmd/ceremony/README.md +++ b/cmd/ceremony/README.md @@ -356,6 +356,7 @@ The certificate profile defines a restricted set of fields that are used to gene | Field | Description | | --- | --- | +| `policy-url` | Required. The URL of a specific subsection of a specific version of our markdown CPS, e.g. `https://github.com/letsencrypt/cp-cps/blob/v6.1/CP-CPS.md#root-ca-certificate-profile`. | | `signature-algorithm` | Specifies the signing algorithm to use, one of `SHA256WithRSA`, `SHA384WithRSA`, `SHA512WithRSA`, `ECDSAWithSHA256`, `ECDSAWithSHA384`, `ECDSAWithSHA512` | | `common-name` | Specifies the subject commonName | | `organization` | Specifies the subject organization | diff --git a/cmd/ceremony/cert.go b/cmd/ceremony/cert.go index e760cde32be..000b4e13bf6 100644 --- a/cmd/ceremony/cert.go +++ b/cmd/ceremony/cert.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "math/big" + "regexp" "slices" "time" ) @@ -20,6 +21,12 @@ type policyInfoConfig struct { // certProfile contains the information required to generate a certificate type certProfile struct { + // PolicyURL is *not* included in the certificate. It is a mandatory pointer + // to the profile documented in our CPS with which this profile complies. + // It must point to a specific subsection of a specific version of the + // markdown source of our CPS. + PolicyURL string `yaml:"policy-url"` + // SignatureAlgorithm should contain one of the allowed signature algorithms // in AllowedSigAlgs SignatureAlgorithm string `yaml:"signature-algorithm"` @@ -84,6 +91,12 @@ const ( requestCert ) +// policyURLRegex matches URLs which point to a specific subsection (see +// trailing fragment) of a specific version (following /blob/) of our markdown +// CPS (which we host at github.com/letsencrypt/cp-cps). +var policyURLRegex = regexp.MustCompile( + `^https://github\.com/letsencrypt/cp-cps/blob/v[0-9]+(\.[0-9]+)+/CP-CPS\.md#[0-9a-zA-Z-]+$`) + // Subject returns a pkix.Name from the appropriate certProfile fields func (profile *certProfile) Subject() pkix.Name { return pkix.Name{ @@ -94,6 +107,13 @@ func (profile *certProfile) Subject() pkix.Name { } func (profile *certProfile) verifyProfile(ct certType) error { + if profile.PolicyURL == "" { + return errors.New("policy-url is required") + } + if !policyURLRegex.MatchString(profile.PolicyURL) { + return fmt.Errorf("policy-url must point to a specific subsection of a specific version of our CPS: %s", policyURLRegex.String()) + } + if ct == requestCert { if profile.NotBefore != "" { return errors.New("not-before cannot be set for a CSR") diff --git a/cmd/ceremony/cert_test.go b/cmd/ceremony/cert_test.go index d3e2905fa36..2fd8f8c11f9 100644 --- a/cmd/ceremony/cert_test.go +++ b/cmd/ceremony/cert_test.go @@ -326,7 +326,7 @@ func TestMakeTemplateRestrictedCrossCertificate(t *testing.T) { } func TestVerifyProfile(t *testing.T) { - for _, tc := range []struct { + for i, tc := range []struct { profile certProfile certType []certType expectedErr string @@ -334,10 +334,25 @@ func TestVerifyProfile(t *testing.T) { { profile: certProfile{}, certType: []certType{intermediateCert, crossCert}, + expectedErr: "policy-url is required", + }, + { + profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/main/CP-CPS.md", + }, + certType: []certType{intermediateCert, crossCert}, + expectedErr: "policy-url must point to a specific subsection of a specific version of our CPS", + }, + { + profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", + }, + certType: []certType{intermediateCert, crossCert}, expectedErr: "not-before is required", }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", }, certType: []certType{intermediateCert, crossCert}, @@ -345,6 +360,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", }, @@ -353,6 +369,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -362,6 +379,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -372,6 +390,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -383,6 +402,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -395,6 +415,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -408,6 +429,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -422,6 +444,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -437,6 +460,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -448,6 +472,7 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", }, certType: []certType{requestCert}, @@ -455,13 +480,15 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ - NotAfter: "a", + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", + NotAfter: "a", }, certType: []certType{requestCert}, expectedErr: "not-after cannot be set for a CSR", }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", SignatureAlgorithm: "a", }, certType: []certType{requestCert}, @@ -469,13 +496,15 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ - CRLURL: "a", + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", + CRLURL: "a", }, certType: []certType{requestCert}, expectedErr: "crl-url cannot be set for a CSR", }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", IssuerURL: "a", }, certType: []certType{requestCert}, @@ -483,13 +512,15 @@ func TestVerifyProfile(t *testing.T) { }, { profile: certProfile{ - Policies: []policyInfoConfig{{OID: "1.2.3"}}, + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", + Policies: []policyInfoConfig{{OID: "1.2.3"}}, }, certType: []certType{requestCert}, expectedErr: "policies cannot be set for a CSR", }, { profile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", KeyUsages: []string{"a"}, }, certType: []certType{requestCert}, @@ -497,14 +528,16 @@ func TestVerifyProfile(t *testing.T) { }, } { for _, ct := range tc.certType { - err := tc.profile.verifyProfile(ct) - if err != nil { - if tc.expectedErr != err.Error() { - t.Fatalf("Expected %q, got %q", tc.expectedErr, err.Error()) + t.Run(fmt.Sprintf("%d/%d", i, ct), func(t *testing.T) { + err := tc.profile.verifyProfile(ct) + if err != nil { + if !strings.Contains(err.Error(), tc.expectedErr) { + t.Errorf("Expected %q, got %q", tc.expectedErr, err.Error()) + } + } else if tc.expectedErr != "" { + t.Errorf("verifyProfile didn't fail, expected %q", tc.expectedErr) } - } else if tc.expectedErr != "" { - t.Fatalf("verifyProfile didn't fail, expected %q", tc.expectedErr) - } + }) } } } diff --git a/cmd/ceremony/main_test.go b/cmd/ceremony/main_test.go index a275f0c6c95..899cb2909cc 100644 --- a/cmd/ceremony/main_test.go +++ b/cmd/ceremony/main_test.go @@ -220,7 +220,7 @@ func TestRootConfigValidate(t *testing.T) { expectedError: "outputs.certificate-path is required", }, { - name: "bad certificate-profile", + name: "no certificate-profile", config: rootConfig{ PKCS11: PKCS11KeyGenConfig{ Module: "module", @@ -238,7 +238,7 @@ func TestRootConfigValidate(t *testing.T) { CertificatePath: "path", }, }, - expectedError: "not-before is required", + expectedError: "policy-url is required", }, { name: "good config", @@ -259,6 +259,7 @@ func TestRootConfigValidate(t *testing.T) { CertificatePath: "path", }, CertProfile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -356,7 +357,7 @@ func TestIntermediateConfigValidate(t *testing.T) { expectedError: "outputs.certificate-path is required", }, { - name: "bad certificate-profile", + name: "no certificate-profile", config: intermediateConfig{ PKCS11: PKCS11SigningConfig{ Module: "module", @@ -375,7 +376,41 @@ func TestIntermediateConfigValidate(t *testing.T) { CertificatePath: "path", }, }, - expectedError: "not-before is required", + expectedError: "policy-url is required", + }, + { + name: "no policy url", + config: intermediateConfig{ + PKCS11: PKCS11SigningConfig{ + Module: "module", + SigningLabel: "label", + }, + Inputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + IssuerCertificatePath string `yaml:"issuer-certificate-path"` + }{ + PublicKeyPath: "path", + IssuerCertificatePath: "path", + }, + Outputs: struct { + CertificatePath string `yaml:"certificate-path"` + }{ + CertificatePath: "path", + }, + CertProfile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + Organization: "e", + Country: "f", + CRLURL: "h", + IssuerURL: "i", + Policies: []policyInfoConfig{{OID: "2.23.140.1.2.1"}}, + }, + SkipLints: []string{}, + }, + expectedError: "policy-url is required", }, { name: "too many policy OIDs", @@ -397,6 +432,7 @@ func TestIntermediateConfigValidate(t *testing.T) { CertificatePath: "path", }, CertProfile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -431,6 +467,7 @@ func TestIntermediateConfigValidate(t *testing.T) { CertificatePath: "path", }, CertProfile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -465,6 +502,7 @@ func TestIntermediateConfigValidate(t *testing.T) { CertificatePath: "path", }, CertProfile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -577,7 +615,31 @@ func TestCrossCertConfigValidate(t *testing.T) { expectedError: "outputs.certificate-path is required", }, { - name: "bad certificate-profile", + name: "no certificate-profile", + config: crossCertConfig{ + PKCS11: PKCS11SigningConfig{ + Module: "module", + SigningLabel: "label", + }, + Inputs: struct { + PublicKeyPath string `yaml:"public-key-path"` + IssuerCertificatePath string `yaml:"issuer-certificate-path"` + CertificateToCrossSignPath string `yaml:"certificate-to-cross-sign-path"` + }{ + PublicKeyPath: "path", + IssuerCertificatePath: "path", + CertificateToCrossSignPath: "path", + }, + Outputs: struct { + CertificatePath string `yaml:"certificate-path"` + }{ + CertificatePath: "path", + }, + }, + expectedError: "policy-url is required", + }, + { + name: "no policy url", config: crossCertConfig{ PKCS11: PKCS11SigningConfig{ Module: "module", @@ -597,8 +659,20 @@ func TestCrossCertConfigValidate(t *testing.T) { }{ CertificatePath: "path", }, + CertProfile: certProfile{ + NotBefore: "a", + NotAfter: "b", + SignatureAlgorithm: "c", + CommonName: "d", + Organization: "e", + Country: "f", + CRLURL: "h", + IssuerURL: "i", + Policies: []policyInfoConfig{{OID: "2.23.140.1.2.1"}}, + }, + SkipLints: []string{}, }, - expectedError: "not-before is required", + expectedError: "policy-url is required", }, { name: "too many policy OIDs", @@ -622,6 +696,7 @@ func TestCrossCertConfigValidate(t *testing.T) { CertificatePath: "path", }, CertProfile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -658,6 +733,7 @@ func TestCrossCertConfigValidate(t *testing.T) { CertificatePath: "path", }, CertProfile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -694,6 +770,7 @@ func TestCrossCertConfigValidate(t *testing.T) { CertificatePath: "path", }, CertProfile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", NotBefore: "a", NotAfter: "b", SignatureAlgorithm: "c", @@ -766,7 +843,7 @@ func TestCSRConfigValidate(t *testing.T) { expectedError: "outputs.csr-path is required", }, { - name: "bad certificate-profile", + name: "no certificate-profile", config: csrConfig{ PKCS11: PKCS11SigningConfig{ Module: "module", @@ -783,7 +860,7 @@ func TestCSRConfigValidate(t *testing.T) { CSRPath: "path", }, }, - expectedError: "common-name is required", + expectedError: "policy-url is required", }, { name: "good config", @@ -803,6 +880,7 @@ func TestCSRConfigValidate(t *testing.T) { CSRPath: "path", }, CertProfile: certProfile{ + PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection", CommonName: "d", Organization: "e", Country: "f", diff --git a/test/certs/intermediate-cert-ceremony-ecdsa-cross.yaml b/test/certs/intermediate-cert-ceremony-ecdsa-cross.yaml index 5da8f6ab52d..02e3be77319 100644 --- a/test/certs/intermediate-cert-ceremony-ecdsa-cross.yaml +++ b/test/certs/intermediate-cert-ceremony-ecdsa-cross.yaml @@ -11,6 +11,7 @@ inputs: outputs: certificate-path: test/certs/webpki/{{ .FileName }}-cross.cert.pem certificate-profile: + policy-url: https://github.com/letsencrypt/cp-cps/blob/v6.1/CP-CPS.md#cross-certified-subordinate-ca-certificate-profile signature-algorithm: SHA256WithRSA common-name: {{ .CommonName }} organization: good guys diff --git a/test/certs/intermediate-cert-ceremony-ecdsa.yaml b/test/certs/intermediate-cert-ceremony-ecdsa.yaml index 28f92400c80..8fe0cd06d80 100644 --- a/test/certs/intermediate-cert-ceremony-ecdsa.yaml +++ b/test/certs/intermediate-cert-ceremony-ecdsa.yaml @@ -10,6 +10,7 @@ inputs: outputs: certificate-path: test/certs/webpki/{{ .FileName }}.cert.pem certificate-profile: + policy-url: https://github.com/letsencrypt/cp-cps/blob/v6.1/CP-CPS.md#tls-subordinate-ca-certificate-profile signature-algorithm: ECDSAWithSHA384 common-name: {{ .CommonName }} organization: good guys diff --git a/test/certs/intermediate-cert-ceremony-rsa.yaml b/test/certs/intermediate-cert-ceremony-rsa.yaml index d216c75e0ef..a3fb85150b5 100644 --- a/test/certs/intermediate-cert-ceremony-rsa.yaml +++ b/test/certs/intermediate-cert-ceremony-rsa.yaml @@ -10,6 +10,7 @@ inputs: outputs: certificate-path: test/certs/webpki/{{ .FileName }}.cert.pem certificate-profile: + policy-url: https://github.com/letsencrypt/cp-cps/blob/v6.1/CP-CPS.md#tls-subordinate-ca-certificate-profile signature-algorithm: SHA256WithRSA common-name: {{ .CommonName }} organization: good guys diff --git a/test/certs/root-ceremony-ecdsa.yaml b/test/certs/root-ceremony-ecdsa.yaml index 573533d481a..b234e18133e 100644 --- a/test/certs/root-ceremony-ecdsa.yaml +++ b/test/certs/root-ceremony-ecdsa.yaml @@ -11,6 +11,7 @@ outputs: public-key-path: test/certs/webpki/root-ecdsa.pubkey.pem certificate-path: test/certs/webpki/root-ecdsa.cert.pem certificate-profile: + policy-url: https://github.com/letsencrypt/cp-cps/blob/v6.1/CP-CPS.md#root-ca-certificate-profile signature-algorithm: ECDSAWithSHA384 common-name: root ecdsa organization: good guys diff --git a/test/certs/root-ceremony-rsa.yaml b/test/certs/root-ceremony-rsa.yaml index 1bc5a323061..a9ce6e8a2e6 100644 --- a/test/certs/root-ceremony-rsa.yaml +++ b/test/certs/root-ceremony-rsa.yaml @@ -11,6 +11,7 @@ outputs: public-key-path: test/certs/webpki/root-rsa.pubkey.pem certificate-path: test/certs/webpki/root-rsa.cert.pem certificate-profile: + policy-url: https://github.com/letsencrypt/cp-cps/blob/v6.1/CP-CPS.md#root-ca-certificate-profile signature-algorithm: SHA256WithRSA common-name: root rsa organization: good guys