Skip to content

Commit

Permalink
Create profiles integration test (#8003)
Browse files Browse the repository at this point in the history
This wasn't previously possible because eggsampler/acme didn't support
profiles until late last week.
  • Loading branch information
aarongable authored Feb 11, 2025
1 parent 3e4bc16 commit 63a0e50
Show file tree
Hide file tree
Showing 18 changed files with 127 additions and 55 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.27.43
github.com/aws/aws-sdk-go-v2/service/s3 v1.65.3
github.com/aws/smithy-go v1.22.0
github.com/eggsampler/acme/v3 v3.6.1
github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941
github.com/go-jose/go-jose/v4 v4.0.1
github.com/go-logr/stdr v1.2.2
github.com/go-sql-driver/mysql v1.5.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/eggsampler/acme/v3 v3.6.1 h1:MPGfIpvSSnsS318quL+25m5dDpV0xyd6cZ98TZvHCM0=
github.com/eggsampler/acme/v3 v3.6.1/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo=
github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941 h1:CnQwymLMJ3MSfjbZQ/bpaLfuXBZuM3LUgAHJ0gO/7d8=
github.com/eggsampler/acme/v3 v3.6.2-0.20250208073118-0466a0230941/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
Expand Down
8 changes: 4 additions & 4 deletions test/integration/ari_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestARI(t *testing.T) {
// the retry-after header are approximately the right amount of time in the
// future.
name := random_domain()
ir, err := authAndIssue(client, key, []string{name}, true)
ir, err := authAndIssue(client, key, []string{name}, true, "")
test.AssertNotError(t, err, "failed to issue test cert")

cert := ir.certs[0]
Expand All @@ -50,15 +50,15 @@ func TestARI(t *testing.T) {
test.AssertEquals(t, ari.RetryAfter.Sub(time.Now()).Round(time.Hour), 6*time.Hour)

// Make a new order which indicates that it replaces the cert issued above.
_, order, err := makeClientAndOrder(client, key, []string{name}, true, cert)
_, order, err := makeClientAndOrder(client, key, []string{name}, true, "", cert)
test.AssertNotError(t, err, "failed to issue test cert")
replaceID, err := acme.GenerateARICertID(cert)
test.AssertNotError(t, err, "failed to generate ARI certID")
test.AssertEquals(t, order.Replaces, replaceID)
test.AssertNotEquals(t, order.Replaces, "")

// Try it again and verify it fails
_, order, err = makeClientAndOrder(client, key, []string{name}, true, cert)
_, order, err = makeClientAndOrder(client, key, []string{name}, true, "", cert)
test.AssertError(t, err, "subsequent ARI replacements for a replaced cert should fail, but didn't")

// Revoke the cert and re-request ARI. The renewal window should now be in
Expand All @@ -78,7 +78,7 @@ func TestARI(t *testing.T) {
name = random_domain()
err = ctAddRejectHost(name)
test.AssertNotError(t, err, "failed to add ct-test-srv reject host")
_, err = authAndIssue(client, key, []string{name}, true)
_, err = authAndIssue(client, key, []string{name}, true, "")
test.AssertError(t, err, "expected error from authAndIssue, was nil")

cert, err = ctFindRejection([]string{name})
Expand Down
2 changes: 1 addition & 1 deletion test/integration/authz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestValidAuthzExpires(t *testing.T) {

// Issue for a random domain
domains := []string{random_domain()}
result, err := authAndIssue(c, nil, domains, true)
result, err := authAndIssue(c, nil, domains, true, "")
// There should be no error
test.AssertNotError(t, err, "authAndIssue failed")
// The order should be valid
Expand Down
8 changes: 4 additions & 4 deletions test/integration/cert_storage_failed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestIssuanceCertStorageFailed(t *testing.T) {
// ---- Test revocation by serial ----
revokeMeDomain := "revokeme.wantserror.com"
// This should fail because the trigger prevented setting the certificate status to "ready"
_, err = authAndIssue(nil, certKey, []string{revokeMeDomain}, true)
_, err = authAndIssue(nil, certKey, []string{revokeMeDomain}, true, "")
test.AssertError(t, err, "expected authAndIssue to fail")

cert, err := getPrecertByName(db, revokeMeDomain)
Expand All @@ -164,7 +164,7 @@ func TestIssuanceCertStorageFailed(t *testing.T) {
// ---- Test revocation by key ----
blockMyKeyDomain := "blockmykey.wantserror.com"
// This should fail because the trigger prevented setting the certificate status to "ready"
_, err = authAndIssue(nil, certKey, []string{blockMyKeyDomain}, true)
_, err = authAndIssue(nil, certKey, []string{blockMyKeyDomain}, true, "")
test.AssertError(t, err, "expected authAndIssue to fail")

cert, err = getPrecertByName(db, blockMyKeyDomain)
Expand All @@ -177,7 +177,7 @@ func TestIssuanceCertStorageFailed(t *testing.T) {
// with the same key, then revoking that certificate for keyCompromise.
revokeClient, err := makeClient()
test.AssertNotError(t, err, "creating second acme client")
res, err := authAndIssue(nil, certKey, []string{random_domain()}, true)
res, err := authAndIssue(nil, certKey, []string{random_domain()}, true, "")
test.AssertNotError(t, err, "issuing second cert")

successfulCert := res.certs[0]
Expand All @@ -200,7 +200,7 @@ func TestIssuanceCertStorageFailed(t *testing.T) {
test.AssertNotError(t, err, "expected status to eventually become revoked")

// Try to issue again with the same key, expecting an error because of the key is blocked.
_, err = authAndIssue(nil, certKey, []string{"123.example.com"}, true)
_, err = authAndIssue(nil, certKey, []string{"123.example.com"}, true, "")
test.AssertError(t, err, "expected authAndIssue to fail")
if !strings.Contains(err.Error(), "public key is forbidden") {
t.Errorf("expected issuance to be rejected with a bad pubkey")
Expand Down
12 changes: 6 additions & 6 deletions test/integration/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func delHTTP01Response(token string) error {
return nil
}

func makeClientAndOrder(c *client, csrKey *ecdsa.PrivateKey, domains []string, cn bool, certToReplace *x509.Certificate) (*client, *acme.Order, error) {
func makeClientAndOrder(c *client, csrKey *ecdsa.PrivateKey, domains []string, cn bool, profile string, certToReplace *x509.Certificate) (*client, *acme.Order, error) {
var err error
if c == nil {
c, err = makeClient()
Expand All @@ -104,9 +104,9 @@ func makeClientAndOrder(c *client, csrKey *ecdsa.PrivateKey, domains []string, c
}
var order acme.Order
if certToReplace != nil {
order, err = c.Client.ReplacementOrder(c.Account, certToReplace, ids)
order, err = c.Client.ReplacementOrderExtension(c.Account, certToReplace, ids, acme.OrderExtension{Profile: profile})
} else {
order, err = c.Client.NewOrder(c.Account, ids)
order, err = c.Client.NewOrderExtension(c.Account, ids, acme.OrderExtension{Profile: profile})
}
if err != nil {
return nil, nil, err
Expand Down Expand Up @@ -153,10 +153,10 @@ type issuanceResult struct {
certs []*x509.Certificate
}

func authAndIssue(c *client, csrKey *ecdsa.PrivateKey, domains []string, cn bool) (*issuanceResult, error) {
func authAndIssue(c *client, csrKey *ecdsa.PrivateKey, domains []string, cn bool, profile string) (*issuanceResult, error) {
var err error

c, order, err := makeClientAndOrder(c, csrKey, domains, cn, nil)
c, order, err := makeClientAndOrder(c, csrKey, domains, cn, profile, nil)
if err != nil {
return nil, err
}
Expand All @@ -174,7 +174,7 @@ type issuanceResultAllChains struct {
}

func authAndIssueFetchAllChains(c *client, csrKey *ecdsa.PrivateKey, domains []string, cn bool) (*issuanceResultAllChains, error) {
c, order, err := makeClientAndOrder(c, csrKey, domains, cn, nil)
c, order, err := makeClientAndOrder(c, csrKey, domains, cn, "", nil)
if err != nil {
return nil, err
}
Expand Down
4 changes: 2 additions & 2 deletions test/integration/crl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestCRLPipeline(t *testing.T) {
// Issue a test certificate and save its serial number.
client, err := makeClient()
test.AssertNotError(t, err, "creating acme client")
res, err := authAndIssue(client, nil, []string{random_domain()}, true)
res, err := authAndIssue(client, nil, []string{random_domain()}, true, "")
test.AssertNotError(t, err, "failed to create test certificate")
cert := res.certs[0]
serial := core.SerialToString(cert.SerialNumber)
Expand Down Expand Up @@ -142,7 +142,7 @@ func TestTemporalAndExplicitShardingCoexist(t *testing.T) {
// (until we move `config` to explicit sharding). This means that in the config world,
// this test only handles temporal sharding, but we don't config-gate it because it passes
// in both worlds.
result, err := authAndIssue(client, certKey, []string{random_domain()}, true)
result, err := authAndIssue(client, certKey, []string{random_domain()}, true, "")
if err != nil {
t.Fatalf("authAndIssue: %s", err)
}
Expand Down
6 changes: 3 additions & 3 deletions test/integration/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestTooBigOrderError(t *testing.T) {
domains = append(domains, fmt.Sprintf("%d.example.com", i))
}

_, err := authAndIssue(nil, nil, domains, true)
_, err := authAndIssue(nil, nil, domains, true, "")
test.AssertError(t, err, "authAndIssue failed")

var prob acme.Problem
Expand Down Expand Up @@ -158,7 +158,7 @@ func TestRejectedIdentifier(t *testing.T) {
domains := []string{
"яџ–Х6яяdь}",
}
_, err := authAndIssue(nil, nil, domains, true)
_, err := authAndIssue(nil, nil, domains, true, "")
test.AssertError(t, err, "issuance should fail for one malformed name")
var prob acme.Problem
test.AssertErrorWraps(t, err, &prob)
Expand All @@ -176,7 +176,7 @@ func TestRejectedIdentifier(t *testing.T) {
"яџ–Х6яя",
"яџ–Х6яя`ь",
}
_, err = authAndIssue(nil, nil, domains, true)
_, err = authAndIssue(nil, nil, domains, true, "")
test.AssertError(t, err, "issuance should fail for multiple malformed names")
test.AssertErrorWraps(t, err, &prob)
test.AssertEquals(t, prob.Type, "urn:ietf:params:acme:error:rejectedIdentifier")
Expand Down
53 changes: 50 additions & 3 deletions test/integration/issuance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"fmt"
"testing"

Expand All @@ -31,7 +32,7 @@ func TestCommonNameInCSR(t *testing.T) {
san2 := random_domain()

// Issue a cert. authAndIssue includes the 0th name as the CN by default.
ir, err := authAndIssue(client, key, []string{cn, san1, san2}, true)
ir, err := authAndIssue(client, key, []string{cn, san1, san2}, true, "")
test.AssertNotError(t, err, "failed to issue test cert")
cert := ir.certs[0]

Expand Down Expand Up @@ -62,7 +63,7 @@ func TestFirstCSRSANHoistedToCN(t *testing.T) {
san2 := "b" + random_domain()

// Issue a cert using a CSR with no CN set, and the SANs in *non*-alpha order.
ir, err := authAndIssue(client, key, []string{san2, san1}, false)
ir, err := authAndIssue(client, key, []string{san2, san1}, false, "")
test.AssertNotError(t, err, "failed to issue test cert")
cert := ir.certs[0]

Expand Down Expand Up @@ -92,7 +93,7 @@ func TestCommonNameSANsTooLong(t *testing.T) {
san2 := fmt.Sprintf("thisdomainnameis.morethan64characterslong.forthesakeoftesting.%s", random_domain())

// Issue a cert using a CSR with no CN set.
ir, err := authAndIssue(client, key, []string{san1, san2}, false)
ir, err := authAndIssue(client, key, []string{san1, san2}, false, "")
test.AssertNotError(t, err, "failed to issue test cert")
cert := ir.certs[0]

Expand All @@ -103,3 +104,49 @@ func TestCommonNameSANsTooLong(t *testing.T) {
// Ensure that the CN is empty.
test.AssertEquals(t, cert.Subject.CommonName, "")
}

// TestIssuanceProfiles verifies that profile selection works, and results in
// measurable differences between certificates issued under different profiles.
// It does not test the omission of the keyEncipherment KU, because all of our
// integration test framework assumes ECDSA pubkeys for the sake of speed,
// and ECDSA certs don't get the keyEncipherment KU in either profile.
func TestIssuanceProfiles(t *testing.T) {
t.Parallel()

// Create an account.
client, err := makeClient("mailto:[email protected]")
test.AssertNotError(t, err, "creating acme client")

profiles := client.Directory().Meta.Profiles
if len(profiles) < 2 {
t.Fatal("ACME server not advertising multiple profiles")
}

// Create a private key.
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
test.AssertNotError(t, err, "creating random cert key")

// Create a set of identifiers to request.
names := []string{random_domain()}

// Get one cert for each profile that we know the test server advertises.
res, err := authAndIssue(client, key, names, true, "legacy")
test.AssertNotError(t, err, "failed to issue under legacy profile")
test.AssertEquals(t, res.Order.Profile, "legacy")
legacy := res.certs[0]

res, err = authAndIssue(client, key, names, true, "modern")
test.AssertNotError(t, err, "failed to issue under modern profile")
test.AssertEquals(t, res.Order.Profile, "modern")
modern := res.certs[0]

// Check that each profile worked as expected.
test.AssertEquals(t, legacy.Subject.CommonName, names[0])
test.AssertEquals(t, modern.Subject.CommonName, "")

test.AssertDeepEquals(t, legacy.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth})
test.AssertDeepEquals(t, modern.ExtKeyUsage, []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth})

test.AssertEquals(t, len(legacy.SubjectKeyId), 20)
test.AssertEquals(t, len(modern.SubjectKeyId), 0)
}
4 changes: 2 additions & 2 deletions test/integration/ocsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func TestOCSPBadIssuerCert(t *testing.T) {
func TestOCSPBadSerialPrefix(t *testing.T) {
t.Parallel()
domain := random_domain()
res, err := authAndIssue(nil, nil, []string{domain}, true)
res, err := authAndIssue(nil, nil, []string{domain}, true, "")
if err != nil || len(res.certs) < 1 {
t.Fatal("Failed to issue dummy cert for OCSP testing")
}
Expand Down Expand Up @@ -73,7 +73,7 @@ func TestOCSPRejectedPrecertificate(t *testing.T) {
t.Fatalf("adding ct-test-srv reject host: %s", err)
}

_, err = authAndIssue(nil, nil, []string{domain}, true)
_, err = authAndIssue(nil, nil, []string{domain}, true, "")
if err != nil {
if !strings.Contains(err.Error(), "urn:ietf:params:acme:error:serverInternal") ||
!strings.Contains(err.Error(), "SCT embedding") {
Expand Down
2 changes: 1 addition & 1 deletion test/integration/otel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ func traceIssuingTestCert(t *testing.T) trace.TraceID {
account, err := c.NewAccount(privKey, false, true)
test.AssertNotError(t, err, "newAccount failed")

_, err = authAndIssue(&client{account, c}, nil, domains, true)
_, err = authAndIssue(&client{account, c}, nil, domains, true, "")
test.AssertNotError(t, err, "authAndIssue failed")

return span.SpanContext().TraceID()
Expand Down
4 changes: 2 additions & 2 deletions test/integration/pausing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func TestIdentifiersPausedForAccount(t *testing.T) {
})
test.AssertNotError(t, err, "Failed to pause domain")

_, err = authAndIssue(c, nil, []string{domain}, true)
_, err = authAndIssue(c, nil, []string{domain}, true, "")
test.AssertError(t, err, "Should not be able to issue a certificate for a paused domain")
test.AssertContains(t, err.Error(), "Your account is temporarily prevented from requesting certificates for")
test.AssertContains(t, err.Error(), "https://boulder.service.consul:4003/sfe/v1/unpause?jwt=")
Expand All @@ -75,6 +75,6 @@ func TestIdentifiersPausedForAccount(t *testing.T) {
})
test.AssertNotError(t, err, "Failed to unpause domain")

_, err = authAndIssue(c, nil, []string{domain}, true)
_, err = authAndIssue(c, nil, []string{domain}, true, "")
test.AssertNotError(t, err, "Should be able to issue a certificate for an unpaused domain")
}
14 changes: 7 additions & 7 deletions test/integration/ratelimit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ func TestDuplicateFQDNRateLimit(t *testing.T) {
domain := random_domain()

// The global rate limit for a duplicate certificates is 2 per 3 hours.
_, err := authAndIssue(nil, nil, []string{domain}, true)
_, err := authAndIssue(nil, nil, []string{domain}, true, "")
test.AssertNotError(t, err, "Failed to issue first certificate")

_, err = authAndIssue(nil, nil, []string{domain}, true)
_, err = authAndIssue(nil, nil, []string{domain}, true, "")
test.AssertNotError(t, err, "Failed to issue second certificate")

_, err = authAndIssue(nil, nil, []string{domain}, true)
_, err = authAndIssue(nil, nil, []string{domain}, true, "")
test.AssertError(t, err, "Somehow managed to issue third certificate")

test.AssertContains(t, err.Error(), "too many certificates (2) already issued for this exact set of domains in the last 3h0m0s")
Expand All @@ -39,19 +39,19 @@ func TestCertificatesPerDomain(t *testing.T) {
}

firstSubDomain := randomSubDomain()
_, err := authAndIssue(nil, nil, []string{firstSubDomain}, true)
_, err := authAndIssue(nil, nil, []string{firstSubDomain}, true, "")
test.AssertNotError(t, err, "Failed to issue first certificate")

_, err = authAndIssue(nil, nil, []string{randomSubDomain()}, true)
_, err = authAndIssue(nil, nil, []string{randomSubDomain()}, true, "")
test.AssertNotError(t, err, "Failed to issue second certificate")

_, err = authAndIssue(nil, nil, []string{randomSubDomain()}, true)
_, err = authAndIssue(nil, nil, []string{randomSubDomain()}, true, "")
test.AssertError(t, err, "Somehow managed to issue third certificate")

test.AssertContains(t, err.Error(), fmt.Sprintf("too many certificates (2) already issued for %q in the last 2160h0m0s", randomDomain))

// Issue a certificate for the first subdomain, which should succeed because
// it's a renewal.
_, err = authAndIssue(nil, nil, []string{firstSubDomain}, true)
_, err = authAndIssue(nil, nil, []string{firstSubDomain}, true, "")
test.AssertNotError(t, err, "Failed to issue renewal certificate")
}
Loading

0 comments on commit 63a0e50

Please sign in to comment.