├── 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 | 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 | [![Go Reference](https://pkg.go.dev/badge/github.com/yaronf/httpsign.svg)](https://pkg.go.dev/github.com/yaronf/httpsign) 37 | [![Test](https://github.com/yaronf/httpsign/actions/workflows/test.yml/badge.svg)](https://github.com/yaronf/httpsign/actions/workflows/test.yml) 38 | [![GoReportCard example](https://goreportcard.com/badge/github.com/yaronf/httpsign)](https://goreportcard.com/report/github.com/yaronf/httpsign) 39 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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 | --------------------------------------------------------------------------------