Switch to the cert-manager/crypto fork which contains @sigmavirus24's ACME profiles patch

Signed-off-by: Richard Wall <richard.wall@cyberark.com>
This commit is contained in:
Richard Wall 2025-06-03 15:49:05 +01:00
parent 5dbcc7571c
commit 0631f8c596
5 changed files with 187 additions and 7 deletions

View File

@ -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
}

View File

@ -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))

View File

@ -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) {

View File

@ -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
}

View File

@ -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