mirror of
https://github.com/go-acme/lego
synced 2026-03-14 22:45:48 +01:00
feat: add support for Profiles Extension (#2415)
Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
parent
2e497ca928
commit
dc992b8d87
12 changed files with 226 additions and 13 deletions
4
.github/workflows/pr.yml
vendored
4
.github/workflows/pr.yml
vendored
|
|
@ -46,10 +46,10 @@ jobs:
|
|||
golangci-lint --version
|
||||
|
||||
- name: Install Pebble
|
||||
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@3fe019bbc0a41ed16e2fee31592bb91751acaa47
|
||||
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble@v2.7.0
|
||||
|
||||
- name: Install challtestsrv
|
||||
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@3fe019bbc0a41ed16e2fee31592bb91751acaa47
|
||||
run: go install github.com/letsencrypt/pebble/v2/cmd/pebble-challtestsrv@v2.7.0
|
||||
|
||||
- name: Set up a Memcached server
|
||||
uses: niden/actions-memcached@v7
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ Let's Encrypt client and ACME library written in Go.
|
|||
- Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension
|
||||
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): certificates for IP addresses
|
||||
- Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
|
||||
- Support [draft-aaron-acme-profiles-00](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/): Profiles Extension
|
||||
- Register with CA
|
||||
- Obtain certificates, both from scratch or with an existing CSR
|
||||
- Renew certificates
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ import (
|
|||
type OrderOptions struct {
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
|
||||
// A string uniquely identifying the profile
|
||||
// which will be used to affect issuance of the certificate requested by this Order.
|
||||
// - https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-4
|
||||
Profile string
|
||||
|
||||
// A string uniquely identifying a previously-issued certificate which this
|
||||
// order is intended to replace.
|
||||
// - 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
|
|||
if o.core.GetDirectory().RenewalInfo != "" {
|
||||
orderReq.Replaces = opts.ReplacesCertID
|
||||
}
|
||||
|
||||
if opts.Profile != "" {
|
||||
orderReq.Profile = opts.Profile
|
||||
}
|
||||
}
|
||||
|
||||
var order acme.Order
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ type Meta struct {
|
|||
// then the CA requires that all new-account requests include an "externalAccountBinding" field
|
||||
// associating the new account with an external account.
|
||||
ExternalAccountRequired bool `json:"externalAccountRequired"`
|
||||
|
||||
// profiles (optional, object):
|
||||
// A map of profile names to human-readable descriptions of those profiles.
|
||||
// https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-3
|
||||
Profiles map[string]string `json:"profiles"`
|
||||
}
|
||||
|
||||
// ExtendedAccount an extended Account.
|
||||
|
|
@ -148,6 +153,12 @@ type Order struct {
|
|||
// An array of identifier objects that the order pertains to.
|
||||
Identifiers []Identifier `json:"identifiers"`
|
||||
|
||||
// profile (string, optional):
|
||||
// A string uniquely identifying the profile
|
||||
// which will be used to affect issuance of the certificate requested by this Order.
|
||||
// https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-4
|
||||
Profile string `json:"profile,omitempty"`
|
||||
|
||||
// notBefore (optional, string):
|
||||
// The requested value of the notBefore field in the certificate,
|
||||
// in the date format defined in [RFC3339].
|
||||
|
|
|
|||
|
|
@ -69,11 +69,18 @@ type ObtainRequest struct {
|
|||
PrivateKey crypto.PrivateKey
|
||||
MustStaple bool
|
||||
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
|
||||
// A string uniquely identifying the profile
|
||||
// which will be used to affect issuance of the certificate requested by this Order.
|
||||
// - https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-4
|
||||
Profile string
|
||||
|
||||
AlwaysDeactivateAuthorizations bool
|
||||
|
||||
// A string uniquely identifying a previously-issued certificate which this
|
||||
// order is intended to replace.
|
||||
// - https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
|
||||
|
|
@ -89,11 +96,18 @@ type ObtainRequest struct {
|
|||
type ObtainForCSRRequest struct {
|
||||
CSR *x509.CertificateRequest
|
||||
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
NotBefore time.Time
|
||||
NotAfter time.Time
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
|
||||
// A string uniquely identifying the profile
|
||||
// which will be used to affect issuance of the certificate requested by this Order.
|
||||
// - https://www.ietf.org/id/draft-aaron-acme-profiles-00.html#section-4
|
||||
Profile string
|
||||
|
||||
AlwaysDeactivateAuthorizations bool
|
||||
|
||||
// A string uniquely identifying a previously-issued certificate which this
|
||||
// order is intended to replace.
|
||||
// - 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) {
|
|||
orderOpts := &api.OrderOptions{
|
||||
NotBefore: request.NotBefore,
|
||||
NotAfter: request.NotAfter,
|
||||
Profile: request.Profile,
|
||||
ReplacesCertID: request.ReplacesCertID,
|
||||
}
|
||||
|
||||
|
|
@ -220,6 +235,7 @@ func (c *Certifier) ObtainForCSR(request ObtainForCSRRequest) (*Resource, error)
|
|||
orderOpts := &api.OrderOptions{
|
||||
NotBefore: request.NotBefore,
|
||||
NotAfter: request.NotAfter,
|
||||
Profile: request.Profile,
|
||||
ReplacesCertID: request.ReplacesCertID,
|
||||
}
|
||||
|
||||
|
|
@ -437,6 +453,7 @@ type RenewOptions struct {
|
|||
// If true, the []byte contains both the issuer certificate and your issued certificate as a bundle.
|
||||
Bundle bool
|
||||
PreferredChain string
|
||||
Profile string
|
||||
AlwaysDeactivateAuthorizations bool
|
||||
// Not supported for CSR request.
|
||||
MustStaple bool
|
||||
|
|
@ -505,6 +522,7 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*
|
|||
request.NotAfter = options.NotAfter
|
||||
request.Bundle = options.Bundle
|
||||
request.PreferredChain = options.PreferredChain
|
||||
request.Profile = options.Profile
|
||||
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
|
||||
}
|
||||
|
||||
|
|
@ -530,6 +548,7 @@ func (c *Certifier) RenewWithOptions(certRes Resource, options *RenewOptions) (*
|
|||
request.NotAfter = options.NotAfter
|
||||
request.Bundle = options.Bundle
|
||||
request.PreferredChain = options.PreferredChain
|
||||
request.Profile = options.Profile
|
||||
request.AlwaysDeactivateAuthorizations = options.AlwaysDeactivateAuthorizations
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ func createRenew() *cli.Command {
|
|||
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
|
||||
" If no match, the default offered chain will be used.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgProfile,
|
||||
Usage: "If the CA offers multiple certificate profiles (draft-aaron-acme-profiles), choose this one.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgAlwaysDeactivateAuthorizations,
|
||||
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
|
|||
NotAfter: getTime(ctx, flgNotAfter),
|
||||
Bundle: bundle,
|
||||
PreferredChain: ctx.String(flgPreferredChain),
|
||||
Profile: ctx.String(flgProfile),
|
||||
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
|
||||
}
|
||||
|
||||
|
|
@ -317,6 +322,7 @@ func renewForCSR(ctx *cli.Context, account *Account, keyType certcrypto.KeyType,
|
|||
NotAfter: getTime(ctx, flgNotAfter),
|
||||
Bundle: bundle,
|
||||
PreferredChain: ctx.String(flgPreferredChain),
|
||||
Profile: ctx.String(flgProfile),
|
||||
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const (
|
|||
flgNotBefore = "not-before"
|
||||
flgNotAfter = "not-after"
|
||||
flgPreferredChain = "preferred-chain"
|
||||
flgProfile = "profile"
|
||||
flgAlwaysDeactivateAuthorizations = "always-deactivate-authorizations"
|
||||
flgRunHook = "run-hook"
|
||||
flgRunHookTimeout = "run-hook-timeout"
|
||||
|
|
@ -68,6 +69,10 @@ func createRun() *cli.Command {
|
|||
Usage: "If the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name." +
|
||||
" If no match, the default offered chain will be used.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgProfile,
|
||||
Usage: "If the CA offers multiple certificate profiles (draft-aaron-acme-profiles), choose this one.",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: flgAlwaysDeactivateAuthorizations,
|
||||
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
|
|||
Bundle: bundle,
|
||||
MustStaple: ctx.Bool(flgMustStaple),
|
||||
PreferredChain: ctx.String(flgPreferredChain),
|
||||
Profile: ctx.String(flgProfile),
|
||||
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
|
||||
}
|
||||
|
||||
|
|
@ -230,6 +236,7 @@ func obtainCertificate(ctx *cli.Context, client *lego.Client) (*certificate.Reso
|
|||
NotAfter: getTime(ctx, flgNotAfter),
|
||||
Bundle: bundle,
|
||||
PreferredChain: ctx.String(flgPreferredChain),
|
||||
Profile: ctx.String(flgProfile),
|
||||
AlwaysDeactivateAuthorizations: ctx.Bool(flgAlwaysDeactivateAuthorizations),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ Let's Encrypt client and ACME library written in Go.
|
|||
- ACME v2 [RFC 8555](https://www.rfc-editor.org/rfc/rfc8555.html)
|
||||
- Support [RFC 8737](https://www.rfc-editor.org/rfc/rfc8737.html): TLS Application‑Layer Protocol Negotiation (ALPN) Challenge Extension
|
||||
- Support [RFC 8738](https://www.rfc-editor.org/rfc/rfc8738.html): issues certificates for IP addresses
|
||||
- Support [draft-ietf-acme-ari-01](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
|
||||
- Support [draft-ietf-acme-ari-03](https://datatracker.ietf.org/doc/draft-ietf-acme-ari/): Renewal Information (ARI) Extension
|
||||
- Support [draft-aaron-acme-profiles-00](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/): Profiles Extension
|
||||
- Register with CA
|
||||
- Obtain certificates, both from scratch or with an existing CSR
|
||||
- Renew certificates
|
||||
|
|
|
|||
|
|
@ -257,6 +257,45 @@ func TestChallengeHTTP_Client_Obtain(t *testing.T) {
|
|||
assert.Empty(t, resource.CSR)
|
||||
}
|
||||
|
||||
func TestChallengeHTTP_Client_Obtain_profile(t *testing.T) {
|
||||
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }()
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
user := &fakeUser{privateKey: privateKey}
|
||||
config := lego.NewConfig(user)
|
||||
config.CADirURL = load.PebbleOptions.HealthCheckURL
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Challenge.SetHTTP01Provider(http01.NewProviderServer("", "5002"))
|
||||
require.NoError(t, err)
|
||||
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
require.NoError(t, err)
|
||||
user.registration = reg
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: []string{"acme.wtf"},
|
||||
Bundle: true,
|
||||
Profile: "shortlived",
|
||||
}
|
||||
resource, err := client.Certificate.Obtain(request)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, resource)
|
||||
assert.Equal(t, "acme.wtf", resource.Domain)
|
||||
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL)
|
||||
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL)
|
||||
assert.NotEmpty(t, resource.Certificate)
|
||||
assert.NotEmpty(t, resource.IssuerCertificate)
|
||||
assert.Empty(t, resource.CSR)
|
||||
}
|
||||
|
||||
func TestChallengeHTTP_Client_Obtain_notBefore_notAfter(t *testing.T) {
|
||||
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
|
||||
require.NoError(t, err)
|
||||
|
|
@ -422,6 +461,50 @@ func TestChallengeTLS_Client_ObtainForCSR(t *testing.T) {
|
|||
assert.NotEmpty(t, resource.CSR)
|
||||
}
|
||||
|
||||
func TestChallengeTLS_Client_ObtainForCSR_profile(t *testing.T) {
|
||||
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }()
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
user := &fakeUser{privateKey: privateKey}
|
||||
config := lego.NewConfig(user)
|
||||
config.CADirURL = load.PebbleOptions.HealthCheckURL
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Challenge.SetTLSALPN01Provider(tlsalpn01.NewProviderServer("", "5001"))
|
||||
require.NoError(t, err)
|
||||
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
require.NoError(t, err)
|
||||
user.registration = reg
|
||||
|
||||
csrRaw, err := os.ReadFile("./fixtures/csr.raw")
|
||||
require.NoError(t, err)
|
||||
|
||||
csr, err := x509.ParseCertificateRequest(csrRaw)
|
||||
require.NoError(t, err)
|
||||
|
||||
resource, err := client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{
|
||||
CSR: csr,
|
||||
Bundle: true,
|
||||
Profile: "shortlived",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, resource)
|
||||
assert.Equal(t, "acme.wtf", resource.Domain)
|
||||
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertURL)
|
||||
assert.Regexp(t, `https://localhost:14000/certZ/[\w\d]{14,}`, resource.CertStableURL)
|
||||
assert.NotEmpty(t, resource.Certificate)
|
||||
assert.NotEmpty(t, resource.IssuerCertificate)
|
||||
assert.NotEmpty(t, resource.CSR)
|
||||
}
|
||||
|
||||
func TestRegistrar_UpdateAccount(t *testing.T) {
|
||||
err := os.Setenv("LEGO_CA_CERTIFICATES", "./fixtures/certs/pebble.minica.pem")
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
|
|
@ -124,6 +124,61 @@ func TestChallengeDNS_Client_Obtain(t *testing.T) {
|
|||
assert.Empty(t, resource.CSR)
|
||||
}
|
||||
|
||||
func TestChallengeDNS_Client_Obtain_profile(t *testing.T) {
|
||||
err := os.Setenv("LEGO_CA_CERTIFICATES", "../fixtures/certs/pebble.minica.pem")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.Unsetenv("LEGO_CA_CERTIFICATES") }()
|
||||
|
||||
err = os.Setenv("EXEC_PATH", "../fixtures/update-dns.sh")
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = os.Unsetenv("EXEC_PATH") }()
|
||||
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
user := &fakeUser{privateKey: privateKey}
|
||||
config := lego.NewConfig(user)
|
||||
config.CADirURL = "https://localhost:15000/dir"
|
||||
|
||||
client, err := lego.NewClient(config)
|
||||
require.NoError(t, err)
|
||||
|
||||
provider, err := dns.NewDNSChallengeProviderByName("exec")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = client.Challenge.SetDNS01Provider(provider,
|
||||
dns01.AddRecursiveNameservers([]string{":8053"}),
|
||||
dns01.DisableAuthoritativeNssPropagationRequirement())
|
||||
require.NoError(t, err)
|
||||
|
||||
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
|
||||
require.NoError(t, err)
|
||||
user.registration = reg
|
||||
|
||||
domains := []string{"*.légo.acme", "légo.acme"}
|
||||
|
||||
// https://github.com/letsencrypt/pebble/issues/285
|
||||
privateKeyCSR, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err, "Could not generate test key")
|
||||
|
||||
request := certificate.ObtainRequest{
|
||||
Domains: domains,
|
||||
Bundle: true,
|
||||
PrivateKey: privateKeyCSR,
|
||||
Profile: "shortlived",
|
||||
}
|
||||
resource, err := client.Certificate.Obtain(request)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, resource)
|
||||
assert.Equal(t, "*.xn--lgo-bma.acme", resource.Domain)
|
||||
assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertURL)
|
||||
assert.Regexp(t, `https://localhost:15000/certZ/[\w\d]{14,}`, resource.CertStableURL)
|
||||
assert.NotEmpty(t, resource.Certificate)
|
||||
assert.NotEmpty(t, resource.IssuerCertificate)
|
||||
assert.Empty(t, resource.CSR)
|
||||
}
|
||||
|
||||
type fakeUser struct {
|
||||
email string
|
||||
privateKey crypto.PrivateKey
|
||||
|
|
|
|||
|
|
@ -4,6 +4,16 @@
|
|||
"certificate": "fixtures/certs/localhost/cert.pem",
|
||||
"privateKey": "fixtures/certs/localhost/key.pem",
|
||||
"httpPort": 5004,
|
||||
"tlsPort": 5003
|
||||
"tlsPort": 5003,
|
||||
"profiles": {
|
||||
"default": {
|
||||
"description": "The profile you know and love",
|
||||
"validityPeriod": 7776000
|
||||
},
|
||||
"shortlived": {
|
||||
"description": "A short-lived cert profile, without actual enforcement",
|
||||
"validityPeriod": 518400
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,16 @@
|
|||
"certificate": "fixtures/certs/localhost/cert.pem",
|
||||
"privateKey": "fixtures/certs/localhost/key.pem",
|
||||
"httpPort": 5002,
|
||||
"tlsPort": 5001
|
||||
"tlsPort": 5001,
|
||||
"profiles": {
|
||||
"default": {
|
||||
"description": "The profile you know and love",
|
||||
"validityPeriod": 7776000
|
||||
},
|
||||
"shortlived": {
|
||||
"description": "A short-lived cert profile, without actual enforcement",
|
||||
"validityPeriod": 518400
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue