Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/ceremony/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
20 changes: 20 additions & 0 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"
"regexp"
"slices"
"time"
)
Expand All @@ -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"`
Expand Down Expand Up @@ -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{
Expand All @@ -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")
Expand Down
55 changes: 44 additions & 11 deletions cmd/ceremony/cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,25 +326,41 @@ 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
}{
{
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},
expectedErr: "not-after is required",
},
{
profile: certProfile{
PolicyURL: "https://github.com/letsencrypt/cp-cps/blob/v0.1/CP-CPS.md#subsection",
NotBefore: "a",
NotAfter: "b",
},
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -448,63 +472,72 @@ 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},
expectedErr: "not-before cannot be set for a CSR",
},
{
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},
expectedErr: "signature-algorithm cannot be set for a CSR",
},
{
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},
expectedErr: "issuer-url cannot be set for a CSR",
},
{
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},
expectedErr: "key-usages cannot be set for a CSR",
},
} {
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)
}
})
}
}
}
Expand Down
Loading