diff --git a/acme/acme.go b/acme/acme.go index b53ea28891..c86901ba4a 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -185,10 +185,11 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) { Nonce string `json:"newNonce"` KeyChange string `json:"keyChange"` Meta struct { - Terms string `json:"termsOfService"` - Website string `json:"website"` - CAA []string `json:"caaIdentities"` - ExternalAcct bool `json:"externalAccountRequired"` + Terms string `json:"termsOfService"` + Website string `json:"website"` + CAA []string `json:"caaIdentities"` + ExternalAcct bool `json:"externalAccountRequired"` + Profiles map[string]string `json:"profiles"` } } if err := json.NewDecoder(res.Body).Decode(&v); err != nil { @@ -208,6 +209,7 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) { Website: v.Meta.Website, CAA: v.Meta.CAA, ExternalAccountRequired: v.Meta.ExternalAcct, + Profiles: v.Meta.Profiles, } return *c.dir, nil } @@ -219,6 +221,20 @@ func (c *Client) directoryURL() string { return LetsEncryptURL } +func (c *Client) validProfile(name string) bool { + // profile names are optional, so empty string ("") is valid + if name == "" { + return true + } + if len(c.dir.Profiles) == 0 { + // no profiles are supported so only valid name is empty string ("") + // which is caught above + return false + } + _, has := c.dir.Profiles[name] + return has +} + // CreateCert was part of the old version of ACME. It is incompatible with RFC 8555. // // Deprecated: this was for the pre-RFC 8555 version of ACME. Callers should use CreateOrderCert. diff --git a/acme/autocert/autocert.go b/acme/autocert/autocert.go index 69461e31d3..74bf84d112 100644 --- a/acme/autocert/autocert.go +++ b/acme/autocert/autocert.go @@ -176,6 +176,14 @@ type Manager struct { // See RFC 8555, Section 7.3.4 for more details. ExternalAccountBinding *acme.ExternalAccountBinding + // Profile optional name of certificate profile to use when creating a new order + // + // available profiles are defined by the ACME server and listed in the + // ACME server's directory response. + // + // See RFC: https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/ + Profile string + clientMu sync.Mutex client *acme.Client // initialized by acmeClient method @@ -693,9 +701,16 @@ func (m *Manager) verifyRFC(ctx context.Context, client *acme.Client, domain str // it will most likely not work on another order's authorization either. challengeTypes := m.supportedChallengeTypes() nextTyp := 0 // challengeTypes index + authOpts := []acme.OrderOption{acme.WithOrderProfile(m.Profile)} AuthorizeOrderLoop: for { - o, err := client.AuthorizeOrder(ctx, acme.DomainIDs(domain)) + var ids []acme.AuthzID + if ip := net.ParseIP(domain); ip != nil { + ids = acme.IPIDs(domain) + } else { + ids = acme.DomainIDs(domain) + } + o, err := client.AuthorizeOrder(ctx, ids, authOpts...) if err != nil { return nil, err } @@ -1060,10 +1075,15 @@ func (s *certState) tlscert() (*tls.Certificate, error) { // certRequest generates a CSR for the given common name. func certRequest(key crypto.Signer, name string, ext []pkix.Extension) ([]byte, error) { req := &x509.CertificateRequest{ - Subject: pkix.Name{CommonName: name}, - DNSNames: []string{name}, + Subject: pkix.Name{}, ExtraExtensions: ext, } + if ip := net.ParseIP(name); ip != nil { + req.IPAddresses = []net.IP{ip} + } else { + req.DNSNames = []string{name} + req.Subject.CommonName = name + } return x509.CreateCertificateRequest(rand.Reader, req, key) } diff --git a/acme/rfc8555.go b/acme/rfc8555.go index 1fb110e08a..f61043763b 100644 --- a/acme/rfc8555.go +++ b/acme/rfc8555.go @@ -208,6 +208,7 @@ func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderO Identifiers []wireAuthzID `json:"identifiers"` NotBefore string `json:"notBefore,omitempty"` NotAfter string `json:"notAfter,omitempty"` + Profile string `json:"profile,omitempty"` }{} for _, v := range id { req.Identifiers = append(req.Identifiers, wireAuthzID{ @@ -221,6 +222,11 @@ func (c *Client) AuthorizeOrder(ctx context.Context, id []AuthzID, opt ...OrderO req.NotBefore = time.Time(o).Format(time.RFC3339) case orderNotAfterOpt: req.NotAfter = time.Time(o).Format(time.RFC3339) + case orderProfileOpt: + req.Profile = string(o) + if !c.validProfile(req.Profile) { + return nil, fmt.Errorf("invalid acme profile: %s", req.Profile) + } default: // Package's fault if we let this happen. panic(fmt.Sprintf("unsupported order option type %T", o)) @@ -308,6 +314,7 @@ func responseOrder(res *http.Response) (*Order, error) { Authorizations []string Finalize string Certificate string + Profile string } if err := json.NewDecoder(res.Body).Decode(&v); err != nil { return nil, fmt.Errorf("acme: error reading order: %v", err) @@ -321,6 +328,7 @@ func responseOrder(res *http.Response) (*Order, error) { AuthzURLs: v.Authorizations, FinalizeURL: v.Finalize, CertURL: v.Certificate, + Profile: v.Profile, } for _, id := range v.Identifiers { o.Identifiers = append(o.Identifiers, AuthzID{Type: id.Type, Value: id.Value}) diff --git a/acme/types.go b/acme/types.go index 322640c453..ee78bbdc8a 100644 --- a/acme/types.go +++ b/acme/types.go @@ -311,6 +311,9 @@ type Directory struct { // ExternalAccountRequired indicates that the CA requires for all account-related // requests to include external account binding information. ExternalAccountRequired bool + + // Profiles the certificate profiles which are supported by the ACME server + Profiles map[string]string } // Order represents a client's request for a certificate. @@ -345,6 +348,13 @@ type Order struct { // NotAfter is the requested value of the notAfter field in the certificate. NotAfter time.Time + // Profile the certificate profile to use with the order (optional) + // available profiles are defined by the ACME server and listed in the + // ACME server's directory response. + // + // RFC: https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/ + Profile string + // AuthzURLs represents authorizations to complete before a certificate // for identifiers specified in the order can be issued. // It also contains unexpired authorizations that the client has completed @@ -386,6 +396,11 @@ func WithOrderNotAfter(t time.Time) OrderOption { return orderNotAfterOpt(t) } +// WithOrderProfile sets tthe order's Profile field. +func WithOrderProfile(p string) OrderOption { + return orderProfileOpt(p) +} + type orderNotBeforeOpt time.Time func (orderNotBeforeOpt) privateOrderOpt() {} @@ -394,6 +409,10 @@ type orderNotAfterOpt time.Time func (orderNotAfterOpt) privateOrderOpt() {} +type orderProfileOpt string + +func (orderProfileOpt) privateOrderOpt() {} + // Authorization encodes an authorization response. type Authorization struct { // URI uniquely identifies a authorization.