Skip to content

Commit

Permalink
cert-submitter: allow custom base domain. (#106)
Browse files Browse the repository at this point in the history
Previously the base domain for all the test certificate subjects was
a static string specific to Let's Encrypt. Allowing this base domain to
be configured in the cert submitter config will help outside
organizations use this tool with their own logs.

Resolves #105
  • Loading branch information
Daniel McCarney authored and jsha committed Jul 22, 2019
1 parent 99c97a0 commit b82c4b3
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 16 deletions.
3 changes: 2 additions & 1 deletion examples/config.dist.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"interval": "600s",
"timeout": "500s",
"certIssuerKeyPath": "/etc/ct-woodpecker/issuer.key",
"certIssuerPath": "/etc/ct-woodpecker/issuer.pem"
"certIssuerPath": "/etc/ct-woodpecker/issuer.pem",
"baseDomain": ".example.com"
},
"inclusionConfig": {
"interval": "60s",
Expand Down
22 changes: 21 additions & 1 deletion monitor/cert_submitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ type SubmitterOptions struct {
// be constrained within the provided window.
WindowStart *time.Time
WindowEnd *time.Time
// BaseDomain is the domain suffix used for the subject common name of
// submitted certificates in combination with the first bytes of the
// random certificate serial. If empty the default value
// `.woodpecker.testing.letsencrypt.org` is used. The BaseDomain must begin
// with a '.' character.
BaseDomain string
}

// Valid checks that the SubmitterOptions has a valid positive interval and that
Expand All @@ -114,6 +120,10 @@ func (o SubmitterOptions) Valid() error {
return errors.New("WindowEnd must be after WindowStart")
}

if o.BaseDomain != "" && o.BaseDomain[0] != '.' {
return errors.New("BaseDomain must start with a '.' character")
}

return nil
}

Expand Down Expand Up @@ -146,6 +156,9 @@ type certSubmitter struct {
// window.
windowStart *time.Time
windowEnd *time.Time
// Base domain that used as the suffix for the submitted certificate subject
// common name. If empty a default domain suffix is used.
baseDomain string
}

func newCertSubmitter(
Expand All @@ -168,6 +181,7 @@ func newCertSubmitter(
resubmitIncluded: opts.ResubmitIncluded,
windowStart: opts.WindowStart,
windowEnd: opts.WindowEnd,
baseDomain: opts.BaseDomain,
}
}

Expand Down Expand Up @@ -204,7 +218,13 @@ func (c *certSubmitter) submitCertificates() {
panic("certSubmitter created with nil certIssuerKey or certIssuer\n")
}

