├── staticcheck.conf
├── .gitignore
├── .idea
├── vcs.xml
├── .gitignore
├── misc.xml
└── modules.xml
├── .editorconfig
├── httpsign.iml
├── .github
└── workflows
│ ├── test.yml
│ └── codeql-analysis.yml
├── internal-docs
├── README.md
├── JWX_V3_RESEARCH_FINDINGS.md
└── JWX_V3_IMPLEMENTATION_SUMMARY.md
├── doc.go
├── config_test.go
├── go.mod
├── message_test.go
├── ecdsa.go
├── fields_test.go
├── signaturesex_test.go
├── README.md
├── ecdsa_test.go
├── urlencode_test.go
├── handlerex_test.go
├── digest_test.go
├── digest.go
├── client.go
├── clientex_test.go
├── go.sum
├── message.go
├── urlencode.go
├── handler.go
├── httpparse.go
├── http2_test.go
├── handler_test.go
├── client_test.go
├── trailer_test.go
├── fields.go
├── fuzz_test.go
├── LICENSE
├── config.go
├── crypto_test.go
└── crypto.go
/staticcheck.conf:
--------------------------------------------------------------------------------
1 | checks = ["all"]
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /http2/
2 | /fuzz/go.sum
3 | /.idea/inspectionProfiles/Project_Default.xml
4 | /testdata/fuzz/FuzzSignAndVerifyHMAC/
5 |
6 | .qodo
7 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # See https://editorconfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Go files
7 | [*._test.go]
8 | # Preserve trailing whitespace in tests since some depend on it
9 | trim_trailing_whitespace = false
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/httpsign.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | on: [push, pull_request]
2 | name: Test
3 | jobs:
4 | test:
5 | runs-on: ubuntu-latest
6 | env:
7 | GOTOOLCHAIN: local
8 | steps:
9 | - name: Checkout code
10 | uses: actions/checkout@v4
11 | - name: Install Go
12 | uses: actions/setup-go@v4
13 | with:
14 | go-version: '1.24'
15 | cache: false
16 | - name: Test
17 | run: go test ./...
18 |
--------------------------------------------------------------------------------
/internal-docs/README.md:
--------------------------------------------------------------------------------
1 | # Internal Documentation
2 |
3 | This directory contains internal documentation for maintainers of the httpsign library.
4 |
5 | ## Contents
6 |
7 | - **JWX_V3_MIGRATION_PLAN.md** - Comprehensive plan for migrating from jwx v2 to v3, including implementation strategy, testing requirements, and timeline.
8 |
9 | ## Purpose
10 |
11 | Internal documentation includes:
12 | - Migration plans for dependencies
13 | - Implementation strategies
14 | - Technical decision records
15 | - Maintenance guides
16 | - Internal architecture notes
17 |
18 | This documentation is not meant for end users of the library. For user-facing documentation, see the main README.md.
19 |
20 |
--------------------------------------------------------------------------------
/doc.go:
--------------------------------------------------------------------------------
1 | // Package httpsign signs HTTP requests and responses as defined in RFC 9421, formerly draft-ietf-httpbis-message-signatures.
2 | //
3 | // For client-side message signing and verification, use the Client wrapper.
4 | // Alternatively you can use SignRequest, VerifyResponse etc. directly, but this is more complicated.
5 | // For server-side operation,
6 | // WrapHandler installs a wrapper around a normal HTTP message handler.
7 | // Digest functionality (creation and validation of the Content-Digest header) is available automatically
8 | // through the Client and WrapHandler interfaces, otherwise it is available separately.
9 | // Use Message and its Verify method if you need more flexibility such as in a non-HTTP context.
10 | package httpsign
11 |
--------------------------------------------------------------------------------
/config_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestConfig_SetSignCreated(t *testing.T) {
9 | type fields struct {
10 | signAlg bool
11 | signCreated bool
12 | fakeCreated int64
13 | }
14 | type args struct {
15 | b bool
16 | }
17 | tests := []struct {
18 | name string
19 | fields fields
20 | args args
21 | want *SignConfig
22 | }{
23 | {
24 | name: "happy path",
25 | fields: fields{
26 | signAlg: false,
27 | signCreated: false,
28 | fakeCreated: 8,
29 | },
30 | args: args{b: true},
31 | want: &SignConfig{
32 | signAlg: false,
33 | signCreated: true,
34 | fakeCreated: 8,
35 | },
36 | },
37 | }
38 | for _, tt := range tests {
39 | t.Run(tt.name, func(t *testing.T) {
40 | c := SignConfig{
41 | signAlg: tt.fields.signAlg,
42 | signCreated: tt.fields.signCreated,
43 | fakeCreated: tt.fields.fakeCreated,
44 | }
45 | if got := c.SignCreated(tt.args.b); !reflect.DeepEqual(got, tt.want) {
46 | t.Errorf("SignCreated() = %v, want %v", got, tt.want)
47 | }
48 | })
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/yaronf/httpsign
2 |
3 | go 1.24.0
4 |
5 | toolchain go1.24.1
6 |
7 | require (
8 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
9 | github.com/dunglas/httpsfv v1.0.2
10 | github.com/lestrrat-go/jwx/v2 v2.1.2
11 | github.com/lestrrat-go/jwx/v3 v3.0.12
12 | github.com/stretchr/testify v1.11.1
13 | )
14 |
15 | require (
16 | github.com/davecgh/go-spew v1.1.1 // indirect
17 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
18 | github.com/goccy/go-json v0.10.3 // indirect
19 | github.com/lestrrat-go/blackmagic v1.0.4 // indirect
20 | github.com/lestrrat-go/dsig v1.0.0 // indirect
21 | github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
22 | github.com/lestrrat-go/httpcc v1.0.1 // indirect
23 | github.com/lestrrat-go/httprc v1.0.6 // indirect
24 | github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
25 | github.com/lestrrat-go/iter v1.0.2 // indirect
26 | github.com/lestrrat-go/option v1.0.1 // indirect
27 | github.com/lestrrat-go/option/v2 v2.0.0 // indirect
28 | github.com/pmezard/go-difflib v1.0.0 // indirect
29 | github.com/segmentio/asm v1.2.1 // indirect
30 | github.com/sergi/go-diff v1.3.1 // indirect
31 | github.com/valyala/fastjson v1.6.4 // indirect
32 | golang.org/x/crypto v0.43.0 // indirect
33 | golang.org/x/sys v0.37.0 // indirect
34 | gopkg.in/yaml.v3 v3.0.1 // indirect
35 | )
36 |
--------------------------------------------------------------------------------
/message_test.go:
--------------------------------------------------------------------------------
1 | package httpsign_test
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "net/http"
8 | "strings"
9 |
10 | "github.com/yaronf/httpsign"
11 | )
12 |
13 | func ExampleMessage_Verify() {
14 | config := httpsign.NewVerifyConfig().SetKeyID("my-shared-secret").SetVerifyCreated(false) // for testing only
15 | verifier, _ := httpsign.NewHMACSHA256Verifier(bytes.Repeat([]byte{0x77}, 64), config,
16 | httpsign.Headers("@authority", "Date", "@method"))
17 | reqStr := `GET /foo HTTP/1.1
18 | Host: example.org
19 | Date: Tue, 20 Apr 2021 02:07:55 GMT
20 | Cache-Control: max-age=60
21 | Signature-Input: sig77=("@authority" "date" "@method");alg="hmac-sha256";keyid="my-shared-secret"
22 | Signature: sig77=:3e9KqLP62NHfHY5OMG4036+U6tvBowZF35ALzTjpsf0=:
23 |
24 | `
25 | req, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(reqStr)))
26 |
27 | // Using WithRequest
28 | msgWithRequest, _ := httpsign.NewMessage(httpsign.NewMessageConfig().WithRequest(req))
29 | _, err1 := msgWithRequest.Verify("sig77", *verifier)
30 |
31 | // Using constituent parts
32 | msgWithConstituents, _ := httpsign.NewMessage(httpsign.NewMessageConfig().
33 | WithMethod(req.Method).
34 | WithURL(req.URL).
35 | WithHeaders(req.Header).
36 | WithTrailers(req.Trailer).
37 | WithBody(&req.Body).
38 | WithAuthority(req.Host).
39 | WithScheme(req.URL.Scheme))
40 |
41 | _, err2 := msgWithConstituents.Verify("sig77", *verifier)
42 |
43 | fmt.Printf("WithRequest: %t\n", err1 == nil)
44 | fmt.Printf("Constituents: %t", err2 == nil)
45 | // Output:
46 | // WithRequest: true
47 | // Constituents: true
48 | }
49 |
--------------------------------------------------------------------------------
/ecdsa.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "crypto/ecdsa"
5 | "fmt"
6 | "io"
7 | "math/big"
8 | )
9 |
10 | // These functions extend the ecdsa package by adding raw, JWS-style signatures
11 |
12 | func ecdsaSignRaw(rd io.Reader, priv *ecdsa.PrivateKey, hash []byte) ([]byte, error) {
13 | if priv == nil {
14 | return nil, fmt.Errorf("nil private key")
15 | }
16 | r, s, err := ecdsa.Sign(rd, priv, hash)
17 | if err != nil {
18 | return nil, err
19 | }
20 | curve := priv.PublicKey.Params().Name
21 | lr, ls, err := sigComponentLen(curve)
22 | if err != nil {
23 | return nil, err
24 | }
25 | rb, sb := make([]byte, lr), make([]byte, ls)
26 | if r.BitLen() > 8*lr || s.BitLen() > 8*ls {
27 | return nil, fmt.Errorf("signature values too long")
28 | }
29 | r.FillBytes(rb)
30 | s.FillBytes(sb)
31 | return append(rb, sb...), nil
32 | }
33 |
34 | func ecdsaVerifyRaw(pub *ecdsa.PublicKey, hash []byte, sig []byte) (bool, error) {
35 | if pub == nil {
36 | return false, fmt.Errorf("nil public key")
37 | }
38 | curve := pub.Params().Name
39 | lr, ls, err := sigComponentLen(curve)
40 | if err != nil {
41 | return false, err
42 | }
43 | if len(sig) != lr+ls {
44 | return false, fmt.Errorf("signature length is %d, expecting %d", len(sig), lr+ls)
45 | }
46 | r := new(big.Int)
47 | r.SetBytes(sig[0:lr])
48 | s := new(big.Int)
49 | s.SetBytes(sig[lr : lr+ls])
50 | return ecdsa.Verify(pub, hash, r, s), nil
51 | }
52 |
53 | func sigComponentLen(curve string) (int, int, error) {
54 | var lr, ls int
55 | switch curve {
56 | case "P-256":
57 | lr = 32
58 | ls = 32
59 | case "P-384":
60 | lr = 48
61 | ls = 48
62 | default:
63 | return 0, 0, fmt.Errorf("unknown curve \"%s\"", curve)
64 | }
65 | return lr, ls, nil
66 | }
67 |
--------------------------------------------------------------------------------
/fields_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "github.com/dunglas/httpsfv"
5 | "github.com/stretchr/testify/assert"
6 | "testing"
7 | )
8 |
9 | func TestFields_asSignatureInput(t *testing.T) {
10 | type args struct {
11 | p *httpsfv.Params
12 | }
13 | tests := []struct {
14 | name string
15 | fs Fields
16 | args args
17 | want string
18 | wantErr bool
19 | }{
20 | {
21 | name: "Just headers",
22 | fs: Headers("hdr1", "hdr2", "@Hdr3"),
23 | args: args{
24 | p: httpsfv.NewParams(),
25 | },
26 | want: `("hdr1" "hdr2" "@hdr3")`,
27 | wantErr: false,
28 | },
29 | {
30 | name: "Misc components",
31 | fs: func() Fields {
32 | f := NewFields()
33 | f.AddHeader("hdr-Name")
34 | f.AddQueryParam("qparamname")
35 | return *f
36 | }(),
37 | args: args{
38 | p: httpsfv.NewParams(),
39 | },
40 | want: `("hdr-name" "@query-param";name="qparamname")`,
41 | wantErr: false,
42 | },
43 | }
44 | for _, tt := range tests {
45 | t.Run(tt.name, func(t *testing.T) {
46 | got, err := tt.fs.asSignatureInput(tt.args.p)
47 | if (err != nil) != tt.wantErr {
48 | t.Errorf("asSignatureBase() error = %v, wantErr %v", err, tt.wantErr)
49 | return
50 | }
51 | if got != tt.want {
52 | t.Errorf("asSignatureBase() got = %v, want %v", got, tt.want)
53 | }
54 | })
55 | }
56 | }
57 |
58 | func Test_field_String(t *testing.T) {
59 | tests := []struct {
60 | name string
61 | f field
62 | want string
63 | }{
64 | {
65 | name: "field to string",
66 | f: *fromQueryParam("qp1"),
67 | want: "\"@query-param\";name=\"qp1\"",
68 | },
69 | }
70 | for _, tt := range tests {
71 | t.Run(tt.name, func(t *testing.T) {
72 | assert.Equalf(t, tt.want, tt.f.String(), "String()")
73 | })
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/signaturesex_test.go:
--------------------------------------------------------------------------------
1 | package httpsign_test
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "net/http"
8 | "strings"
9 |
10 | "github.com/yaronf/httpsign"
11 | )
12 |
13 | func ExampleSignRequest() {
14 | config := httpsign.NewSignConfig().SignCreated(false).SetNonce("BADCAB").SetKeyID("my-shared-secret") // SignCreated should be "true" to protect against replay attacks
15 | fields := httpsign.Headers("@authority", "Date", "@method")
16 | signer, _ := httpsign.NewHMACSHA256Signer(bytes.Repeat([]byte{0x77}, 64), config, fields)
17 | reqStr := `GET /foo HTTP/1.1
18 | Host: example.org
19 | Date: Tue, 20 Apr 2021 02:07:55 GMT
20 | Cache-Control: max-age=60
21 |
22 | `
23 | req, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(reqStr)))
24 | signatureInput, signature, _ := httpsign.SignRequest("sig77", *signer, req)
25 | fmt.Printf("Signature-Input: %s\n", signatureInput)
26 | fmt.Printf("Signature: %s", signature)
27 | // Output: Signature-Input: sig77=("@authority" "date" "@method");nonce="BADCAB";alg="hmac-sha256";keyid="my-shared-secret"
28 | //Signature: sig77=:BBxhfE6GoDVcohZvc+pT448u7GAK7EjJYTu+i26YZW0=:
29 | }
30 |
31 | func ExampleVerifyRequest() {
32 | config := httpsign.NewVerifyConfig().SetKeyID("my-shared-secret").SetVerifyCreated(false) // for testing only
33 | verifier, _ := httpsign.NewHMACSHA256Verifier(bytes.Repeat([]byte{0x77}, 64), config,
34 | httpsign.Headers("@authority", "Date", "@method"))
35 | reqStr := `GET /foo HTTP/1.1
36 | Host: example.org
37 | Date: Tue, 20 Apr 2021 02:07:55 GMT
38 | Cache-Control: max-age=60
39 | Signature-Input: sig77=("@authority" "date" "@method");alg="hmac-sha256";keyid="my-shared-secret"
40 | Signature: sig77=:3e9KqLP62NHfHY5OMG4036+U6tvBowZF35ALzTjpsf0=:
41 |
42 | `
43 | req, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(reqStr)))
44 | err := httpsign.VerifyRequest("sig77", *verifier, req)
45 | fmt.Printf("verified: %t", err == nil)
46 | // Output: verified: true
47 | }
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | A Golang implementation of HTTP Message Signatures, as defined by
2 | [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421.html)
3 | (the former [draft-ietf-httpbis-message-signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures)).
4 |
5 | This is a nearly feature-complete implementation of the RFC, including all test vectors.
6 |
7 | ### Usage
8 |
9 | The library provides natural integration points with Go HTTP clients and servers, as well as direct usage of the
10 | _sign_ and _verify_ functions.
11 |
12 | Below is what a basic client-side integration looks like. Additional examples are available
13 | in the [API reference](https://pkg.go.dev/github.com/yaronf/httpsign).
14 |
15 | ```cgo
16 | // Create a signer and a wrapped HTTP client
17 | signer, _ := httpsign.NewRSAPSSSigner(*prvKey, httpsign.NewSignConfig(),
18 | httpsign.Headers("@request-target", "content-digest")) // The Content-Digest header will be auto-generated
19 | client := httpsign.NewDefaultClient(httpsign.NewClientConfig().SetSignatureName("sig1").SetSigner(signer)) // sign requests, don't verify responses
20 |
21 | // Send an HTTP POST, get response -- signing happens behind the scenes
22 | body := `{"hello": "world"}`
23 | res, _ := client.Post(ts.URL, "application/json", bufio.NewReader(strings.NewReader(body)))
24 |
25 | // Read the response
26 | serverText, _ := io.ReadAll(res.Body)
27 | _ = res.Body.Close()
28 | ```
29 | ### Notes and Missing Features
30 | * The `Accept-Signature` header is unimplemented.
31 | * 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.
32 |
33 | ### Contributing
34 | Contributions to this project are welcome, both as issues and pull requests.
35 |
36 | [](https://pkg.go.dev/github.com/yaronf/httpsign)
37 | [](https://github.com/yaronf/httpsign/actions/workflows/test.yml)
38 | [](https://goreportcard.com/report/github.com/yaronf/httpsign)
39 | [](https://deepwiki.com/yaronf/httpsign)
40 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ main ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ main ]
20 | schedule:
21 | - cron: '35 18 * * 4'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 | permissions:
28 | actions: read
29 | contents: read
30 | security-events: write
31 |
32 | strategy:
33 | fail-fast: false
34 | matrix:
35 | language: [ 'go' ]
36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support
38 |
39 | steps:
40 | - name: Checkout repository
41 | uses: actions/checkout@v3
42 |
43 | # Initializes the CodeQL tools for scanning.
44 | - name: Initialize CodeQL
45 | uses: github/codeql-action/init@v3
46 | with:
47 | languages: ${{ matrix.language }}
48 | # If you wish to specify custom queries, you can do so here or in a config file.
49 | # By default, queries listed here will override any specified in a config file.
50 | # Prefix the list here with "+" to use these queries and those in the config file.
51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
52 |
53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
54 | # If this step fails, then you should remove it and run the build manually (see below)
55 | - name: Autobuild
56 | uses: github/codeql-action/autobuild@v3
57 |
58 | # ℹ️ Command-line programs to run using the OS shell.
59 | # 📚 https://git.io/JvXDl
60 |
61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
62 | # and modify them (or add more) to build your code if your project
63 | # uses a compiled language
64 |
65 | #- run: |
66 | # make bootstrap
67 | # make release
68 |
69 | - name: Perform CodeQL Analysis
70 | uses: github/codeql-action/analyze@v3
71 |
--------------------------------------------------------------------------------
/ecdsa_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "bytes"
5 | "crypto/ecdsa"
6 | "crypto/elliptic"
7 | "crypto/rand"
8 | "testing"
9 | )
10 |
11 | func Test_sigComponentLen(t *testing.T) {
12 | type args struct {
13 | curve string
14 | }
15 | tests := []struct {
16 | name string
17 | args args
18 | want int
19 | want1 int
20 | wantErr bool
21 | }{
22 | {
23 | name: "bad curve",
24 | args: args{
25 | curve: "P-77",
26 | },
27 | want: 0,
28 | want1: 0,
29 | wantErr: true,
30 | },
31 | }
32 | for _, tt := range tests {
33 | t.Run(tt.name, func(t *testing.T) {
34 | got, got1, err := sigComponentLen(tt.args.curve)
35 | if (err != nil) != tt.wantErr {
36 | t.Errorf("sigComponentLen() error = %v, wantErr %v", err, tt.wantErr)
37 | return
38 | }
39 | if got != tt.want {
40 | t.Errorf("sigComponentLen() got = %v, want %v", got, tt.want)
41 | }
42 | if got1 != tt.want1 {
43 | t.Errorf("sigComponentLen() got1 = %v, want %v", got1, tt.want1)
44 | }
45 | })
46 | }
47 | }
48 |
49 | func Test_ecdsaVerifyRaw(t *testing.T) {
50 | privKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
51 | if err != nil {
52 | t.Errorf("Failed to generate private key")
53 | }
54 | pubKey := privKey.Public().(*ecdsa.PublicKey)
55 | privKey2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
56 | if err != nil {
57 | t.Errorf("Failed to generate private key")
58 | }
59 | pubKey2 := privKey2.Public().(*ecdsa.PublicKey)
60 | type args struct {
61 | pub *ecdsa.PublicKey
62 | hash []byte
63 | sig []byte
64 | }
65 | tests := []struct {
66 | name string
67 | args args
68 | want bool
69 | wantErr bool
70 | }{
71 | {
72 | name: "nil pub",
73 | args: args{
74 | pub: nil,
75 | hash: bytes.Repeat([]byte{88}, 1024),
76 | sig: nil,
77 | },
78 | want: false,
79 | wantErr: true,
80 | },
81 | {
82 | name: "bad curve",
83 | args: args{
84 | pub: pubKey,
85 | hash: bytes.Repeat([]byte{88}, 1024),
86 | sig: nil,
87 | },
88 | want: false,
89 | wantErr: true,
90 | },
91 | {
92 | name: "bad sig",
93 | args: args{
94 | pub: pubKey2,
95 | hash: bytes.Repeat([]byte{88}, 1024),
96 | sig: nil,
97 | },
98 | want: false,
99 | wantErr: true,
100 | },
101 | }
102 | for _, tt := range tests {
103 | t.Run(tt.name, func(t *testing.T) {
104 | got, err := ecdsaVerifyRaw(tt.args.pub, tt.args.hash, tt.args.sig)
105 | if (err != nil) != tt.wantErr {
106 | t.Errorf("ecdsaVerifyRaw() error = %v, wantErr %v", err, tt.wantErr)
107 | return
108 | }
109 | if got != tt.want {
110 | t.Errorf("ecdsaVerifyRaw() got = %v, want %v", got, tt.want)
111 | }
112 | })
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/urlencode_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import "testing"
4 |
5 | // Correctly identifies unreserved alphanumeric characters as not needing escape
6 | func TestUnreservedAlphanumericCharacters(t *testing.T) {
7 | for c := byte('a'); c <= byte('z'); c++ {
8 | if shouldEscape(c, encodePath) {
9 | t.Errorf("Expected %c not to be escaped", c)
10 | }
11 | }
12 | for c := byte('A'); c <= byte('Z'); c++ {
13 | if shouldEscape(c, encodePath) {
14 | t.Errorf("Expected %c not to be escaped", c)
15 | }
16 | }
17 | for c := byte('0'); c <= byte('9'); c++ {
18 | if shouldEscape(c, encodePath) {
19 | t.Errorf("Expected %c not to be escaped", c)
20 | }
21 | }
22 | }
23 |
24 | // Handles empty input or non-ASCII characters gracefully
25 | // Ensure that empty input is correctly identified as not needing escape.
26 | func TestEmptyInput(t *testing.T) {
27 | if !shouldEscape(0, encodePath) {
28 | t.Error("Expected empty input to be escaped")
29 | }
30 | }
31 |
32 | // Properly escapes characters in encodeQueryComponent and encodeQueryComponentForSignature modes
33 | func TestEscapeInQueryComponentModes(t *testing.T) {
34 | reservedChars := []byte{'$', '&', '+', ',', '/', ':', ';', '=', '?', '@'}
35 | for _, mode := range []encoding{encodeQueryComponent, encodeQueryComponentForSignature} {
36 | for _, c := range reservedChars {
37 | if !shouldEscape(c, mode) {
38 | t.Errorf("shouldEscape(%q, %v) = false; want true", c, mode)
39 | }
40 | }
41 | }
42 | }
43 |
44 | // Encodes spaces as '+' when mode is encodeQueryComponent
45 | func TestEncodeSpacesAsPlus(t *testing.T) {
46 | input := "hello world"
47 | expected := "hello+world"
48 | result := escape(input, encodeQueryComponent)
49 | if result != expected {
50 | t.Errorf("Expected %s, but got %s", expected, result)
51 | }
52 | }
53 |
54 | // Handles empty strings without errors
55 | func TestHandleEmptyString(t *testing.T) {
56 | input := ""
57 | expected := ""
58 | result := escape(input, encodeQueryComponent)
59 | if result != expected {
60 | t.Errorf("Expected %s, but got %s", expected, result)
61 | }
62 | }
63 |
64 | // Encodes characters as '%XX' when they should be escaped
65 | func TestEscapeEncodesCharacters(t *testing.T) {
66 | input := "hello world!"
67 | expected := "hello%20world%21"
68 | result := escape(input, encodeQueryComponentForSignature)
69 | if result != expected {
70 | t.Errorf("Expected %s, but got %s", expected, result)
71 | }
72 | }
73 |
74 | // Returns the original string if no characters need escaping
75 | func TestEscapeReturnsOriginalString(t *testing.T) {
76 | input := "helloworld"
77 | expected := "helloworld"
78 | result := escape(input, encodeQueryComponent)
79 | if result != expected {
80 | t.Errorf("Expected %s, but got %s", expected, result)
81 | }
82 | }
83 |
84 | // Handles strings with mixed characters requiring different encodings
85 | func TestEscapeHandlesMixedCharacters(t *testing.T) {
86 | input := "hello world! @2023"
87 | expected := "hello+world%21+%402023"
88 | result := escape(input, encodeQueryComponent)
89 | if result != expected {
90 | t.Errorf("Expected %s, but got %s", expected, result)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/handlerex_test.go:
--------------------------------------------------------------------------------
1 | package httpsign_test
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "fmt"
7 | "github.com/yaronf/httpsign"
8 | "io"
9 | "log"
10 | "net/http"
11 | "net/http/httptest"
12 | "strings"
13 | )
14 |
15 | func ExampleWrapHandler_clientSigns() {
16 | // Note: client/server examples may fail in the Go Playground, https://github.com/golang/go/issues/45855
17 | // Callback to let the server locate its verifying key and configuration
18 | fetchVerifier := func(r *http.Request) (string, *httpsign.Verifier) {
19 | sigName := "sig1"
20 | verifier, _ := httpsign.NewHMACSHA256Verifier(bytes.Repeat([]byte{0x99}, 64), httpsign.NewVerifyConfig().SetKeyID("key1"),
21 | httpsign.Headers("@method"))
22 | return sigName, verifier
23 | }
24 |
25 | // The basic handler (HTTP server) that gets wrapped
26 | simpleHandler := func(w http.ResponseWriter, r *http.Request) {
27 | w.WriteHeader(200)
28 | w.Header().Set("bar", "baz")
29 | fmt.Fprintln(w, "Hey client, your message verified just fine")
30 | }
31 |
32 | // Configure the wrapper and set it up
33 | config := httpsign.NewHandlerConfig().SetFetchVerifier(fetchVerifier)
34 | ts := httptest.NewServer(httpsign.WrapHandler(http.HandlerFunc(simpleHandler), *config))
35 | defer ts.Close()
36 |
37 | // HTTP client code, with a signer
38 | signer, _ := httpsign.NewHMACSHA256Signer(bytes.Repeat([]byte{0x99}, 64), httpsign.NewSignConfig().SetKeyID("key1"),
39 | *httpsign.NewFields().AddHeader("content-type").AddQueryParam("pet").AddHeader("@method"))
40 |
41 | client := httpsign.NewDefaultClient(httpsign.NewClientConfig().SetSignatureName("sig1").SetSigner(signer))
42 | body := `{"hello": "world"}`
43 | host := ts.URL // test server
44 | path := "/foo?param=value&pet=dog"
45 | res, _ := client.Post(host+path, "application/json", bufio.NewReader(strings.NewReader(body)))
46 |
47 | serverText, _ := io.ReadAll(res.Body)
48 | res.Body.Close()
49 |
50 | fmt.Println("Status: ", res.Status)
51 | fmt.Println("Server sent: ", string(serverText))
52 | // output: Status: 200 OK
53 | //Server sent: Hey client, your message verified just fine
54 | }
55 |
56 | func ExampleWrapHandler_serverSigns() {
57 | // Note: client/server examples may fail in the Go Playground, https://github.com/golang/go/issues/45855
58 | // Callback to let the server locate its signing key and configuration
59 | fetchSigner := func(res http.Response, r *http.Request) (string, *httpsign.Signer) {
60 | sigName := "sig1"
61 | signer, _ := httpsign.NewHMACSHA256Signer(bytes.Repeat([]byte{0}, 64), httpsign.NewSignConfig().SetKeyID("key"),
62 | httpsign.Headers("@status", "bar", "date", "content-type"))
63 | return sigName, signer
64 | }
65 |
66 | simpleHandler := func(w http.ResponseWriter, r *http.Request) { // this handler gets wrapped
67 | w.WriteHeader(200)
68 | w.Header().Set("bar", "some text here") // note: a single word in the header value would be interpreted is a trivial dictionary!
69 | w.Header().Set("Content-Type", "text/plain")
70 | fmt.Fprintln(w, "Hello, client")
71 | }
72 |
73 | // Configure the wrapper and set it up
74 | config := httpsign.NewHandlerConfig().SetFetchSigner(fetchSigner)
75 | ts := httptest.NewServer(httpsign.WrapHandler(http.HandlerFunc(simpleHandler), *config))
76 | defer ts.Close()
77 |
78 | // HTTP client code
79 | verifier, _ := httpsign.NewHMACSHA256Verifier(bytes.Repeat([]byte{0}, 64), httpsign.NewVerifyConfig().SetKeyID("key"), *httpsign.NewFields())
80 | client := httpsign.NewDefaultClient(httpsign.NewClientConfig().SetSignatureName("sig1").SetVerifier(verifier))
81 | res, err := client.Get(ts.URL)
82 | if err != nil {
83 | log.Fatal(err)
84 | }
85 | serverText, err := io.ReadAll(res.Body)
86 | if err != nil {
87 | log.Fatal(err)
88 | }
89 | res.Body.Close()
90 |
91 | fmt.Println("Server sent: ", string(serverText))
92 | // output: Server sent: Hello, client
93 | }
94 |
--------------------------------------------------------------------------------
/digest_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "github.com/stretchr/testify/assert"
5 | "testing"
6 | )
7 |
8 | // digest draft, B.1
9 | var resdigest1 = `HTTP/1.1 200 OK
10 | Content-Type: application/json
11 | Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
12 |
13 | {"hello": "world"}`
14 |
15 | // digest draft, B.3
16 | var resdigest2 = `HTTP/1.1 206 Partial Content
17 | Content-Type: application/json
18 | Content-Range: bytes 1-7/18
19 | Content-Digest: sha-256=:Wqdirjg/u3J688ejbUlApbjECpiUUtIwT8lY/z81Tno=:
20 |
21 | "hello"`
22 |
23 | var resdigest3 = `HTTP/1.1 206 Partial Content
24 | Content-Type: application/json
25 | Content-Range: bytes 1-7/18
26 | Content-Digest: sha-256=:Wqdirjg/u3J688ejbUlApbjECpiUUtIwT8lY/z81Tno=:
27 |
28 | "hello!!"`
29 |
30 | var resdigest5 = `HTTP/1.1 206 Partial Content
31 | Content-Type: application/json
32 | Content-Range: bytes 1-7/18
33 | Content-Digest: sha-256=:Wqdirjg/u3J688ejbUlApbjECpiUUtIwT8lY/z81Tno=:, sha-512=:A8pplr4vsk4xdLkJruCXWp6+i+dy/3pSW5HW5ke1jDWS70Dv6Fstf1jS+XEcLqEVhW3i925IPlf/4tnpnvAQDw==:
34 |
35 | "hello"`
36 |
37 | var resdigest6 = `HTTP/1.1 200 OK
38 | Content-Type: application/json
39 | Content-Digest: sha-256=:X47E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
40 |
41 | {"hello": "world"}`
42 |
43 | func TestMessages(t *testing.T) {
44 | res1 := readResponse(resdigest1)
45 | d, err := GenerateContentDigestHeader(&res1.Body, []string{DigestSha256})
46 | assert.NoError(t, err, "should not fail to generate digest")
47 | assert.Equal(t, "sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:", d)
48 | h := res1.Header.Get("Content-Digest")
49 | assert.Equal(t, h, d)
50 |
51 | res2 := readResponse(resdigest2)
52 | d, err = GenerateContentDigestHeader(&res2.Body, []string{DigestSha256})
53 | assert.NoError(t, err, "should not fail to generate digest")
54 | h = res2.Header.Get("Content-Digest")
55 | assert.Equal(t, h, d)
56 |
57 | res3 := readResponse(resdigest3)
58 | d, err = GenerateContentDigestHeader(&res3.Body, []string{DigestSha256})
59 | assert.NoError(t, err, "should not fail to generate digest")
60 | h = res3.Header.Get("Content-Digest")
61 | assert.NotEqual(t, h, d)
62 |
63 | res4 := readResponse(resdigest3)
64 | _, err = GenerateContentDigestHeader(&res4.Body, []string{DigestSha256, "sha-999"})
65 | assert.Error(t, err, "bad digest scheme")
66 |
67 | res5 := readResponse(resdigest5)
68 | d, err = GenerateContentDigestHeader(&res5.Body, []string{DigestSha256, DigestSha512})
69 | assert.NoError(t, err, "should not fail to generate digest")
70 | h = res5.Header.Get("Content-Digest")
71 | assert.Equal(t, h, d)
72 | }
73 |
74 | func TestValidateContentDigestHeader(t *testing.T) {
75 | res1 := readResponse(resdigest1)
76 | hdr := res1.Header.Values("Content-Digest")
77 | err := ValidateContentDigestHeader(hdr, &res1.Body, []string{DigestSha256})
78 | assert.NoError(t, err, "should not fail")
79 |
80 | err = ValidateContentDigestHeader(hdr, &res1.Body, []string{})
81 | assert.Error(t, err, "empty list of accepted schemes")
82 |
83 | err = ValidateContentDigestHeader(hdr, &res1.Body, []string{"kuku"})
84 | assert.Error(t, err, "unknown scheme in list of accepted schemes")
85 |
86 | hdr = []string{"123"}
87 | err = ValidateContentDigestHeader(hdr, &res1.Body, []string{DigestSha256})
88 | assert.Error(t, err, "bad received header")
89 |
90 | hdr = res1.Header.Values("Content-Digest")
91 | err = ValidateContentDigestHeader(hdr, &res1.Body, []string{DigestSha512})
92 | assert.Error(t, err, "no acceptable scheme")
93 |
94 | res6 := readResponse(resdigest6)
95 | hdr = res6.Header.Values("Content-Digest")
96 | err = ValidateContentDigestHeader(hdr, &res6.Body, []string{DigestSha256})
97 | assert.Error(t, err, "digest mismatch")
98 |
99 | // Response taken from the draft,see https://github.com/httpwg/http-extensions/pull/2049
100 | res7 := readResponse(httpres4)
101 | hdr = res7.Header.Values("Content-Digest")
102 | err = ValidateContentDigestHeader(hdr, &res7.Body, []string{DigestSha512})
103 | assert.NoError(t, err, "digest mismatch?")
104 | }
105 |
--------------------------------------------------------------------------------
/digest.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha256"
6 | "crypto/sha512"
7 | "errors"
8 | "fmt"
9 | "github.com/dunglas/httpsfv"
10 | "io"
11 | )
12 |
13 | // Constants define the hash algorithm to be used for the digest
14 | const (
15 | DigestSha256 = "sha-256"
16 | DigestSha512 = "sha-512"
17 | )
18 |
19 | // GenerateContentDigestHeader generates a digest of the message body according to the given scheme(s)
20 | // (currently supporting DigestSha256 and DigestSha512).
21 | // Side effect: the message body is fully read, and replaced by a static buffer
22 | // containing the body contents.
23 | func GenerateContentDigestHeader(body *io.ReadCloser, schemes []string) (string, error) {
24 | if len(schemes) == 0 {
25 | return "", fmt.Errorf("received empty list of digest schemes")
26 | }
27 | err := validateSchemes(schemes)
28 | if err != nil {
29 | return "", err
30 | }
31 | buff, err := duplicateBody(body)
32 | if err != nil {
33 | return "", err
34 | }
35 | dict := httpsfv.NewDictionary()
36 | for _, scheme := range schemes {
37 | raw, err := rawDigest(buff.String(), scheme)
38 | if err != nil { // When sending, must recognize all schemes
39 | return "", err
40 | }
41 | i := httpsfv.NewItem(raw)
42 | dict.Add(scheme, httpsfv.Member(i))
43 | }
44 | return httpsfv.Marshal(dict)
45 | }
46 |
47 | // Note side effect: the value of body is replaced
48 | func duplicateBody(body *io.ReadCloser) (*bytes.Buffer, error) {
49 | buff := &bytes.Buffer{}
50 | if body != nil && *body != nil {
51 | _, err := buff.ReadFrom(*body)
52 | if err != nil {
53 | return nil, err
54 | }
55 |
56 | _ = (*body).Close()
57 |
58 | *body = io.NopCloser(bytes.NewReader(buff.Bytes()))
59 | }
60 | return buff, nil
61 | }
62 |
63 | var errUnknownDigestScheme = fmt.Errorf("unknown digest scheme")
64 |
65 | func rawDigest(s string, scheme string) ([]byte, error) {
66 | switch scheme {
67 | case DigestSha256:
68 | s := sha256.Sum256([]byte(s))
69 | return s[:], nil
70 | case DigestSha512:
71 | s := sha512.Sum512([]byte(s))
72 | return s[:], nil
73 | default:
74 | return nil, errUnknownDigestScheme
75 | }
76 | }
77 |
78 | func validateSchemes(schemes []string) error {
79 | valid := map[string]bool{DigestSha256: true, DigestSha512: true}
80 | for _, s := range schemes {
81 | if !valid[s] {
82 | return fmt.Errorf("invalid scheme %s", s)
83 | }
84 | }
85 | return nil
86 | }
87 |
88 | // ValidateContentDigestHeader validates that the Content-Digest header complies to policy: at least
89 | // one of the "accepted" schemes is used, and all known schemes are associated with a correct
90 | // digest of the message body. Schemes are constants defined in this file, e.g. DigestSha256.
91 | // Note that "received" is a string array, typically retrieved through the
92 | // "Values" method of the header. Returns nil if validation is successful.
93 | func ValidateContentDigestHeader(received []string, body *io.ReadCloser, accepted []string) error {
94 | if len(accepted) == 0 {
95 | return fmt.Errorf("received an empty list of acceptable digest schemes")
96 | }
97 | err := validateSchemes(accepted)
98 | if err != nil {
99 | return err
100 | }
101 | receivedDict, err := httpsfv.UnmarshalDictionary(received)
102 | if err != nil {
103 | return fmt.Errorf("received Content-Digest header: %w", err)
104 | }
105 | buff, err := duplicateBody(body)
106 | if err != nil {
107 | return err
108 | }
109 | var ok bool
110 | found:
111 | for _, a := range accepted {
112 | for _, r := range receivedDict.Names() {
113 | if a == r {
114 | ok = true
115 | break found
116 | }
117 | }
118 | }
119 | if !ok {
120 | return fmt.Errorf("no acceptable digest scheme found in Content-Digest header")
121 | }
122 | // But regardless of the list of accepted schemes, all included digest values (if recognized) must be correct
123 | for _, scheme := range receivedDict.Names() {
124 | raw, err := rawDigest(buff.String(), scheme)
125 | if errors.Is(err, errUnknownDigestScheme) {
126 | continue // unknown schemes are ignored
127 | } else if err != nil {
128 | return err
129 | }
130 | m, _ := receivedDict.Get(scheme)
131 | i, ok := m.(httpsfv.Item)
132 | if !ok {
133 | return fmt.Errorf("received Content-Digest header is malformed")
134 | }
135 | b, ok := i.Value.([]byte)
136 | if !ok {
137 | return fmt.Errorf("non-byte string in received Content-Digest header")
138 | }
139 | if !bytes.Equal(raw, b) {
140 | return fmt.Errorf("digest mismatch for scheme %s", scheme)
141 | }
142 | }
143 | return nil
144 | }
145 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "net/url"
8 | "strings"
9 | )
10 |
11 | // Client represents an HTTP client that optionally signs requests and optionally verifies responses.
12 | // The Signer may be nil to avoid signing. Similarly, if both Verifier and fetchVerifier are nil, no verification takes place.
13 | // The fetchVerifier callback allows to generate a Verifier based on the particular response.
14 | // Either Verifier or fetchVerifier may be specified, but not both.
15 | // The client embeds an http.Client, which in most cases can be http.DefaultClient.
16 | // Side effects: when signing, the Client adds a Signature and a Signature-input header. If the
17 | // Content-Digest header is included in the list of signed components, it is generated and added to the request.
18 | type Client struct {
19 | config ClientConfig
20 | client http.Client
21 | }
22 |
23 | // NewClient constructs a new client, with the flexibility of including a custom http.Client.
24 | func NewClient(client http.Client, config *ClientConfig) *Client {
25 | return &Client{config: *config, client: client}
26 | }
27 |
28 | // NewDefaultClient constructs a new client, based on the http.DefaultClient.
29 | func NewDefaultClient(config *ClientConfig) *Client {
30 | return NewClient(*http.DefaultClient, config)
31 | }
32 |
33 | func validateClient(c *Client) error {
34 | if c == nil {
35 | return fmt.Errorf("nil client")
36 | }
37 | if (c.config.signer != nil || c.config.verifier != nil) && c.config.signatureName == "" {
38 | return fmt.Errorf("empty signature name")
39 | }
40 | if c.config.verifier != nil && c.config.fetchVerifier != nil {
41 | return fmt.Errorf("at most one of \"verifier\" and \"fetchVerifier\" must be set")
42 | }
43 | return nil
44 | }
45 |
46 | // Do sends an http.Request, with optional signing and/or verification. Errors may be produced by any of
47 | // these operations.
48 | func (c *Client) Do(req *http.Request) (*http.Response, error) {
49 | if err := validateClient(c); err != nil {
50 | return nil, err
51 | }
52 | conf := c.config
53 | if conf.signer != nil {
54 | err := signClientRequest(req, conf)
55 | if err != nil {
56 | return nil, err
57 | }
58 | }
59 |
60 | // Send the request, receive response
61 | res, err := c.client.Do(req)
62 | if err != nil {
63 | return res, err
64 | }
65 |
66 | if conf.verifier != nil || conf.fetchVerifier != nil {
67 | err := verifyClientResponse(req, conf, res)
68 | if err != nil {
69 | return nil, err
70 | }
71 | }
72 | return res, nil
73 | }
74 |
75 | func verifyClientResponse(req *http.Request, conf ClientConfig, res *http.Response) error {
76 | var signatureName string
77 | var verifier *Verifier
78 | if conf.verifier != nil {
79 | signatureName = conf.signatureName
80 | verifier = conf.verifier
81 | } else if conf.fetchVerifier != nil {
82 | signatureName, verifier = conf.fetchVerifier(res, req)
83 | if verifier == nil {
84 | return fmt.Errorf("fetchVerifier returned a nil verifier")
85 | }
86 | }
87 | receivedContentDigest := res.Header.Values("Content-Digest")
88 | if conf.computeDigest &&
89 | res.Body != nil && len(receivedContentDigest) > 0 {
90 | // verify the header even if not explicitly required by verifier field list
91 | err := ValidateContentDigestHeader(receivedContentDigest, &res.Body, conf.digestSchemesRecv)
92 | if err != nil {
93 | return err
94 | }
95 | }
96 | err := VerifyResponse(signatureName, *verifier, res, req)
97 | if err != nil {
98 | return err
99 | }
100 | return nil
101 | }
102 |
103 | func signClientRequest(req *http.Request, conf ClientConfig) error {
104 | if conf.computeDigest && conf.signer.fields.hasHeader("Content-Digest") &&
105 | req.Body != nil && req.Header.Get("Content-Digest") == "" {
106 | header, err := GenerateContentDigestHeader(&req.Body, conf.digestSchemesSend)
107 | if err != nil {
108 | return err
109 | }
110 | req.Header.Set("Content-Digest", header)
111 | }
112 | sigInput, sig, err := SignRequest(conf.signatureName, *conf.signer, req)
113 | if err != nil {
114 | return fmt.Errorf("failed to sign request: %v", err)
115 | }
116 | req.Header.Add("Signature", sig)
117 | req.Header.Add("Signature-Input", sigInput)
118 | return nil
119 | }
120 |
121 | // Get sends an HTTP GET, a wrapper for Do.
122 | func (c *Client) Get(url string) (res *http.Response, err error) {
123 | req, err := http.NewRequest("GET", url, nil)
124 | if err != nil {
125 | return nil, err
126 | }
127 | return c.Do(req)
128 | }
129 |
130 | // Head sends an HTTP HEAD, a wrapper for Do.
131 | func (c *Client) Head(url string) (res *http.Response, err error) {
132 | req, err := http.NewRequest("HEAD", url, nil)
133 | if err != nil {
134 | return nil, err
135 | }
136 | return c.Do(req)
137 | }
138 |
139 | // Post sends an HTTP POST, a wrapper for Do.
140 | func (c *Client) Post(url, contentType string, body io.Reader) (res *http.Response, err error) {
141 | req, err := http.NewRequest("POST", url, body)
142 | if err != nil {
143 | return nil, err
144 | }
145 | req.Header.Set("Content-Type", contentType)
146 | return c.Do(req)
147 | }
148 |
149 | // PostForm sends an HTTP POST, with data keys and values URL-encoded as the request body.
150 | func (c *Client) PostForm(url string, data url.Values) (resp *http.Response, err error) {
151 | return c.Post(url, "application/x-www-form-urlencoded", strings.NewReader(data.Encode()))
152 | }
153 |
--------------------------------------------------------------------------------
/clientex_test.go:
--------------------------------------------------------------------------------
1 | package httpsign_test
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "crypto/rsa"
7 | "crypto/x509"
8 | "encoding/pem"
9 | "fmt"
10 | "github.com/yaronf/httpsign"
11 | "io"
12 | "net/http"
13 | "net/http/httptest"
14 | "strings"
15 | "testing"
16 | )
17 |
18 | func ExampleClient_Get() {
19 | // Note: client/server examples may fail in the Go Playground, https://github.com/golang/go/issues/45855
20 | // Set up a test server
21 | simpleHandler := func(w http.ResponseWriter, r *http.Request) {
22 | w.WriteHeader(200)
23 | w.Header().Set("Content-Type", "text/plain")
24 | _, _ = fmt.Fprintf(w, "Hey client, you sent a signature with these parameters: %s\n",
25 | r.Header.Get("Signature-Input"))
26 | }
27 | ts := httptest.NewServer(http.HandlerFunc(simpleHandler))
28 | defer ts.Close()
29 |
30 | // Client code starts here
31 | // Create a signer and a wrapped HTTP client (we set SignCreated to false to make the response deterministic,
32 | // don't do that in production.)
33 | signer, _ := httpsign.NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64),
34 | httpsign.NewSignConfig().SignCreated(false).SetKeyID("key1"), httpsign.Headers("@method"))
35 | client := httpsign.NewDefaultClient(httpsign.NewClientConfig().SetSignatureName("sig22").SetSigner(signer)) // sign, don't verify
36 |
37 | // Send an HTTP GET, get response -- signing and verification happen behind the scenes
38 | res, _ := client.Get(ts.URL)
39 |
40 | // Read the response
41 | serverText, _ := io.ReadAll(res.Body)
42 | _ = res.Body.Close()
43 |
44 | fmt.Println("Server sent: ", string(serverText))
45 | // Output: Server sent: Hey client, you sent a signature with these parameters: sig22=("@method");alg="hmac-sha256";keyid="key1"
46 | }
47 |
48 | var rsaPrvKey = `-----BEGIN PRIVATE KEY-----
49 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDIPoeuHmZXXqz+
50 | NCeAIXUh+nMu4lOtp6okWU0RKy9t40xwDh1CNT8sxfZcDe/IXuc7KV5OKx2bMVlv
51 | 1MugLUdSRwQFvWWQSHR5yborPrAjqKYqWh6gOgBKVekhrq+vl1PeFx96TXlIMGQ6
52 | Bt8Qmh5QW9BmsF8hYVC3996Q+x+BI9P0U3DUKGLf5yuacRyTyPY3CjGLjg7iNpU1
53 | seb4JUz8UkfuM+MP1JjlR0LWumYbeIGimdkPARhbMOYfjKNhU1NvhS+bCv69t7YU
54 | SgvPdTX660QnxEDTcQTMASRRXKnvBinK8z8VZdtEJLBj9kFK9GOs+CdbwL3TyTka
55 | 8LxAtKw5AgMBAAECggEBAKcW9mSmXUN+bt/XaTaTtIfb0o1GsghvpZubIKG45WTO
56 | jBPc0zFR+RtFPONnhbQu7MgDJvwXIidDsJuOdzN7VM4lEAgyGDOjIf4WBFDdiGDY
57 | 837XoEKW43Mj6NsARv1ASu1BYjTNvOwt5RQ+c5gI4k6vrmBhv5+88nvwSzmzMoCw
58 | h3ZLz4DfyOoBu7dqlnw9EttZuW7k1SXXW/cC5Sh90j8gZmYlNN76O1LsiCxZowCj
59 | Ys5Qdm5tcNuV8jK3XIFE4uYyBRHx5+haNjgKeM8n8IEEPYhzqcYIAYWGRHSkTvGy
60 | DxAb8AJBwuFCsFQz0oXyzVd8Mqz8RbqC7N50LdncCWECgYEA9zE9u/x8r7/De25U
61 | FcDDLt63qkqDmSn1PMkwf1DdOj734fYWd8Ay2R5E43NJMQalcR7Q7++O5KOQOFUl
62 | mpd79U9LO3b9FE0vR8xG81wushKy3xhHQdB2ucKliGwcYvcfgjWUoD7aKfrlHmNA
63 | olj1/21tJQGotEGg9NpiinJaiT0CgYEAz2ENkkEH3ZXtMKr3DXoqLNU+er4pHzm1
64 | cRxzpCNqNwZBlv0pxeIo6izH4TIrBPdIqSApUpZ0N+NgA0bjj0527GATGkGDgo+b
65 | TZFAhOhg7bfUyLsbgL/zycnyQwDWw2fo5ei9Bb2pPqfeQgrgYE+ag+ucJrhJNymv
66 | 3gG6Vmdwhq0CgYEAr6rwwl2Ghqdy1o7rdqIMk4x3Xa+iogBtZYtcyb2/2hrRsmVe
67 | Ri/yctXOAw3038BnZmKN/VVzaQzL+xyXoqswzn5Raqr+46SOiymi6mOCU85yC5WH
68 | XkA1f4HSfYbHDZWtcK1/N/oytE628Md8MWOjPqiXPgtVxvQ03I0uJlFqAckCgYB6
69 | w/yxwTez0MaqkftRCiofglnLdfmIF7S28l3vJFwDmPuJM/PfxoPsJXhqczWOagmk
70 | vXpY/uJsF3nGVtfuBUhXpISKfZAp4XPR1pQ4WgzPjY01C7c7X+clZRy616tL4J66
71 | RC5qUJ35joz/0cqEmXtibz9wmJYXRuFq7uDtt6ygvQKBgQCMopIJCcH5+DmbXmyw
72 | J8fxjxp8YpkEoFMtloaJ7lWHkiCUSWYCbGlvG1Nb1CoVqOuMffGXAZKAU9cw7YA2
73 | cJQuDUjlA0haDD4W3IibLGbANw414qqpqRmo5kM6aMpnShGsvxpp/0+XKrfcwgiC
74 | Ufa6y08wtZ/O7ZCBBbJTY90uqA==
75 | -----END PRIVATE KEY-----
76 | `
77 |
78 | func parseRsaPrivateKeyFromPemStr(pemString string) (*rsa.PrivateKey, error) {
79 | block, _ := pem.Decode([]byte(pemString))
80 | if block == nil {
81 | return nil, fmt.Errorf("cannot decode PEM")
82 | }
83 | k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
84 | if err != nil {
85 | return nil, err
86 | }
87 | return k.(*rsa.PrivateKey), nil
88 | }
89 |
90 | // This code is used in the README file
91 | func TestClientUsage(t *testing.T) {
92 | // Note: client/server examples may fail in the Go Playground, https://github.com/golang/go/issues/45855
93 | // Set up a test server
94 | simpleHandler := func(w http.ResponseWriter, r *http.Request) {
95 | w.WriteHeader(200)
96 | w.Header().Set("Content-Type", "text/plain")
97 | _, _ = fmt.Fprintf(w, "Hey client, you sent a signature with these parameters: %s\n",
98 | r.Header.Get("Signature-Input"))
99 | }
100 | ts := httptest.NewServer(http.HandlerFunc(simpleHandler))
101 | defer ts.Close()
102 |
103 | prvKey, err := parseRsaPrivateKeyFromPemStr(rsaPrvKey)
104 | if err != nil {
105 | t.Errorf("could not read private key")
106 | }
107 |
108 | // Client code starts here
109 | // Create a signer and a wrapped HTTP client
110 | signer, _ := httpsign.NewRSAPSSSigner(*prvKey,
111 | httpsign.NewSignConfig().SetKeyID("key1"),
112 | httpsign.Headers("@request-target", "content-digest")) // The Content-Digest header will be auto-generated
113 | client := httpsign.NewDefaultClient(httpsign.NewClientConfig().SetSignatureName("sig1").SetSigner(signer)) // sign requests, don't verify responses
114 |
115 | // Send an HTTP POST, get response -- signing happens behind the scenes
116 | body := `{"hello": "world"}`
117 | res, _ := client.Post(ts.URL, "application/json", bufio.NewReader(strings.NewReader(body)))
118 |
119 | // Read the response
120 | serverText, _ := io.ReadAll(res.Body)
121 | _ = res.Body.Close()
122 |
123 | fmt.Println("Server sent: ", string(serverText))
124 | }
125 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
2 | github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
7 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
8 | github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
9 | github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
10 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
11 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
12 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
15 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
17 | github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
18 | github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
19 | github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
20 | github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
21 | github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
22 | github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
23 | github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
24 | github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
25 | github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k=
26 | github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
27 | github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
28 | github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
29 | github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
30 | github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
31 | github.com/lestrrat-go/jwx/v2 v2.1.2 h1:6poete4MPsO8+LAEVhpdrNI4Xp2xdiafgl2RD89moBc=
32 | github.com/lestrrat-go/jwx/v2 v2.1.2/go.mod h1:pO+Gz9whn7MPdbsqSJzG8TlEpMZCwQDXnFJ+zsUVh8Y=
33 | github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
34 | github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
35 | github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
36 | github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
37 | github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
38 | github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
41 | github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
42 | github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
43 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
44 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
46 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
47 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
48 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
49 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
50 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
51 | github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
52 | github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
53 | golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
54 | golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
55 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
56 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
57 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
58 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
59 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
60 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
61 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
62 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
63 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
64 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
65 |
--------------------------------------------------------------------------------
/message.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "net/url"
8 | "time"
9 | )
10 |
11 | // MessageDetails aggregates the details of a signed message, for a given signature
12 | type MessageDetails struct {
13 | KeyID string
14 | Alg string
15 | Fields Fields
16 | Created *time.Time
17 | Expires *time.Time
18 | Nonce *string
19 | Tag *string
20 | }
21 |
22 | // Message represents a parsed HTTP message ready for signature verification.
23 | type Message struct {
24 | headers http.Header
25 | trailers http.Header
26 | body *io.ReadCloser
27 |
28 | method string
29 | url *url.URL
30 | authority string
31 | scheme string
32 | statusCode *int
33 | assocReq *Message
34 | }
35 |
36 | // NewMessage constructs a new Message from the provided config.
37 | func NewMessage(config *MessageConfig) (*Message, error) {
38 | if config == nil {
39 | config = NewMessageConfig()
40 | }
41 |
42 | hasRequest := config.method != ""
43 | hasResponse := config.statusCode != nil
44 |
45 | if !hasRequest && !hasResponse {
46 | return nil, fmt.Errorf("message config must have either method (for request) or status code (for response)")
47 | }
48 |
49 | if hasRequest && hasResponse {
50 | return nil, fmt.Errorf("message config cannot have both request and response fields set")
51 | }
52 |
53 | if hasRequest {
54 | if config.headers == nil {
55 | return nil, fmt.Errorf("request message must have headers")
56 | }
57 | }
58 |
59 | if hasResponse {
60 | if config.headers == nil {
61 | return nil, fmt.Errorf("response message must have headers")
62 | }
63 | }
64 |
65 | var assocReq *Message
66 | if config.assocReq != nil {
67 | method := config.assocReq.method
68 | u := config.assocReq.url
69 | headers := config.assocReq.headers
70 | authority := config.assocReq.authority
71 | scheme := config.assocReq.scheme
72 | if method == "" || u == nil || headers == nil || authority == "" || scheme == "" {
73 | return nil, fmt.Errorf("invalid associated request")
74 | }
75 | assocReq = &Message{
76 | method: method,
77 | url: u,
78 | headers: headers,
79 | authority: authority,
80 | scheme: scheme,
81 | }
82 | }
83 |
84 | return &Message{
85 | headers: config.headers,
86 | trailers: config.trailers,
87 | body: config.body,
88 | method: config.method,
89 | url: config.url,
90 | authority: config.authority,
91 | scheme: config.scheme,
92 | statusCode: config.statusCode,
93 | assocReq: assocReq,
94 | }, nil
95 | }
96 |
97 | // MessageConfig configures a Message for signature verification.
98 | type MessageConfig struct {
99 | method string
100 | url *url.URL
101 | headers http.Header
102 | trailers http.Header
103 | body *io.ReadCloser
104 | authority string
105 | scheme string
106 |
107 | statusCode *int
108 |
109 | assocReq *MessageConfig
110 | }
111 |
112 | // NewMessageConfig returns a new MessageConfig.
113 | func NewMessageConfig() *MessageConfig {
114 | return &MessageConfig{}
115 | }
116 |
117 | func (b *MessageConfig) WithMethod(method string) *MessageConfig {
118 | b.method = method
119 | return b
120 | }
121 |
122 | func (b *MessageConfig) WithURL(u *url.URL) *MessageConfig {
123 | b.url = u
124 | return b
125 | }
126 |
127 | func (b *MessageConfig) WithHeaders(headers http.Header) *MessageConfig {
128 | b.headers = headers
129 | return b
130 | }
131 |
132 | func (b *MessageConfig) WithTrailers(trailers http.Header) *MessageConfig {
133 | b.trailers = trailers
134 | return b
135 | }
136 |
137 | func (b *MessageConfig) WithBody(body *io.ReadCloser) *MessageConfig {
138 | b.body = body
139 | return b
140 | }
141 |
142 | func (b *MessageConfig) WithAuthority(authority string) *MessageConfig {
143 | b.authority = authority
144 | return b
145 | }
146 |
147 | func (b *MessageConfig) WithScheme(scheme string) *MessageConfig {
148 | b.scheme = scheme
149 | return b
150 | }
151 |
152 | func (b *MessageConfig) WithStatusCode(statusCode int) *MessageConfig {
153 | b.statusCode = &statusCode
154 | return b
155 | }
156 |
157 | func (b *MessageConfig) WithAssociatedRequest(method string, u *url.URL, headers http.Header, authority, scheme string) *MessageConfig {
158 | b.assocReq = &MessageConfig{
159 | method: method,
160 | url: u,
161 | headers: headers,
162 | authority: authority,
163 | scheme: scheme,
164 | }
165 | return b
166 | }
167 |
168 | func (b *MessageConfig) WithRequest(req *http.Request) *MessageConfig {
169 | if req == nil {
170 | return b
171 | }
172 |
173 | scheme := "http"
174 | if req.TLS != nil {
175 | scheme = "https"
176 | }
177 |
178 | return b.
179 | WithMethod(req.Method).
180 | WithURL(req.URL).
181 | WithHeaders(req.Header).
182 | WithTrailers(req.Trailer).
183 | WithBody(&req.Body).
184 | WithAuthority(req.Host).
185 | WithScheme(scheme)
186 | }
187 |
188 | func (b *MessageConfig) WithResponse(res *http.Response, req *http.Request) *MessageConfig {
189 | if res == nil {
190 | return b
191 | }
192 |
193 | b = b.
194 | WithStatusCode(res.StatusCode).
195 | WithHeaders(res.Header).
196 | WithTrailers(res.Trailer).
197 | WithBody(&res.Body)
198 |
199 | if req != nil {
200 | scheme := "http"
201 | if req.TLS != nil {
202 | scheme = "https"
203 | }
204 | b = b.WithAssociatedRequest(req.Method, req.URL, req.Header, req.Host, scheme)
205 | }
206 |
207 | return b
208 | }
209 |
210 | // Verify verifies a signature on this message.
211 | func (m *Message) Verify(signatureName string, verifier Verifier) (*MessageDetails, error) {
212 | _, psiSig, err := verifyDebug(signatureName, verifier, m)
213 | if err != nil {
214 | return nil, err
215 | }
216 | return signatureDetails(psiSig)
217 | }
218 |
--------------------------------------------------------------------------------
/urlencode.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import "strconv"
4 |
5 | // Unfortunately we need our own encoder for query parameters, an encoder that only differs from
6 | // url.QueryEscape in that spaces are encoded into %20.
7 | // No, using PathEscape doesn't work either because of colons (":").
8 |
9 | // Code is copied from url.go
10 |
11 | const upperhex = "0123456789ABCDEF"
12 |
13 | type encoding int
14 |
15 | const (
16 | encodePath encoding = 1 + iota
17 | encodePathSegment
18 | encodeHost
19 | encodeZone
20 | encodeUserPassword
21 | encodeQueryComponent
22 | encodeFragment
23 | encodeQueryComponentForSignature // added
24 | )
25 |
26 | type EscapeError string
27 |
28 | func (e EscapeError) Error() string {
29 | return "invalid URL escape " + strconv.Quote(string(e))
30 | }
31 |
32 | type InvalidHostError string
33 |
34 | func (e InvalidHostError) Error() string {
35 | return "invalid character " + strconv.Quote(string(e)) + " in host name"
36 | }
37 |
38 | // Return true if the specified character should be escaped when
39 | // appearing in a URL string, according to RFC 3986.
40 | //
41 | // Please be informed that for now shouldEscape does not check all
42 | // reserved characters correctly. See golang.org/issue/5684.
43 | func shouldEscape(c byte, mode encoding) bool {
44 | // §2.3 Unreserved characters (alphanum)
45 | if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
46 | return false
47 | }
48 |
49 | if mode == encodeHost || mode == encodeZone {
50 | // §3.2.2 Host allows
51 | // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
52 | // as part of reg-name.
53 | // We add : because we include :port as part of host.
54 | // We add [ ] because we include [ipv6]:port as part of host.
55 | // We add < > because they're the only characters left that
56 | // we could possibly allow, and Parse will reject them if we
57 | // escape them (because hosts can't use %-encoding for
58 | // ASCII bytes).
59 | switch c {
60 | case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"':
61 | return false
62 | }
63 | }
64 |
65 | switch c {
66 | case '-', '_', '.', '~': // §2.3 Unreserved characters (mark)
67 | return false
68 |
69 | case '$', '&', '+', ',', '/', ':', ';', '=', '?', '@': // §2.2 Reserved characters (reserved)
70 | // Different sections of the URL allow a few of
71 | // the reserved characters to appear unescaped.
72 | switch mode {
73 | case encodePath: // §3.3
74 | // The RFC allows : @ & = + $ but saves / ; , for assigning
75 | // meaning to individual path segments. This package
76 | // only manipulates the path as a whole, so we allow those
77 | // last three as well. That leaves only ? to escape.
78 | return c == '?'
79 |
80 | case encodePathSegment: // §3.3
81 | // The RFC allows : @ & = + $ but saves / ; , for assigning
82 | // meaning to individual path segments.
83 | return c == '/' || c == ';' || c == ',' || c == '?'
84 |
85 | case encodeUserPassword: // §3.2.1
86 | // The RFC allows ';', ':', '&', '=', '+', '$', and ',' in
87 | // userinfo, so we must escape only '@', '/', and '?'.
88 | // The parsing of userinfo treats ':' as special so we must escape
89 | // that too.
90 | return c == '@' || c == '/' || c == '?' || c == ':'
91 |
92 | case encodeQueryComponent: // §3.4
93 | case encodeQueryComponentForSignature: // added
94 | // The RFC reserves (so we must escape) everything.
95 | return true
96 |
97 | case encodeFragment: // §4.1
98 | // The RFC text is silent but the grammar allows
99 | // everything, so escape nothing.
100 | return false
101 | }
102 | }
103 |
104 | if mode == encodeFragment {
105 | // RFC 3986 §2.2 allows not escaping sub-delims. A subset of sub-delims are
106 | // included in reserved from RFC 2396 §2.2. The remaining sub-delims do not
107 | // need to be escaped. To minimize potential breakage, we apply two restrictions:
108 | // (1) we always escape sub-delims outside of the fragment, and (2) we always
109 | // escape single quote to avoid breaking callers that had previously assumed that
110 | // single quotes would be escaped. See issue #19917.
111 | switch c {
112 | case '!', '(', ')', '*':
113 | return false
114 | }
115 | }
116 |
117 | // Everything else must be escaped.
118 | return true
119 | }
120 |
121 | // QueryEscapeForSignature escapes the string, so it can be safely placed
122 | // inside a URL query.
123 | // Specifically this is "Percent-encode after encoding" in the WhatWG URL standard
124 | func QueryEscapeForSignature(s string) string {
125 | return escape(s, encodeQueryComponentForSignature)
126 | }
127 |
128 | func escape(s string, mode encoding) string {
129 | spaceCount, hexCount := 0, 0
130 | for i := 0; i < len(s); i++ {
131 | c := s[i]
132 | if shouldEscape(c, mode) {
133 | if c == ' ' && mode == encodeQueryComponent { // but not encodeQueryComponentForSignature
134 | spaceCount++
135 | } else {
136 | hexCount++
137 | }
138 | }
139 | }
140 |
141 | if spaceCount == 0 && hexCount == 0 {
142 | return s
143 | }
144 |
145 | var buf [64]byte
146 | var t []byte
147 |
148 | required := len(s) + 2*hexCount
149 | if required <= len(buf) {
150 | t = buf[:required]
151 | } else {
152 | t = make([]byte, required)
153 | }
154 |
155 | if hexCount == 0 {
156 | copy(t, s)
157 | for i := 0; i < len(s); i++ {
158 | if s[i] == ' ' {
159 | t[i] = '+'
160 | }
161 | }
162 | return string(t)
163 | }
164 |
165 | j := 0
166 | for i := 0; i < len(s); i++ {
167 | switch c := s[i]; {
168 | case c == ' ' && mode == encodeQueryComponent: // but not when encoding for signatures
169 | t[j] = '+'
170 | j++
171 | case shouldEscape(c, mode):
172 | t[j] = '%'
173 | t[j+1] = upperhex[c>>4]
174 | t[j+2] = upperhex[c&15]
175 | j += 3
176 | default:
177 | t[j] = s[i]
178 | j++
179 | }
180 | }
181 | return string(t)
182 | }
183 |
--------------------------------------------------------------------------------
/handler.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "log"
8 | "net/http"
9 | "strconv"
10 | "strings"
11 | "time"
12 | )
13 |
14 | // WrapHandler wraps a server's HTTP request handler so that the incoming request is verified
15 | // and the response is signed. Both operations are optional.
16 | // Side effects: when signing, the wrapped handler adds a Signature and a Signature-input header. If the
17 | // Content-Digest header is included in the list of signed components, it is generated and added to the response.
18 | // Note: unlike the standard net.http behavior, for the "Content-Type" header to be signed,
19 | // it should be created explicitly.
20 | func WrapHandler(h http.Handler, config HandlerConfig) http.Handler {
21 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22 | if config.fetchVerifier != nil {
23 | err := verifyServerRequest(r, config)
24 | if err != nil {
25 | config.reqNotVerified(w, r, config.logger, err)
26 | return
27 | }
28 | }
29 | wrapped := newWrappedResponseWriter(w, r, config) // and this includes response signature
30 | h.ServeHTTP(wrapped, r)
31 | if config.fetchSigner != nil {
32 | err := signServerResponse(wrapped, r, config)
33 | if err != nil {
34 | sigFailed(wrapped.ResponseWriter, r, config.logger, err)
35 | return
36 | }
37 | }
38 | err := finalizeResponseBody(wrapped)
39 | if err != nil {
40 | sigFailed(wrapped.ResponseWriter, r, config.logger, err)
41 | return
42 | }
43 | })
44 | }
45 |
46 | // This error case is not optional, as it's always a server bug
47 | func sigFailed(w http.ResponseWriter, _ *http.Request, logger *log.Logger, err error) {
48 | w.WriteHeader(http.StatusInternalServerError)
49 | if logger != nil { // sanitize error string, just in case
50 | escapedErr := err.Error()
51 | escapedErr = strings.Replace(escapedErr, "\n", "", -1)
52 | escapedErr = strings.Replace(escapedErr, "\r", "", -1)
53 | logger.Printf("Failed to sign response: %v\n", escapedErr)
54 | }
55 | _, _ = fmt.Fprintln(w, "Failed to sign response.") // For security reasons, error is not printed
56 | }
57 |
58 | func finalizeResponseBody(wrapped *wrappedResponseWriter) error {
59 | wrapped.ResponseWriter.WriteHeader(wrapped.status)
60 | if wrapped.body != nil {
61 | _, err := wrapped.ResponseWriter.Write(wrapped.body.Bytes())
62 | return err
63 | }
64 | return nil
65 | }
66 |
67 | func signServerResponse(wrapped *wrappedResponseWriter, r *http.Request, config HandlerConfig) error {
68 | if wrapped.Header().Get("Date") == "" {
69 | wrapped.Header().Set("Date", time.Now().UTC().Format(http.TimeFormat))
70 | }
71 |
72 | response := http.Response{
73 | Status: strconv.Itoa(wrapped.status),
74 | StatusCode: wrapped.status,
75 | Proto: r.Proto,
76 | ProtoMajor: r.ProtoMajor,
77 | ProtoMinor: r.ProtoMinor,
78 | Header: wrapped.Header(),
79 | Body: nil, // Not required for the signature
80 | ContentLength: 0,
81 | TransferEncoding: nil,
82 | Close: false,
83 | Uncompressed: false,
84 | Trailer: nil,
85 | Request: r,
86 | TLS: nil,
87 | }
88 | if config.fetchSigner == nil {
89 | return fmt.Errorf("could not fetch a Signer")
90 | }
91 | sigName, signer := config.fetchSigner(response, r)
92 | if signer == nil {
93 | return fmt.Errorf("could not fetch a Signer, check key ID")
94 | }
95 |
96 | if signer.fields.hasHeader("Content-Digest") &&
97 | wrapped.body != nil && config.computeDigest && wrapped.Header().Get("Content-Digest") == "" {
98 | closer := io.NopCloser(bytes.NewReader(wrapped.body.Bytes()))
99 | digest, err := GenerateContentDigestHeader(&closer, config.digestSchemesSend)
100 | if err != nil {
101 | return err
102 | }
103 | wrapped.Header().Add("Content-Digest", digest)
104 | }
105 |
106 | signatureInput, signature, err := SignResponse(sigName, *signer, &response, r)
107 | if err != nil {
108 | return fmt.Errorf("failed to sign the response: %w", err)
109 | }
110 | wrapped.Header().Add("Signature-Input", signatureInput)
111 | wrapped.Header().Add("Signature", signature)
112 | return nil
113 | }
114 |
115 | type wrappedResponseWriter struct {
116 | http.ResponseWriter
117 | status int
118 | body *bytes.Buffer
119 | config HandlerConfig
120 | wroteHeader bool
121 | r *http.Request
122 | }
123 |
124 | func newWrappedResponseWriter(w http.ResponseWriter, r *http.Request, config HandlerConfig) *wrappedResponseWriter {
125 | return &wrappedResponseWriter{ResponseWriter: w, r: r, config: config}
126 | }
127 |
128 | func (w *wrappedResponseWriter) Write(p []byte) (n int, err error) {
129 | if !w.wroteHeader {
130 | w.status = http.StatusOK
131 | }
132 | w.wroteHeader = true
133 | if w.body == nil {
134 | w.body = new(bytes.Buffer)
135 | }
136 | return w.body.Write(p)
137 | }
138 |
139 | func (w *wrappedResponseWriter) WriteHeader(code int) {
140 | w.status = code
141 | w.wroteHeader = true
142 | }
143 |
144 | func verifyServerRequest(r *http.Request, config HandlerConfig) error {
145 | if config.fetchVerifier == nil {
146 | return fmt.Errorf("could not fetch a Verifier")
147 | }
148 | sigName, verifier := config.fetchVerifier(r)
149 | if verifier == nil {
150 | return fmt.Errorf("could not fetch a Verifier, check key ID")
151 | }
152 | details, err := RequestDetails(sigName, r)
153 | if err != nil {
154 | return err
155 | }
156 | if config.computeDigest && details.Fields.hasHeader("Content-Digest") { // if Content-Digest is signed
157 | receivedContentDigest := r.Header.Values("Content-Digest")
158 | if r.Body == nil && len(receivedContentDigest) > 0 {
159 | return fmt.Errorf("found Content-Digest but no message body")
160 | }
161 | err := ValidateContentDigestHeader(receivedContentDigest, &r.Body, config.digestSchemesRecv)
162 | if err != nil {
163 | return err
164 | }
165 | }
166 | return VerifyRequest(sigName, *verifier, r)
167 | }
168 |
--------------------------------------------------------------------------------
/httpparse.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "net/url"
7 | "strconv"
8 | "strings"
9 | )
10 |
11 | // some fields (specifically, query params) may appear more than once, and those occurrences are ordered.
12 | type components map[string]string
13 |
14 | type parsedMessage struct {
15 | derived components
16 | url *url.URL
17 | headers, trailers http.Header // we abuse this type: names are lowercase instead of canonicalized
18 | qParams url.Values
19 | }
20 |
21 | func parseRequest(req *http.Request, withTrailers bool) (*parsedMessage, error) {
22 | if req == nil {
23 | return nil, nil
24 | }
25 |
26 | scheme := "http"
27 | if req.TLS != nil {
28 | scheme = "https"
29 | }
30 |
31 | msg := &Message{
32 | method: req.Method,
33 | url: req.URL,
34 | headers: req.Header,
35 | trailers: req.Trailer,
36 | body: &req.Body,
37 | authority: req.Host,
38 | scheme: scheme,
39 | }
40 |
41 | return parseMessage(msg, withTrailers)
42 | }
43 |
44 | //lint:ignore ST1003 QPs is intentional abbreviation for Query Parameters
45 | func reEncodeQPs(values url.Values) url.Values {
46 | escaped := url.Values{}
47 | for key, v := range values { // Re-escape query parameters, both names and values
48 | escapedKey := QueryEscapeForSignature(key)
49 | escaped[escapedKey] = make([]string, len(values[key]))
50 | for key2 := range v {
51 | escaped[escapedKey][key2] = QueryEscapeForSignature(values[key][key2])
52 | }
53 | }
54 | return escaped
55 | }
56 |
57 | func normalizeHeaderNames(header http.Header) http.Header {
58 | if header == nil {
59 | return nil
60 | }
61 | var t = http.Header{}
62 | for k, v := range header {
63 | t[strings.ToLower(k)] = v
64 | }
65 | return t
66 | }
67 |
68 | func parseResponse(res *http.Response, withTrailers bool) (*parsedMessage, error) {
69 | msg := &Message{
70 | statusCode: &res.StatusCode,
71 | headers: res.Header,
72 | trailers: res.Trailer,
73 | body: &res.Body,
74 | }
75 |
76 | return parseMessage(msg, withTrailers)
77 | }
78 |
79 | func validateMessageHeaders(header http.Header) error {
80 | // Go accepts header names that start with "@", which is forbidden by the RFC
81 | for k := range header {
82 | if strings.HasPrefix(k, "@") {
83 | return fmt.Errorf("potentially malicious header detected \"%s\"", k)
84 | }
85 | }
86 | return nil
87 | }
88 |
89 | func foldFields(fields []string) string {
90 | if len(fields) == 0 {
91 | return ""
92 | }
93 | ff := strings.TrimSpace(fields[0])
94 | for i := 1; i < len(fields); i++ {
95 | ff += ", " + strings.TrimSpace(fields[i])
96 | }
97 | return ff
98 | }
99 |
100 | func derivedComponent(name, v string, components components) {
101 | components[name] = v
102 | }
103 |
104 | func generateReqDerivedComponents(method string, u *url.URL, authority string, components components) {
105 | derivedComponent("@method", method, components)
106 | derivedComponent("@target-uri", scTargetURI(u), components)
107 | derivedComponent("@path", scPath(u), components)
108 | derivedComponent("@authority", authority, components)
109 | derivedComponent("@scheme", scScheme(u), components)
110 | derivedComponent("@request-target", scRequestTarget(u), components)
111 | derivedComponent("@query", scQuery(u), components)
112 | }
113 |
114 | func scPath(theURL *url.URL) string {
115 | return theURL.EscapedPath()
116 | }
117 |
118 | func scQuery(url *url.URL) string {
119 | return "?" + url.RawQuery
120 | }
121 |
122 | func scRequestTarget(url *url.URL) string {
123 | path := url.Path
124 | if path == "" {
125 | path = "/" // Normalize path, issue #8, and see https://www.rfc-editor.org/rfc/rfc9110#section-4.2.3
126 | }
127 | if url.RawQuery == "" {
128 | return path
129 | }
130 | return path + "?" + url.RawQuery
131 | }
132 |
133 | func scScheme(url *url.URL) string {
134 | if url.Scheme == "" {
135 | return "http"
136 | }
137 | return url.Scheme
138 | }
139 |
140 | func scTargetURI(url *url.URL) string {
141 | return url.String()
142 | }
143 |
144 | func scStatus(statusCode int) string {
145 | return strconv.Itoa(statusCode)
146 | }
147 |
148 | func parseMessage(msg *Message, withTrailers bool) (*parsedMessage, error) {
149 | if msg == nil {
150 | return nil, nil
151 | }
152 |
153 | err := validateMessageHeaders(msg.headers)
154 | if err != nil {
155 | return nil, err
156 | }
157 |
158 | if withTrailers {
159 | if msg.body != nil {
160 | _, err = duplicateBody(msg.body)
161 | if err != nil {
162 | return nil, fmt.Errorf("cannot duplicate message body: %w", err)
163 | }
164 | }
165 | err = validateMessageHeaders(msg.trailers)
166 | if err != nil {
167 | return nil, fmt.Errorf("could not validate trailers: %w", err)
168 | }
169 | }
170 |
171 | derived := components{}
172 | var u *url.URL
173 | var qParams url.Values
174 |
175 | if msg.method != "" || msg.url != nil {
176 | if msg.method == "" || msg.url == nil {
177 | return nil, fmt.Errorf("invalid state: method or url without the other")
178 | }
179 |
180 | u = msg.url
181 | if u == nil {
182 | u = &url.URL{Path: "/"}
183 | }
184 | if u.Host == "" && msg.authority != "" {
185 | u.Host = msg.authority
186 | }
187 | if u.Scheme == "" {
188 | if msg.scheme != "" {
189 | u.Scheme = msg.scheme
190 | } else {
191 | u.Scheme = "http"
192 | }
193 | }
194 |
195 | if u.RawQuery != "" {
196 | values, err := url.ParseQuery(u.RawQuery)
197 | if err != nil {
198 | return nil, fmt.Errorf("cannot parse query: %s", u.RawQuery)
199 | }
200 | qParams = reEncodeQPs(values)
201 | }
202 |
203 | generateReqDerivedComponents(msg.method, u, msg.authority, derived)
204 | } else if msg.statusCode != nil {
205 | derivedComponent("@status", scStatus(*msg.statusCode), derived)
206 | } else {
207 | return nil, fmt.Errorf("invalid state: method and url, or status required")
208 | }
209 |
210 | return &parsedMessage{
211 | derived: derived,
212 | url: u,
213 | headers: normalizeHeaderNames(msg.headers),
214 | trailers: normalizeHeaderNames(msg.trailers),
215 | qParams: qParams,
216 | }, nil
217 | }
218 |
--------------------------------------------------------------------------------
/http2_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "crypto/tls"
7 | "io"
8 | "net/http"
9 | "net/http/httptest"
10 | "strconv"
11 | "strings"
12 | "testing"
13 | "text/template"
14 |
15 | "github.com/andreyvit/diff"
16 | )
17 |
18 | var wantFields = `"kuku": my awesome header
19 | "@query": ?k1=v1&k2
20 | "@method": GET
21 | "@target-uri": {{.Scheme}}://127.0.0.1:{{.Port}}/path?k1=v1&k2
22 | "@request-target": /path?k1=v1&k2
23 | "@authority": 127.0.0.1:{{.Port}}
24 | "@scheme": {{.Scheme}}
25 | "@target-uri": {{.Scheme}}://127.0.0.1:{{.Port}}/path?k1=v1&k2
26 | "@path": /path
27 | "@query": ?k1=v1&k2
28 | "@query-param";name="k1": v1
29 | "@query-param";name="k2":
30 | "@signature-params": ("kuku" "@query" "@method" "@target-uri" "@request-target" "@authority" "@scheme" "@target-uri" "@path" "@query" "@query-param";name="k1" "@query-param";name="k2");alg="hmac-sha256";keyid="key1"`
31 |
32 | func execTemplate(t template.Template, name string, data interface{}) (string, error) {
33 | buf := &bytes.Buffer{}
34 | err := t.ExecuteTemplate(buf, name, data)
35 | return buf.String(), err
36 | }
37 |
38 | func newClientRequest(t *testing.T, method, url, body string) *http.Request {
39 | in := strings.NewReader(body)
40 | req, err := http.NewRequest(method, url, bufio.NewReader(in))
41 | if err != nil {
42 | t.Errorf("could not read request")
43 | }
44 | return req
45 | }
46 |
47 | var ts *httptest.Server // global, so can be used *inside* the server, too
48 |
49 | func testHTTP(t *testing.T, proto string) {
50 | simpleHandler := func(w http.ResponseWriter, r *http.Request) {
51 | reqProto := r.Proto
52 | if reqProto != proto {
53 | t.Errorf("expected %s, got %s", proto, reqProto)
54 | }
55 | var scheme string
56 | if ts.TLS == nil {
57 | scheme = "http"
58 | } else {
59 | scheme = "https"
60 | }
61 | sp := bytes.Split([]byte(ts.URL), []byte(":"))
62 | portval, err := strconv.Atoi(string(sp[2]))
63 | if err != nil {
64 | t.Errorf("cannot parse server port number")
65 | }
66 | tpl, err := template.New("fields").Parse(wantFields)
67 | if err != nil {
68 | t.Errorf("could not parse template")
69 | }
70 | type inputs struct {
71 | Port int
72 | Scheme string
73 | }
74 | // Use the Template facility to create the list of expected signed fields
75 | wf, err := execTemplate(*tpl, "fields", inputs{Port: portval, Scheme: scheme})
76 | if err != nil {
77 | t.Errorf("execTemplate failed")
78 | }
79 | verifier, err := NewHMACSHA256Verifier(bytes.Repeat([]byte{0x03}, 64),
80 | NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"),
81 | Headers("@query"))
82 | if err != nil {
83 | t.Errorf("could not create verifier")
84 | }
85 | sigInput, err := verifyRequestDebug("sig1", *verifier, r)
86 | if err != nil {
87 | t.Errorf("failed to verify request: sig input: %s\nerr: %v", sigInput, err)
88 | }
89 |
90 | if sigInput != wf {
91 | t.Errorf("unexpected fields: %s\n", diff.CharacterDiff(sigInput, wantFields))
92 | }
93 | w.WriteHeader(200)
94 | }
95 |
96 | // And run the client code...
97 | simpleClient(t, proto, simpleHandler)
98 | }
99 |
100 | func testMessageHTTP(t *testing.T, proto string) {
101 | simpleHandler := func(w http.ResponseWriter, r *http.Request) {
102 | reqProto := r.Proto
103 | if reqProto != proto {
104 | t.Errorf("expected %s, got %s", proto, reqProto)
105 | }
106 | var scheme string
107 | if ts.TLS == nil {
108 | scheme = "http"
109 | } else {
110 | scheme = "https"
111 | }
112 | sp := bytes.Split([]byte(ts.URL), []byte(":"))
113 | portval, err := strconv.Atoi(string(sp[2]))
114 | if err != nil {
115 | t.Errorf("cannot parse server port number")
116 | }
117 | tpl, err := template.New("fields").Parse(wantFields)
118 | if err != nil {
119 | t.Errorf("could not parse template")
120 | }
121 | type inputs struct {
122 | Port int
123 | Scheme string
124 | }
125 | // Use the Template facility to create the list of expected signed fields
126 | wf, err := execTemplate(*tpl, "fields", inputs{Port: portval, Scheme: scheme})
127 | if err != nil {
128 | t.Errorf("execTemplate failed")
129 | }
130 | verifier, err := NewHMACSHA256Verifier(bytes.Repeat([]byte{0x03}, 64),
131 | NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"),
132 | Headers("@query"))
133 | if err != nil {
134 | t.Errorf("could not create verifier")
135 | }
136 | msg, err := NewMessage(NewMessageConfig().WithRequest(r))
137 | if err != nil {
138 | t.Errorf("could not create message")
139 | }
140 | sigInput, _, err := verifyDebug("sig1", *verifier, msg)
141 | if err != nil {
142 | t.Errorf("failed to verify request: sig input: %s\nerr: %v", sigInput, err)
143 | }
144 |
145 | if sigInput != wf {
146 | t.Errorf("unexpected fields: %s\n", diff.CharacterDiff(sigInput, wantFields))
147 | }
148 | w.WriteHeader(200)
149 | }
150 |
151 | // And run the client code...
152 | simpleClient(t, proto, simpleHandler)
153 | }
154 |
155 | func simpleClient(t *testing.T, proto string, simpleHandler func(w http.ResponseWriter, r *http.Request)) {
156 | // Client code
157 | switch proto {
158 | case "HTTP/1.1":
159 | ts = httptest.NewServer(http.HandlerFunc(simpleHandler))
160 | case "HTTP/2.0":
161 | ts = httptest.NewUnstartedServer(http.HandlerFunc(simpleHandler))
162 | ts.EnableHTTP2 = true
163 | ts.StartTLS()
164 | default:
165 | t.Errorf("no server")
166 | }
167 | defer ts.Close()
168 |
169 | signer, err := NewHMACSHA256Signer(bytes.Repeat([]byte{0x03}, 64),
170 | NewSignConfig().SetKeyID("key1").SignCreated(false),
171 | *NewFields().AddHeaders("kuku", "@query", "@method", "@target-uri", "@request-target", "@authority", "@scheme", "@target-uri",
172 | "@path", "@query").AddQueryParam("k1").AddQueryParam("k2"))
173 | if err != nil {
174 | t.Errorf("failed to create signer")
175 | }
176 |
177 | var client *Client
178 | switch proto {
179 | case "HTTP/1.1":
180 | client = NewDefaultClient(NewClientConfig().SetSignatureName("sig1").SetSigner(signer))
181 | case "HTTP/2.0":
182 | tr := &http.Transport{
183 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // Do not verify server certificate
184 | ForceAttemptHTTP2: true,
185 | }
186 | c := &http.Client{Transport: tr}
187 | client = NewClient(*c, NewClientConfig().SetSignatureName("sig1").SetSigner(signer))
188 | default:
189 | t.Errorf("no client for you")
190 | }
191 | req := newClientRequest(t, "GET", ts.URL+"/path"+"?k1=v1&k2", "")
192 | req.Header.Set("Kuku", "my awesome header")
193 | res, err := client.Do(req)
194 | if err != nil {
195 | t.Errorf("%v", err)
196 | }
197 | if res != nil {
198 | _, err = io.ReadAll(res.Body)
199 | _ = res.Body.Close()
200 | if err != nil {
201 | t.Errorf("%v", err)
202 | }
203 |
204 | if res.Status != "200 OK" {
205 | t.Errorf("Bad status returned")
206 | }
207 | }
208 | }
209 |
210 | func TestHTTP11(t *testing.T) {
211 | testHTTP(t, "HTTP/1.1")
212 | testMessageHTTP(t, "HTTP/1.1")
213 | }
214 |
215 | func TestHTTP20(t *testing.T) {
216 | testHTTP(t, "HTTP/2.0")
217 | testMessageHTTP(t, "HTTP/2.0")
218 | }
219 |
--------------------------------------------------------------------------------
/handler_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "log"
8 | "net/http"
9 | "net/http/httptest"
10 | "strings"
11 | "testing"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func Test_WrapHandler(t *testing.T) {
17 | fetchVerifier := func(r *http.Request) (string, *Verifier) {
18 | sigName := "sig1"
19 | verifier, _ := NewHMACSHA256Verifier(bytes.Repeat([]byte{1}, 64), nil,
20 | Headers("@method"))
21 | return sigName, verifier
22 | }
23 |
24 | fetchSigner := func(res http.Response, r *http.Request) (string, *Signer) {
25 | sigName := "sig1"
26 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{0}, 64), NewSignConfig().SetKeyID("key"),
27 | Headers("@status", "bar", "date", "Content-Digest"))
28 | return sigName, signer
29 | }
30 |
31 | simpleHandler := func(w http.ResponseWriter, r *http.Request) {
32 | w.WriteHeader(200)
33 | w.Header().Set("bar", "baz and baz again")
34 | _, _ = fmt.Fprintln(w, "Hello, client")
35 | _, _ = fmt.Fprintln(w, "Hello again")
36 | }
37 | config := NewHandlerConfig().SetFetchVerifier(fetchVerifier).
38 | SetFetchSigner(fetchSigner).SetDigestSchemesSend([]string{DigestSha256}).SetDigestSchemesRecv([]string{DigestSha256})
39 | ts := httptest.NewServer(WrapHandler(http.HandlerFunc(simpleHandler), *config))
40 | defer ts.Close()
41 |
42 | signer, err := NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64), NewSignConfig().SetKeyID("key"),
43 | Headers("@method", "content-digest", "@request-target"))
44 | assert.NoError(t, err)
45 |
46 | verifier, err := NewHMACSHA256Verifier(bytes.Repeat([]byte{0}, 64), NewVerifyConfig().SetKeyID("key"), *NewFields())
47 | assert.NoError(t, err)
48 | client := NewDefaultClient(NewClientConfig().SetSignatureName("sig1").SetSigner(signer).SetVerifier(verifier).SetDigestSchemesSend([]string{DigestSha256}))
49 | res, err := client.Post(ts.URL+"?foo", "text/plain", strings.NewReader("Message body here"))
50 | assert.NoError(t, err)
51 | if res != nil {
52 | _, err = io.ReadAll(res.Body)
53 | _ = res.Body.Close()
54 | assert.NoError(t, err)
55 |
56 | assert.Equal(t, res.Status, "200 OK", "Bad status returned")
57 | }
58 | }
59 |
60 | // test various failures
61 | func TestWrapHandlerServerSigns(t *testing.T) {
62 | serverSignsTestCase := func(t *testing.T, nilSigner, dontSignResponse, earlyExpires, noSigner, badKey, badAlgs, verifyRequest bool) {
63 | // Callback to let the server locate its signing key and configuration
64 | var signConfig *SignConfig
65 | if !earlyExpires {
66 | signConfig = NewSignConfig()
67 | } else {
68 | signConfig = NewSignConfig().SetExpires(2000)
69 | }
70 | fetchSigner := func(res http.Response, r *http.Request) (string, *Signer) {
71 | sigName := "sig1"
72 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{0}, 64), signConfig.SetKeyID("key1"),
73 | Headers("@status", "bar", "date"))
74 | return sigName, signer
75 | }
76 | badFetchSigner := func(res http.Response, r *http.Request) (string, *Signer) {
77 | return "just a name", nil
78 | }
79 |
80 | simpleHandler := func(w http.ResponseWriter, r *http.Request) { // this handler gets wrapped
81 | w.WriteHeader(200)
82 | w.Header().Set("bar", "baz me")
83 | _, _ = fmt.Fprintln(w, "Hello, client")
84 | }
85 |
86 | // Configure the wrapper and set it up
87 | var config *HandlerConfig
88 | if !nilSigner {
89 | if !noSigner {
90 | config = NewHandlerConfig().SetFetchSigner(fetchSigner)
91 | } else {
92 | config = NewHandlerConfig().SetFetchSigner(badFetchSigner)
93 | }
94 |
95 | } else {
96 | config = NewHandlerConfig().SetFetchSigner(nil)
97 |
98 | }
99 | if dontSignResponse {
100 | config = config.SetFetchSigner(nil)
101 | }
102 | if verifyRequest {
103 | serverVerifier, _ := NewHMACSHA256Verifier(bytes.Repeat([]byte{9}, 64), NewVerifyConfig().SetKeyID("key"), *NewFields())
104 | config = config.SetFetchVerifier(func(r *http.Request) (sigName string, verifier *Verifier) {
105 | return "sig333", serverVerifier
106 | })
107 | }
108 | ts := httptest.NewServer(WrapHandler(http.HandlerFunc(simpleHandler), *config))
109 | defer ts.Close()
110 |
111 | // HTTP client code
112 | var key []byte
113 | if !badKey {
114 | key = bytes.Repeat([]byte{0}, 64)
115 | } else {
116 | key = bytes.Repeat([]byte{3}, 64)
117 | }
118 | verifyConfig := NewVerifyConfig().SetKeyID("key")
119 | if badAlgs {
120 | verifyConfig = verifyConfig.SetAllowedAlgs([]string{"zuzu"})
121 | }
122 | verifier, _ := NewHMACSHA256Verifier(key, verifyConfig, *NewFields())
123 |
124 | client := NewDefaultClient(NewClientConfig().SetSignatureName("sig1").SetVerifier(verifier))
125 | res, err := client.Get(ts.URL)
126 | if err == nil && res.StatusCode == 200 {
127 | t.Errorf("Surprise! Server sent 200 OK and signature validation was successful.")
128 | }
129 | }
130 | nilSigner := func(t *testing.T) {
131 | serverSignsTestCase(t, true, false, false, false, false, false, false)
132 | }
133 | dontSignResponse := func(t *testing.T) {
134 | serverSignsTestCase(t, false, true, false, false, false, false, false)
135 | }
136 | earlyExpires := func(t *testing.T) {
137 | serverSignsTestCase(t, false, false, true, false, false, false, false)
138 | }
139 | noSigner := func(t *testing.T) {
140 | serverSignsTestCase(t, false, false, false, true, false, false, false)
141 | }
142 | badKey := func(t *testing.T) {
143 | serverSignsTestCase(t, false, false, false, false, true, false, false)
144 | }
145 | badAlgs := func(t *testing.T) {
146 | serverSignsTestCase(t, false, false, false, false, false, true, false)
147 | }
148 | failVerify := func(t *testing.T) {
149 | serverSignsTestCase(t, false, false, false, false, false, false, true)
150 | }
151 | t.Run("nil Signer", nilSigner)
152 | t.Run("don't sign response", dontSignResponse)
153 | t.Run("early expires field", earlyExpires)
154 | t.Run("bad fetch Signer", noSigner)
155 | t.Run("wrong verification key", badKey)
156 | t.Run("failed algorithm check", badAlgs)
157 | t.Run("failed request verification", failVerify)
158 | }
159 |
160 | func TestWrapHandlerServerFails(t *testing.T) { // non-default verify handler
161 | // Set up a test server
162 | simpleHandler := func(w http.ResponseWriter, r *http.Request) {
163 | w.WriteHeader(200)
164 | w.Header().Set("Content-Type", "text/plain")
165 | _, _ = fmt.Fprintf(w, "Hey client, you sent a signature with these parameters: %s\n",
166 | r.Header.Get("Signature-Input"))
167 | }
168 | verifyFailed := func(w http.ResponseWriter, r *http.Request, logger *log.Logger, err error) {
169 | w.WriteHeader(599)
170 | if err == nil { // should not happen
171 | t.Errorf("Test failed, handler received an error: %v", err)
172 | }
173 | if logger != nil {
174 | log.Println("Could not verify request signature: " + err.Error())
175 | }
176 | _, _ = fmt.Fprintln(w, "Could not verify request signature")
177 | }
178 | fetchVerifier := func(r *http.Request) (string, *Verifier) {
179 | sigName := "sig1"
180 | verifier, _ := NewHMACSHA256Verifier(bytes.Repeat([]byte{0}, 64), NewVerifyConfig().SetKeyID("key"),
181 | Headers("@method"))
182 | return sigName, verifier
183 | }
184 | config := NewHandlerConfig().SetReqNotVerified(verifyFailed).SetFetchVerifier(fetchVerifier)
185 | ts := httptest.NewServer(WrapHandler(http.HandlerFunc(simpleHandler), *config))
186 | defer ts.Close()
187 |
188 | // Client code starts here
189 | // Create a signer and a wrapped HTTP client (we set SignCreated to false to make the response deterministic,
190 | // don't do that in production.)
191 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64),
192 | NewSignConfig().SetKeyID("key1").SignCreated(false), Headers("@method"))
193 | client := NewDefaultClient(NewClientConfig().SetSignatureName("sig22").SetSigner(signer)) // sign, don't verify
194 |
195 | // Send an HTTP GET, get response -- signing and verification happen behind the scenes
196 | res, err := client.Get(ts.URL)
197 | assert.NoError(t, err, "Get failed")
198 |
199 | assert.Equal(t, res.StatusCode, 599, "Verification did not fail?")
200 | }
201 |
--------------------------------------------------------------------------------
/internal-docs/JWX_V3_RESEARCH_FINDINGS.md:
--------------------------------------------------------------------------------
1 | # jwx v3 Research Findings
2 |
3 | ## Status: COMPLETED
4 |
5 | This document tracks research findings about jwx v3 API changes before implementing the migration.
6 |
7 | ## Research Questions
8 |
9 | ### 1. Does jwx v3 exist and is it stable?
10 | - **Status**: ✅ CONFIRMED
11 | - **Finding**: jwx v3 exists and is stable
12 | - **Latest Version**: v3.0.12 (as of research date)
13 | - **Version History**: v3.0.0 through v3.0.12 (12 patch releases)
14 | - **Conclusion**: READY FOR PRODUCTION USE
15 |
16 | ### 2. What are the actual API changes in jwx v3?
17 | - **Status**: ✅ RESEARCHED
18 |
19 | #### jws.NewSigner()
20 | - **v2 Signature**: `func NewSigner(alg jwa.SignatureAlgorithm) (Signer, error)`
21 | - **v3 Signature**: `func NewSigner(alg jwa.SignatureAlgorithm) (Signer, error)` - **SAME**
22 | - **⚠️ DEPRECATION**: v3 marks NewSigner as DEPRECATED, recommends `SignerFor()` instead
23 | - **Migration Note**: Still works in v3 but may be removed in future versions
24 |
25 | #### jws.SignerFor() (NEW in v3)
26 | - **Signature**: `func SignerFor(alg jwa.SignatureAlgorithm) (Signer2, error)`
27 | - **Returns**: `Signer2` interface (new) instead of `Signer` (legacy)
28 | - **Behavior**: Never fails, provides fallback signers
29 | - **Recommended**: This is the preferred way to get signers in v3
30 |
31 | #### jws.Signer vs jws.Signer2
32 | - **Signer (legacy)**: Type alias to `legacy.Signer`
33 | - Method: `Sign(payload []byte, key any) ([]byte, error)`
34 | - **Signer2 (new)**: New interface
35 | - Method: `Sign(key any, payload []byte) ([]byte, error)`
36 | - **⚠️ CRITICAL**: **Parameter order is SWAPPED!** key before payload
37 |
38 | #### jws.NewVerifier()
39 | - **v2 Signature**: `func NewVerifier(alg jwa.SignatureAlgorithm) (Verifier, error)`
40 | - **v3 Signature**: `func NewVerifier(alg jwa.SignatureAlgorithm) (Verifier, error)` - **SAME**
41 | - **Status**: NOT DEPRECATED (still recommended in v3)
42 | - **Migration Note**: No changes needed
43 |
44 | #### jws.Verifier
45 | - **Type**: Alias to `legacy.Verifier`
46 | - **Status**: Still used, no deprecation
47 | - **Migration Note**: No changes needed
48 |
49 | ### 3. Are there breaking changes in the JWS package?
50 | - **Status**: ✅ ANALYZED
51 | - **Breaking Changes**: YES, but gradual
52 | - `NewSigner()` is deprecated (but still works with legacy interface)
53 | - New `SignerFor()` returns `Signer2` with swapped parameter order
54 | - `NewVerifier()` is NOT deprecated
55 | - **Compatibility**: Old code using `NewSigner()` will still work
56 | - **Migration Path**: Can use deprecated API initially, then migrate to `SignerFor()`
57 |
58 | ### 4. Are v2 and v3 compatible in the same binary?
59 | - **Status**: ✅ CONFIRMED COMPATIBLE
60 | - **Finding**: Yes, v2 and v3 can coexist
61 | - Different import paths: `github.com/lestrrat-go/jwx/v2` vs `github.com/lestrrat-go/jwx/v3`
62 | - No symbol conflicts (different module versions)
63 | - Aliased imports work correctly (e.g., `jwav2`, `jwav3`)
64 | - **Conclusion**: Option B (dual functions) is technically feasible
65 |
66 | ## Alternative Approach
67 |
68 | If jwx v3 doesn't exist yet or isn't stable, we have options:
69 |
70 | ### Option 1: Wait for jwx v3 Release
71 | - Monitor the jwx repository for v3 release
72 | - Implement migration when v3 is stable
73 | - Keep current implementation unchanged
74 |
75 | ### Option 2: Implement Based on Assumptions
76 | - Create the dual-function structure now
77 | - When v3 is released, fill in the V3 function implementations
78 | - Mark V3 functions as "EXPERIMENTAL - requires jwx v3" until ready
79 |
80 | ### Option 3: Check jwx v3 Development Branch
81 | - If v3 is in development, review the develop/v3 branch
82 | - Document planned changes
83 | - Prepare implementation based on upcoming changes
84 |
85 | ## Next Steps
86 |
87 | 1. **Verify jwx v3 Existence**: Check https://github.com/lestrrat-go/jwx
88 | - Look for v3.x.x tags
89 | - Check release notes
90 | - Review branches for v3 development
91 |
92 | 2. **If v3 Exists**:
93 | - Download and examine the API
94 | - Test compatibility with v2
95 | - Proceed with implementation
96 |
97 | 3. **If v3 Doesn't Exist Yet**:
98 | - Update migration plan with realistic timeline
99 | - Consider implementing stub V3 functions
100 | - Wait for official release
101 |
102 | ## Manual Investigation Required
103 |
104 | Since automated web searches didn't provide specific technical details, manual investigation of the jwx repository is needed:
105 |
106 | ```bash
107 | # Check for v3 tags
108 | git ls-remote --tags https://github.com/lestrrat-go/jwx.git | grep v3
109 |
110 | # Or check go proxy
111 | go list -m -versions github.com/lestrrat-go/jwx/v3
112 | ```
113 |
114 | ## Decision Point
115 |
116 | **✅ UNBLOCKED**: All prerequisites verified:
117 | 1. ✅ jwx v3 exists and is available (v3.0.12)
118 | 2. ✅ jwx v3 API is documented (via go doc)
119 | 3. ✅ Breaking changes are understood (see above)
120 |
121 | ## Summary and Recommendations
122 |
123 | ### Key Findings
124 | 1. **jwx v3 is production-ready** - v3.0.12 with 12 patch releases
125 | 2. **Backward compatibility exists** - v2 and v3 can coexist in same binary
126 | 3. **NewSigner() is deprecated** but still works (uses legacy Signer interface)
127 | 4. **NewVerifier() is NOT deprecated** and unchanged
128 | 5. **New `SignerFor()` API** is preferred in v3 (returns Signer2 with swapped params)
129 |
130 | ### Implementation Strategy
131 |
132 | #### Option A: Use Legacy NewSigner() (Simpler, Works But Deprecated)
133 | ```go
134 | // V3 functions using deprecated but compatible API
135 | signer, err := jwsv3.NewSigner(alg) // Deprecated but works
136 | verifier, err := jwsv3.NewVerifier(alg) // Not deprecated, fine to use
137 | ```
138 | **Pros**:
139 | - Minimal code changes
140 | - Same interface as v2
141 | - Works with existing sign() and verify() methods
142 | **Cons**:
143 | - Uses deprecated API
144 | - May break in future jwx releases
145 |
146 | #### Option B: Use New SignerFor() (Future-proof, More Complex)
147 | ```go
148 | // V3 functions using new recommended API
149 | signer2, err := jwsv3.SignerFor(alg) // Returns Signer2 interface
150 | verifier, err := jwsv3.NewVerifier(alg) // Still use NewVerifier
151 | ```
152 | **Pros**:
153 | - Uses recommended v3 API
154 | - Future-proof
155 | **Cons**:
156 | - Requires adapter in sign() method (parameter order swapped)
157 | - More complex implementation
158 |
159 | ### Recommended Approach: **Option B - Use Non-Deprecated APIs**
160 |
161 | **Rationale**:
162 | 1. Avoids using deprecated `NewSigner()` API
163 | 2. Uses recommended `SignerFor()` and `VerifierFor()` APIs
164 | 3. Future-proof implementation
165 | 4. Handles parameter order differences with adapters
166 | 5. Professional implementation using best practices
167 |
168 | ### Implementation Plan (COMPLETED)
169 |
170 | 1. **Add jwx v3 dependency** alongside v2 ✅
171 | 2. **Create NewJWSSignerV3()** using `jwsv3.SignerFor()` ✅
172 | - Returns `Signer2` interface
173 | - Handles parameter order: `Sign(key, payload)` (swapped vs v2)
174 | 3. **Create NewJWSVerifierV3()** using `jwsv3.VerifierFor()` ✅
175 | - Returns `Verifier2` interface
176 | - Handles parameter order: `Verify(key, payload, sig)` (key moved to first)
177 | 4. **Update sign()/verify() methods** ✅
178 | - Added support for `Signer2` interface
179 | - Added support for `Verifier2` interface
180 | - Adapts parameter order automatically
181 | - Maintains backward compatibility with v2 interfaces
182 | 5. **Add comprehensive tests** ✅
183 | - Tests for V3 functions
184 | - Cross-compatibility tests (v2 ↔ v3)
185 | - All tests passing
186 | 6. **Document** implementation details ✅
187 |
188 | ### Implementation Complete
189 |
190 | ✅ **FULLY IMPLEMENTED**: Using recommended non-deprecated jwx v3 APIs
191 | - `SignerFor()` for creating signers (not deprecated `NewSigner()`)
192 | - `VerifierFor()` for creating verifiers
193 | - Proper parameter order handling for new interfaces
194 | - All tests passing
195 | - Zero deprecated code paths
196 |
197 | **Status**: ✅ COMPLETED - USING BEST PRACTICES
198 |
199 |
--------------------------------------------------------------------------------
/client_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "github.com/stretchr/testify/assert"
7 | "log"
8 | "net/http"
9 | "net/http/httptest"
10 | "net/url"
11 | "reflect"
12 | "sort"
13 | "testing"
14 | )
15 |
16 | func TestClient_Get(t *testing.T) {
17 | type fields struct {
18 | sigName string
19 | signer *Signer
20 | verifier *Verifier
21 | fetchVerifier func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier)
22 | Client http.Client
23 | }
24 | type args struct {
25 | url string
26 | }
27 | tests := []struct {
28 | name string
29 | fields fields
30 | args args
31 | wantRes string
32 | wantErr bool
33 | }{
34 | {
35 | name: "Happy path",
36 | fields: fields{
37 | sigName: "sig1",
38 | signer: func() *Signer {
39 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64), NewSignConfig().SetKeyID("key1"), Headers("@method"))
40 | return signer
41 | }(),
42 | verifier: nil,
43 | fetchVerifier: nil,
44 | Client: *http.DefaultClient,
45 | },
46 | args: args{
47 | url: "",
48 | },
49 | wantRes: "200 OK",
50 | wantErr: false,
51 | },
52 | {
53 | name: "not found",
54 | fields: fields{
55 | sigName: "sig1",
56 | signer: func() *Signer {
57 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64), NewSignConfig().SetKeyID("key1"), Headers("@method"))
58 | return signer
59 | }(),
60 | verifier: nil,
61 | fetchVerifier: nil,
62 | Client: *http.DefaultClient,
63 | },
64 | args: args{
65 | url: "/thisaintaurl",
66 | },
67 | wantRes: "404 Not Found",
68 | wantErr: false,
69 | },
70 | {
71 | name: "bad signature name",
72 | fields: fields{
73 | sigName: "",
74 | signer: func() *Signer {
75 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64), NewSignConfig().SetKeyID("key1"), Headers("@method"))
76 | return signer
77 | }(),
78 | verifier: nil,
79 | fetchVerifier: nil,
80 | Client: *http.DefaultClient,
81 | },
82 | args: args{
83 | url: "",
84 | },
85 | wantRes: "",
86 | wantErr: true,
87 | },
88 | {
89 | name: "bad fetch verifier",
90 | fields: fields{
91 | sigName: "sig1",
92 | signer: func() *Signer {
93 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64), NewSignConfig().SetKeyID("key1"), Headers("@method"))
94 | return signer
95 | }(),
96 | verifier: nil,
97 | fetchVerifier: func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier) {
98 | return "name", nil
99 | },
100 | Client: *http.DefaultClient,
101 | },
102 | args: args{
103 | url: "",
104 | },
105 | wantRes: "",
106 | wantErr: true,
107 | },
108 | {
109 | name: "verifier fails",
110 | fields: fields{
111 | sigName: "sig1",
112 | signer: func() *Signer {
113 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64), NewSignConfig().SetKeyID("key1"), Headers("@method"))
114 | return signer
115 | }(),
116 | verifier: nil,
117 | fetchVerifier: func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier) {
118 | verifier, _ = NewHMACSHA256Verifier(bytes.Repeat([]byte{2}, 64), NewVerifyConfig(), Headers("@method"))
119 | return "name", verifier
120 | },
121 | Client: *http.DefaultClient,
122 | },
123 | args: args{
124 | url: "",
125 | },
126 | wantRes: "",
127 | wantErr: true,
128 | },
129 | }
130 |
131 | ts := makeTestServer()
132 | defer ts.Close()
133 |
134 | for _, tt := range tests {
135 | t.Run(tt.name, func(t *testing.T) {
136 | c := &Client{config: ClientConfig{
137 | signatureName: tt.fields.sigName,
138 | signer: tt.fields.signer,
139 | verifier: tt.fields.verifier,
140 | fetchVerifier: tt.fields.fetchVerifier,
141 | },
142 | client: tt.fields.Client,
143 | }
144 | res, err := c.Get(ts.URL + tt.args.url)
145 | var gotRes string
146 | if res != nil {
147 | gotRes = res.Status
148 | }
149 | if (err != nil) != tt.wantErr {
150 | t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
151 | return
152 | }
153 | if !reflect.DeepEqual(gotRes, tt.wantRes) {
154 | t.Errorf("Get() gotRes = %v, want %v", gotRes, tt.wantRes)
155 | }
156 | })
157 | }
158 | }
159 |
160 | func makeTestServer() *httptest.Server {
161 | simpleHandler := func(w http.ResponseWriter, r *http.Request) {
162 | if r.RequestURI == "/" {
163 | w.WriteHeader(200)
164 | } else {
165 | w.WriteHeader(404)
166 | }
167 | _, err := fmt.Fprintln(w, "Hey client, good to see ya")
168 | if err != nil {
169 | log.Fatal("Server could not send response")
170 | }
171 | }
172 | ts := httptest.NewServer(http.HandlerFunc(simpleHandler))
173 | return ts
174 | }
175 |
176 | func TestClient_Head(t *testing.T) {
177 | type fields struct {
178 | sigName string
179 | signer *Signer
180 | verifier *Verifier
181 | fetchVerifier func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier)
182 | Client http.Client
183 | }
184 | type args struct {
185 | url string
186 | }
187 | tests := []struct {
188 | name string
189 | fields fields
190 | args args
191 | wantRes string
192 | wantErr bool
193 | }{
194 | {
195 | name: "Happy Path",
196 | fields: fields{
197 | sigName: "sig1",
198 | signer: func() *Signer {
199 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64), NewSignConfig().SetKeyID("key1"),
200 | Headers("@method"))
201 | return signer
202 | }(),
203 | verifier: nil,
204 | fetchVerifier: nil,
205 | Client: *http.DefaultClient,
206 | },
207 | args: args{
208 | url: "/",
209 | },
210 | wantRes: "200 OK",
211 | wantErr: false,
212 | },
213 | }
214 |
215 | ts := makeTestServer()
216 | defer ts.Close()
217 |
218 | for _, tt := range tests {
219 | t.Run(tt.name, func(t *testing.T) {
220 | c := &Client{
221 | config: ClientConfig{
222 | signatureName: tt.fields.sigName,
223 | signer: tt.fields.signer,
224 | verifier: tt.fields.verifier,
225 | fetchVerifier: tt.fields.fetchVerifier,
226 | },
227 | client: tt.fields.Client,
228 | }
229 |
230 | res, err := c.Head(ts.URL + tt.args.url)
231 | var gotRes string
232 | if res != nil {
233 | gotRes = res.Status
234 | }
235 | if (err != nil) != tt.wantErr {
236 | t.Errorf("Head() error = %v, wantErr %v", err, tt.wantErr)
237 | return
238 | }
239 | if !reflect.DeepEqual(gotRes, tt.wantRes) {
240 | t.Errorf("Head() gotRes = %v, want %v", gotRes, tt.wantRes)
241 | }
242 | })
243 | }
244 | }
245 |
246 | func TestClient_PostForm(t *testing.T) {
247 | type fields struct {
248 | config ClientConfig
249 | client http.Client
250 | }
251 | type args struct {
252 | url string
253 | data url.Values
254 | }
255 |
256 | ts := makeTestServer()
257 | defer ts.Close()
258 |
259 | tests := []struct {
260 | name string
261 | fields fields
262 | args args
263 | wantRespHeaders []string
264 | wantErr assert.ErrorAssertionFunc
265 | }{
266 | {
267 | name: "happy path",
268 | fields: fields{
269 | config: *NewClientConfig(),
270 | client: *http.DefaultClient,
271 | },
272 | args: args{
273 | url: ts.URL,
274 | data: func() url.Values {
275 | var v = url.Values{}
276 | v.Add("k1", "v1")
277 | v.Set("k2", "v2")
278 | return v
279 | }(),
280 | },
281 | wantRespHeaders: []string{"Content-Length", "Content-Type", "Date"},
282 | wantErr: assert.NoError,
283 | },
284 | }
285 |
286 | for _, tt := range tests {
287 | t.Run(tt.name, func(t *testing.T) {
288 | c := &Client{
289 | config: tt.fields.config,
290 | client: tt.fields.client,
291 | }
292 | gotResp, err := c.PostForm(tt.args.url, tt.args.data)
293 | if !tt.wantErr(t, err, fmt.Sprintf("PostForm(%v, %v)", tt.args.url, tt.args.data)) {
294 | return
295 | }
296 | headers := []string{}
297 | for k := range gotResp.Header {
298 | headers = append(headers, k)
299 | }
300 | sort.Strings(headers)
301 | assert.Equalf(t, tt.wantRespHeaders, headers, "PostForm(%v, %v)", tt.args.url, tt.args.data)
302 | })
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/trailer_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "bytes"
5 | "encoding/base64"
6 | "fmt"
7 | "net/http"
8 | "net/http/httptest"
9 | "net/url"
10 | "strings"
11 | "testing"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | var rawPost1 = `POST /foo HTTP/1.1
17 | Content-Type: text/plain
18 | Transfer-Encoding: chunked
19 | Trailer: Expires, Hdr
20 |
21 | 4
22 | HTTP
23 | 7
24 | Message
25 | a
26 | Signatures
27 | 0
28 | Expires: Wed, 9 Nov 2022 07:28:00 GMT
29 | Hdr: zoom
30 |
31 | `
32 |
33 | var rawPost2 = `POST /foo HTTP/1.1
34 | Content-Type: text/plain
35 | Transfer-Encoding: chunked
36 | Trailer: Expires, Hdr
37 |
38 | 4
39 | HTTP
40 | 7
41 | Message
42 | a
43 | Signatures
44 | 0
45 | Expires: Wed, 9 Nov 2022 07:28:00 GMT
46 | Hdr: zoom
47 | `
48 |
49 | var rawHeaders1 = `POST /foo HTTP/1.1
50 | Content-Type: text/plain
51 | Transfer-Encoding: chunked
52 | Trailer: Expires, Hdr
53 |
54 | `
55 |
56 | var longReq1 = rawHeaders1 + "5000\r\n" + strings.Repeat("x", 0x5000) + "\r\n0\r\n" + "Hdr: zoomba\r\n\r\n"
57 |
58 | func TestTrailer_Get(t *testing.T) {
59 | fetchVerifier := func(r *http.Request) (string, *Verifier) {
60 | sigName := "sig1"
61 | verifier, _ := NewHMACSHA256Verifier(bytes.Repeat([]byte{1}, 64), NewVerifyConfig().SetKeyID("key1"),
62 | *NewFields().AddHeader("@method").
63 | AddHeaderExt("hdr", false, false, false, true))
64 | return sigName, verifier
65 | }
66 |
67 | fetchSigner := func(res http.Response, r *http.Request) (string, *Signer) {
68 | sigName := "sig1"
69 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{0}, 64), NewSignConfig().SetKeyID("key"),
70 | Headers("@status", "bar", "date", "Content-Digest"))
71 | return sigName, signer
72 | }
73 |
74 | simpleHandler := func(w http.ResponseWriter, r *http.Request) {
75 | w.WriteHeader(200)
76 | w.Header().Set("bar", "baz and baz again")
77 | _, _ = fmt.Fprintln(w, "Hello, client")
78 | _, _ = fmt.Fprintln(w, "Hello again")
79 | }
80 | config := NewHandlerConfig().SetFetchVerifier(fetchVerifier).
81 | SetFetchSigner(fetchSigner).SetDigestSchemesSend([]string{DigestSha256}).SetDigestSchemesRecv([]string{DigestSha256})
82 | ts := httptest.NewServer(WrapHandler(http.HandlerFunc(simpleHandler), *config))
83 | defer ts.Close()
84 |
85 | c := &Client{config: ClientConfig{
86 | signatureName: "sig1",
87 | signer: func() *Signer {
88 | signer, _ := NewHMACSHA256Signer(bytes.Repeat([]byte{1}, 64), NewSignConfig().SetKeyID("key1"),
89 | *NewFields().AddHeader("@method").
90 | AddHeaderExt("hdr", false, false, false, true))
91 | return signer
92 | }(),
93 | verifier: nil,
94 | fetchVerifier: nil,
95 | },
96 | client: *http.DefaultClient,
97 | }
98 |
99 | req := readRequestChunked(rawPost1)
100 |
101 | req.RequestURI = "" // otherwise Do will complain
102 | u, err := url.Parse(ts.URL + "/")
103 | if err != nil {
104 | panic(err)
105 | }
106 | req.URL = u
107 |
108 | res, err := c.Do(req)
109 | var gotRes string
110 | if res != nil {
111 | gotRes = res.Status
112 | }
113 | if err != nil {
114 | t.Errorf("Get() error = %v", err)
115 | return
116 | }
117 | if gotRes != "200 OK" {
118 | t.Errorf("Get() gotRes = %v", gotRes)
119 | }
120 |
121 | req2 := readRequest(longReq1)
122 | req2.RequestURI = "" // otherwise Do will complain
123 | req2.URL = u
124 |
125 | res, err = c.Do(req2)
126 | if res != nil {
127 | gotRes = res.Status
128 | }
129 | if err != nil {
130 | t.Errorf("Get() error = %v", err)
131 | return
132 | }
133 | if gotRes != "200 OK" {
134 | t.Errorf("Get() gotRes = %v", gotRes)
135 | }
136 | }
137 |
138 | func TestTrailer_SigFields(t *testing.T) {
139 | config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-shared-secret")
140 | fields := Headers("@authority", "@method", "content-type")
141 | signatureName := "sig1"
142 | key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==")
143 | signer, _ := NewHMACSHA256Signer(key, config, fields)
144 | req := readRequest(rawPost2)
145 | sigInput, sig, err := SignRequest(signatureName, *signer, req)
146 | assert.NoError(t, err, "signature failed")
147 | // Add signature correctly
148 | signedMessage := rawPost2 + "Signature: " + sig + "\n" + "Signature-Input: " + sigInput + "\n\n"
149 | signedMessage = strings.Replace(signedMessage, "Trailer: Expires, Hdr", "Trailer: Expires, Hdr, Signature, Signature-Input",
150 | 1)
151 | req2 := readRequestChunked(signedMessage)
152 | verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields)
153 | assert.NoError(t, err, "could not generate Verifier")
154 | err = VerifyRequest(signatureName, *verifier, req2)
155 | assert.NoError(t, err, "verification error")
156 |
157 | // Missing Signature-Input
158 | signedMessage = rawPost2 + "Signature: " + sig + "\n\n"
159 | signedMessage = strings.Replace(signedMessage, "Trailer: Expires, Hdr", "Trailer: Expires, Hdr, Signature, Signature-Input",
160 | 1)
161 | req2 = readRequestChunked(signedMessage)
162 | verifier, err = NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields)
163 | assert.NoError(t, err, "could not generate Verifier")
164 | err = VerifyRequest(signatureName, *verifier, req2)
165 | assert.Error(t, err, "verification error")
166 |
167 | // Missing Signature
168 | signedMessage = rawPost2 + "Signature-Input: " + sigInput + "\n\n"
169 | signedMessage = strings.Replace(signedMessage, "Trailer: Expires, Hdr", "Trailer: Expires, Hdr, Signature, Signature-Input",
170 | 1)
171 | req2 = readRequestChunked(signedMessage)
172 | verifier, err = NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields)
173 | assert.NoError(t, err, "could not generate Verifier")
174 | err = VerifyRequest(signatureName, *verifier, req2)
175 | assert.Error(t, err, "verification error")
176 | }
177 |
178 | // Same as TestTrailer_SigFields but using Message
179 | func TestMessageTrailer_SigFields(t *testing.T) {
180 | config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475).SetKeyID("test-shared-secret")
181 | fields := Headers("@authority", "@method", "content-type")
182 | signatureName := "sig1"
183 | key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==")
184 | signer, _ := NewHMACSHA256Signer(key, config, fields)
185 | req := readRequest(rawPost2)
186 | sigInput, sig, err := SignRequest(signatureName, *signer, req)
187 | assert.NoError(t, err, "signature failed")
188 | // Add signature correctly
189 | signedMessage := rawPost2 + "Signature: " + sig + "\n" + "Signature-Input: " + sigInput + "\n\n"
190 | signedMessage = strings.Replace(signedMessage, "Trailer: Expires, Hdr", "Trailer: Expires, Hdr, Signature, Signature-Input",
191 | 1)
192 | req2 := readRequestChunked(signedMessage)
193 | verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields)
194 | assert.NoError(t, err, "could not generate Verifier")
195 | msg, err := NewMessage(NewMessageConfig().WithRequest(req2))
196 | assert.NoError(t, err, "could not create Message")
197 | _, err = msg.Verify(signatureName, *verifier)
198 | assert.NoError(t, err, "verification error")
199 |
200 | // Missing Signature-Input
201 | signedMessage = rawPost2 + "Signature: " + sig + "\n\n"
202 | signedMessage = strings.Replace(signedMessage, "Trailer: Expires, Hdr", "Trailer: Expires, Hdr, Signature, Signature-Input",
203 | 1)
204 | req2 = readRequestChunked(signedMessage)
205 | verifier, err = NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields)
206 | assert.NoError(t, err, "could not generate Verifier")
207 | msg, err = NewMessage(NewMessageConfig().WithRequest(req2))
208 | assert.NoError(t, err, "could not create Message")
209 | _, err = msg.Verify(signatureName, *verifier)
210 | assert.Error(t, err, "verification error")
211 |
212 | // Missing Signature
213 | signedMessage = rawPost2 + "Signature-Input: " + sigInput + "\n\n"
214 | signedMessage = strings.Replace(signedMessage, "Trailer: Expires, Hdr", "Trailer: Expires, Hdr, Signature, Signature-Input",
215 | 1)
216 | req2 = readRequestChunked(signedMessage)
217 | verifier, err = NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields)
218 | assert.NoError(t, err, "could not generate Verifier")
219 | msg, err = NewMessage(NewMessageConfig().WithRequest(req2))
220 | assert.NoError(t, err, "could not create Message")
221 | _, err = msg.Verify(signatureName, *verifier)
222 | assert.Error(t, err, "verification error")
223 | }
224 |
--------------------------------------------------------------------------------
/internal-docs/JWX_V3_IMPLEMENTATION_SUMMARY.md:
--------------------------------------------------------------------------------
1 | # jwx v3 Implementation Summary
2 |
3 | ## Status: ✅ COMPLETED
4 |
5 | Implementation of jwx v3 support alongside existing jwx v2 functionality has been successfully completed.
6 |
7 | ## Implementation Date
8 | October 31, 2025
9 |
10 | ## Approach Taken
11 | **Option B: Backward Compatible Migration** - Separate V3 functions alongside existing v2 functions
12 |
13 | ## Changes Made
14 |
15 | ### 1. Dependencies (go.mod)
16 | - ✅ Added `github.com/lestrrat-go/jwx/v3 v3.0.12`
17 | - ✅ Kept `github.com/lestrrat-go/jwx/v2 v2.1.2` for backward compatibility
18 | - ✅ Go version upgraded to 1.24.0 (automatic)
19 | - ✅ Various dependency updates (automatic)
20 |
21 | ### 2. Source Code (crypto.go)
22 |
23 | #### Imports
24 | - Added jwx v3 imports with aliases: `jwav3`, `jwsv3`
25 | - Kept jwx v2 imports without aliases: `jwa`, `jws`
26 | - Clear comments indicating which version is used where
27 |
28 | #### New Functions
29 | **NewJWSSignerV3()**
30 | ```go
31 | func NewJWSSignerV3(alg jwav3.SignatureAlgorithm, key interface{}, config *SignConfig, fields Fields) (*Signer, error)
32 | ```
33 | - Uses `jwsv3.SignerFor()` - **recommended non-deprecated API**
34 | - Returns `Signer2` interface with parameter order: `Sign(key, payload)` (key first!)
35 | - Returns same `*Signer` type as v2 version
36 | - Handles `jwav3.NoSignature()` (function call, not constant)
37 | - Compatible with existing signing infrastructure via interface adapters
38 |
39 | **NewJWSVerifierV3()**
40 | ```go
41 | func NewJWSVerifierV3(alg jwav3.SignatureAlgorithm, key interface{}, config *VerifyConfig, fields Fields) (*Verifier, error)
42 | ```
43 | - Uses `jwsv3.VerifierFor()` - **recommended non-deprecated API**
44 | - Returns `Verifier2` interface with parameter order: `Verify(key, payload, sig)` (key first!)
45 | - Returns same `*Verifier` type as v2 version
46 | - Handles `jwav3.NoSignature()` (function call, not constant)
47 | - Compatible with existing verification infrastructure via interface adapters
48 |
49 | #### Updated Internal Methods
50 | **sign() method**
51 | - Enhanced to handle both v2 (`jws.Signer`) and v3 (`Signer2`) interfaces
52 | - Handles parameter order differences:
53 | - v2: `Sign(payload, key)`
54 | - v3: `Sign(key, payload)` - **parameter order swapped!**
55 | - Uses interface type assertions to detect version
56 | - Falls back to legacy interface for backward compatibility
57 | - No changes to existing v2 behavior
58 |
59 | **verify() method**
60 | - Enhanced to handle both v2 (`jws.Verifier`) and v3 (`Verifier2`) interfaces
61 | - Handles parameter order differences:
62 | - v2: `Verify(payload, sig, key)`
63 | - v3: `Verify(key, payload, sig)` - **key moved to first position!**
64 | - Uses interface type assertions to detect version
65 | - Falls back to legacy interface for backward compatibility
66 | - No changes to existing v2 behavior
67 |
68 | #### Updated Existing Functions
69 | **NewJWSSigner()** (v2)
70 | - Added documentation noting it uses jwx v2
71 | - Added note recommending `NewJWSSignerV3` for new code with jwx v3
72 | - No functional changes - complete backward compatibility
73 |
74 | **NewJWSVerifier()** (v2)
75 | - Added documentation noting it uses jwx v2
76 | - Added note recommending `NewJWSVerifierV3` for new code with jwx v3
77 | - No functional changes - complete backward compatibility
78 |
79 | ### 3. Tests (crypto_test.go)
80 |
81 | #### New Test Functions
82 | 1. **TestForeignSignerV3()** - Tests ES256 signing and verification with v3
83 | 2. **TestMessageForeignSignerV3()** - Tests Message API with v3
84 | 3. **TestNewJWSVerifierV3()** - Tests verifier creation with v3 (with subtests)
85 | 4. **TestCrossVersionCompatibility()** - Critical test for cross-version compatibility
86 | - Subtest: `v2_sign_v3_verify` - Sign with v2, verify with v3
87 | - Subtest: `v3_sign_v2_verify` - Sign with v3, verify with v2
88 |
89 | #### Test Results
90 | - ✅ All existing v2 tests pass (backward compatibility verified)
91 | - ✅ All new v3 tests pass
92 | - ✅ Cross-compatibility tests pass (v2 ↔ v3 signatures are compatible)
93 | - ✅ Full test suite passes: `go test ./...`
94 |
95 | ## API Surface Changes
96 |
97 | ### New Public Functions
98 | - `NewJWSSignerV3(alg jwav3.SignatureAlgorithm, ...) (*Signer, error)`
99 | - `NewJWSVerifierV3(alg jwav3.SignatureAlgorithm, ...) (*Verifier, error)`
100 |
101 | ### Existing Functions (Unchanged)
102 | - `NewJWSSigner(alg jwa.SignatureAlgorithm, ...) (*Signer, error)` - ✅ Still works
103 | - `NewJWSVerifier(alg jwa.SignatureAlgorithm, ...) (*Verifier, error)` - ✅ Still works
104 | - All native algorithm functions (HMAC, RSA, ECDSA, Ed25519) - ✅ Unaffected
105 |
106 | ### Breaking Changes
107 | **None** - This is a backward compatible release
108 |
109 | ## User Impact
110 |
111 | ### Who Benefits
112 | - Users who want to adopt jwx v3 for new code
113 | - Users who need jwx v3 features
114 | - Users who want to future-proof their code
115 |
116 | ### Who is NOT Affected
117 | - Users of existing `NewJWSSigner()` and `NewJWSVerifier()` functions
118 | - Users of native algorithm functions (most users)
119 | - Users who don't use JWS algorithms at all
120 |
121 | ### Migration Path for Users
122 | 1. **Optional** - Users can continue using v2 functions indefinitely
123 | 2. **When Ready** - Users can migrate to V3 functions at their own pace
124 | 3. **Easy** - Just change function name and import: `jwa.ES256` → `jwav3.ES256()`
125 | 4. **Compatible** - Signatures remain compatible between v2 and v3
126 |
127 | ## Technical Notes
128 |
129 | ### jwx v3 API Differences
130 | 1. **SignatureAlgorithm constants** - Changed from constants to functions
131 | - v2: `jwa.ES256` (constant)
132 | - v3: `jwav3.ES256()` (function call)
133 |
134 | 2. **NewSigner() deprecated** - We use recommended API instead
135 | - v3 recommends `SignerFor()` which returns `Signer2` interface
136 | - **We now use `SignerFor()`** - non-deprecated API
137 | - `Signer2.Sign(key, payload)` has **swapped parameter order** vs v2
138 |
139 | 3. **NewVerifier() deprecated** - We use recommended API instead
140 | - v3 recommends `VerifierFor()` which returns `Verifier2` interface
141 | - **We now use `VerifierFor()`** - non-deprecated API
142 | - `Verifier2.Verify(key, payload, sig)` has **different parameter order** vs v2
143 |
144 | 4. **Interface incompatibilities** - Handled via adapters in sign()/verify() methods
145 | - v2 Signer: `Sign(payload, key)`
146 | - v3 Signer2: `Sign(key, payload)` - **parameters swapped**
147 | - v2 Verifier: `Verify(payload, sig, key)`
148 | - v3 Verifier2: `Verify(key, payload, sig)` - **key moved to first position**
149 | - Our implementation detects interface types and adapts parameter order automatically
150 |
151 | ### Future Considerations
152 |
153 | #### Deprecation Timeline
154 | 1. **Now (v1.3.0 estimate)**: Both v2 and v3 functions available
155 | 2. **Future (v1.4.0+)**: Mark v2 functions as deprecated in documentation
156 | 3. **Much Later (v2.0.0)**: Remove v2 functions in next major version
157 |
158 | #### Already Using Best Practices
159 | ✅ **Already implemented**: We use the recommended non-deprecated APIs
160 | - `SignerFor()` instead of deprecated `NewSigner()`
161 | - `VerifierFor()` instead of `NewVerifier()`
162 | - Proper parameter order handling for `Signer2` and `Verifier2` interfaces
163 | - No deprecated code paths in V3 functions
164 |
165 | ## Dependencies
166 |
167 | ### Production Dependencies
168 | - `github.com/lestrrat-go/jwx/v2 v2.1.2` (existing)
169 | - `github.com/lestrrat-go/jwx/v3 v3.0.12` (new)
170 |
171 | ### Transitive Dependencies Added
172 | - `github.com/lestrrat-go/option/v2 v2.0.0`
173 | - Various other dependencies updated automatically
174 |
175 | ### Dependency Size Impact
176 | - Both v2 and v3 libraries are now dependencies (temporary)
177 | - Will be reduced when v2 functions are removed in future major version
178 |
179 | ## Validation
180 |
181 | ### Code Quality
182 | - ✅ No linter errors (except pre-existing warning)
183 | - ✅ All tests pass
184 | - ✅ Code coverage maintained
185 | - ✅ No breaking changes
186 |
187 | ### Compatibility
188 | - ✅ v2 functions work identically
189 | - ✅ v3 functions work correctly
190 | - ✅ Cross-version signatures are compatible
191 | - ✅ RFC 9421 compliance maintained
192 |
193 | ### Documentation
194 | - ✅ Function documentation updated
195 | - ✅ Comments explain v2 vs v3 usage
196 | - ✅ Research findings documented
197 | - ✅ Implementation summary documented (this file)
198 |
199 | ## Documentation Generated
200 |
201 | 1. **JWX_V3_MIGRATION_PLAN.md** - Comprehensive migration plan (Option B selected)
202 | 2. **JWX_V3_RESEARCH_FINDINGS.md** - Research on jwx v3 API changes
203 | 3. **JWX_V3_IMPLEMENTATION_SUMMARY.md** - This file
204 |
205 | ## Success Metrics
206 |
207 | ✅ All success criteria met:
208 | - All existing unit tests pass
209 | - All new unit tests pass
210 | - No performance regressions
211 | - Signatures remain RFC 9421 compliant
212 | - Cross-version compatibility verified
213 | - Zero breaking changes
214 | - Documentation complete
215 |
216 | ## Recommendations
217 |
218 | ### For Maintainers
219 | 1. Monitor jwx v3 updates for API changes
220 | 2. Consider deprecating v2 functions in 6-12 months
221 | 3. Plan for v2 function removal in next major version
222 | 4. Watch for `NewSigner()` deprecation in jwx v3
223 |
224 | ### For Users
225 | 1. New code should use `NewJWSSignerV3()` and `NewJWSVerifierV3()`
226 | 2. Existing code can continue using v2 functions
227 | 3. Migration is optional and low-risk
228 | 4. Signatures are fully compatible between versions
229 |
230 | ## Conclusion
231 |
232 | The jwx v3 implementation is complete, tested, and production-ready. The backward-compatible approach ensures zero disruption to existing users while providing a clear path forward for jwx v3 adoption.
233 |
234 | **Status**: ✅ READY FOR RELEASE
235 |
236 |
--------------------------------------------------------------------------------
/fields.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "fmt"
5 | "github.com/dunglas/httpsfv"
6 | "strings"
7 | )
8 |
9 | // Fields is a list of fields to be signed or verified. To initialize, use Headers or for more complex
10 | // cases, NewFields followed by a chain of Add... methods.
11 | //
12 | // Several component types may be marked as optional. When signing a message, an optional component (e.g., header)
13 | // is signed if it exists in the message to be signed, otherwise it is not included in the signature input.
14 | // Upon verification, a field marked optional must be included in the signed components if it appears at all.
15 | // This allows for intuitive handling of application components (headers, query parameters) whose presence in
16 | // the message depends on application logic. Please do NOT use this functionality for headers that may legitimately be
17 | // added by a proxy, such as X-Forwarded-For.
18 | type Fields struct {
19 | f []field
20 | }
21 |
22 | // The SFV representation of a field is name;flagName="flagValue"
23 | // Note that this is a subset of SFV, we only support string-valued params, and only one param
24 | // per field for now.
25 | type field httpsfv.Item
26 |
27 | func (f field) String() string {
28 | i := httpsfv.Item(f)
29 | s, err := httpsfv.Marshal(i)
30 | if err == nil {
31 | return s
32 | }
33 | return fmt.Sprintf("malformed field: %v", err)
34 | }
35 |
36 | func (f field) Equal(f2 field) bool {
37 | n1, err1 := f.name()
38 | n2, err2 := f2.name()
39 | if err1 == nil && err2 == nil && n1 == n2 {
40 | for _, p := range f.Params.Names() {
41 | v1, _ := f.Params.Get(p)
42 | v2, ok := f2.Params.Get(p)
43 | if !ok || v1 != v2 {
44 | return false
45 | }
46 | }
47 | for _, p := range f2.Params.Names() {
48 | v1, _ := f2.Params.Get(p)
49 | v2, ok := f.Params.Get(p)
50 | if !ok || v1 != v2 {
51 | return false
52 | }
53 | }
54 | return true
55 | }
56 | return false
57 | }
58 |
59 | // Headers is a simple way to generate a Fields list, where only simple header names and derived headers
60 | // are needed.
61 | func Headers(hs ...string) Fields {
62 | fs := NewFields()
63 | return *fs.AddHeaders(hs...)
64 | }
65 |
66 | // AddHeaders adds a list of simple or derived header names.
67 | func (fs *Fields) AddHeaders(hs ...string) *Fields {
68 | for _, h := range hs {
69 | fs.f = append(fs.f, *fromHeaderName(h))
70 | }
71 | return fs
72 | }
73 |
74 | // NewFields returns an empty list of fields.
75 | func NewFields() *Fields {
76 | fs := Fields{}
77 | return &fs
78 | }
79 |
80 | func (f field) name() (string, error) {
81 | i := httpsfv.Item(f)
82 | n, ok := i.Value.(string)
83 | if !ok {
84 | return "", fmt.Errorf("field has a non-string value")
85 | }
86 | return n, nil
87 | }
88 |
89 | func fromHeaderName(hdr string) *field {
90 | h := strings.ToLower(hdr)
91 | f := field(httpsfv.NewItem(h))
92 | return &f
93 | }
94 |
95 | func (f field) headerName() (bool, string) {
96 | _, ok1 := f.Params.Get("name")
97 | _, ok2 := f.Params.Get("key")
98 | if !ok1 && !ok2 {
99 | s, ok := f.Value.(string)
100 | if ok {
101 | return true, s
102 | } else {
103 | return false, ""
104 | }
105 | }
106 | return false, ""
107 | }
108 |
109 | // AddHeader appends a bare header name, e.g. "cache-control".
110 | func (fs *Fields) AddHeader(hdr string) *Fields {
111 | return fs.AddHeaderExt(hdr, false, false, false, false)
112 | }
113 |
114 | // AddHeaderExt appends a bare header name, e.g. "cache-control". See type documentation
115 | // for details on optional parameters. The component can be marked as coming from an associated request.
116 | func (fs *Fields) AddHeaderExt(hdr string, optional bool, binarySequence bool, associatedRequest bool, trailer bool) *Fields {
117 | f := fromHeaderName(hdr)
118 | f.markField(optional, associatedRequest, trailer)
119 | if binarySequence {
120 | f.markBinarySequence()
121 | }
122 | fs.f = append(fs.f, *f)
123 | return fs
124 | }
125 |
126 | // AddHeaderOptional appends a bare header name, e.g. "cache-control". However, the header is not required to exist
127 | // in the message. This is a convenience function and AddHeaderExt is more general.
128 | func (fs *Fields) AddHeaderOptional(hdr string) *Fields {
129 | return fs.AddHeaderExt(hdr, true, false, false, false)
130 | }
131 |
132 | func fromQueryParam(qp string) *field {
133 | i := httpsfv.NewItem("@query-param")
134 | i.Params.Add("name", QueryEscapeForSignature(qp))
135 | f := field(i)
136 | return &f
137 | }
138 |
139 | func (f field) queryParam() (bool, string) {
140 | name, err := f.name()
141 | if err == nil && name == "@query-param" {
142 | v, ok := httpsfv.Item(f).Params.Get("name")
143 | if ok {
144 | s, ok := v.(string)
145 | if ok {
146 | return true, s
147 | } else {
148 | return false, ""
149 | }
150 | }
151 | }
152 | return false, ""
153 | }
154 |
155 | // AddQueryParam indicates a request for a specific query parameter to be signed.
156 | func (fs *Fields) AddQueryParam(qp string) *Fields {
157 | return fs.AddQueryParamExt(qp, false, false, false)
158 | }
159 |
160 | // AddQueryParamExt indicates a request for a specific query parameter to be signed. See type documentation
161 | // for details on optional parameters. The component can be marked as coming from an associated request.
162 | func (fs *Fields) AddQueryParamExt(qp string, optional, associatedRequest, trailer bool) *Fields {
163 | f := fromQueryParam(qp)
164 | f.markField(optional, associatedRequest, trailer)
165 | fs.f = append(fs.f, *f)
166 | return fs
167 | }
168 |
169 | func fromDictHeader(hdr, key string) *field {
170 | h := strings.ToLower(hdr)
171 | i := httpsfv.NewItem(h)
172 | i.Params.Add("key", key)
173 | f := field(i)
174 | return &f
175 | }
176 |
177 | func (f field) dictHeader() (ok bool, hdr, key string) {
178 | v, ok := f.Params.Get("key")
179 | if ok {
180 | s1, ok1 := f.Value.(string)
181 | s2, ok2 := v.(string)
182 | if ok1 && ok2 {
183 | return true, s1, s2
184 | } else {
185 | return false, "", ""
186 | }
187 | }
188 | return false, "", ""
189 | }
190 |
191 | // AddDictHeader indicates that out of a header structured as a dictionary, a specific key value is signed/verified.
192 | func (fs *Fields) AddDictHeader(hdr, key string) *Fields {
193 | return fs.AddDictHeaderExt(hdr, key, false, false, false)
194 | }
195 |
196 | // AddDictHeaderExt indicates that out of a header structured as a dictionary, a specific key value is signed/verified.
197 | // See type documentation
198 | // for details on optional parameters. The component can be marked as coming from an associated request.
199 | func (fs *Fields) AddDictHeaderExt(hdr, key string, optional, associatedRequest, trailer bool) *Fields {
200 | f := fromDictHeader(hdr, key)
201 | f.markField(optional, associatedRequest, trailer)
202 | fs.f = append(fs.f, *f)
203 | return fs
204 | }
205 |
206 | func fromStructuredField(hdr string) *field {
207 | h := strings.ToLower(hdr)
208 | i := httpsfv.NewItem(h)
209 | i.Params.Add("sf", true)
210 | f := field(i)
211 | return &f
212 | }
213 |
214 | func (f field) structuredField() bool {
215 | v, ok := f.Params.Get("sf")
216 | return ok && v.(bool)
217 | }
218 |
219 | func (f field) binarySequence() bool {
220 | v, ok := f.Params.Get("bs")
221 | return ok && v.(bool)
222 | }
223 |
224 | func (f field) trailer() bool {
225 | v, ok := f.Params.Get("tr")
226 | return ok && v.(bool)
227 | }
228 |
229 | func (f field) optional() bool {
230 | v, ok := f.Params.Get("optional")
231 | return ok && v.(bool)
232 | }
233 |
234 | func (f field) associatedRequest() bool {
235 | v, ok := f.Params.Get("req")
236 | return ok && v.(bool)
237 | }
238 |
239 | // AddStructuredField indicates that a header should be interpreted as a structured field, per RFC 8941.
240 | func (fs *Fields) AddStructuredField(hdr string) *Fields {
241 | return fs.AddStructuredFieldExt(hdr, false, false, false)
242 | }
243 |
244 | // AddStructuredFieldExt indicates that a header should be interpreted as a structured field, per RFC 8941.
245 | // See type documentation
246 | // for details on optional parameters. The component can be marked as coming from an associated request.
247 | func (fs *Fields) AddStructuredFieldExt(hdr string, optional, associatedRequest, trailer bool) *Fields {
248 | f := fromStructuredField(hdr)
249 | f.markField(optional, associatedRequest, trailer)
250 | fs.f = append(fs.f, *f)
251 | return fs
252 | }
253 |
254 | func (f field) toItem() httpsfv.Item {
255 | return httpsfv.Item(f)
256 | }
257 |
258 | func (f field) asSignatureBase() (string, error) {
259 | s, err := httpsfv.Marshal(f.toItem())
260 | return s, err
261 | }
262 |
263 | func (f field) markField(optional bool, associatedRequest bool, trailer bool) {
264 | if optional {
265 | f.markOptional()
266 | }
267 | if associatedRequest {
268 | f.markAssociatedRequest()
269 | }
270 | if trailer {
271 | f.markTrailer()
272 | }
273 | }
274 |
275 | func (f field) markFlag(name string) {
276 | if f.Params == nil {
277 | f.Params = httpsfv.NewParams()
278 | }
279 | f.Params.Add(name, true)
280 | }
281 |
282 | func (f field) markOptional() {
283 | f.markFlag("optional")
284 | }
285 |
286 | func (f field) markBinarySequence() {
287 | f.markFlag("bs")
288 | }
289 |
290 | func (f field) markAssociatedRequest() {
291 | f.markFlag("req")
292 | }
293 |
294 | func (f field) markTrailer() {
295 | f.markFlag("tr")
296 | }
297 |
298 | func (f field) unmarkOptional() {
299 | if f.Params == nil {
300 | f.Params = httpsfv.NewParams()
301 | }
302 | f.Params.Del("optional")
303 | }
304 |
305 | // Not a full deep copy, but good enough for mutating params
306 | func (f field) copy() field {
307 | ff := field{
308 | Value: f.Value,
309 | }
310 | if f.Params == nil {
311 | ff.Params = nil
312 | } else {
313 | ff.Params = httpsfv.NewParams()
314 | for _, n := range f.Params.Names() {
315 | v, _ := f.Params.Get(n)
316 | ff.Params.Add(n, v)
317 | }
318 | }
319 | return ff
320 | }
321 |
322 | func (fs *Fields) asSignatureInput(p *httpsfv.Params) (string, error) {
323 | il := httpsfv.InnerList{
324 | Items: []httpsfv.Item{},
325 | Params: httpsfv.NewParams(),
326 | }
327 | for _, f := range fs.f {
328 | il.Items = append(il.Items, f.toItem())
329 | }
330 | il.Params = p
331 | s, err := httpsfv.Marshal(il)
332 | return s, err
333 | }
334 |
335 | // contains verifies that all required fields are in the given list of fields (yes, this is O(n^2)).
336 | func (fs *Fields) contains(requiredFields *Fields) bool {
337 | outer:
338 | for _, f1 := range requiredFields.f {
339 | for _, f2 := range fs.f {
340 | if f1.Equal(f2) {
341 | continue outer
342 | }
343 | }
344 | return false
345 | }
346 | return true
347 | }
348 |
349 | // TODO: should only compare the header name, Equal() would fail if there are params
350 | func (fs *Fields) hasHeader(name string) bool {
351 | h := *fromHeaderName(name)
352 | for _, f := range fs.f {
353 | if f.Equal(h) {
354 | return true
355 | }
356 | }
357 | return false
358 | }
359 |
360 | func (fs *Fields) hasTrailerFields(forAssocRequest bool) bool {
361 | for _, f := range fs.f {
362 | _, tr := f.Params.Get("tr")
363 | _, req := f.Params.Get("req")
364 | if tr && (req && forAssocRequest) {
365 | return true
366 | }
367 | if tr && (!req && !forAssocRequest) {
368 | return true
369 | }
370 | }
371 | return false
372 | }
373 |
--------------------------------------------------------------------------------
/fuzz_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "encoding/base64"
5 | "net/http"
6 | "net/url"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | var httpreq1pssNoSig = `POST /foo?param=Value&Pet=dog HTTP/1.1
13 | Host: example.com
14 | Date: Tue, 20 Apr 2021 02:07:55 GMT
15 | Content-Type: application/json
16 | Content-Digest: sha-512=:WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==:
17 | Content-Length: 18
18 |
19 | {"hello": "world"}
20 | `
21 |
22 | func FuzzVerifyRequest(f *testing.F) {
23 | type inputs struct {
24 | req, sigInput, sig string
25 | }
26 | testcases := []inputs{
27 | {httpreq1pssNoSig,
28 | "sig-b21=();created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"b3k2pp5k7z-50gnwp.yemd\"",
29 | "sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
30 | },
31 | {httpreq1pssNoSig,
32 | "sig-b21=(date);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"",
33 | "sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
34 | },
35 | {httpreq1pssNoSig,
36 | "sig-b21=(some-field;tr);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"",
37 | "sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
38 | },
39 | {httpreq1pssNoSig,
40 | "sig-b22=(some-field;tr;bs);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"",
41 | "sig-b22=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
42 | },
43 | }
44 | for _, tc := range testcases {
45 | f.Add(tc.req, tc.sigInput, tc.sig) // Use f.Add to provide a seed corpus
46 | }
47 | f.Fuzz(func(t *testing.T, reqString, sigInput, sig string) {
48 | req := readRequest(reqString)
49 | if req != nil {
50 | req.Header.Set("Signature-Input", sigInput)
51 | req.Header.Set("Signature", sig)
52 | }
53 |
54 | sigName := "sig-b21"
55 | verifier := makeRSAVerifier(f, "key1", *NewFields())
56 | _ = VerifyRequest(sigName, verifier, req)
57 | // only report panics
58 | })
59 | }
60 |
61 | // Same as FuzzVerifyRequest but using Message
62 | func FuzzMessageVerifyRequest(f *testing.F) {
63 | type inputs struct {
64 | req, sigInput, sig string
65 | }
66 | testcases := []inputs{
67 | {httpreq1pssNoSig,
68 | "sig-b21=();created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"b3k2pp5k7z-50gnwp.yemd\"",
69 | "sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
70 | },
71 | {httpreq1pssNoSig,
72 | "sig-b21=(date);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"",
73 | "sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
74 | },
75 | {httpreq1pssNoSig,
76 | "sig-b21=(some-field;tr);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"",
77 | "sig-b21=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
78 | },
79 | {httpreq1pssNoSig,
80 | "sig-b22=(some-field;tr;bs);created=1618884473;keyid=\"test-key-rsa-pss\";nonce=\"xxxb3k5k7z-50gnwp.yemd\"",
81 | "sig-b22=:d2pmTvmbncD3xQm8E9ZV2828BjQWGgiwAaw5bAkgibUopemLJcWDy/lkbbHAve4cRAtx31Iq786U7it++wgGxbtRxf8Udx7zFZsckzXaJMkA7ChG52eSkFxykJeNqsrWH5S+oxNFlD4dzVuwe8DhTSja8xxbR/Z2cOGdCbzR72rgFWhzx2VjBqJzsPLMIQKhO4DGezXehhWwE56YCE+O6c0mKZsfxVrogUvA4HELjVKWmAvtl6UnCh8jYzuVG5WSb/QEVPnP5TmcAnLH1g+s++v6d4s8m0gCw1fV5/SITLq9mhho8K3+7EPYTU8IU1bLhdxO5Nyt8C8ssinQ98Xw9Q==:",
82 | },
83 | }
84 | for _, tc := range testcases {
85 | f.Add(tc.req, tc.sigInput, tc.sig) // Use f.Add to provide a seed corpus
86 | }
87 | f.Fuzz(func(t *testing.T, reqString, sigInput, sig string) {
88 | req := readRequest(reqString)
89 | if req != nil {
90 | req.Header.Set("Signature-Input", sigInput)
91 | req.Header.Set("Signature", sig)
92 | }
93 |
94 | sigName := "sig-b21"
95 | verifier := makeRSAVerifier(f, "key1", *NewFields())
96 | msg, err := NewMessage(NewMessageConfig().WithRequest(req))
97 | if err != nil {
98 | t.Errorf("Failed to create Message")
99 | }
100 | _, _ = msg.Verify(sigName, verifier)
101 | // only report panics
102 | })
103 | }
104 |
105 | func FuzzSignAndVerifyHMAC(f *testing.F) {
106 | type inputs struct {
107 | req string
108 | }
109 | testcases := []inputs{
110 | {httpreq1},
111 | }
112 | for _, tc := range testcases {
113 | f.Add(tc.req)
114 | }
115 | f.Fuzz(func(t *testing.T, reqString string) {
116 | config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475)
117 | fields := Headers("@authority", "date", "content-type")
118 | signatureName := "sig1"
119 | key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==")
120 | signer, _ := NewHMACSHA256Signer(key, config.SetKeyID("test-shared-secret"), fields)
121 | req := readRequest(reqString)
122 | sigInput, sig, err := SignRequest(signatureName, *signer, req)
123 | if err == nil {
124 | req.Header.Add("Signature", sig)
125 | req.Header.Add("Signature-Input", sigInput)
126 | verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields)
127 | assert.NoError(t, err, "could not generate Verifier")
128 | err = VerifyRequest(signatureName, *verifier, req)
129 | assert.NoError(t, err, "verification error")
130 | }
131 | })
132 | }
133 |
134 | // Same as FuzzSignAndVerifyHMAC but using Message
135 | func FuzzMessageSignAndVerifyHMAC(f *testing.F) {
136 | type inputs struct {
137 | req string
138 | }
139 | testcases := []inputs{
140 | {httpreq1},
141 | }
142 | for _, tc := range testcases {
143 | f.Add(tc.req)
144 | }
145 | f.Fuzz(func(t *testing.T, reqString string) {
146 | config := NewSignConfig().SignAlg(false).setFakeCreated(1618884475)
147 | fields := Headers("@authority", "date", "content-type")
148 | signatureName := "sig1"
149 | key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==")
150 | signer, _ := NewHMACSHA256Signer(key, config.SetKeyID("test-shared-secret"), fields)
151 | req := readRequest(reqString)
152 | sigInput, sig, err := SignRequest(signatureName, *signer, req)
153 | if err == nil {
154 | req.Header.Add("Signature", sig)
155 | req.Header.Add("Signature-Input", sigInput)
156 | verifier, err := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("test-shared-secret"), fields)
157 | assert.NoError(t, err, "could not generate Verifier")
158 | msg, err := NewMessage(NewMessageConfig().WithRequest(req))
159 | if err != nil {
160 | t.Errorf("Failed to create Message")
161 | }
162 | _, err = msg.Verify(signatureName, *verifier)
163 | assert.NoError(t, err, "verification error")
164 | }
165 | })
166 | }
167 |
168 | func FuzzMessageVerify(f *testing.F) {
169 | f.Add("GET", "https://example.com/path", "example.com", "https", 0, "", "", "", "", true, false)
170 | f.Add("POST", "https://api.example.com", "api.example.com", "https", 0, "", "", "", "", false, true)
171 | f.Add("", "", "", "", 200, "GET", "https://example.com", "example.com", "https", true, false)
172 | f.Add("PUT", "", "", "http", 0, "", "", "", "", false, false)
173 | f.Add("", "", "", "", 404, "", "", "", "", false, false)
174 | f.Add("0", "%", "0", "0", 0, "", "", "", "", true, false)
175 |
176 | f.Fuzz(func(t *testing.T, method, urlStr, authority, scheme string, statusCode int,
177 | assocMethod, assocURLStr, assocAuthority, assocScheme string,
178 | hasHeaders, hasTrailers bool) {
179 |
180 | config := NewMessageConfig()
181 |
182 | if method != "" {
183 | config = config.WithMethod(method)
184 | }
185 | if urlStr != "" {
186 | u, err := url.Parse(urlStr)
187 | if err == nil {
188 | config = config.WithURL(u)
189 | }
190 | }
191 | if authority != "" {
192 | config = config.WithAuthority(authority)
193 | }
194 | if scheme != "" {
195 | config = config.WithScheme(scheme)
196 | }
197 |
198 | if statusCode > 0 {
199 | config = config.WithStatusCode(statusCode)
200 | }
201 |
202 | if hasHeaders {
203 | headers := http.Header{
204 | "Content-Type": []string{"application/json"},
205 | "X-Test": []string{"fuzz"},
206 | }
207 | config = config.WithHeaders(headers)
208 | }
209 | if hasTrailers {
210 | trailers := http.Header{
211 | "X-Trailer": []string{"test"},
212 | }
213 | config = config.WithTrailers(trailers)
214 | }
215 |
216 | if statusCode > 0 && assocMethod != "" {
217 | var assocURL *url.URL
218 | if assocURLStr != "" {
219 | assocURL, _ = url.Parse(assocURLStr)
220 | }
221 | assocHeaders := http.Header{"X-Assoc": []string{"test"}}
222 | config = config.WithAssociatedRequest(assocMethod, assocURL, assocHeaders, assocAuthority, assocScheme)
223 | }
224 |
225 | msg, err := NewMessage(config)
226 |
227 | if err == nil {
228 | if msg.headers == nil && msg.method != "" {
229 | t.Errorf("Request message created without headers")
230 | }
231 | if msg.headers == nil && msg.statusCode != nil {
232 | t.Errorf("Response message created without headers")
233 | }
234 |
235 | key, _ := base64.StdEncoding.DecodeString("uzvJfB4u3N0Jy4T7NZ75MDVcr8zSTInedJtkgcu46YW4XByzNJjxBdtjUkdJPBtbmHhIDi6pcl8jsasjlTMtDQ==")
236 | verifier, _ := NewHMACSHA256Verifier(key, NewVerifyConfig().SetVerifyCreated(false), Fields{})
237 |
238 | if msg.headers != nil {
239 | msg.headers.Set("Signature-Input", `sig1=("@method");created=1618884473;keyid="test-key"`)
240 | msg.headers.Set("Signature", `sig1=:test:`)
241 | }
242 |
243 | _, _ = msg.Verify("sig1", *verifier)
244 | }
245 |
246 | if err != nil {
247 | hasRequest := method != ""
248 | hasResponse := statusCode > 0
249 |
250 | if !hasRequest && !hasResponse {
251 | assert.Contains(t, err.Error(), "must have either method")
252 | } else if hasRequest && hasResponse {
253 | assert.Contains(t, err.Error(), "cannot have both request and response")
254 | } else if (hasRequest || hasResponse) && !hasHeaders {
255 | assert.Contains(t, err.Error(), "must have headers")
256 | }
257 | }
258 | })
259 | }
260 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "os"
8 | "time"
9 | )
10 |
11 | // SignConfig contains additional configuration for the signer.
12 | type SignConfig struct {
13 | signAlg bool
14 | signCreated bool
15 | fakeCreated int64
16 | expires int64
17 | nonce string
18 | tag string
19 | keyID *string
20 | }
21 |
22 | // NewSignConfig generates a default configuration.
23 | func NewSignConfig() *SignConfig {
24 | return &SignConfig{
25 | signAlg: true,
26 | signCreated: true,
27 | fakeCreated: 0,
28 | expires: 0,
29 | nonce: "",
30 | tag: "", // we disallow an empty tag
31 | keyID: nil,
32 | }
33 | }
34 |
35 | // SignAlg indicates that an "alg" signature parameters must be generated and signed (default: true).
36 | func (c *SignConfig) SignAlg(b bool) *SignConfig {
37 | c.signAlg = b
38 | return c
39 | }
40 |
41 | // SignCreated indicates that a "created" signature parameters must be generated and signed (default: true).
42 | func (c *SignConfig) SignCreated(b bool) *SignConfig {
43 | c.signCreated = b
44 | return c
45 | }
46 |
47 | // setFakeCreated indicates that the specified Unix timestamp must be used instead of the current time
48 | // (default: 0, meaning use current time). Only used for testing.
49 | func (c *SignConfig) setFakeCreated(ts int64) *SignConfig {
50 | c.fakeCreated = ts
51 | return c
52 | }
53 |
54 | // SetExpires adds an "expires" parameter containing an expiration deadline, as Unix time.
55 | // Default: 0 (do not add the parameter).
56 | func (c *SignConfig) SetExpires(expires int64) *SignConfig {
57 | c.expires = expires
58 | return c
59 | }
60 |
61 | // SetNonce adds a "nonce" string parameter whose content should be unique per signed message.
62 | // Default: empty string (do not add the parameter).
63 | func (c *SignConfig) SetNonce(nonce string) *SignConfig {
64 | c.nonce = nonce
65 | return c
66 | }
67 |
68 | // SetTag adds a "tag" string parameter that defines a per-application or per-protocol signature
69 | // tag, to mitigate cross-protocol attacks.
70 | func (c *SignConfig) SetTag(tag string) *SignConfig {
71 | c.tag = tag
72 | return c
73 | }
74 |
75 | // SetKeyID configures a keyid value that will be included as a signature parameter.
76 | func (c *SignConfig) SetKeyID(keyID string) *SignConfig {
77 | c.keyID = &keyID
78 | return c
79 | }
80 |
81 | // VerifyConfig contains additional configuration for the verifier.
82 | type VerifyConfig struct {
83 | verifyCreated bool
84 | notNewerThan time.Duration
85 | notOlderThan time.Duration
86 | allowedAlgs []string
87 | rejectExpired bool
88 | keyID *string
89 | dateWithin time.Duration
90 | allowedTags []string
91 | }
92 |
93 | // SetNotNewerThan sets the window for messages that appear to be newer than the current time,
94 | // which can only happen if clocks are out of sync. Default: 1,000 ms.
95 | func (v *VerifyConfig) SetNotNewerThan(notNewerThan time.Duration) *VerifyConfig {
96 | v.notNewerThan = notNewerThan
97 | return v
98 | }
99 |
100 | // SetNotOlderThan sets the window for messages that are older than the current time,
101 | // because of network latency. Default: 10,000 ms.
102 | func (v *VerifyConfig) SetNotOlderThan(notOlderThan time.Duration) *VerifyConfig {
103 | v.notOlderThan = notOlderThan
104 | return v
105 | }
106 |
107 | // SetVerifyCreated indicates that the "created" parameter must be within some time window,
108 | // defined by NotNewerThan and NotOlderThan. Default: true.
109 | func (v *VerifyConfig) SetVerifyCreated(verifyCreated bool) *VerifyConfig {
110 | v.verifyCreated = verifyCreated
111 | return v
112 | }
113 |
114 | // SetRejectExpired indicates that expired messages (according to the "expires" parameter) must fail verification.
115 | // Default: true.
116 | func (v *VerifyConfig) SetRejectExpired(rejectExpired bool) *VerifyConfig {
117 | v.rejectExpired = rejectExpired
118 | return v
119 | }
120 |
121 | // SetAllowedAlgs defines the allowed values of the "alg" parameter.
122 | // This is useful if the actual algorithm used in verification is taken from the message - not a recommended practice.
123 | // Default: an empty list, signifying all values are accepted.
124 | func (v *VerifyConfig) SetAllowedAlgs(allowedAlgs []string) *VerifyConfig {
125 | v.allowedAlgs = allowedAlgs
126 | return v
127 | }
128 |
129 | // SetKeyID defines how to verify the keyid parameter, if one exists. If this value is a non-nil string,
130 | // the signature verifies only if the value is the same as was specified here.
131 | // Default: nil.
132 | func (v *VerifyConfig) SetKeyID(keyID string) *VerifyConfig {
133 | v.keyID = &keyID
134 | return v
135 | }
136 |
137 | // SetVerifyDateWithin indicates that the Date header should be verified if it exists, and its value
138 | // must be within a certain time duration (positive or negative) of the Created signature parameter.
139 | // This verification is only available if the Created field itself is verified.
140 | // Default: 0, meaning no verification of the Date header.
141 | func (v *VerifyConfig) SetVerifyDateWithin(d time.Duration) *VerifyConfig {
142 | v.dateWithin = d
143 | return v
144 | }
145 |
146 | // SetAllowedTags defines the allowed values of the "tag" parameter.
147 | // Default: an empty list, signifying all values are accepted.
148 | func (v *VerifyConfig) SetAllowedTags(allowedTags []string) *VerifyConfig {
149 | v.allowedTags = allowedTags
150 | return v
151 | }
152 |
153 | // NewVerifyConfig generates a default configuration.
154 | func NewVerifyConfig() *VerifyConfig {
155 | return &VerifyConfig{
156 | verifyCreated: true,
157 | notNewerThan: 2 * time.Second,
158 | notOlderThan: 10 * time.Second,
159 | rejectExpired: true,
160 | allowedAlgs: []string{},
161 | keyID: nil,
162 | dateWithin: 0, // meaning no constraint
163 | allowedTags: nil, // no constraint
164 | }
165 | }
166 |
167 | // HandlerConfig contains additional configuration for the HTTP message handler wrapper.
168 | // Either or both of fetchVerifier and fetchSigner may be nil for the corresponding operation
169 | // to be skipped.
170 | type HandlerConfig struct {
171 | reqNotVerified func(w http.ResponseWriter,
172 | r *http.Request, logger *log.Logger, err error)
173 | fetchVerifier func(r *http.Request) (sigName string, verifier *Verifier)
174 | fetchSigner func(res http.Response, r *http.Request) (sigName string, signer *Signer)
175 | logger *log.Logger
176 | computeDigest bool
177 | digestSchemesSend []string
178 | digestSchemesRecv []string
179 | }
180 |
181 | // NewHandlerConfig generates a default configuration. When verification or respectively,
182 | // signing is required, the respective "fetch" callback must be supplied.
183 | func NewHandlerConfig() *HandlerConfig {
184 | return &HandlerConfig{
185 | reqNotVerified: defaultReqNotVerified,
186 | fetchVerifier: nil,
187 | fetchSigner: nil,
188 | logger: log.New(os.Stderr, "httpsign: ", log.LstdFlags|log.Lmsgprefix),
189 | computeDigest: true,
190 | digestSchemesSend: []string{DigestSha256},
191 | digestSchemesRecv: []string{DigestSha256, DigestSha512},
192 | }
193 | }
194 |
195 | func defaultReqNotVerified(w http.ResponseWriter, _ *http.Request, logger *log.Logger, err error) {
196 | w.WriteHeader(http.StatusUnauthorized)
197 | if err == nil { // should not happen
198 | _, _ = fmt.Fprintf(w, "Unknown error")
199 | } else {
200 | if logger != nil {
201 | logger.Println("Could not verify request signature: " + err.Error())
202 | }
203 | _, _ = fmt.Fprintln(w, "Could not verify request signature") // For security reasons, do not print error
204 | }
205 | }
206 |
207 | // SetReqNotVerified defines a callback to be called when a request fails to verify. The default
208 | // callback sends an unsigned 401 status code with a generic error message. For production, you
209 | // probably need to sign it.
210 | func (h *HandlerConfig) SetReqNotVerified(f func(w http.ResponseWriter, r *http.Request, l *log.Logger,
211 | err error)) *HandlerConfig {
212 | h.reqNotVerified = f
213 | return h
214 | }
215 |
216 | // SetFetchVerifier defines a callback that looks at the incoming request and provides
217 | // a Verifier structure. In the simplest case, the signature name is a constant, and the key ID
218 | // and key value are fetched based on the sender's identity, which in turn is gleaned
219 | // from a header or query parameter. If a Verifier cannot be determined, the function should return Verifier as nil.
220 | func (h *HandlerConfig) SetFetchVerifier(f func(r *http.Request) (sigName string, verifier *Verifier)) *HandlerConfig {
221 | h.fetchVerifier = f
222 | return h
223 | }
224 |
225 | // SetFetchSigner defines a callback that looks at the incoming request and the response, just before it is sent,
226 | // and provides
227 | // a Signer structure. In the simplest case, the signature name is a constant, and the key ID
228 | // and key value are fetched based on the sender's identity. To simplify this logic,
229 | // it is recommended to use the request's ctx (Context) member
230 | // to store this information. If a Signer cannot be determined, the function should return Signer as nil.
231 | func (h *HandlerConfig) SetFetchSigner(f func(res http.Response, r *http.Request) (sigName string, signer *Signer)) *HandlerConfig {
232 | h.fetchSigner = f
233 | return h
234 | }
235 |
236 | // SetLogger defines a logger for cases where an error cannot be returned. The default logger prints to stderr.
237 | // Set to nil to prevent logging.
238 | func (h *HandlerConfig) SetLogger(l *log.Logger) *HandlerConfig {
239 | h.logger = l
240 | return h
241 | }
242 |
243 | // SetComputeDigest when set to its default value (true), this flag indicates that
244 | // if the Content-Digest header is in the set of covered components but the header itself is missing,
245 | // the header value will be computed
246 | // and added to the message before sending it; conversely in received messages, if Content-Digest is covered, the digest
247 | // will be computed and validated. Setting the flag to false inhibits this behavior.
248 | func (h *HandlerConfig) SetComputeDigest(b bool) *HandlerConfig {
249 | h.computeDigest = b
250 | return h
251 | }
252 |
253 | // SetDigestSchemesSend defines the scheme(s) (cryptographic hash algorithms) to be used to generate the message digest.
254 | // It only needs to be set if a Content-Digest header is signed. Default: DigestSha256
255 | func (h *HandlerConfig) SetDigestSchemesSend(s []string) *HandlerConfig {
256 | h.digestSchemesSend = s
257 | return h
258 | }
259 |
260 | // SetDigestSchemesRecv defines the cryptographic algorithms to accept when receiving the
261 | // Content-Digest header. Any recognized algorithm's digest must be correct, but the overall header is valid if at least
262 | // one accepted digest is included. Default: DigestSha256, DigestSha512.
263 | func (h *HandlerConfig) SetDigestSchemesRecv(s []string) *HandlerConfig {
264 | h.digestSchemesRecv = s
265 | return h
266 | }
267 |
268 | // ClientConfig contains additional configuration for the HTTP client-side wrapper.
269 | // Signing and verification may either be skipped, independently.
270 | type ClientConfig struct {
271 | signatureName string
272 | signer *Signer
273 | verifier *Verifier
274 | fetchVerifier func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier)
275 | computeDigest bool
276 | digestSchemesSend []string
277 | digestSchemesRecv []string
278 | }
279 |
280 | // NewClientConfig creates a new, default ClientConfig.
281 | func NewClientConfig() *ClientConfig {
282 | return &ClientConfig{
283 | computeDigest: true,
284 | digestSchemesSend: []string{DigestSha256},
285 | digestSchemesRecv: []string{DigestSha256, DigestSha512},
286 | }
287 | }
288 |
289 | // SetSignatureName sets the signature name to be used for signing or verification.
290 | func (c *ClientConfig) SetSignatureName(s string) *ClientConfig {
291 | c.signatureName = s
292 | return c
293 | }
294 |
295 | // SetSigner defines a signer for outgoing requests.
296 | func (c *ClientConfig) SetSigner(s *Signer) *ClientConfig {
297 | c.signer = s
298 | return c
299 | }
300 |
301 | // SetVerifier defines a verifier for incoming responses.
302 | func (c *ClientConfig) SetVerifier(v *Verifier) *ClientConfig {
303 | c.verifier = v
304 | return c
305 | }
306 |
307 | // SetFetchVerifier defines a function that fetches a verifier which may be customized for the incoming response.
308 | func (c *ClientConfig) SetFetchVerifier(fv func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier)) *ClientConfig {
309 | c.fetchVerifier = fv
310 | return c
311 | }
312 |
313 | // SetComputeDigest when set to its default value (true), this flag indicates that
314 | // if the Content-Digest header is in the set of covered components but the header itself is missing,
315 | // the header value will be computed
316 | // and added to the message before sending it; conversely in received messages, if Content-Digest is covered, the digest
317 | // will be computed and validated. Setting the flag to false inhibits this behavior.
318 | func (c *ClientConfig) SetComputeDigest(b bool) *ClientConfig {
319 | c.computeDigest = b
320 | return c
321 | }
322 |
323 | // SetDigestSchemesSend defines the cryptographic algorithms to use when generating the
324 | // Content-Digest header. Default: DigestSha256.
325 | func (c *ClientConfig) SetDigestSchemesSend(s []string) *ClientConfig {
326 | c.digestSchemesSend = s
327 | return c
328 | }
329 |
330 | // SetDigestSchemesRecv defines the cryptographic algorithms to accept when receiving the
331 | // Content-Digest header. Any recognized algorithm's digest must be correct, but the overall header is valid if at least
332 | // one accepted digest is included. Default: DigestSha256, DigestSha512.
333 | func (c *ClientConfig) SetDigestSchemesRecv(s []string) *ClientConfig {
334 | c.digestSchemesRecv = s
335 | return c
336 | }
337 |
--------------------------------------------------------------------------------
/crypto_test.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "crypto/ed25519"
5 | "crypto/rand"
6 | "crypto/rsa"
7 | "reflect"
8 | "strings"
9 | "testing"
10 |
11 | // JWX v2 - for existing tests
12 | "github.com/lestrrat-go/jwx/v2/jwa"
13 | "github.com/lestrrat-go/jwx/v2/jws"
14 |
15 | // JWX v3 - for new V3 tests
16 | jwav3 "github.com/lestrrat-go/jwx/v3/jwa"
17 | jwsv3 "github.com/lestrrat-go/jwx/v3/jws"
18 |
19 | "github.com/stretchr/testify/assert"
20 | )
21 |
22 | func TestNewHMACSHA256Signer(t *testing.T) {
23 | type args struct {
24 | key []byte
25 | c *SignConfig
26 | f Fields
27 | }
28 | tests := []struct {
29 | name string
30 | args args
31 | want *Signer
32 | wantErr bool
33 | }{
34 | {
35 | name: "happy path",
36 | args: args{
37 | key: []byte(strings.Repeat("c", 64)),
38 | c: nil,
39 | f: Fields{},
40 | },
41 | want: &Signer{
42 | key: []byte(strings.Repeat("c", 64)),
43 | alg: "hmac-sha256",
44 | config: NewSignConfig(),
45 | fields: Fields{},
46 | },
47 | wantErr: false,
48 | },
49 | {
50 | name: "key too short",
51 | args: args{
52 | key: []byte("abc"),
53 | },
54 | want: nil,
55 | wantErr: true,
56 | },
57 | }
58 | for _, tt := range tests {
59 | t.Run(tt.name, func(t *testing.T) {
60 | got, err := NewHMACSHA256Signer(tt.args.key, tt.args.c, tt.args.f)
61 | if (err != nil) != tt.wantErr {
62 | t.Errorf("NewHMACSHA256Signer() error = %v, wantErr %v", err, tt.wantErr)
63 | return
64 | }
65 | if !reflect.DeepEqual(got, tt.want) {
66 | t.Errorf("NewHMACSHA256Signer() got = %v, want %v", got, tt.want)
67 | }
68 | })
69 | }
70 | }
71 |
72 | func TestSigner_sign(t *testing.T) {
73 | type fields struct {
74 | key any
75 | alg string
76 | }
77 | type args struct {
78 | buff []byte
79 | }
80 | tests := []struct {
81 | name string
82 | fields fields
83 | args args
84 | want []byte
85 | wantErr bool
86 | }{
87 | {
88 | name: "happy path",
89 | fields: fields{
90 | key: []byte(strings.Repeat("a", 64)),
91 | alg: "hmac-sha256",
92 | },
93 | args: args{
94 | buff: []byte("abc"),
95 | },
96 | want: []byte{102, 8, 172, 130, 220, 161, 203, 31, 221, 187, 93, 129, 227, 217, 135, 118, 66, 183, 68, 245, 101, 205, 150, 151, 172, 39, 218, 162, 80, 200, 13, 40},
97 | wantErr: false,
98 | },
99 | {
100 | name: "bad alg",
101 | fields: fields{
102 | key: []byte(strings.Repeat("a", 64)),
103 | alg: "hmac-sha999",
104 | },
105 | args: args{
106 | buff: []byte("abc"),
107 | },
108 | want: nil,
109 | wantErr: true,
110 | },
111 | {
112 | name: "ed25519 key not 64 bytes",
113 | fields: fields{
114 | key: ed25519.PrivateKey(strings.Repeat("a", 63)),
115 | alg: "ed25519",
116 | },
117 | args: args{
118 | buff: []byte("abc"),
119 | },
120 | want: nil,
121 | wantErr: true,
122 | },
123 | }
124 | for _, tt := range tests {
125 | t.Run(tt.name, func(t *testing.T) {
126 | s := Signer{
127 | key: tt.fields.key,
128 | alg: tt.fields.alg,
129 | }
130 | got, err := s.sign(tt.args.buff)
131 | if (err != nil) != tt.wantErr {
132 | t.Errorf("sign() error = %v, wantErr %v", err, tt.wantErr)
133 | return
134 | }
135 | if !reflect.DeepEqual(got, tt.want) {
136 | t.Errorf("sign() got = %v, want %v", got, tt.want)
137 | }
138 | })
139 | }
140 | }
141 |
142 | func TestForeignSigner(t *testing.T) {
143 | priv, pub, err := genP256KeyPair()
144 | if err != nil {
145 | t.Errorf("Failed to generate keypair: %v", err)
146 | }
147 |
148 | config := NewSignConfig().setFakeCreated(1618884475).SignAlg(false)
149 | signatureName := "sig1"
150 | fields := *NewFields().AddHeader("@method").AddHeader("date").AddHeader("content-type").AddQueryParam("pet")
151 | signer, err := NewJWSSigner(jwa.ES256, priv, config.SetKeyID("key1"), fields)
152 | if err != nil {
153 | t.Errorf("Failed to create JWS signer")
154 | }
155 | req := readRequest(httpreq2)
156 | sigInput, sig, err := SignRequest(signatureName, *signer, req)
157 | if err != nil {
158 | t.Errorf("signature failed: %v", err)
159 | }
160 | req.Header.Add("Signature", sig)
161 | req.Header.Add("Signature-Input", sigInput)
162 | verifier, err := NewJWSVerifier(jwa.ES256, pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), fields)
163 | if err != nil {
164 | t.Errorf("could not generate Verifier: %s", err)
165 | }
166 | err = VerifyRequest(signatureName, *verifier, req)
167 | if err != nil {
168 | t.Errorf("verification error: %s", err)
169 | }
170 | }
171 |
172 | // Same as TestForeignSigner but using Message
173 | func TestMessageForeignSigner(t *testing.T) {
174 | priv, pub, err := genP256KeyPair()
175 | if err != nil {
176 | t.Errorf("Failed to generate keypair: %v", err)
177 | }
178 |
179 | config := NewSignConfig().setFakeCreated(1618884475).SignAlg(false)
180 | signatureName := "sig1"
181 | fields := *NewFields().AddHeader("@method").AddHeader("date").AddHeader("content-type").AddQueryParam("pet")
182 | signer, err := NewJWSSigner(jwa.ES256, priv, config.SetKeyID("key1"), fields)
183 | if err != nil {
184 | t.Errorf("Failed to create JWS signer")
185 | }
186 | req := readRequest(httpreq2)
187 | sigInput, sig, err := SignRequest(signatureName, *signer, req)
188 | if err != nil {
189 | t.Errorf("signature failed: %v", err)
190 | }
191 | req.Header.Add("Signature", sig)
192 | req.Header.Add("Signature-Input", sigInput)
193 | verifier, err := NewJWSVerifier(jwa.ES256, pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), fields)
194 | if err != nil {
195 | t.Errorf("could not generate Verifier: %s", err)
196 | }
197 | msg, err := NewMessage(NewMessageConfig().WithRequest(req))
198 | if err != nil {
199 | t.Errorf("Failed to create Message")
200 | }
201 | _, err = msg.Verify(signatureName, *verifier)
202 | if err != nil {
203 | t.Errorf("verification error: %s", err)
204 | }
205 | }
206 |
207 | func makeRSAPrivateKey() *rsa.PrivateKey {
208 | priv, _ := rsa.GenerateKey(rand.Reader, 2048)
209 | return priv
210 | }
211 | func TestNewRSASigner1(t *testing.T) {
212 | type args struct {
213 | key *rsa.PrivateKey
214 | config *SignConfig
215 | fields Fields
216 | }
217 | key := makeRSAPrivateKey()
218 | tests := []struct {
219 | name string
220 | args args
221 | want *Signer
222 | wantErr bool
223 | }{
224 | {
225 | name: "happy path",
226 | args: args{
227 | key: key,
228 | config: nil,
229 | fields: *NewFields(),
230 | },
231 | want: &Signer{
232 | key: *key,
233 | alg: "rsa-v1_5-sha256",
234 | config: NewSignConfig(),
235 | fields: Fields{},
236 | foreignSigner: nil,
237 | },
238 | wantErr: false,
239 | },
240 | }
241 | for _, tt := range tests {
242 | t.Run(tt.name, func(t *testing.T) {
243 | got, err := NewRSASigner(*tt.args.key, tt.args.config, tt.args.fields)
244 | if (err != nil) != tt.wantErr {
245 | t.Errorf("NewRSASigner() error = %v, wantErr %v", err, tt.wantErr)
246 | return
247 | }
248 | if !reflect.DeepEqual(got, tt.want) {
249 | t.Errorf("NewRSASigner() got = %v, want %v", got, tt.want)
250 | }
251 | })
252 | }
253 | }
254 |
255 | func TestNewJWSVerifier(t *testing.T) {
256 | type args struct {
257 | alg jwa.SignatureAlgorithm
258 | key any
259 | keyID string
260 | config *VerifyConfig
261 | fields Fields
262 | }
263 | verifier, _ := jws.NewVerifier("HS256")
264 | tests := []struct {
265 | name string
266 | args args
267 | want *Verifier
268 | wantErr bool
269 | }{
270 | {
271 | name: "happy path",
272 | args: args{
273 | alg: jwa.SignatureAlgorithm("HS256"),
274 | key: "1234",
275 | keyID: "key200",
276 | config: nil,
277 | fields: *NewFields(),
278 | },
279 | want: &Verifier{
280 | key: "1234",
281 | alg: "",
282 | config: NewVerifyConfig(),
283 | fields: *NewFields(),
284 | foreignVerifier: verifier,
285 | },
286 | wantErr: false,
287 | },
288 | {
289 | name: "none",
290 | args: args{
291 | alg: jwa.NoSignature,
292 | key: "1234",
293 | keyID: "key200",
294 | config: NewVerifyConfig(),
295 | fields: *NewFields(),
296 | },
297 | want: nil,
298 | wantErr: true,
299 | },
300 | {
301 | name: "bad verifier",
302 | args: args{
303 | alg: jwa.SignatureAlgorithm("bad"),
304 | key: "1234",
305 | keyID: "key200",
306 | config: NewVerifyConfig(),
307 | fields: *NewFields(),
308 | },
309 | want: nil,
310 | wantErr: true,
311 | },
312 | }
313 | for _, tt := range tests {
314 | t.Run(tt.name, func(t *testing.T) {
315 | got, err := NewJWSVerifier(tt.args.alg, tt.args.key, tt.args.config, tt.args.fields)
316 | if (err != nil) != tt.wantErr {
317 | t.Errorf("NewJWSVerifier() error = %v, wantErr %v", err, tt.wantErr)
318 | return
319 | }
320 | if got != nil {
321 | got.foreignVerifier = nil
322 | }
323 | if tt.want != nil {
324 | tt.want.foreignVerifier = nil
325 | }
326 | if !reflect.DeepEqual(got, tt.want) {
327 | t.Errorf("NewJWSVerifier() got = %v, want %v", got, tt.want)
328 | }
329 | })
330 | }
331 | }
332 |
333 | func TestVerify(t *testing.T) {
334 | v := Verifier{
335 | key: nil,
336 | alg: "bad-alg",
337 | config: NewVerifyConfig(),
338 | fields: Fields{},
339 | foreignVerifier: nil,
340 | }
341 | _, err := v.verify([]byte{1, 2, 3}, []byte{4, 5, 6})
342 | assert.ErrorContains(t, err, "unknown", "bad algorithm")
343 |
344 | v.alg = "hmac-sha256"
345 | v.foreignVerifier = struct{ xx int }{7}
346 | _, err = v.verify([]byte{1, 2, 3}, []byte{4, 5, 6})
347 | assert.ErrorContains(t, err, "expected", "bad algorithm")
348 | }
349 |
350 | // V3 Tests - Testing jwx v3 functionality
351 |
352 | func TestForeignSignerV3(t *testing.T) {
353 | priv, pub, err := genP256KeyPair()
354 | if err != nil {
355 | t.Errorf("Failed to generate keypair: %v", err)
356 | }
357 |
358 | config := NewSignConfig().setFakeCreated(1618884475).SignAlg(false)
359 | signatureName := "sig1"
360 | fields := *NewFields().AddHeader("@method").AddHeader("date").AddHeader("content-type").AddQueryParam("pet")
361 | signer, err := NewJWSSignerV3(jwav3.ES256(), priv, config.SetKeyID("key1"), fields)
362 | if err != nil {
363 | t.Errorf("Failed to create JWS V3 signer: %v", err)
364 | }
365 | req := readRequest(httpreq2)
366 | sigInput, sig, err := SignRequest(signatureName, *signer, req)
367 | if err != nil {
368 | t.Errorf("signature failed: %v", err)
369 | }
370 | req.Header.Add("Signature", sig)
371 | req.Header.Add("Signature-Input", sigInput)
372 | verifier, err := NewJWSVerifierV3(jwav3.ES256(), pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), fields)
373 | if err != nil {
374 | t.Errorf("could not generate V3 Verifier: %s", err)
375 | }
376 | err = VerifyRequest(signatureName, *verifier, req)
377 | if err != nil {
378 | t.Errorf("verification error: %s", err)
379 | }
380 | }
381 |
382 | // Same as TestForeignSignerV3 but using Message
383 | func TestMessageForeignSignerV3(t *testing.T) {
384 | priv, pub, err := genP256KeyPair()
385 | if err != nil {
386 | t.Errorf("Failed to generate keypair: %v", err)
387 | }
388 |
389 | config := NewSignConfig().setFakeCreated(1618884475).SignAlg(false)
390 | signatureName := "sig1"
391 | fields := *NewFields().AddHeader("@method").AddHeader("date").AddHeader("content-type").AddQueryParam("pet")
392 | signer, err := NewJWSSignerV3(jwav3.ES256(), priv, config.SetKeyID("key1"), fields)
393 | if err != nil {
394 | t.Errorf("Failed to create JWS V3 signer: %v", err)
395 | }
396 | req := readRequest(httpreq2)
397 | sigInput, sig, err := SignRequest(signatureName, *signer, req)
398 | if err != nil {
399 | t.Errorf("signature failed: %v", err)
400 | }
401 | req.Header.Add("Signature", sig)
402 | req.Header.Add("Signature-Input", sigInput)
403 | verifier, err := NewJWSVerifierV3(jwav3.ES256(), pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), fields)
404 | if err != nil {
405 | t.Errorf("could not generate V3 Verifier: %s", err)
406 | }
407 | msg, err := NewMessage(NewMessageConfig().WithRequest(req))
408 | if err != nil {
409 | t.Errorf("Failed to create Message")
410 | }
411 | _, err = msg.Verify(signatureName, *verifier)
412 | if err != nil {
413 | t.Errorf("verification error: %s", err)
414 | }
415 | }
416 |
417 | func TestNewJWSVerifierV3(t *testing.T) {
418 | type args struct {
419 | alg jwav3.SignatureAlgorithm
420 | key any
421 | config *VerifyConfig
422 | fields Fields
423 | }
424 | verifier, _ := jwsv3.NewVerifier(jwav3.HS256())
425 | tests := []struct {
426 | name string
427 | args args
428 | want *Verifier
429 | wantErr bool
430 | }{
431 | {
432 | name: "happy path",
433 | args: args{
434 | alg: jwav3.HS256(),
435 | key: "1234",
436 | config: nil,
437 | fields: *NewFields(),
438 | },
439 | want: &Verifier{
440 | key: "1234",
441 | alg: "",
442 | config: NewVerifyConfig(),
443 | fields: *NewFields(),
444 | foreignVerifier: verifier,
445 | },
446 | wantErr: false,
447 | },
448 | {
449 | name: "none",
450 | args: args{
451 | alg: jwav3.NoSignature(),
452 | key: "1234",
453 | config: NewVerifyConfig(),
454 | fields: *NewFields(),
455 | },
456 | want: nil,
457 | wantErr: true,
458 | },
459 | {
460 | name: "nil key",
461 | args: args{
462 | alg: jwav3.HS256(),
463 | key: nil,
464 | config: NewVerifyConfig(),
465 | fields: *NewFields(),
466 | },
467 | want: nil,
468 | wantErr: true,
469 | },
470 | }
471 | for _, tt := range tests {
472 | t.Run(tt.name, func(t *testing.T) {
473 | got, err := NewJWSVerifierV3(tt.args.alg, tt.args.key, tt.args.config, tt.args.fields)
474 | if (err != nil) != tt.wantErr {
475 | t.Errorf("NewJWSVerifierV3() error = %v, wantErr %v", err, tt.wantErr)
476 | return
477 | }
478 | if got != nil {
479 | got.foreignVerifier = nil
480 | }
481 | if tt.want != nil {
482 | tt.want.foreignVerifier = nil
483 | }
484 | if !reflect.DeepEqual(got, tt.want) {
485 | t.Errorf("NewJWSVerifierV3() got = %v, want %v", got, tt.want)
486 | }
487 | })
488 | }
489 | }
490 |
491 | // Test cross-compatibility between v2 and v3
492 | func TestCrossVersionCompatibility(t *testing.T) {
493 | priv, pub, err := genP256KeyPair()
494 | if err != nil {
495 | t.Fatalf("Failed to generate keypair: %v", err)
496 | }
497 |
498 | config := NewSignConfig().setFakeCreated(1618884475).SignAlg(false)
499 | signatureName := "sig1"
500 | fields := *NewFields().AddHeader("@method").AddHeader("date").AddHeader("content-type").AddQueryParam("pet")
501 |
502 | // Test 1: Sign with v2, verify with v3
503 | t.Run("v2_sign_v3_verify", func(t *testing.T) {
504 | signerV2, err := NewJWSSigner(jwa.ES256, priv, config.SetKeyID("key1"), fields)
505 | if err != nil {
506 | t.Fatalf("Failed to create v2 signer: %v", err)
507 | }
508 |
509 | req := readRequest(httpreq2)
510 | sigInput, sig, err := SignRequest(signatureName, *signerV2, req)
511 | if err != nil {
512 | t.Fatalf("v2 signature failed: %v", err)
513 | }
514 |
515 | req.Header.Add("Signature", sig)
516 | req.Header.Add("Signature-Input", sigInput)
517 |
518 | verifierV3, err := NewJWSVerifierV3(jwav3.ES256(), pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), fields)
519 | if err != nil {
520 | t.Fatalf("Failed to create v3 verifier: %v", err)
521 | }
522 |
523 | err = VerifyRequest(signatureName, *verifierV3, req)
524 | if err != nil {
525 | t.Errorf("v3 verification of v2 signature failed: %v", err)
526 | }
527 | })
528 |
529 | // Test 2: Sign with v3, verify with v2
530 | t.Run("v3_sign_v2_verify", func(t *testing.T) {
531 | signerV3, err := NewJWSSignerV3(jwav3.ES256(), priv, config.SetKeyID("key1"), fields)
532 | if err != nil {
533 | t.Fatalf("Failed to create v3 signer: %v", err)
534 | }
535 |
536 | req := readRequest(httpreq2)
537 | sigInput, sig, err := SignRequest(signatureName, *signerV3, req)
538 | if err != nil {
539 | t.Fatalf("v3 signature failed: %v", err)
540 | }
541 |
542 | req.Header.Add("Signature", sig)
543 | req.Header.Add("Signature-Input", sigInput)
544 |
545 | verifierV2, err := NewJWSVerifier(jwa.ES256, pub, NewVerifyConfig().SetVerifyCreated(false).SetKeyID("key1"), fields)
546 | if err != nil {
547 | t.Fatalf("Failed to create v2 verifier: %v", err)
548 | }
549 |
550 | err = VerifyRequest(signatureName, *verifierV2, req)
551 | if err != nil {
552 | t.Errorf("v2 verification of v3 signature failed: %v", err)
553 | }
554 | })
555 | }
556 |
--------------------------------------------------------------------------------
/crypto.go:
--------------------------------------------------------------------------------
1 | package httpsign
2 |
3 | import (
4 | "crypto"
5 | "crypto/ecdsa"
6 | "crypto/ed25519"
7 | "crypto/elliptic"
8 | "crypto/hmac"
9 | "crypto/rand"
10 | "crypto/rsa"
11 | "crypto/sha256"
12 | "crypto/sha512"
13 | "crypto/subtle"
14 | "fmt"
15 |
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"
23 | )
24 |
25 | // Signer includes a cryptographic key (typically a private key) and configuration of what needs to be signed.
26 | type Signer struct {
27 | key interface{}
28 | alg string
29 | config *SignConfig
30 | fields Fields
31 | foreignSigner interface{}
32 | }
33 |
34 | // NewHMACSHA256Signer returns a new Signer structure. Key must be at least 64 bytes long.
35 | // Config may be nil for a default configuration.
36 | func NewHMACSHA256Signer(key []byte, config *SignConfig, fields Fields) (*Signer, error) {
37 | if len(key) < 64 {
38 | return nil, fmt.Errorf("key must be at least 64 bytes long")
39 | }
40 | if config == nil {
41 | config = NewSignConfig()
42 | }
43 | return &Signer{
44 | key: key,
45 | alg: "hmac-sha256",
46 | config: config,
47 | fields: fields,
48 | }, nil
49 | }
50 |
51 | // NewRSASigner returns a new Signer structure. Key is an RSA private key.
52 | // Config may be nil for a default configuration.
53 | func NewRSASigner(key rsa.PrivateKey, config *SignConfig, fields Fields) (*Signer, error) {
54 | if config == nil {
55 | config = NewSignConfig()
56 | }
57 | return &Signer{
58 | key: key,
59 | alg: "rsa-v1_5-sha256",
60 | config: config,
61 | fields: fields,
62 | }, nil
63 | }
64 |
65 | // NewRSAPSSSigner returns a new Signer structure. Key is an RSA private key.
66 | // Config may be nil for a default configuration.
67 | func NewRSAPSSSigner(key rsa.PrivateKey, config *SignConfig, fields Fields) (*Signer, error) {
68 | if config == nil {
69 | config = NewSignConfig()
70 | }
71 | return &Signer{
72 | key: key,
73 | alg: "rsa-pss-sha512",
74 | config: config,
75 | fields: fields,
76 | }, nil
77 | }
78 |
79 | // NewP256Signer returns a new Signer structure. Key is an elliptic curve P-256 private key.
80 | // Config may be nil for a default configuration.
81 | func NewP256Signer(key ecdsa.PrivateKey, config *SignConfig, fields Fields) (*Signer, error) {
82 | return newECCSigner(key, config, fields, elliptic.P256(), "P-256", "ecdsa-p256-sha256")
83 | }
84 |
85 | // NewP384Signer returns a new Signer structure. Key is an elliptic curve P-384 private key.
86 | // Config may be nil for a default configuration.
87 | func NewP384Signer(key ecdsa.PrivateKey, config *SignConfig, fields Fields) (*Signer, error) {
88 | return newECCSigner(key, config, fields, elliptic.P384(), "P-384", "ecdsa-p384-sha384")
89 | }
90 |
91 | func newECCSigner(key ecdsa.PrivateKey, config *SignConfig, fields Fields, curve elliptic.Curve, curveName, alg string) (*Signer, error) {
92 | if key.Curve != curve {
93 | return nil, fmt.Errorf("key curve must be %s", curveName)
94 | }
95 | if config == nil {
96 | config = NewSignConfig()
97 | }
98 | return &Signer{
99 | key: key,
100 | alg: alg,
101 | config: config,
102 | fields: fields,
103 | }, nil
104 | }
105 |
106 | // NewEd25519Signer returns a new Signer structure. Key is an EdDSA Curve 25519 private key.
107 | // Config may be nil for a default configuration.
108 | func NewEd25519Signer(key ed25519.PrivateKey, config *SignConfig, fields Fields) (*Signer, error) {
109 | if key == nil {
110 | return nil, fmt.Errorf("key must not be nil")
111 | }
112 | if config == nil {
113 | config = NewSignConfig()
114 | }
115 | return &Signer{
116 | key: key,
117 | alg: "ed25519",
118 | config: config,
119 | fields: fields,
120 | }, nil
121 | }
122 |
123 | // NewEd25519SignerFromSeed returns a new Signer structure. Key is an EdDSA Curve 25519 private key,
124 | // a 32 byte buffer according to RFC 8032.
125 | // Config may be nil for a default configuration.
126 | func NewEd25519SignerFromSeed(seed []byte, config *SignConfig, fields Fields) (*Signer, error) {
127 | if seed == nil || len(seed) != ed25519.SeedSize {
128 | return nil, fmt.Errorf("seed must not be nil, and must have length %d", ed25519.SeedSize)
129 | }
130 | key := ed25519.NewKeyFromSeed(seed)
131 | return NewEd25519Signer(key, config, fields)
132 | }
133 |
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.
139 | func NewJWSSigner(alg jwa.SignatureAlgorithm, key interface{}, config *SignConfig, fields Fields) (*Signer, error) {
140 | if key == nil {
141 | return nil, fmt.Errorf("key must not be nil")
142 | }
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() {
173 | return nil, fmt.Errorf("the NONE signing algorithm is expressly disallowed")
174 | }
175 | if config == nil {
176 | config = NewSignConfig()
177 | }
178 | jwsSigner, err := jwsv3.SignerFor(alg)
179 | if err != nil {
180 | return nil, err
181 | }
182 | return &Signer{
183 | key: key,
184 | alg: "",
185 | config: config,
186 | fields: fields,
187 | foreignSigner: jwsSigner,
188 | }, nil
189 | }
190 |
191 | func (s Signer) sign(buff []byte) ([]byte, error) {
192 | 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
205 | }
206 |
207 | return nil, fmt.Errorf("expected jws.Signer or Signer2 interface, got %T", s.foreignSigner)
208 | }
209 | switch s.alg {
210 | case "hmac-sha256":
211 | mac := hmac.New(sha256.New, s.key.([]byte))
212 | mac.Write(buff)
213 | return mac.Sum(nil), nil
214 | case "rsa-v1_5-sha256":
215 | hashed := sha256.Sum256(buff)
216 | key := s.key.(rsa.PrivateKey)
217 | sig, err := rsa.SignPKCS1v15(nil, &key, crypto.SHA256, hashed[:])
218 | if err != nil {
219 | return nil, fmt.Errorf("RSA signature failed")
220 | }
221 | return sig, nil
222 | case "rsa-pss-sha512":
223 | hashed := sha512.Sum512(buff)
224 | key := s.key.(rsa.PrivateKey)
225 | sig, err := rsa.SignPSS(rand.Reader, &key, crypto.SHA512, hashed[:], nil)
226 | if err != nil {
227 | return nil, fmt.Errorf("RSA-PSS signature failed")
228 | }
229 | return sig, nil
230 | case "ecdsa-p256-sha256":
231 | hashed := sha256.Sum256(buff)
232 | key := s.key.(ecdsa.PrivateKey)
233 | return ecdsaSignRaw(rand.Reader, &key, hashed[:])
234 | case "ecdsa-p384-sha384":
235 | hashed := sha512.Sum384(buff)
236 | key := s.key.(ecdsa.PrivateKey)
237 | return ecdsaSignRaw(rand.Reader, &key, hashed[:])
238 | case "ed25519":
239 | key := s.key.(ed25519.PrivateKey)
240 | if len(key) != ed25519.PrivateKeySize {
241 | return nil, fmt.Errorf("key must be %d bytes long", ed25519.PrivateKeySize)
242 | }
243 | return ed25519.Sign(key, buff), nil
244 | default:
245 | return nil, fmt.Errorf("sign: unknown algorithm \"%s\"", s.alg)
246 | }
247 | }
248 |
249 | // Verifier includes a cryptographic key (typically a public key) and configuration of what needs to be verified.
250 | type Verifier struct {
251 | key interface{}
252 | alg string
253 | config *VerifyConfig
254 | fields Fields
255 | foreignVerifier interface{}
256 | }
257 |
258 | // NewHMACSHA256Verifier generates a new Verifier for HMAC-SHA256 signatures. Set config to nil for a default configuration.
259 | // Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
260 | func NewHMACSHA256Verifier(key []byte, config *VerifyConfig, fields Fields) (*Verifier, error) {
261 | if key == nil {
262 | return nil, fmt.Errorf("key must not be nil")
263 | }
264 | if len(key) < 64 {
265 | return nil, fmt.Errorf("key must be at least 64 bytes long")
266 | }
267 | if config == nil {
268 | config = NewVerifyConfig()
269 | }
270 | return &Verifier{
271 | key: key,
272 | alg: "hmac-sha256",
273 | config: config,
274 | fields: fields,
275 | }, nil
276 | }
277 |
278 | // NewRSAVerifier generates a new Verifier for RSA signatures. Set config to nil for a default configuration.
279 | // Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
280 | func NewRSAVerifier(key rsa.PublicKey, config *VerifyConfig, fields Fields) (*Verifier, error) {
281 | if config == nil {
282 | config = NewVerifyConfig()
283 | }
284 | return &Verifier{
285 | key: key,
286 | alg: "rsa-v1_5-sha256",
287 | config: config,
288 | fields: fields,
289 | }, nil
290 | }
291 |
292 | // NewRSAPSSVerifier generates a new Verifier for RSA-PSS signatures. Set config to nil for a default configuration.
293 | // Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
294 | func NewRSAPSSVerifier(key rsa.PublicKey, config *VerifyConfig, fields Fields) (*Verifier, error) {
295 | if config == nil {
296 | config = NewVerifyConfig()
297 | }
298 | return &Verifier{
299 | key: key,
300 | alg: "rsa-pss-sha512",
301 | config: config,
302 | fields: fields,
303 | }, nil
304 | }
305 |
306 | // NewP256Verifier generates a new Verifier for ECDSA (P-256) signatures. Set config to nil for a default configuration.
307 | // Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
308 | func NewP256Verifier(key ecdsa.PublicKey, config *VerifyConfig, fields Fields) (*Verifier, error) {
309 | return newECCVerifier(key, config, fields, elliptic.P256(), "P-256", "ecdsa-p256-sha256")
310 | }
311 |
312 | // NewP384Verifier generates a new Verifier for ECDSA (P-384) signatures. Set config to nil for a default configuration.
313 | // Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
314 | func NewP384Verifier(key ecdsa.PublicKey, config *VerifyConfig, fields Fields) (*Verifier, error) {
315 | return newECCVerifier(key, config, fields, elliptic.P384(), "P-384", "ecdsa-p384-sha384")
316 | }
317 |
318 | func newECCVerifier(key ecdsa.PublicKey, config *VerifyConfig, fields Fields, curve elliptic.Curve, curveName, alg string) (*Verifier, error) {
319 | if config == nil {
320 | config = NewVerifyConfig()
321 | }
322 | if key.Curve != curve {
323 | return nil, fmt.Errorf("key curve must be %s", curveName)
324 | }
325 | return &Verifier{
326 | key: key,
327 | alg: alg,
328 | config: config,
329 | fields: fields,
330 | }, nil
331 | }
332 |
333 | // NewEd25519Verifier generates a new Verifier for EdDSA Curve 25519 signatures. Set config to nil for a default configuration.
334 | // Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
335 | func NewEd25519Verifier(key ed25519.PublicKey, config *VerifyConfig, fields Fields) (*Verifier, error) {
336 | if key == nil {
337 | return nil, fmt.Errorf("key must not be nil")
338 | }
339 | if config == nil {
340 | config = NewVerifyConfig()
341 | }
342 | return &Verifier{
343 | key: key,
344 | alg: "ed25519",
345 | config: config,
346 | fields: fields,
347 | }, nil
348 | }
349 |
350 | // NewJWSVerifier creates a generic verifier for JWS algorithms, using the go-jwx v2 package. The particular key type for each algorithm
351 | // is documented in that package. Set config to nil for a default configuration.
352 | // Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
353 | //
354 | // Note: This function uses jwx v2. For jwx v3 support, use NewJWSVerifierV3 instead.
355 | func NewJWSVerifier(alg jwa.SignatureAlgorithm, key interface{}, config *VerifyConfig, fields Fields) (*Verifier, error) {
356 | if key == nil {
357 | return nil, fmt.Errorf("key must not be nil")
358 | }
359 | if config == nil {
360 | config = NewVerifyConfig()
361 | }
362 | if alg == jwa.NoSignature {
363 | return nil, fmt.Errorf("the NONE signing algorithm is expressly disallowed")
364 | }
365 | verifier, err := jws.NewVerifier(alg)
366 | if err != nil {
367 | return nil, err
368 | }
369 | return &Verifier{
370 | key: key,
371 | alg: "",
372 | config: config,
373 | fields: fields,
374 | foreignVerifier: verifier,
375 | }, nil
376 | }
377 |
378 | // NewJWSVerifierV3 creates a generic verifier for JWS algorithms, using the go-jwx v3 package. The particular key type for each algorithm
379 | // is documented in that package. Set config to nil for a default configuration.
380 | // Fields is the list of required headers and fields, which may be empty (but this is typically insecure).
381 | //
382 | // This function uses jwx v3 and is the recommended choice for new code using jwx v3.
383 | // It uses the recommended VerifierFor() API which returns Verifier2 interface.
384 | func NewJWSVerifierV3(alg jwav3.SignatureAlgorithm, key interface{}, config *VerifyConfig, fields Fields) (*Verifier, error) {
385 | if key == nil {
386 | return nil, fmt.Errorf("key must not be nil")
387 | }
388 | if config == nil {
389 | config = NewVerifyConfig()
390 | }
391 | if alg == jwav3.NoSignature() {
392 | return nil, fmt.Errorf("the NONE signing algorithm is expressly disallowed")
393 | }
394 | verifier, err := jwsv3.VerifierFor(alg)
395 | if err != nil {
396 | return nil, err
397 | }
398 | return &Verifier{
399 | key: key,
400 | alg: "",
401 | config: config,
402 | fields: fields,
403 | foreignVerifier: verifier,
404 | }, nil
405 | }
406 |
407 | func (v Verifier) verify(buff []byte, sig []byte) (bool, error) {
408 | if v.foreignVerifier != nil {
409 | // Try v2 verifier first (jws.Verifier interface: Verify(payload, sig, key))
410 | if verifierV2, ok := v.foreignVerifier.(jws.Verifier); ok {
411 | err := verifierV2.Verify(buff, sig, v.key)
412 | if err != nil {
413 | return false, err
414 | }
415 | return true, nil
416 | }
417 |
418 | // Try v3 Verifier2 interface (new recommended API: Verify(key, payload, sig))
419 | // Note: parameter order is DIFFERENT compared to v2!
420 | type Verifier2 interface {
421 | Verify(key interface{}, payload, signature []byte) error
422 | }
423 | if verifierV3, ok := v.foreignVerifier.(Verifier2); ok {
424 | err := verifierV3.Verify(v.key, buff, sig) // Note: key first, then payload, then signature
425 | if err != nil {
426 | return false, err
427 | }
428 | return true, nil
429 | }
430 |
431 | return false, fmt.Errorf("expected jws.Verifier or Verifier2 interface, got %T", v.foreignVerifier)
432 | }
433 |
434 | switch v.alg {
435 | case "hmac-sha256":
436 | mac := hmac.New(sha256.New, v.key.([]byte))
437 | mac.Write(buff)
438 | return subtle.ConstantTimeCompare(mac.Sum(nil), sig) == 1, nil
439 | case "rsa-v1_5-sha256":
440 | hashed := sha256.Sum256(buff)
441 | key := v.key.(rsa.PublicKey)
442 | err := rsa.VerifyPKCS1v15(&key, crypto.SHA256, hashed[:], sig)
443 | if err != nil {
444 | return false, fmt.Errorf("RSA verification failed: %w", err)
445 | }
446 | return true, nil
447 | case "rsa-pss-sha512":
448 | hashed := sha512.Sum512(buff)
449 | key := v.key.(rsa.PublicKey)
450 | err := rsa.VerifyPSS(&key, crypto.SHA512, hashed[:], sig, nil)
451 | if err != nil {
452 | return false, fmt.Errorf("RSA-PSS verification failed: %w", err)
453 | }
454 | return true, nil
455 | case "ecdsa-p256-sha256":
456 | hashed := sha256.Sum256(buff)
457 | key := v.key.(ecdsa.PublicKey)
458 | return ecdsaVerifyRaw(&key, hashed[:], sig)
459 | case "ecdsa-p384-sha384":
460 | hashed := sha512.Sum384(buff)
461 | key := v.key.(ecdsa.PublicKey)
462 | return ecdsaVerifyRaw(&key, hashed[:], sig)
463 | case "ed25519":
464 | key := v.key.(ed25519.PublicKey)
465 | verified := ed25519.Verify(key, buff, sig)
466 | if !verified {
467 | return false, fmt.Errorf("failed Ed25519 verification")
468 | }
469 | return true, nil
470 | default:
471 | return false, fmt.Errorf("verify: unknown algorithm \"%s\"", v.alg)
472 | }
473 | }
474 |
--------------------------------------------------------------------------------