diff --git a/third_party/forked/acme/acme.go b/third_party/forked/acme/acme.go index cfb1dfd8c..c61165bd3 100644 --- a/third_party/forked/acme/acme.go +++ b/third_party/forked/acme/acme.go @@ -186,10 +186,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 { @@ -209,6 +210,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 } diff --git a/third_party/forked/acme/rfc8555.go b/third_party/forked/acme/rfc8555.go index 3152e531b..169bf802e 100644 --- a/third_party/forked/acme/rfc8555.go +++ b/third_party/forked/acme/rfc8555.go @@ -205,6 +205,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{ @@ -218,6 +219,15 @@ 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: + if !dir.Profiles.isSupported() { + return nil, ErrCADoesNotSupportProfiles + } + profileName := string(o) + if !dir.Profiles.Has(profileName) { + return nil, fmt.Errorf("%w %s", ErrProfileNotInSetOfSupportedProfiles, profileName) + } + req.Profile = profileName default: // Package's fault if we let this happen. panic(fmt.Sprintf("unsupported order option type %T", o)) diff --git a/third_party/forked/acme/rfc8555_test.go b/third_party/forked/acme/rfc8555_test.go index d65720a35..b53f3d2ab 100644 --- a/third_party/forked/acme/rfc8555_test.go +++ b/third_party/forked/acme/rfc8555_test.go @@ -99,6 +99,61 @@ func TestRFC_Discover(t *testing.T) { if !dir.ExternalAccountRequired { t.Error("dir.Meta.ExternalAccountRequired is false") } + if dir.Profiles != nil { + t.Errorf("dir.Profiles is expected to be nil, got %+v", dir.Profiles) + } +} + +func TestDiscover_WithProfiles(t *testing.T) { + const ( + nonce = "https://example.com/acme/new-nonce" + reg = "https://example.com/acme/new-acct" + order = "https://example.com/acme/new-order" + authz = "https://example.com/acme/new-authz" + revoke = "https://example.com/acme/revoke-cert" + keychange = "https://example.com/acme/key-change" + metaTerms = "https://example.com/acme/terms/2017-5-30" + metaWebsite = "https://www.example.com/" + metaCAA = "example.com" + ) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{ + "newNonce": %q, + "newAccount": %q, + "newOrder": %q, + "newAuthz": %q, + "revokeCert": %q, + "keyChange": %q, + "meta": { + "termsOfService": %q, + "website": %q, + "caaIdentities": [%q], + "externalAccountRequired": true, + "profiles": { + "default": "Your favorite default profile", + "tlsserver": "New and improved", + "client": "For all your mutual TLS needs" + } + } + }`, nonce, reg, order, authz, revoke, keychange, metaTerms, metaWebsite, metaCAA) + })) + defer ts.Close() + c := &Client{DirectoryURL: ts.URL} + dir, err := c.Discover(context.Background()) + if err != nil { + t.Fatal(err) + } + expected := Profiles(map[string]string{"default": "Your favorite default profile", "tlsserver": "New and improved", "client": "For all your mutual TLS needs"}) + if dir.Profiles == nil { + t.Errorf("expected directory to be %+v; got nil", expected) + } + + for key, value := range dir.Profiles { + if expValue := expected.GetDescription(key); value != expValue { + t.Errorf("expected key %+q to have description %+q; got %+q", key, expected, value) + } + } } func TestRFC_popNonce(t *testing.T) { @@ -247,6 +302,27 @@ func (s *acmeServer) start() { return } + if r.URL.Path == "/directory-with-profiles" { + fmt.Fprintf(w, `{ + "newNonce": %q, + "newAccount": %q, + "newOrder": %q, + "newAuthz": %q, + "revokeCert": %q, + "keyChange": %q, + "meta": {"termsOfService": %q, "profiles": {"default": "Default", "server": "Server", "client": "Client"}} + }`, + s.url("/acme/new-nonce"), + s.url("/acme/new-account"), + s.url("/acme/new-order"), + s.url("/acme/new-authz"), + s.url("/acme/revoke-cert"), + s.url("/acme/key-change"), + s.url("/terms"), + ) + return + } + // All other responses contain a nonce value unconditionally. w.Header().Set("Replay-Nonce", s.nonce()) if r.URL.Path == "/acme/new-nonce" { @@ -788,6 +864,51 @@ func TestRFC_AuthorizeOrder(t *testing.T) { } } +func TestRFC_AuthorizeOrder_WithOrderProfile(t *testing.T) { + s := newACMEServer() + s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", s.url("/accounts/1")) + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status": "valid"}`)) + }) + s.handle("/acme/new-order", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", s.url("/orders/1")) + w.WriteHeader(http.StatusCreated) + fmt.Fprintf(w, `{ + "status": "pending", + "expires": "2019-09-01T00:00:00Z", + "notBefore": "2019-08-31T00:00:00Z", + "notAfter": "2019-09-02T00:00:00Z", + "identifiers": [{"type":"dns", "value":"example.org"}], + "authorizations": [%q] + }`, s.url("/authz/1")) + }) + s.start() + defer s.close() + + cl := &Client{Key: testKeyEC, DirectoryURL: s.url("/directory-with-profiles")} + o, err := cl.AuthorizeOrder(context.Background(), DomainIDs("example.org"), + WithOrderNotBefore(time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC)), + WithOrderNotAfter(time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC)), + WithOrderProfile("server"), + ) + if err != nil { + t.Fatal(err) + } + okOrder := &Order{ + URI: s.url("/orders/1"), + Status: StatusPending, + Expires: time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC), + NotBefore: time.Date(2019, 8, 31, 0, 0, 0, 0, time.UTC), + NotAfter: time.Date(2019, 9, 2, 0, 0, 0, 0, time.UTC), + Identifiers: []AuthzID{{Type: "dns", Value: "example.org"}}, + AuthzURLs: []string{s.url("/authz/1")}, + } + if !reflect.DeepEqual(o, okOrder) { + t.Errorf("AuthorizeOrder = %+v; want %+v", o, okOrder) + } +} + func TestRFC_GetOrder(t *testing.T) { s := newACMEServer() s.handle("/acme/new-account", func(w http.ResponseWriter, r *http.Request) { diff --git a/third_party/forked/acme/types.go b/third_party/forked/acme/types.go index 640223cb7..4d8fe9c81 100644 --- a/third_party/forked/acme/types.go +++ b/third_party/forked/acme/types.go @@ -60,6 +60,15 @@ var ( // errPreAuthorizationNotSupported indicates that the server does not // support pre-authorization of identifiers. errPreAuthorizationNotSupported = errors.New("acme: pre-authorization is not supported") + + // ErrCADoesNotSupportProfiles indicates that [WithOrderProfile] was + // included with a CA that does not advertise support for profiles in + // their directory. + ErrCADoesNotSupportProfiles = errors.New("acme: certificate authority does not support profiles") + + // ErrProfileNotInSetOfSupportedProfiles indicates that the profile + // specified with [WithOrderProfile} is not one supported by the CA + ErrProfileNotInSetOfSupportedProfiles = errors.New("acme: certificate authority does not advertise a profile with name") ) // A Subproblem describes an ACME subproblem as reported in an Error. @@ -308,6 +317,10 @@ type Directory struct { // ExternalAccountRequired indicates that the CA requires for all account-related // requests to include external account binding information. ExternalAccountRequired bool + + // Profiles indicates that the CA supports specifying a profile for an + // order. See also [WithOrderNotAfter]. + Profiles Profiles } // Order represents a client's request for a certificate. @@ -383,6 +396,15 @@ func WithOrderNotAfter(t time.Time) OrderOption { return orderNotAfterOpt(t) } +// WithOrderProfile sets an order's Profile field for servers which support +// profiles. +// See also: +// * https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/ +// * https://letsencrypt.org/docs/profiles/ +func WithOrderProfile(name string) OrderOption { + return orderProfileOpt(name) +} + type orderNotBeforeOpt time.Time func (orderNotBeforeOpt) privateOrderOpt() {} @@ -391,6 +413,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. @@ -627,3 +653,18 @@ func WithTemplate(t *x509.Certificate) CertOption { type certOptTemplate x509.Certificate func (*certOptTemplate) privateCertOpt() {} + +type Profiles map[string]string + +func (ps Profiles) isSupported() bool { + return len(ps) > 0 +} + +func (ps Profiles) GetDescription(name string) string { + return ps[name] +} + +func (ps Profiles) Has(name string) bool { + _, ok := ps[name] + return ok +} diff --git a/third_party/klone.yaml b/third_party/klone.yaml index c190cc1f6..acb2014f3 100644 --- a/third_party/klone.yaml +++ b/third_party/klone.yaml @@ -1,9 +1,15 @@ # Clone folders from third_party repos and forks. # More info can be found here: https://github.com/cert-manager/klone +# +# - acme +# We vendor just the acme package, from a cert-manager fork of +# golang.org/x/crypto. The acme-profiles branch has a patch by @sigmavirus24, +# with support for ACME profiles. +# See https://github.com/golang/go/issues/73101#issuecomment-2764923702 targets: forked: - folder_name: acme - repo_url: https://go.googlesource.com/crypto - repo_ref: v0.38.0 - repo_hash: aae6e61070421a51c1ba3bd9bba4b9b3979ed488 + repo_url: https://github.com/cert-manager/crypto + repo_ref: acme-profiles + repo_hash: 20ccc126e2ac0b2d9da2e78f84f5bb7649d8100a repo_path: acme