Skip to content

Commit dc992b8

Browse files
bllfr0gldez
andauthored
feat: add support for Profiles Extension (#2415)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
1 parent 2e497ca commit dc992b8

File tree

12 files changed

+226
-13
lines changed

12 files changed

+226
-13
lines changed

.github/workflows/pr.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ jobs:
4646
golangci-lint --version
4747
4848
- name: Install Pebble
49-
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@3fe019bbc0a41ed16e2fee31592bb91751acaa47
49+
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.7.0
5050

5151
- name: Install challtestsrv
52-
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@3fe019bbc0a41ed16e2fee31592bb91751acaa47
52+
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.7.0
5353

5454
- name: Set up a Memcached server
5555
uses: niden/actions-memcached@v7

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Let's Encrypt client and ACME library written in Go.
1717
- Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension
1818
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses
1919
- Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
20+
- Support [draft-aaron-acme-profiles-00](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/): Profiles Extension
2021
- Register with CA
2122
- Obtain certificates, both from scratch or with an existing CSR
2223
- Renew certificates

acme/api/order.go

+10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import (
1313
type OrderOptions struct {
1414
NotBefore time.Time
1515
NotAfter time.Time
16+
17+
// A string uniquely identifying the profile
18+
// which will be used to affect issuance of the certificate requested by this Order.
19+
// - https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-4
20+
Profile string
21+
1622
// A string uniquely identifying a previously-issued certificate which this
1723
// order is intended to replace.
1824
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
@@ -53,6 +59,10 @@ func (o *OrderService) NewWithOptions(domains []string, opts *OrderOptions) (acm
5359
if o.core.GetDirectory().RenewalInfo != "" {
5460
orderReq.Replaces = opts.ReplacesCertID
5561
}
62+
63+
if opts.Profile != "" {
64+
orderReq.Profile = opts.Profile
65+
}
5666
}
5767

5868
var order acme.Order

acme/commons.go

+11
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ type Meta struct {
7474
// then the CA requires that all new-account requests include an "externalAccountBinding" field
7575
// associating the new account with an external account.
7676
ExternalAccountRequired bool `json:"externalAccountRequired"`
77+
78+
// profiles (optional, object):
79+
// A map of profile names to human-readable descriptions of those profiles.
80+
// https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-3
81+
Profiles map[string]string `json:"profiles"`
7782
}
7883

7984
// ExtendedAccount an extended Account.
@@ -148,6 +153,12 @@ type Order struct {
148153
// An array of identifier objects that the order pertains to.
149154
Identifiers []Identifier `json:"identifiers"`
150155

156+
// profile (string, optional):
157+
// A string uniquely identifying the profile
158+
// which will be used to affect issuance of the certificate requested by this Order.
159+
// https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-4
160+
Profile string `json:"profile,omitempty"`
161+
151162
// notBefore (optional, string):
152163
// The requested value of the notBefore field in the certificate,
153164
// in the date format defined in [RFC3339].

certificate/certificates.go

+27-8
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,18 @@ type ObtainRequest struct {
6969
PrivateKey crypto.PrivateKey
7070
MustStaple bool
7171

72-
NotBefore time.Time
73-
NotAfter time.Time
74-
Bundle bool
75-
PreferredChain string
72+
NotBefore time.Time
73+
NotAfter time.Time
74+
Bundle bool
75+
PreferredChain string
76+
77+
// A string uniquely identifying the profile
78+
// which will be used to affect issuance of the certificate requested by this Order.
79+
// - https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-4
80+
Profile string
81+
7682
AlwaysDeactivateAuthorizations bool
83+
7784
// A string uniquely identifying a previously-issued certificate which this
7885
// order is intended to replace.
7986
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
@@ -89,11 +96,18 @@ type ObtainRequest struct {
8996
type ObtainForCSRRequest struct {
9097
CSR *x509.CertificateRequest
9198

92-
NotBefore time.Time
93-
NotAfter time.Time
94-
Bundle bool
95-
PreferredChain string
99+
NotBefore time.Time
100+
NotAfter time.Time
101+
Bundle bool
102+
PreferredChain string
103+
104+
// A string uniquely identifying the profile
105+
// which will be used to affect issuance of the certificate requested by this Order.
106+
// - https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-4
107+
Profile string
108+
96109
AlwaysDeactivateAuthorizations bool
110+
97111
// A string uniquely identifying a previously-issued certificate which this
98112
// order is intended to replace.
99113
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
@@ -154,6 +168,7 @@ func (c *Certifier) Obtain(request ObtainRequest) (*Resource, error) {
154168
orderOpts := &api.OrderOptions{
155169
NotBefore: request.NotBefore,
156170
NotAfter: request.NotAfter,
171+
Profile: request.Profile,
157172
ReplacesCertID: request.ReplacesCertID,
158173
}
159174

@@ -220,6 +235,7 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
220235
orderOpts := &api.OrderOptions{
221236
NotBefore: request.NotBefore,
222237
NotAfter: request.NotAfter,
238+
Profile: request.Profile,
223239
ReplacesCertID: request.ReplacesCertID,
224240
}
225241

@@ -437,6 +453,7 @@ type RenewOptions struct {
437453
// If true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
438454
Bundle bool
439455
PreferredChain string
456+
Profile string
440457
AlwaysDeactivateAuthorizations bool
441458
// Not supported for CSR request.
442459
MustStaple bool
@@ -505,6 +522,7 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*
505522
request.NotAfter = options.NotAfter
506523
request.Bundle = options.Bundle
507524
request.PreferredChain = options.PreferredChain
525+
request.Profile = options.Profile
508526
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
509527
}
510528

@@ -530,6 +548,7 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*
530548
request.NotAfter = options.NotAfter
531549
request.Bundle = options.Bundle
532550
request.PreferredChain = options.PreferredChain
551+
request.Profile = options.Profile
533552
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
534553
}
535554

cmd/cmd_renew.go

+6
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ func createRenew() *cli.Command {
9292
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
9393
" If no match, the default offered chain will be used.",
9494
},
95+
&cli.StringFlag{
96+
Name: flgProfile,
97+
Usage: "If the CA offers multiple certificate profiles (draft-aaron-acme-profiles), choose this one.",
98+
},
9599
&cli.StringFlag{
96100
Name: flgAlwaysDeactivateAuthorizations,
97101
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
@@ -234,6 +238,7 @@ func renewForDomains(ctx *cli.Context, account *Account, keyType certcrypto.KeyT
234238
NotAfter: getTime(ctx, flgNotAfter),
235239
Bundle: bundle,
236240
PreferredChain: ctx.String(flgPreferredChain),
241+
Profile: ctx.String(flgProfile),
237242
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
238243
}
239244

@@ -317,6 +322,7 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
317322
NotAfter: getTime(ctx, flgNotAfter),
318323
Bundle: bundle,
319324
PreferredChain: ctx.String(flgPreferredChain),
325+
Profile: ctx.String(flgProfile),
320326
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
321327
}
322328

cmd/cmd_run.go

+7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const (
2121
flgNotBefore = "not-before"
2222
flgNotAfter = "not-after"
2323
flgPreferredChain = "preferred-chain"
24+
flgProfile = "profile"
2425
flgAlwaysDeactivateAuthorizations = "always-deactivate-authorizations"
2526
flgRunHook = "run-hook"
2627
flgRunHookTimeout = "run-hook-timeout"
@@ -68,6 +69,10 @@ func createRun() *cli.Command {
6869
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
6970
" If no match, the default offered chain will be used.",
7071
},
72+
&cli.StringFlag{
73+
Name: flgProfile,
74+
Usage: "If the CA offers multiple certificate profiles (draft-aaron-acme-profiles), choose this one.",
75+
},
7176
&cli.StringFlag{
7277
Name: flgAlwaysDeactivateAuthorizations,
7378
Usage: "Force the authorizations to be relinquished even if the certificate request was successful.",
@@ -201,6 +206,7 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
201206
Bundle: bundle,
202207
MustStaple: ctx.Bool(flgMustStaple),
203208
PreferredChain: ctx.String(flgPreferredChain),
209+
Profile: ctx.String(flgProfile),
204210
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
205211
}
206212

@@ -230,6 +236,7 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
230236
NotAfter: getTime(ctx, flgNotAfter),
231237
Bundle: bundle,
232238
PreferredChain: ctx.String(flgPreferredChain),
239+
Profile: ctx.String(flgProfile),
233240
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
234241
}
235242

docs/content/_index.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ Let's Encrypt client and ACME library written in Go.
1212
- ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html)
1313
- Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension
1414
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses
15-
- Support [draft-ietf-acme-ari-01](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
15+
- Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
16+
- Support [draft-aaron-acme-profiles-00](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/): Profiles Extension
1617
- Register with CA
1718
- Obtain certificates, both from scratch or with an existing CSR
1819
- Renew certificates

e2e/challenges_test.go

+83
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,45 @@ func TestChallengeHTTP_Client_Obtain(t *testing.T) {
257257
assert.Empty(t, resource.CSR)
258258
}
259259

260+
func TestChallengeHTTP_Client_Obtain_profile(t *testing.T) {
261+
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
262+
require.NoError(t, err)
263+
defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }()
264+
265+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
266+
require.NoError(t, err, "Could not generate test key")
267+
268+
user := &fakeUser{privateKey: privateKey}
269+
config := lego.NewConfig(user)
270+
config.CADirURL = load.PebbleOptions.HealthCheckURL
271+
272+
client, err := lego.NewClient(config)
273+
require.NoError(t, err)
274+
275+
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002"))
276+
require.NoError(t, err)
277+
278+
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
279+
require.NoError(t, err)
280+
user.registration = reg
281+
282+
request := certificate.ObtainRequest{
283+
Domains: []string{"acme.wtf"},
284+
Bundle: true,
285+
Profile: "shortlived",
286+
}
287+
resource, err := client.Certificate.Obtain(request)
288+
require.NoError(t, err)
289+
290+
require.NotNil(t, resource)
291+
assert.Equal(t, "acme.wtf", resource.Domain)
292+
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL)
293+
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL)
294+
assert.NotEmpty(t, resource.Certificate)
295+
assert.NotEmpty(t, resource.IssuerCertificate)
296+
assert.Empty(t, resource.CSR)
297+
}
298+
260299
func TestChallengeHTTP_Client_Obtain_notBefore_notAfter(t *testing.T) {
261300
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
262301
require.NoError(t, err)
@@ -422,6 +461,50 @@ func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) {
422461
assert.NotEmpty(t, resource.CSR)
423462
}
424463

464+
func TestChallengeTLS_Client_ObtainForCSR_profile(t *testing.T) {
465+
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
466+
require.NoError(t, err)
467+
defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }()
468+
469+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
470+
require.NoError(t, err, "Could not generate test key")
471+
472+
user := &fakeUser{privateKey: privateKey}
473+
config := lego.NewConfig(user)
474+
config.CADirURL = load.PebbleOptions.HealthCheckURL
475+
476+
client, err := lego.NewClient(config)
477+
require.NoError(t, err)
478+
479+
err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001"))
480+
require.NoError(t, err)
481+
482+
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
483+
require.NoError(t, err)
484+
user.registration = reg
485+
486+
csrRaw, err := os.ReadFile("./fixtures/csr.raw")
487+
require.NoError(t, err)
488+
489+
csr, err := x509.ParseCertificateRequest(csrRaw)
490+
require.NoError(t, err)
491+
492+
resource, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
493+
CSR: csr,
494+
Bundle: true,
495+
Profile: "shortlived",
496+
})
497+
require.NoError(t, err)
498+
499+
require.NotNil(t, resource)
500+
assert.Equal(t, "acme.wtf", resource.Domain)
501+
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL)
502+
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL)
503+
assert.NotEmpty(t, resource.Certificate)
504+
assert.NotEmpty(t, resource.IssuerCertificate)
505+
assert.NotEmpty(t, resource.CSR)
506+
}
507+
425508
func TestRegistrar_UpdateAccount(t *testing.T) {
426509
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
427510
require.NoError(t, err)

e2e/dnschallenge/dns_challenges_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,61 @@ func TestChallengeDNS_Client_Obtain(t *testing.T) {
124124
assert.Empty(t, resource.CSR)
125125
}
126126

127+
func TestChallengeDNS_Client_Obtain_profile(t *testing.T) {
128+
err := os.Setenv("LEGO_CA_CERTIFICATES", "../fixtures/certs/pebble.minica.pem")
129+
require.NoError(t, err)
130+
defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }()
131+
132+
err = os.Setenv("EXEC_PATH", "../fixtures/update-dns.sh")
133+
require.NoError(t, err)
134+
defer func() { _ = os.Unsetenv("EXEC_PATH") }()
135+
136+
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
137+
require.NoError(t, err, "Could not generate test key")
138+
139+
user := &fakeUser{privateKey: privateKey}
140+
config := lego.NewConfig(user)
141+
config.CADirURL = "https://localhost:15000/dir"
142+
143+
client, err := lego.NewClient(config)
144+
require.NoError(t, err)
145+
146+
provider, err := dns.NewDNSChallengeProviderByName("exec")
147+
require.NoError(t, err)
148+
149+
err = client.Challenge.SetDNS01Provider(provider,
150+
dns01.AddRecursiveNameservers([]string{":8053"}),
151+
dns01.DisableAuthoritativeNssPropagationRequirement())
152+
require.NoError(t, err)
153+
154+
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
155+
require.NoError(t, err)
156+
user.registration = reg
157+
158+
domains := []string{"*.légo.acme", "légo.acme"}
159+
160+
// https://github.com/letsencrypt/pebble/issues/285
161+
privateKeyCSR, err := rsa.GenerateKey(rand.Reader, 2048)
162+
require.NoError(t, err, "Could not generate test key")
163+
164+
request := certificate.ObtainRequest{
165+
Domains: domains,
166+
Bundle: true,
167+
PrivateKey: privateKeyCSR,
168+
Profile: "shortlived",
169+
}
170+
resource, err := client.Certificate.Obtain(request)
171+
require.NoError(t, err)
172+
173+
require.NotNil(t, resource)
174+
assert.Equal(t, "*.xn--lgo-bma.acme", resource.Domain)
175+
assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertURL)
176+
assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertStableURL)
177+
assert.NotEmpty(t, resource.Certificate)
178+
assert.NotEmpty(t, resource.IssuerCertificate)
179+
assert.Empty(t, resource.CSR)
180+
}
181+
127182
type fakeUser struct {
128183
email string
129184
privateKey crypto.PrivateKey

0 commit comments

Comments
 (0)