certPair, err := pki.IssueTestCertificate(c.certIssuerKey, c.certIssuer, c.clk, c.windowStart, c.windowEnd)
certPair, err := pki.IssueTestCertificate(
c.baseDomain,
c.certIssuerKey,
c.certIssuer,
c.clk,
c.windowStart,
c.windowEnd)
if err != nil {
// This should not occur and if it does we should abort hard
panic(fmt.Sprintf("!!! Error issuing certificate: %s\n", err.Error()))
Expand Down
25 changes: 25 additions & 0 deletions monitor/cert_submitter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,31 @@ func TestSubmitterOptions(t *testing.T) {
WindowEnd: &goodEnd,
},
},
{
Name: "Invalid base domain",
Opts: SubmitterOptions{
Interval: time.Second * 10,
Timeout: time.Second * 10,
IssuerKey: k,
IssuerCert: cert,
WindowStart: &goodStart,
WindowEnd: &goodEnd,
BaseDomain: "aaaa",
},
ExpectedError: "BaseDomain must start with a '.' character",
},
{
Name: "Good base domain",
Opts: SubmitterOptions{
Interval: time.Second * 10,
Timeout: time.Second * 10,
IssuerKey: k,
IssuerCert: cert,
WindowStart: &goodStart,
WindowEnd: &goodEnd,
BaseDomain: ".my.base.domain.example.com",
},
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
Expand Down
25 changes: 18 additions & 7 deletions pki/certs.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ import (
)

const (
// Domain suffix for the subject common name of certificates generated for submission
// to logs. The prefix will be generated randomly from the certificate serial number.
testCertDomain = ".woodpecker.testing.letsencrypt.org"
// defaultTestCertDomain is the Domain suffix for the subject common name of
// certificates generated for submission to logs unless customized in
// ct-woodpecker config. The prefix will be generated randomly from the
// certificate serial number.
defaultTestCertDomain = ".woodpecker.testing.letsencrypt.org"
)

var (
errNilSubjKey = errors.New("cannot IssueCertificate with nil subjectKey")
errNilIssuerKey = errors.New("cannot IssueCertificate with nil issuerKey")
errNilIssuerCert = errors.New("cannot IssueCertificate with nil issuerCert")
errNilTemplate = errors.New("cannot IssueCertificate with nil template")
errBadBaseDomain = errors.New("baseDomain must start with '.' to be used as a domain prefix")

ctPoisonExtensionID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 3}
ctPoisonExtension = pkix.Extension{
Expand Down Expand Up @@ -101,7 +104,8 @@ type CertificatePair struct {
// IssueTestCertificate uses the monitor's certIssuer and certIssuerKey to generate
// a precertificate and a matching final leaf-certificate that can be submitted
// to a log. The certificate's subject common name will be a random subdomain
// based on the certificate serial under the `testCertDomain` domain.
// based on the certificate serial under the provided baseDomain (or
// defaultTestCertDomain domain if baseDomain is empty).
//
// If windowStart is nil the certificate NotBefore will be set to the current
// time based on the provided clock. If windowStart is not nil then the
Expand All @@ -115,11 +119,18 @@ type CertificatePair struct {
// so while they are not issued by a trusted root we try to avoid cablint
// errors to avoid requiring log monitors special-case our submissions.
func IssueTestCertificate(
baseDomain string,
issuerKey *ecdsa.PrivateKey,
issuerCert *x509.Certificate,
clk clock.Clock,
windowStart *time.Time,
windowEnd *time.Time) (CertificatePair, error) {
if baseDomain == "" {
baseDomain = defaultTestCertDomain
}
if baseDomain[0] != '.' {
return CertificatePair{}, errBadBaseDomain
}

certKey, err := RandKey()
if err != nil {
Expand All @@ -142,7 +153,7 @@ func IssueTestCertificate(
latest.AddDate(0, 0, -1)
}

domain := hex.EncodeToString(serial.Bytes()[:5]) + testCertDomain
domain := hex.EncodeToString(serial.Bytes()[:5]) + baseDomain

issueLeafCert := func(precert bool) (*x509.Certificate, error) {
tmpl := &x509.Certificate{
Expand All @@ -157,8 +168,8 @@ func IssueTestCertificate(
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: false,
IssuingCertificateURL: []string{"http://issuer" + testCertDomain},
CRLDistributionPoints: []string{"http://crls" + testCertDomain},
IssuingCertificateURL: []string{"http://issuer" + baseDomain},
CRLDistributionPoints: []string{"http://crls" + baseDomain},
}
if precert {
tmpl.ExtraExtensions = []pkix.Extension{ctPoisonExtension}
Expand Down
65 changes: 59 additions & 6 deletions pki/certs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"crypto/ecdsa"
"crypto/x509"
"encoding/hex"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -69,7 +70,7 @@ func TestIssueTestCertificate(t *testing.T) {
issuerCert := &x509.Certificate{}
clk := clock.New()

certPair, err := IssueTestCertificate(issuerKey, issuerCert, clk, nil, nil)
certPair, err := IssueTestCertificate("", issuerKey, issuerCert, clk, nil, nil)
if err != nil {
t.Fatalf("unexpected error from IssueTestCertificate: %s", err.Error())
}
Expand All @@ -94,7 +95,7 @@ func TestIssueTestCertificate(t *testing.T) {
t.Errorf("SerialNumbers of CertPair did not match")
}

expectedDomain := hex.EncodeToString(certPair.PreCert.SerialNumber.Bytes()[:5]) + testCertDomain
expectedDomain := hex.EncodeToString(certPair.PreCert.SerialNumber.Bytes()[:5]) + defaultTestCertDomain
if certPair.PreCert.Subject.CommonName != expectedDomain {
t.Errorf("PreCert had wrong CommonName. Expected %q, had %q",
expectedDomain, certPair.PreCert.Subject.CommonName)
Expand Down Expand Up @@ -131,15 +132,15 @@ func TestIssueTestCertificate(t *testing.T) {
t.Errorf("Cert had wrong KeyUsage. Expected %#v, found %#v", x509.KeyUsageDigitalSignature, certPair.Cert.KeyUsage)
}

expectedIssuerURL := "http://issuer" + testCertDomain
expectedIssuerURL := "http://issuer" + defaultTestCertDomain
if len(certPair.PreCert.IssuingCertificateURL) != 1 || certPair.PreCert.IssuingCertificateURL[0] != expectedIssuerURL {
t.Errorf("PreCert had wrong IssuingCertificateURL. Expected [%q], found %#v", expectedIssuerURL, certPair.PreCert.IssuingCertificateURL)
}
if len(certPair.Cert.IssuingCertificateURL) != 1 || certPair.Cert.IssuingCertificateURL[0] != expectedIssuerURL {
t.Errorf("Cert had wrong IssuingCertificateURL. Expected [%q], found %#v", expectedIssuerURL, certPair.Cert.IssuingCertificateURL)
}

expectedCRLURL := "http://crls" + testCertDomain
expectedCRLURL := "http://crls" + defaultTestCertDomain
if len(certPair.PreCert.CRLDistributionPoints) != 1 || certPair.PreCert.CRLDistributionPoints[0] != expectedCRLURL {
t.Errorf("PreCert had wrong CRLDistributionPoints. Expected [%q], found %#v", expectedCRLURL, certPair.PreCert.CRLDistributionPoints)
}
Expand Down Expand Up @@ -170,7 +171,7 @@ func TestIssueTestCertificateWindow(t *testing.T) {
clk := clock.New()

// Issue a cert pair with nil WindowStart and WindowEnd
certPair, err := IssueTestCertificate(issuerKey, issuerCert, clk, nil, nil)
certPair, err := IssueTestCertificate("", issuerKey, issuerCert, clk, nil, nil)
if err != nil {
t.Fatalf("unexpected error from IssueTestCertificate: %s", err.Error())
}
Expand Down Expand Up @@ -217,7 +218,7 @@ func TestIssueTestCertificateWindow(t *testing.T) {
windowEnd, _ := time.Parse(time.RFC3339, "2001-01-01T00:00:00Z")

// Issue a cert pair with specific WindowStart and WindowEnd
certPair, err = IssueTestCertificate(issuerKey, issuerCert, clk, &windowStart, &windowEnd)
certPair, err = IssueTestCertificate("", issuerKey, issuerCert, clk, &windowStart, &windowEnd)
if err != nil {
t.Fatalf("unexpected error from IssueTestCertificate: %s", err.Error())
}
Expand Down Expand Up @@ -256,3 +257,55 @@ func TestIssueTestCertificateWindow(t *testing.T) {
notAfter, expectedEndDate)
}
}

func TestIssueTestCertificateBaseDomain(t *testing.T) {
issuerKey, _ := RandKey()
issuerCert := &x509.Certificate{}
clk := clock.New()

testCases := []struct {
name string
baseName string
expectedErr string
}{
{
name: "invalid base name",
baseName: "example.com",
expectedErr: "baseDomain must start with '.' to be used as a domain prefix",
},
{
name: "default base name",
},
{
name: "custom base name",
baseName: ".custom.example.com",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
certPair, err := IssueTestCertificate(tc.baseName, issuerKey, issuerCert, clk, nil, nil)

if tc.expectedErr == "" && err != nil {
t.Errorf("unexpected error from IssueTestCertificate: %s", err.Error())
} else if tc.expectedErr != "" && err == nil {
t.Errorf("expected err %q got nil", tc.expectedErr)
} else if tc.expectedErr != "" && err != nil {
if actual := err.Error(); actual != tc.expectedErr {
t.Errorf("expected err %q got %q", tc.expectedErr, actual)
}
} else {
expected := tc.baseName
if expected == "" {
expected = defaultTestCertDomain
}
if certPair.Cert == nil {
t.Fatalf("unexpected nil Cert in CertPair returned from IssueTestCertificate")
}
if !strings.HasSuffix(certPair.Cert.Subject.CommonName, expected) {
t.Errorf("expected cert to have subj. CN suffix %q, was %q", expected, certPair.Cert.Subject.CommonName)
}
}
})
}
}
3 changes: 2 additions & 1 deletion test/config/ct-woodpecker.localdev.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"interval": "5s",
"timeout": "5s",
"certIssuerKeyPath": "test/issuer.key",
"certIssuerPath": "test/issuer.pem"
"certIssuerPath": "test/issuer.pem",
"baseDomain": ".local.dev"
},
"inclusionConfig": {
"interval": "30s",
Expand Down

0 comments on commit b82c4b3

Please sign in to comment.