From ce732ecb75f402a78c81f6126db148ff01e80dcb Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Wed, 4 Mar 2026 13:01:55 +0100 Subject: [PATCH] refactor: JWS --- acme/api/account.go | 8 +- acme/api/api.go | 9 -- acme/api/internal/nonces/nonce_manager.go | 4 + acme/api/internal/secure/jws.go | 110 +++++++++++----------- acme/api/internal/secure/jws_test.go | 78 +++++++++++++++ 5 files changed, 142 insertions(+), 67 deletions(-) create mode 100644 acme/api/internal/secure/jws_test.go diff --git a/acme/api/account.go b/acme/api/account.go index 4d37267cc..646af6222 100644 --- a/acme/api/account.go +++ b/acme/api/account.go @@ -29,20 +29,20 @@ func (a *AccountService) New(ctx context.Context, req acme.Account) (acme.Extend } // NewEAB Creates a new account with an External Account Binding. -func (a *AccountService) NewEAB(ctx context.Context, accMsg acme.Account, kid, hmacEncoded string) (acme.ExtendedAccount, error) { +func (a *AccountService) NewEAB(ctx context.Context, req acme.Account, kid, hmacEncoded string) (acme.ExtendedAccount, error) { hmac, err := decodeEABHmac(hmacEncoded) if err != nil { return acme.ExtendedAccount{}, err } - eabJWS, err := a.core.signEABContent(a.core.GetDirectory().NewAccountURL, kid, hmac) + eabJWS, err := a.core.jws().SignEAB(a.core.GetDirectory().NewAccountURL, kid, hmac) if err != nil { return acme.ExtendedAccount{}, fmt.Errorf("acme: error signing eab content: %w", err) } - accMsg.ExternalAccountBinding = eabJWS + req.ExternalAccountBinding = []byte(eabJWS.FullSerialize()) - return a.New(ctx, accMsg) + return a.New(ctx, req) } // Get Retrieves an account. diff --git a/acme/api/api.go b/acme/api/api.go index 494f9cc74..fad7cdff2 100644 --- a/acme/api/api.go +++ b/acme/api/api.go @@ -152,15 +152,6 @@ func (a *Core) signedPost(ctx context.Context, uri string, content []byte, respo return resp, err } -func (a *Core) signEABContent(newAccountURL, kid string, hmac []byte) ([]byte, error) { - eabJWS, err := a.jws().SignEABContent(newAccountURL, kid, hmac) - if err != nil { - return nil, err - } - - return []byte(eabJWS.FullSerialize()), nil -} - // GetKeyAuthorization Gets the key authorization. func (a *Core) GetKeyAuthorization(token string) (string, error) { return a.jws().GetKeyAuthorization(token) diff --git a/acme/api/internal/nonces/nonce_manager.go b/acme/api/internal/nonces/nonce_manager.go index f2d0c5ca7..1d2447c60 100644 --- a/acme/api/internal/nonces/nonce_manager.go +++ b/acme/api/internal/nonces/nonce_manager.go @@ -78,6 +78,10 @@ func GetFromResponse(resp *http.Response) (string, error) { return nonce, nil } +func (n *Manager) NewNonceSource(ctx context.Context) jose.NonceSource { + return NewNonceSource(ctx, n) +} + var _ jose.NonceSource = (*NonceSource)(nil) // NonceSource implements [jose.NonceSource]. diff --git a/acme/api/internal/secure/jws.go b/acme/api/internal/secure/jws.go index b97f3a766..21a8d7642 100644 --- a/acme/api/internal/secure/jws.go +++ b/acme/api/internal/secure/jws.go @@ -9,19 +9,22 @@ import ( "encoding/base64" "fmt" - "github.com/go-acme/lego/v5/acme/api/internal/nonces" "github.com/go-jose/go-jose/v4" ) +type nonceSourceCreator interface { + NewNonceSource(ctx context.Context) jose.NonceSource +} + // JWS Represents a JWS. type JWS struct { privKey crypto.PrivateKey kid string // Key identifier - nonces *nonces.Manager + nonces nonceSourceCreator } // NewJWS Create a new JWS. -func NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager *nonces.Manager) *JWS { +func NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager nonceSourceCreator) *JWS { return &JWS{ privKey: privateKey, nonces: nonceManager, @@ -31,50 +34,24 @@ func NewJWS(privateKey crypto.PrivateKey, kid string, nonceManager *nonces.Manag // SignContent Signs a content with the JWS. func (j *JWS) SignContent(ctx context.Context, url string, content []byte) (*jose.JSONWebSignature, error) { - var alg jose.SignatureAlgorithm - - switch k := j.privKey.(type) { - case *rsa.PrivateKey: - alg = jose.RS256 - case *ecdsa.PrivateKey: - if k.Curve == elliptic.P256() { - alg = jose.ES256 - } else if k.Curve == elliptic.P384() { - alg = jose.ES384 - } - } - signKey := jose.SigningKey{ - Algorithm: alg, + Algorithm: signatureAlgorithm(j.privKey), Key: jose.JSONWebKey{Key: j.privKey, KeyID: j.kid}, } - options := jose.SignerOptions{ - NonceSource: nonces.NewNonceSource(ctx, j.nonces), + options := &jose.SignerOptions{ + NonceSource: j.nonces.NewNonceSource(ctx), ExtraHeaders: map[jose.HeaderKey]any{ "url": url, }, + EmbedJWK: j.kid == "", } - if j.kid == "" { - options.EmbedJWK = true - } - - signer, err := jose.NewSigner(signKey, &options) - if err != nil { - return nil, fmt.Errorf("failed to create jose signer: %w", err) - } - - signed, err := signer.Sign(content) - if err != nil { - return nil, fmt.Errorf("failed to sign content: %w", err) - } - - return signed, nil + return sign(content, signKey, options) } -// SignEABContent Signs an external account binding content with the JWS. -func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { +// SignEAB Signs an external account binding with the JWS. +func (j *JWS) SignEAB(url, kid string, hmac []byte) (*jose.JSONWebSignature, error) { jwk := jose.JSONWebKey{Key: j.privKey} jwkJSON, err := jwk.Public().MarshalJSON() @@ -82,23 +59,19 @@ func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignatu return nil, fmt.Errorf("acme: error encoding eab jwk key: %w", err) } - signer, err := jose.NewSigner( - jose.SigningKey{Algorithm: jose.HS256, Key: hmac}, - &jose.SignerOptions{ - EmbedJWK: false, - ExtraHeaders: map[jose.HeaderKey]any{ - "kid": kid, - "url": url, - }, + signKey := jose.SigningKey{Algorithm: jose.HS256, Key: hmac} + + options := &jose.SignerOptions{ + EmbedJWK: false, + ExtraHeaders: map[jose.HeaderKey]any{ + "kid": kid, + "url": url, }, - ) - if err != nil { - return nil, fmt.Errorf("failed to create External Account Binding jose signer: %w", err) } - signed, err := signer.Sign(jwkJSON) + signed, err := sign(jwkJSON, signKey, options) if err != nil { - return nil, fmt.Errorf("failed to External Account Binding sign content: %w", err) + return nil, fmt.Errorf("EAB: %w", err) } return signed, nil @@ -108,11 +81,9 @@ func (j *JWS) SignEABContent(url, kid string, hmac []byte) (*jose.JSONWebSignatu func (j *JWS) GetKeyAuthorization(token string) (string, error) { var publicKey crypto.PublicKey - switch k := j.privKey.(type) { - case *ecdsa.PrivateKey: - publicKey = k.Public() - case *rsa.PrivateKey: - publicKey = k.Public() + signer, ok := j.privKey.(crypto.Signer) + if ok { + publicKey = signer.Public() } // Generate the Key Authorization for the challenge @@ -128,3 +99,34 @@ func (j *JWS) GetKeyAuthorization(token string) (string, error) { return token + "." + keyThumb, nil } + +func sign(content []byte, signKey jose.SigningKey, options *jose.SignerOptions) (*jose.JSONWebSignature, error) { + signer, err := jose.NewSigner(signKey, options) + if err != nil { + return nil, fmt.Errorf("new jose signer: %w", err) + } + + signed, err := signer.Sign(content) + if err != nil { + return nil, fmt.Errorf("sign content: %w", err) + } + + return signed, nil +} + +func signatureAlgorithm(privKey crypto.PrivateKey) jose.SignatureAlgorithm { + var alg jose.SignatureAlgorithm + + switch k := privKey.(type) { + case *rsa.PrivateKey: + alg = jose.RS256 + case *ecdsa.PrivateKey: + if k.Curve == elliptic.P256() { + alg = jose.ES256 + } else if k.Curve == elliptic.P384() { + alg = jose.ES384 + } + } + + return alg +} diff --git a/acme/api/internal/secure/jws_test.go b/acme/api/internal/secure/jws_test.go new file mode 100644 index 000000000..e26247be8 --- /dev/null +++ b/acme/api/internal/secure/jws_test.go @@ -0,0 +1,78 @@ +package secure + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "testing" + + "github.com/go-jose/go-jose/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +type MockNonceManager struct{} + +func (f *MockNonceManager) NewNonceSource(ctx context.Context) jose.NonceSource { + return &MockNonceSource{} +} + +type MockNonceSource struct{} + +func (b *MockNonceSource) Nonce() (string, error) { + return "xxxNoncexxx", nil +} + +func TestJWS_SignContent(t *testing.T) { + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + jws := NewJWS(privKey, "https://example.com", &MockNonceManager{}) + + content, err := jws.SignContent(t.Context(), "https://foo.example", []byte("{}")) + require.NoError(t, err) + + check(t, content) +} + +func TestJWS_SignEAB(t *testing.T) { + privKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + jws := NewJWS(privKey, "https://example.com", &MockNonceManager{}) + + content, err := jws.SignEAB("https://foo.example/a", "https://foo.example/b", x509.MarshalPKCS1PrivateKey(privKey)) + require.NoError(t, err) + + check(t, content) +} + +func check(t *testing.T, content *jose.JSONWebSignature) { + t.Helper() + + serialized := content.FullSerialize() + + var data map[string]any + + err := json.Unmarshal([]byte(serialized), &data) + require.NoError(t, err) + + assert.Len(t, data, 3) + + assert.Contains(t, data, "protected") + assert.Contains(t, data, "payload") + assert.Contains(t, data, "signature") + + dec, err := base64.RawStdEncoding.DecodeString(data["protected"].(string)) + require.NoError(t, err) + + t.Log("protected:", string(dec)) + + dec, err = base64.RawStdEncoding.DecodeString(data["payload"].(string)) + require.NoError(t, err) + + t.Log("payload:", string(dec)) +}