Skip to content

Commit 7950328

Browse files
yaronfcursoragent
andcommitted
feat!: cut over to jwx v4 and ML-DSA on Go 1.27
Drop dual jwx v2/v3 support for a single NewJWSSigner/NewJWSVerifier path on lestrrat-go/jwx/v4, raise the Go floor to 1.27, and add an ML-DSA-65 foreign-JWS round-trip. Prepares httpsign v0.6.0. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent c057eef commit 7950328

9 files changed

Lines changed: 86 additions & 416 deletions

File tree

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
- name: Install Go
1212
uses: actions/setup-go@v6
1313
with:
14-
go-version: '1.24'
14+
go-version: '1.27'
1515
cache: false
1616
- name: golangci-lint
1717
uses: golangci/golangci-lint-action@v9

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
- name: Install Go
1212
uses: actions/setup-go@v6
1313
with:
14-
go-version: '1.24'
14+
go-version: '1.27'
1515
cache: false
1616
- name: Test
1717
run: go test ./...

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ fields.go / digest.go ← Component field abstraction + Content-Digest head
5151

5252
### JWX dual-version support
5353

54-
The library supports both `lestrrat-go/jwx/v2` (kept for backward compatibility) and `lestrrat-go/jwx/v3` (recommended for new code). Use `NewJWSSignerV3` / `NewJWSVerifierV3` for new integrations.
54+
Optional foreign JWS uses `lestrrat-go/jwx/v4` via `NewJWSSigner` / `NewJWSVerifier` (including ML-DSA with `crypto/mldsa` on Go 1.27+). Requires Go 1.27+.
5555

