refactor: JWS

This commit is contained in:
Fernandez Ludovic 2026-03-04 13:01:55 +01:00
commit ce732ecb75
5 changed files with 142 additions and 67 deletions

View file

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

View file

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

View file

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

View file

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

View file

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