5656
### Content-Digest
5757

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ in the [API reference](https://pkg.go.dev/github.com/yaronf/httpsign).
2727
_ = res.Body.Close()
2828
```
2929
### Notes and Missing Features
30+
* Requires **Go 1.27+**.
31+
* Optional foreign JWS (including **ML-DSA** via `crypto/mldsa`) uses [`lestrrat-go/jwx/v4`](https://github.com/lestrrat-go/jwx) through `NewJWSSigner` / `NewJWSVerifier`.
3032
* The `Accept-Signature` header is unimplemented.
3133
* In responses, when using the "wrapped handler" feature, the `Content-Type` header is only signed if set explicitly by the server. This is different, but arguably more secure, than the normal `net.http` behavior.
3234
* **Behind a TLS-terminating reverse proxy:** The `@scheme` derived component defaults to `req.TLS != nil`. Behind nginx, Envoy, AWS ALB, etc., `req.TLS` is nil, so `@scheme` becomes `"http"` even for HTTPS traffic. Use `SetSchemeFromRequest` on `SignConfig` and `VerifyConfig` to derive the scheme from `X-Forwarded-Proto` or similar headers.

crypto.go

Lines changed: 24 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,8 @@ import (
1313
"crypto/subtle"
1414
"fmt"
1515

16-
// JWX v2 - for backward compatibility (used by existing NewJWSSigner/NewJWSVerifier)
17-
"github.com/lestrrat-go/jwx/v2/jwa"
18-
"github.com/lestrrat-go/jwx/v2/jws"
19-
20-
// JWX v3 - for new V3 functions (used by NewJWSSignerV3/NewJWSVerifierV3)
21-
jwav3 "github.com/lestrrat-go/jwx/v3/jwa"
22-
jwsv3 "github.com/lestrrat-go/jwx/v3/jws"
16+
"github.com/lestrrat-go/jwx/v4/jwa"
17+
"github.com/lestrrat-go/jwx/v4/jws"
2318
)
2419

2520
// Signer includes a cryptographic key (typically a private key) and configuration of what needs to be signed.
@@ -131,51 +126,20 @@ func NewEd25519SignerFromSeed(seed []byte, config *SignConfig, fields Fields) (*
131126
return NewEd25519Signer(key, config, fields)
132127
}
133128

134-
// NewJWSSigner creates a generic signer for JWS algorithms, using the go-jwx v2 package. The particular key type for each algorithm
135-
// is documented in that package.
136-
// Config may be nil for a default configuration.
137-
//
138-
// Note: This function uses jwx v2. For jwx v3 support, use NewJWSSignerV3 instead.
129+
// NewJWSSigner creates a generic signer for JWS algorithms via github.com/lestrrat-go/jwx/v4.
130+
// The particular key type for each algorithm is documented in that package (including
131+
// crypto/mldsa keys for ML-DSA on Go 1.27+). Config may be nil for a default configuration.
139132
func NewJWSSigner(alg jwa.SignatureAlgorithm, key interface{}, config *SignConfig, fields Fields) (*Signer, error) {
140133
if key == nil {
141134
return nil, fmt.Errorf("key must not be nil")
142135
}
143-
if alg == jwa.NoSignature {
144-
return nil, fmt.Errorf("the NONE signing algorithm is expressly disallowed")
145-
}
146-
if config == nil {
147-
config = NewSignConfig()
148-
}
149-
jwsSigner, err := jws.NewSigner(alg)
150-
if err != nil {
151-
return nil, err
152-
}
153-
return &Signer{
154-
key: key,
155-
alg: "",
156-
config: config,
157-
fields: fields,
158-
foreignSigner: jwsSigner,
159-
}, nil
160-
}
161-
162-
// NewJWSSignerV3 creates a generic signer for JWS algorithms, using the go-jwx v3 package. The particular key type for each algorithm
163-
// is documented in that package.
164-
// Config may be nil for a default configuration.
165-
//
166-
// This function uses jwx v3 and is the recommended choice for new code using jwx v3.
167-
// It uses the recommended SignerFor() API which returns Signer2 interface.
168-
func NewJWSSignerV3(alg jwav3.SignatureAlgorithm, key interface{}, config *SignConfig, fields Fields) (*Signer, error) {
169-
if key == nil {
170-
return nil, fmt.Errorf("key must not be nil")
171-
}
172-
if alg == jwav3.NoSignature() {
136+
if alg == jwa.NoSignature() {
173137
return nil, fmt.Errorf("the NONE signing algorithm is expressly disallowed")
174138
}
175139
if config == nil {
176140
config = NewSignConfig()
177141
}
178-
jwsSigner, err := jwsv3.SignerFor(alg)
142+
jwsSigner, err := jws.SignerFor(alg)
179143
if err != nil {
180144
return nil, err
181145
}
@@ -190,21 +154,11 @@ func NewJWSSignerV3(alg jwav3.SignatureAlgorithm, key interface{}, config *SignC
190154

191155
func (s Signer) sign(buff []byte) ([]byte, error) {
192156
if s.foreignSigner != nil {
193-
// Try v2 signer first (jws.Signer interface: Sign(payload, key))
194-
if signerV2, ok := s.foreignSigner.(jws.Signer); ok {
195-
return signerV2.Sign(buff, s.key)
196-
}
197-
198-
// Try v3 Signer2 interface (new recommended API: Sign(key, payload))
199-
// Note: parameter order is SWAPPED compared to v2!
200-
type Signer2 interface {
201-
Sign(key interface{}, payload []byte) ([]byte, error)
202-
}
203-
if signerV3, ok := s.foreignSigner.(Signer2); ok {
204-
return signerV3.Sign(s.key, buff) // Note: key first, payload second
157+
signer, ok := s.foreignSigner.(jws.Signer)
158+
if !ok {
159+
return nil, fmt.Errorf("expected jws.Signer, got %T", s.foreignSigner)
205160
}
206-
207-
return nil, fmt.Errorf("expected jws.Signer or Signer2 interface, got %T", s.foreignSigner)
161+
return signer.Sign(s.key, buff)
208162
}
209163
switch s.alg {
210164
case "hmac-sha256":
@@ -369,51 +323,21 @@ func NewEd25519Verifier(key ed25519.PublicKey, config *VerifyConfig, fields Fiel
369323
}, nil
370324
}
371325

372-
// NewJWSVerifier creates a generic verifier for JWS algorithms, using the go-jwx v2 package. The particular key type for each algorithm
373-
// is documented in that package. Set config to nil for a default configuration.
326+
// NewJWSVerifier creates a generic verifier for JWS algorithms via github.com/lestrrat-go/jwx/v4.
327+
// The particular key type for each algorithm is documented in that package (including
328+
// crypto/mldsa keys for ML-DSA on Go 1.27+). Set config to nil for a default configuration.
374329
// Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
375-
//
376-
// Note: This function uses jwx v2. For jwx v3 support, use NewJWSVerifierV3 instead.
377330
func NewJWSVerifier(alg jwa.SignatureAlgorithm, key interface{}, config *VerifyConfig, fields Fields) (*Verifier, error) {
378331
if key == nil {
379332
return nil, fmt.Errorf("key must not be nil")
380333
}
381334
if config == nil {
382335
config = NewVerifyConfig()
383336
}
384-
if alg == jwa.NoSignature {
337+
if alg == jwa.NoSignature() {
385338
return nil, fmt.Errorf("the NONE signing algorithm is expressly disallowed")
386339
}
387-
verifier, err := jws.NewVerifier(alg)
388-
if err != nil {
389-
return nil, err
390-
}
391-
return &Verifier{
392-
key: key,
393-
alg: "",
394-
config: config,
395-
fields: fields,
396-
foreignVerifier: verifier,
397-
}, nil
398-
}
399-
400-
// NewJWSVerifierV3 creates a generic verifier for JWS algorithms, using the go-jwx v3 package. The particular key type for each algorithm
401-
// is documented in that package. Set config to nil for a default configuration.
402-
// Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
403-
//
404-
// This function uses jwx v3 and is the recommended choice for new code using jwx v3.
405-
// It uses the recommended VerifierFor() API which returns Verifier2 interface.
406-
func NewJWSVerifierV3(alg jwav3.SignatureAlgorithm, key interface{}, config *VerifyConfig, fields Fields) (*Verifier, error) {
407-
if key == nil {
408-
return nil, fmt.Errorf("key must not be nil")
409-
}
410-
if config == nil {
411-
config = NewVerifyConfig()
412-
}
413-
if alg == jwav3.NoSignature() {
414-
return nil, fmt.Errorf("the NONE signing algorithm is expressly disallowed")
415-
}
416-
verifier, err := jwsv3.VerifierFor(alg)
340+
verifier, err := jws.VerifierFor(alg)
417341
if err != nil {
418342
return nil, err
419343
}
@@ -428,31 +352,16 @@ func NewJWSVerifierV3(alg jwav3.SignatureAlgorithm, key interface{}, config *Ver
428352

429353
func (v Verifier) verify(buff []byte, sig []byte) (bool, error) {
430354
if v.foreignVerifier != nil {
431-
// Try v2 verifier first (jws.Verifier interface: Verify(payload, sig, key))
432-
if verifierV2, ok := v.foreignVerifier.(jws.Verifier); ok {
433-
err := verifierV2.Verify(buff, sig, v.key)
434-
if err != nil {
435-
// Return opaque error; underlying err discarded for consistency
436-
return false, fmt.Errorf("signature verification failed")
437-
}
438-
return true, nil
439-
}
440-
441-
// Try v3 Verifier2 interface (new recommended API: Verify(key, payload, sig))
442-
// Note: parameter order is DIFFERENT compared to v2!
443-
type Verifier2 interface {
444-
Verify(key interface{}, payload, signature []byte) error
355+
verifier, ok := v.foreignVerifier.(jws.Verifier)
356+
if !ok {
357+
return false, fmt.Errorf("expected jws.Verifier, got %T", v.foreignVerifier)
445358
}
446-
if verifierV3, ok := v.foreignVerifier.(Verifier2); ok {
447-
err := verifierV3.Verify(v.key, buff, sig) // Note: key first, then payload, then signature
448-
if err != nil {
449-
// Return opaque error; underlying err discarded for consistency
450-
return false, fmt.Errorf("signature verification failed")
451-
}
452-
return true, nil
359+
err := verifier.Verify(v.key, buff, sig)
360+
if err != nil {
361+
// Return opaque error; underlying err discarded for consistency
362+
return false, fmt.Errorf("signature verification failed")
453363
}
454-
455-
return false, fmt.Errorf("expected jws.Verifier or Verifier2 interface, got %T", v.foreignVerifier)
364+
return true, nil
456365
}
457366

458367
switch v.alg {

0 commit comments

Comments
 (0)