From b039f9a9050428d8e90fd9e0a4661045f371695f Mon Sep 17 00:00:00 2001 From: Thibault NORMAND Date: Fri, 25 Jun 2021 15:11:42 +0200 Subject: [PATCH 1/2] feat(ietf): draft-ietf-httpbis-message-signatures-05 --- README.md | 23 ++-- api.go | 12 ++- go.mod | 2 +- go.sum | 2 + helpers.go | 11 +- helpers_test.go | 73 ++++++++----- httpsig.go | 20 ++-- httpsig_test.go | 29 +++-- lib_test.go | 258 +++++++++++++-------------------------------- signature_input.go | 16 ++- signer.go | 70 +++++++++--- signer_test.go | 152 ++++++++++++++------------ verifier.go | 9 +- verifier_test.go | 100 +++++++++++------- 14 files changed, 413 insertions(+), 364 deletions(-) diff --git a/README.md b/README.md index 087b532..214bd7a 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,19 @@ # HTTP Request Signature Use [RFC 8941](https://www.rfc-editor.org/rfc/rfc8941.html) (a.k.a. Structured Field Values) to implements -the draft specification [draft-ietf-httpbis-message-signatures](https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-01.html). +the draft specification [draft-ietf-httpbis-message-signatures](https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-05.html)(version 5). > Message integrity and authenticity are important security properties that are critical to the secure operation of many HTTP applications. Application developers typically rely on the transport layer to provide these properties, by operating their application over [TLS]. However, TLS only guarantees these properties over a single TLS connection, and the path between client and application may be composed of multiple independent TLS connections (for example, if the application is hosted behind a TLS-terminating gateway or if the client is behind a TLS Inspection appliance). In such cases, TLS cannot guarantee end-to-end message integrity or authenticity between the client and application. Additionally, some operating environments present obstacles that make it impractical to use TLS, or to use features necessary to provide message authenticity. Furthermore, some applications require the binding of an application-level key to the HTTP message, separate from any TLS certificates in use. Consequently, while TLS can meet message integrity and authenticity needs for many HTTP-based applications, it is not a universal solution. ## Limitations -* Only `hs2019` suite is supported +* can't reproduce `RSASSA-PSS` signatures from standard because Go can't load these kind of private keys. +* Algorithms supported + * `rsa-pss-sha512` (equiv. JWA PS512) + * `rsa-v1_5-sha256` (equiv. JWA RS256) + * `hmac-sha256` (equiv. JWA HS256) + * `ecdsa-p256-sha256` '(equiv. JWA ES256) + * `eddsa-ed25519-blake2b512` (not in the standard) (equiv JWA EdDSA) ## Protocol @@ -21,9 +27,9 @@ the draft specification [draft-ietf-httpbis-message-signatures](https://www.ietf * `value` is a `List with Params` ```sh -Signature-Input: sig1=(*request-target, *created, host, date, - cache-control, x-empty-header, x-example); kid="test-key-a"; - alg=hs2019; created=1402170695; expires=1402170995 +Signature-Input: sig1=(@request-target, @created, host, date, + cache-control, x-empty-header, x-example); keyid="test-key-a"; + alg="rsa-pss-sha512"; created=1402170695; expires=1402170995 ``` `Signature` - HTTP Header @@ -48,14 +54,15 @@ Sign a request ```go // Generate a key -pub, priv, _ := ed25519.GenerateKey(rand.Reader) +priv, pub := rsa.GenerateKey(rand.Reader, 2048) // Prepare a signature-input si := httpsig.SignatureInput{ ID: "sig1", KeyID: "my-wonderful-key-identifier", - Headers: []string{"*created","*request-target","Authorization"}, + Headers: []string{"@created","@request-target","Authorization"}, Created: uint64(time.Now().Unix()), + Nonce: uniuri.NewLen(32), } // Key resolver function @@ -64,7 +71,7 @@ privateKeyResolver := func(ctx context.Context, kid string){ } // Prepare a signer -signer := httpsig.NewSigner(privateKeyResolver) +signer := httpsig.NewSigner(httpsig.AlgorithmRSAPSSSHA512, privateKeyResolver) // Create your request req := http.NewRequest(...) diff --git a/api.go b/api.go index 7b165ae..b189b00 100644 --- a/api.go +++ b/api.go @@ -27,8 +27,16 @@ import ( type Algorithm string const ( - // AlgorithmHS2019 represents signing suite (RSASSA-PSS/SHA512, ECDSA/SHA512, EdDSA/SHA512, HMAC-SHA512) - AlgorithmHS2019 Algorithm = "hs2019" + // AlgorithmRSAPSSSHA512 represents signature algorithm RSASSA-PSS using SHA-512 + AlgorithmRSAPSSSHA512 Algorithm = "rsa-pss-sha512" + // AlgorithmRSAV15SHA256 represents signature algorithm RSASSA-PKCS1-v1_5 using SHA-256 + AlgorithmRSAV15SHA256 Algorithm = "rsa-v1_5-sha256" + // AlgorithmRSAV15SHA256 represents signature algorithm HMAC using SHA-256 + AlgorithmHMACSHA256 Algorithm = "hmac-sha256" + // AlgorithmECDSAP256SHA256 represents signature algorithm using ECDA P-256 curve with SHA-256 + AlgorithmECDSAP256SHA256 Algorithm = "ecdsa-p256-sha256" + // AlgorithmEdDSAEd25519BLAKE512 represents signature algorithm using EdDSA Ed25519 curve with BLAKE2B-512 + AlgorithmEdDSAEd25519BLAKE2B512 Algorithm = "eddsa-ed25519-blake2b512" ) // Verifier describes signature verification implementation contract. diff --git a/go.mod b/go.mod index a907350..364c115 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module zntr.io/httpsig go 1.16 -require github.com/ucarion/sfv v0.1.0 +require github.com/ucarion/sfv v0.1.1 diff --git a/go.sum b/go.sum index 21c7637..4b717df 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,4 @@ github.com/ucarion/sfv v0.1.0 h1:lkIPBdl7g8+CX2YzDipqYgzvqqURCfbcJPsYqt5GUS0= github.com/ucarion/sfv v0.1.0/go.mod h1:xxupTcJV5G5JI2dCI0ytdXXI6BMRJrgx9bu5co7MtOc= +github.com/ucarion/sfv v0.1.1 h1:trgLQ1/VBuykHPFeCjipXJAmGYjJaUKeZu9rS5D/dyc= +github.com/ucarion/sfv v0.1.1/go.mod h1:xxupTcJV5G5JI2dCI0ytdXXI6BMRJrgx9bu5co7MtOc= diff --git a/helpers.go b/helpers.go index ca073a6..520271f 100644 --- a/helpers.go +++ b/helpers.go @@ -39,14 +39,14 @@ func protected(sigMeta *SignatureInput, r *http.Request) ([]byte, error) { // Clean headers canonicalHeaders := map[string]string{} - canonicalHeaders["*created"] = fmt.Sprintf("%d", sigMeta.Created) - canonicalHeaders["*request-target"] = requestTarget(r) + canonicalHeaders["@request-target"] = requestTarget(r) + canonicalHeaders["@created"] = fmt.Sprintf("%d", sigMeta.Created) if sigMeta.Expires > 0 { - canonicalHeaders["*expires"] = fmt.Sprintf("%d", sigMeta.Expires) + canonicalHeaders["@expires"] = fmt.Sprintf("%d", sigMeta.Expires) } canonicalHeaders["host"] = r.Host - // https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-01.html#name-http-header-fields + // https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-05.html#name-http-header-fields for key, values := range r.Header { var hdrs []string for _, v := range values { @@ -63,6 +63,9 @@ func protected(sigMeta *SignatureInput, r *http.Request) ([]byte, error) { } } + // Append signature params + fmt.Fprintf(&protected, "@signature-params: %s\n", sigMeta.Params()) + // No error return protected.Bytes(), nil } diff --git a/helpers_test.go b/helpers_test.go index da62850..bfa32e0 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -39,7 +39,7 @@ Digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE= Content-Length: 18 Signature-Input: sig1=(*request-target, *created, host, date, cache-control, x-empty-header, x-example); kid="test-key-a"; - alg=hs2019; created=1402170695; expires=1402170995 + alg=rsa-pss-sha512; created=1402170695; expires=1402170995 Signature: sig1=:K2qGT5srn2OGbOIDzQ6kYT+ruaycnDAAUpKv+ePFfD0RAxn/1BUe Zx/Kdrq32DrfakQ6bPsvB9aqZqognNT6be4olHROIkeV879RrsrObury8L9SCEibe oHyqU/yCjphSmEdd7WD+zrchK57quskKwRefy2iEC5S2uAH0EPyOZKWlvbKmKu5q4 @@ -49,7 +49,7 @@ Signature: sig1=:K2qGT5srn2OGbOIDzQ6kYT+ruaycnDAAUpKv+ePFfD0RAxn/1BUe X-Forwarded-For: 192.0.2.123 Signature-Input: reverse_proxy_sig=(*created, host, date, signature:sig1, x-forwarded-for); kid="test-key-a"; - alg=hs2019; created=1402170695; expires=1402170695.25 + alg=rsa-pss-sha512; created=1402170695; expires=1402170695.25 Signature: reverse_proxy_sig=:ON3HsnvuoTlX41xfcGWaOEVo1M3bJDRBOp0Pc/O jAOWKQn0VMY0SvMMWXS7xG+xYVa152rRVAo6nMV7FS3rv0rR5MzXL8FCQ2A35DCEN LOhEgj/S1IstEAEFsKmE9Bs7McBsCtJwQ3hMqdtFenkDffSoHOZOInkTYGafkoy78 @@ -92,80 +92,105 @@ func Test_protected(t *testing.T) { args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Headers: []string{"*request-target", "*created", "*expires", "host", "date"}, + Headers: []string{"@request-target", "@created", "@expires", "host", "date"}, KeyID: "test-key-a", - Algorithm: "hs2019", + Algorithm: "rsa-pss-sha512", Created: 1402170695, Expires: 1402170995, + Nonce: "1234567890", }, r: sampleRequest(), }, wantErr: false, - want: "*request-target: post /foo?param=value&pet=dog\n" + - "*created: 1402170695\n" + - "*expires: 1402170995\n" + + want: "@request-target: post /foo?param=value&pet=dog\n" + + "@created: 1402170695\n" + + "@expires: 1402170995\n" + "host: example.com\n" + - "date: Tue, 07 Jun 2014 20:51:35 GMT\n", + "date: Tue, 07 Jun 2014 20:51:35 GMT\n" + + `@signature-params: (@request-target, @created, @expires, host, date); alg="rsa-pss-sha512"; keyid="test-key-a"; created=1402170695; expires=1402170995; nonce="1234567890"` + "\n", + }, + { + name: "valid - no header", + args: args{ + sigMeta: &SignatureInput{ + ID: "sig1", + Headers: []string{}, + KeyID: "test-key-a", + Algorithm: "rsa-pss-sha512", + Created: 1402170695, + Expires: 1402170995, + Nonce: "1234567890", + }, + r: sampleRequest(), + }, + wantErr: false, + want: `@signature-params: (); alg="rsa-pss-sha512"; keyid="test-key-a"; created=1402170695; expires=1402170995; nonce="1234567890"` + "\n", }, { name: "valid - double header", args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Headers: []string{"*request-target", "*created", "*expires", "host", "date", "x-custom"}, + Headers: []string{"@request-target", "@created", "@expires", "host", "date", "x-custom"}, KeyID: "test-key-a", - Algorithm: "hs2019", + Algorithm: "rsa-pss-sha512", Created: 1402170695, Expires: 1402170995, + Nonce: "1234567890", }, r: sampleRequest(), }, wantErr: false, - want: "*request-target: post /foo?param=value&pet=dog\n" + - "*created: 1402170695\n" + - "*expires: 1402170995\n" + + want: "@request-target: post /foo?param=value&pet=dog\n" + + "@created: 1402170695\n" + + "@expires: 1402170995\n" + "host: example.com\n" + "date: Tue, 07 Jun 2014 20:51:35 GMT\n" + - "x-custom: 1, 2\n", + "x-custom: 1, 2\n" + + `@signature-params: (@request-target, @created, @expires, host, date, x-custom); alg="rsa-pss-sha512"; keyid="test-key-a"; created=1402170695; expires=1402170995; nonce="1234567890"` + "\n", }, { name: "valid - dictionary prefix", args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Headers: []string{"*request-target", "*created", "x-dictionary:a", "x-dictionary:b", "x-dictionary:c"}, + Headers: []string{"@request-target", "@created", "x-dictionary:a", "x-dictionary:b", "x-dictionary:c"}, KeyID: "test-key-a", - Algorithm: "hs2019", + Algorithm: "rsa-pss-sha512", Created: 1402170695, Expires: 1402170995, + Nonce: "1234567890", }, r: sampleRequest(), }, wantErr: false, - want: "*request-target: post /foo?param=value&pet=dog\n" + - "*created: 1402170695\n" + + want: "@request-target: post /foo?param=value&pet=dog\n" + + "@created: 1402170695\n" + "x-dictionary: a=1\n" + "x-dictionary: b=2;x=1;y=2\n" + - "x-dictionary: c=(a b c)\n", + "x-dictionary: c=(a b c)\n" + + `@signature-params: (@request-target, @created, x-dictionary:a, x-dictionary:b, x-dictionary:c); alg="rsa-pss-sha512"; keyid="test-key-a"; created=1402170695; expires=1402170995; nonce="1234567890"` + "\n", }, { name: "valid - list prefix", args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Headers: []string{"*request-target", "*created", "x-list-a:0", "x-list-a:2"}, + Headers: []string{"@request-target", "@created", "x-list-a:0", "x-list-a:2"}, KeyID: "test-key-a", - Algorithm: "hs2019", + Algorithm: "rsa-pss-sha512", Created: 1402170695, Expires: 1402170995, + Nonce: "1234567890", }, r: sampleRequest(), }, wantErr: false, - want: "*request-target: post /foo?param=value&pet=dog\n" + - "*created: 1402170695\n" + + want: "@request-target: post /foo?param=value&pet=dog\n" + + "@created: 1402170695\n" + "x-list-a: \n" + - "x-list-a: a, b\n", + "x-list-a: a, b\n" + + `@signature-params: (@request-target, @created, x-list-a:0, x-list-a:2); alg="rsa-pss-sha512"; keyid="test-key-a"; created=1402170695; expires=1402170995; nonce="1234567890"` + "\n", }, } for _, tt := range tests { diff --git a/httpsig.go b/httpsig.go index 7a207f6..3e529ac 100644 --- a/httpsig.go +++ b/httpsig.go @@ -31,7 +31,8 @@ type sfvSigInput struct { Created uint64 `sfv:"created"` Expires uint64 `sfv:"expires"` Algorithm string `sfv:"alg"` - KeyID string `sfv:"kid"` + KeyID string `sfv:"keyid"` + Nonce string `sfv:"nonce"` } // ParseSignatureInput returns the SignatureInput descriptor. @@ -49,22 +50,25 @@ func ParseSignatureInput(input string) ([]*SignatureInput, error) { for id, meta := range sigInputMap { sig := &SignatureInput{ ID: id, + Algorithm: Algorithm(meta.Algorithm), Created: meta.Created, Expires: meta.Expires, KeyID: meta.KeyID, - Algorithm: AlgorithmHS2019, Headers: []string{}, + Nonce: meta.Nonce, } // Filter not supported algorithm - if meta.Algorithm != "" && meta.Algorithm != string(AlgorithmHS2019) { - // Skip unsupported signature + switch sig.Algorithm { + case AlgorithmRSAPSSSHA512: + case AlgorithmRSAV15SHA256: + case AlgorithmHMACSHA256: + case AlgorithmECDSAP256SHA256: + case AlgorithmEdDSAEd25519BLAKE2B512: + default: + // Skip invalid signature algorithm continue } - if meta.Algorithm == "" { - // Fallback to hs2019 - sig.Algorithm = AlgorithmHS2019 - } // Extract headers for _, h := range meta.Headers { diff --git a/httpsig_test.go b/httpsig_test.go index 95a0937..b4f09a5 100644 --- a/httpsig_test.go +++ b/httpsig_test.go @@ -42,16 +42,33 @@ func TestParse(t *testing.T) { { name: "valid", args: args{ - signature: `sig1=(*request-target *created host date cache-control x-empty-header x-example);kid="test-key-a";alg=hs2019;created=1402170695;expires=1402170995`, + signature: `sig1=();created=1618884475;keyid="test-key-rsa-pss";alg="rsa-pss-sha512"`, }, want: []*SignatureInput{ { ID: "sig1", - Headers: []string{"*request-target", "*created", "host", "date", "cache-control", "x-empty-header", "x-example"}, - KeyID: "test-key-a", - Algorithm: "hs2019", - Created: 1402170695, - Expires: 1402170995, + Headers: []string{}, + KeyID: "test-key-rsa-pss", + Algorithm: "rsa-pss-sha512", + Created: 1618884475, + }, + }, + wantErr: false, + }, + { + name: "valid with headers", + args: args{ + signature: `sig1=("@request-target" "host" "date" "content-type" "digest" "content-length");created=1618884475;keyid="test-key-rsa-pss";alg="rsa-pss-sha512";expires=1618884495;nonce="fpxObpaLKpEdHRErAMmaeEURhibYFdBMvuExQWpMlScKnvQeNGEMXaWEvYDwEWgQ"`, + }, + want: []*SignatureInput{ + { + ID: "sig1", + Headers: []string{"@request-target", "host", "date", "content-type", "digest", "content-length"}, + KeyID: "test-key-rsa-pss", + Algorithm: "rsa-pss-sha512", + Created: 1618884475, + Expires: 1618884495, + Nonce: "fpxObpaLKpEdHRErAMmaeEURhibYFdBMvuExQWpMlScKnvQeNGEMXaWEvYDwEWgQ", }, }, wantErr: false, diff --git a/lib_test.go b/lib_test.go index bcac9a5..134168a 100644 --- a/lib_test.go +++ b/lib_test.go @@ -21,6 +21,7 @@ import ( "bufio" "bytes" "context" + "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/pem" @@ -30,10 +31,50 @@ import ( "zntr.io/httpsig" ) +// ----------------------------------------------------------------------------- + +func rsaPKCS1PublicKeyDecode(data []byte) *rsa.PublicKey { + block, _ := pem.Decode(data) + key, _ := x509.ParsePKCS1PublicKey(block.Bytes) + if key == nil { + panic("key must not be nil") + } + return key +} + +func rsaPKCS1PrivateKeyDecode(data []byte) *rsa.PrivateKey { + block, _ := pem.Decode(data) + key, _ := x509.ParsePKCS1PrivateKey(block.Bytes) + if key == nil { + panic("key must not be nil") + } + return key +} + +func eccPrivateKeyDecode(data []byte) *ecdsa.PrivateKey { + block, _ := pem.Decode(data) + key, _ := x509.ParseECPrivateKey(block.Bytes) + if key == nil { + panic("key must not be nil") + } + return key +} + +func eccPublicKeyDecode(data []byte) *ecdsa.PublicKey { + block, _ := pem.Decode(data) + key, _ := x509.ParsePKIXPublicKey(block.Bytes) + if key == nil { + panic("key must not be nil") + } + return key.(*ecdsa.PublicKey) +} + +// ----------------------------------------------------------------------------- + // Imported from spec. -// https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-01.html#name-example-key-rsa-test +// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-05.html#section-b.1.1 -var testRSAPublicKey = publicKeyDecode([]byte(`-----BEGIN RSA PUBLIC KEY----- +var testRSAPublicKey = rsaPKCS1PublicKeyDecode([]byte(`-----BEGIN RSA PUBLIC KEY----- MIIBCgKCAQEAhAKYdtoeoy8zcAcR874L8cnZxKzAGwd7v36APp7Pv6Q2jdsPBRrw WEBnez6d0UDKDwGbc6nxfEXAy5mbhgajzrw3MOEt8uA5txSKobBpKDeBLOsdJKFq MGmXCQvEG7YemcxDTRPxAleIAgYYRjTSd/QBwVW9OwNFhekro3RtlinV0a75jfZg @@ -42,7 +83,7 @@ uKxI4T+HIaFpv8+rdV6eUgOrB2xeI1dSFFn/nnv5OoZJEIB+VmuKn3DCUcCZSFlQ PSXSfBDiUGhwOw76WuSSsf1D4b/vLoJ10wIDAQAB -----END RSA PUBLIC KEY-----`)) -var testRSAPrivateKey = privateKeyDecode([]byte(`-----BEGIN RSA PRIVATE KEY----- +var testRSAPrivateKey = rsaPKCS1PrivateKeyDecode([]byte(`-----BEGIN RSA PRIVATE KEY----- MIIEqAIBAAKCAQEAhAKYdtoeoy8zcAcR874L8cnZxKzAGwd7v36APp7Pv6Q2jdsP BRrwWEBnez6d0UDKDwGbc6nxfEXAy5mbhgajzrw3MOEt8uA5txSKobBpKDeBLOsd JKFqMGmXCQvEG7YemcxDTRPxAleIAgYYRjTSd/QBwVW9OwNFhekro3RtlinV0a75 @@ -70,26 +111,35 @@ WtP+fG5Q6Dpdz3LRfm+KwBCWFKQjg7uTxcjerhBWEYPmEMKYwTJF5PBG9/ddvHLQ EQeNC8fHGg4UXU8mhHnSBt3EA10qQJfRDs15M38eG2cYwB1PZpDHScDnDA0= -----END RSA PRIVATE KEY-----`)) -func publicKeyDecode(data []byte) *rsa.PublicKey { - block, _ := pem.Decode(data) - key, _ := x509.ParsePKCS1PublicKey(block.Bytes) - return key -} +// ----------------------------------------------------------------------------- -func privateKeyDecode(data []byte) *rsa.PrivateKey { - block, _ := pem.Decode(data) - key, _ := x509.ParsePKCS1PrivateKey(block.Bytes) - return key -} +// Imported from spec. +// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-05.html#section-b.1.3 + +var testECCP256PublicKey = eccPublicKeyDecode([]byte(`-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqIVYZVLCrPZHGHjP17CTW0/+D9Lf +w0EkjqF7xB4FivAxzic30tMM4GF+hR6Dxh71Z50VGGdldkkDXZCnTNnoXQ== +-----END PUBLIC KEY-----`)) + +var testECCP256PrivateKey = eccPrivateKeyDecode([]byte(`-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIFKbhfNZfpDsW43+0+JjUr9K+bTeuxopu653+hBaXGA7oAoGCCqGSM49 +AwEHoUQDQgAEqIVYZVLCrPZHGHjP17CTW0/+D9Lfw0EkjqF7xB4FivAxzic30tMM +4GF+hR6Dxh71Z50VGGdldkkDXZCnTNnoXQ== +-----END EC PRIVATE KEY-----`)) + +// ----------------------------------------------------------------------------- + +// Imported from spec. +// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-05.html#section-b.1.3 // ----------------------------------------------------------------------------- // Extracted from spec -// https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-01.html#name-test-cases -func sampleRequest() *http.Request { +// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-05.html#section-b.2 +func sampleTestRequest() *http.Request { r, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(`POST /foo?param=value&pet=dog HTTP/1.1 Host: example.com -Date: Tue, 07 Jun 2014 20:51:35 GMT +Date: Tue, 20 Apr 2021 02:07:55 GMT Content-Type: application/json Digest: SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE= Content-Length: 18 @@ -104,127 +154,9 @@ Content-Length: 18 // ----------------------------------------------------------------------------- -// https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-01.html#section-a.3.1.1 -func Test_SigGen_HS2019Minimal(t *testing.T) { - si := &httpsig.SignatureInput{ - ID: "sig1", - Algorithm: httpsig.AlgorithmHS2019, - KeyID: "test-key-a", - Created: 1402170695, - Headers: []string{"*created", "*request-target"}, - } - - // Assert conanical syntax - expectedCanonical := `sig1=(*created, *request-target); alg="hs2019"; kid="test-key-a"; created=1402170695` - if si.String() != expectedCanonical { - t.Fatalf("invalid canonical syntax expected `%s`, got `%s`", expectedCanonical, si.String()) - } - - // Create operations - signer := httpsig.NewSigner(func(ctx context.Context, kid string) (interface{}, error) { - return testRSAPrivateKey, nil - }) - verifier := httpsig.NewVerifier(func(ctx context.Context, kid string) (interface{}, error) { - return testRSAPublicKey, nil - }) - - // Create request - r := sampleRequest() - - sig, err := signer.Sign(context.Background(), si, r) - if err != nil { - t.Fatalf("unable to sign: %v", err) - } - - valid, errVerify := verifier.Verify(context.Background(), si, sig, r) - if errVerify != nil { - t.Fatalf("unable to verify: %v", errVerify) - } - - // Expected - if !valid { - t.Fatalf("expected valid, got %v", valid) - } -} - -// https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-01.html#section-a.3.1.1 -func Test_SigGen_Default(t *testing.T) { - si := httpsig.DefaultSignatureInput("test-key-a") - - // Create operations - signer := httpsig.NewSigner(func(ctx context.Context, kid string) (interface{}, error) { - return testRSAPrivateKey, nil - }) - verifier := httpsig.NewVerifier(func(ctx context.Context, kid string) (interface{}, error) { - return testRSAPublicKey, nil - }) - - // Create request - r := sampleRequest() - - sig, err := signer.Sign(context.Background(), si, r) - if err != nil { - t.Fatalf("unable to sign: %v", err) - } - - valid, errVerify := verifier.Verify(context.Background(), si, sig, r) - if errVerify != nil { - t.Fatalf("unable to verify: %v", errVerify) - } - - // Expected - if !valid { - t.Fatalf("expected valid, got %v", valid) - } -} - -// https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-01.html#name-hs2019-signature-covering-a -func Test_SigGen_HS2019AllFields(t *testing.T) { - si := &httpsig.SignatureInput{ - ID: "sig1", - Algorithm: httpsig.AlgorithmHS2019, - KeyID: "test-key-a", - Created: 1402170695, - Headers: []string{"*created", "*request-target", "host", "date", "content-type", "digest", "content-length"}, - } - - // Assert conanical syntax - expectedCanonical := `sig1=(*created, *request-target, host, date, content-type, digest, content-length); alg="hs2019"; kid="test-key-a"; created=1402170695` - if si.String() != expectedCanonical { - t.Fatalf("invalid canonical syntax expected `%s`, got `%s`", expectedCanonical, si.String()) - } - - // Create operations - signer := httpsig.NewSigner(func(ctx context.Context, kid string) (interface{}, error) { - return testRSAPrivateKey, nil - }) - verifier := httpsig.NewVerifier(func(ctx context.Context, kid string) (interface{}, error) { - return testRSAPublicKey, nil - }) - - // Create request - r := sampleRequest() - - sig, err := signer.Sign(context.Background(), si, r) - if err != nil { - t.Fatalf("unable to sign: %v", err) - } - - valid, errVerify := verifier.Verify(context.Background(), si, sig, r) - if errVerify != nil { - t.Fatalf("unable to verify: %v", errVerify) - } - - // Expected - if !valid { - t.Fatalf("expected valid, got %v", valid) - } -} - -// https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-01.html#name-minimal-required-signature- -func Test_SigVer_Minimal(t *testing.T) { - rawSigInput := `sig1=(); kid="test-key-a"; created=1402170695` - rawSignature := `sig1=:F0KlO2pMfxIbW11zInSXIciUA517Q+MLclZoWd0zEwAgLPriBudnbrjd6C6+OKsEX1hxlFchALhZ4eTso/7iHgRZV2geuIrtBOjPMRiTJc8OIEvCUc518JYQK4ZXUfLx58Gp1gggWPf9Eh/2xdRl0dIFTvdX8B9im+kEMaMT+fA1OB/T643P2d9MZRkAVQUnmZA2/atH+sbCjNeeOniWe7Bk3HYvrYUHNnFXjApbzSO97goK9O5zONqkJ8vjnZtynotXaL+fAsGxAiDwXXVZ8JLXrAAu/k7gdkgq0o5oxSNBPtKBAI5EogfZBN9k87lWBfcqNV2ZQd+UJ8TMAziYEQ==:` +// https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-05.html#section-b.2.1 +func Test_SigGen_Minimal(t *testing.T) { + rawSigInput := `sig1=();created=1618884475;keyid="test-key-rsa-pss";alg="rsa-pss-sha512"` // Parse signature-inputs sigInputs, err := httpsig.ParseSignatureInput(rawSigInput) @@ -232,61 +164,23 @@ func Test_SigVer_Minimal(t *testing.T) { t.Fatalf("unexpected error httpsig.ParseSignatureInput(), got %v", err) } - // Create operations - verifier := httpsig.NewVerifier(func(ctx context.Context, kid string) (interface{}, error) { - return testRSAPublicKey, nil + // Create signer + signer := httpsig.NewSigner(httpsig.AlgorithmRSAPSSSHA512, func(ctx context.Context, kid string) (interface{}, error) { + return testRSAPrivateKey, nil }) - - // Create request - r := sampleRequest() - - // Parse signature set - signatures, err := httpsig.ParseSignatureSet(rawSignature) - if err != nil { - t.Fatalf("unexpected error httpsig.ParseSignatureSet(), got %v", err) - } - sigFromSigSet, _ := signatures.Get(sigInputs[0].ID) - - // Check validity - valid, errVerify := verifier.Verify(context.Background(), sigInputs[0], sigFromSigSet, r) - if errVerify != nil { - t.Fatalf("unable to verify: %v", errVerify) - } - - // Expected - if !valid { - t.Fatalf("expected valid, got %v", valid) - } -} - -// https://www.ietf.org/id/draft-ietf-httpbis-message-signatures-01.html#section-a.3.2.2 -func Test_SigVer_Minimal_Recommended(t *testing.T) { - rawSigInput := `sig1=(); alg=hs2019; kid="test-key-a"; created=1402170695` - rawSignature := `sig1=:F0KlO2pMfxIbW11zInSXIciUA517Q+MLclZoWd0zEwAgLPriBudnbrjd6C6+OKsEX1hxlFchALhZ4eTso/7iHgRZV2geuIrtBOjPMRiTJc8OIEvCUc518JYQK4ZXUfLx58Gp1gggWPf9Eh/2xdRl0dIFTvdX8B9im+kEMaMT+fA1OB/T643P2d9MZRkAVQUnmZA2/atH+sbCjNeeOniWe7Bk3HYvrYUHNnFXjApbzSO97goK9O5zONqkJ8vjnZtynotXaL+fAsGxAiDwXXVZ8JLXrAAu/k7gdkgq0o5oxSNBPtKBAI5EogfZBN9k87lWBfcqNV2ZQd+UJ8TMAziYEQ==:` - - // Parse signature-inputs - sigInputs, err := httpsig.ParseSignatureInput(rawSigInput) - if err != nil { - t.Fatalf("unexpected error httpsig.ParseSignatureInput(), got %v", err) - } - - // Create operations verifier := httpsig.NewVerifier(func(ctx context.Context, kid string) (interface{}, error) { return testRSAPublicKey, nil }) // Create request - r := sampleRequest() + r := sampleTestRequest() - // Parse signature set - signatures, err := httpsig.ParseSignatureSet(rawSignature) + sig, err := signer.Sign(context.Background(), sigInputs[0], r) if err != nil { - t.Fatalf("unexpected error httpsig.ParseSignatureSet(), got %v", err) + t.Fatalf("unable to sign: %v", err) } - sigFromSigSet, _ := signatures.Get(sigInputs[0].ID) - // Check validity - valid, errVerify := verifier.Verify(context.Background(), sigInputs[0], sigFromSigSet, r) + valid, errVerify := verifier.Verify(context.Background(), sigInputs[0], sig, r) if errVerify != nil { t.Fatalf("unable to verify: %v", errVerify) } diff --git a/signature_input.go b/signature_input.go index 8cdeac5..083bbc7 100644 --- a/signature_input.go +++ b/signature_input.go @@ -27,10 +27,10 @@ import ( func DefaultSignatureInput(kid string) *SignatureInput { return &SignatureInput{ ID: "sig1", - Algorithm: AlgorithmHS2019, + Algorithm: AlgorithmRSAPSSSHA512, KeyID: kid, Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + Headers: []string{}, Expires: 0, // No expiration } } @@ -44,17 +44,25 @@ type SignatureInput struct { KeyID string Expires uint64 Created uint64 + Nonce string Headers []string } func (s *SignatureInput) String() string { + return fmt.Sprintf("%s=%s", s.ID, s.Params()) +} + +func (s *SignatureInput) Params() string { res := fmt.Sprintf( - `%s=(%s); alg="%s"; kid="%s"; created=%d`, - s.ID, strings.Join(s.Headers, ", "), s.Algorithm, s.KeyID, s.Created, + `(%s); alg="%s"; keyid="%s"; created=%d`, + strings.Join(s.Headers, ", "), s.Algorithm, s.KeyID, s.Created, ) if s.Expires > 0 { res = fmt.Sprintf("%s; expires=%d", res, s.Expires) } + if s.Nonce != "" { + res = fmt.Sprintf(`%s; nonce="%s"`, res, s.Nonce) + } return res } diff --git a/signer.go b/signer.go index 4682c7a..b78f15a 100644 --- a/signer.go +++ b/signer.go @@ -22,9 +22,11 @@ import ( "crypto" "crypto/ecdsa" "crypto/ed25519" + "crypto/elliptic" "crypto/hmac" "crypto/rand" "crypto/rsa" + "crypto/sha256" "crypto/sha512" "errors" "fmt" @@ -32,8 +34,9 @@ import ( ) // NewSigner returns a signer implementation instance for `hs2019` only. -func NewSigner(krf KeyResolverFunc) Signer { +func NewSigner(alg Algorithm, krf KeyResolverFunc) Signer { return &signer{ + alg: alg, keyResolverFunc: krf, } } @@ -41,6 +44,7 @@ func NewSigner(krf KeyResolverFunc) Signer { // ----------------------------------------------------------------------------- type signer struct { + alg Algorithm keyResolverFunc KeyResolverFunc } @@ -54,16 +58,17 @@ func (s *signer) Sign(ctx context.Context, sigMeta *SignatureInput, r *http.Requ if r == nil { return nil, errors.New("unable to sign nil request") } + if s.keyResolverFunc == nil { + return nil, errors.New("key resolver function is mandatory") + } // Check expiration if sigMeta.IsExpired() { return nil, ErrExpiredSignature } - // Only 'hs2019' algorithms are supported - if sigMeta.Algorithm != AlgorithmHS2019 { - return nil, ErrNotSupportedSignature - } + // Override sigMeta algorithm + sigMeta.Algorithm = s.alg // Retrieve key from repository key, err := s.keyResolverFunc(ctx, sigMeta.KeyID) @@ -80,12 +85,24 @@ func (s *signer) Sign(ctx context.Context, sigMeta *SignatureInput, r *http.Requ return nil, fmt.Errorf("unable to generate message: %w", err) } - // Use appropriate verification according to key type + // Use appropriate signature according to key type switch k := key.(type) { case *rsa.PrivateKey: - return s.signRSA(k, msg) + switch s.alg { + case AlgorithmRSAPSSSHA512: + return s.signRSAPSS(k, msg) + case AlgorithmRSAV15SHA256: + return s.signRSAPKCS1(k, msg) + default: + return s.signRSAPSS(k, msg) + } case *ecdsa.PrivateKey: - return s.signECDSA(k, msg) + switch s.alg { + case AlgorithmECDSAP256SHA256: + return s.signECDSA(k, msg) + default: + return s.signECDSA(k, msg) + } case ed25519.PrivateKey: return s.signEdDSA(k, msg) case []byte: @@ -99,8 +116,8 @@ func (s *signer) Sign(ctx context.Context, sigMeta *SignatureInput, r *http.Requ // ----------------------------------------------------------------------------- -// signRSA uses RSASSA-PSS with SHA-512 -func (s *signer) signRSA(priv *rsa.PrivateKey, protected []byte) ([]byte, error) { +// signRSAPSS uses RSASSA-PSS with SHA-512 +func (s *signer) signRSAPSS(priv *rsa.PrivateKey, protected []byte) ([]byte, error) { // Compute SHA-512 h := sha512.Sum512(protected) @@ -114,10 +131,31 @@ func (s *signer) signRSA(priv *rsa.PrivateKey, protected []byte) ([]byte, error) return sig, nil } -// signECDSA uses private key curve with SHA-512 +// signRSAPKCS1 uses RSASSA-PKCS1-v1_5 with SHA-256 +func (s *signer) signRSAPKCS1(priv *rsa.PrivateKey, protected []byte) ([]byte, error) { + // Compute SHA-256 + h := sha256.Sum256(protected) + + // Sign the request + sig, err := rsa.SignPSS(rand.Reader, priv, crypto.SHA256, h[:], nil) + if err != nil { + return nil, fmt.Errorf("unable to sign request: %w", err) + } + + // Default to false + return sig, nil +} + +// signECDSA uses private key curve with SHA-256 func (s *signer) signECDSA(priv *ecdsa.PrivateKey, protected []byte) ([]byte, error) { - // Compute SHA-512 - h := sha512.Sum512(protected) + var h [32]byte + + switch priv.Curve { + case elliptic.P256(): + h = sha256.Sum256(protected) + default: + return nil, fmt.Errorf("unsupported curve: %s", priv.Curve) + } // Sign the request sig, err := ecdsa.SignASN1(rand.Reader, priv, h[:]) @@ -136,10 +174,10 @@ func (s *signer) signEdDSA(priv ed25519.PrivateKey, protected []byte) ([]byte, e return sig, nil } -// sealHMAC uses HMAC with SHA-512 +// sealHMAC uses HMAC with SHA-256 func (s *signer) sealHMAC(secret, protected []byte) ([]byte, error) { - // Compute HMAC-SHA-512 - hm := hmac.New(sha512.New, secret) + // Compute HMAC-SHA-256 + hm := hmac.New(sha256.New, secret) if _, err := hm.Write(protected); err != nil { return nil, fmt.Errorf("unable to write payload for hmac: %w", err) } diff --git a/signer_test.go b/signer_test.go index 26e9ec1..ac5134e 100644 --- a/signer_test.go +++ b/signer_test.go @@ -35,6 +35,7 @@ import ( func Test_signer_Sign(t *testing.T) { type fields struct { + alg Algorithm keyResolverFunc KeyResolverFunc } type args struct { @@ -57,12 +58,11 @@ func Test_signer_Sign(t *testing.T) { name: "nil request", args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, }, wantErr: true, @@ -71,12 +71,11 @@ func Test_signer_Sign(t *testing.T) { name: "sigInput expired", args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 1, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 1, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -86,12 +85,11 @@ func Test_signer_Sign(t *testing.T) { name: "invalid algorithm", args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: "", - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -103,15 +101,15 @@ func Test_signer_Sign(t *testing.T) { keyResolverFunc: func(ctx context.Context, kid string) (interface{}, error) { return nil, ErrKeyNotFound }, + alg: AlgorithmRSAPSSSHA512, }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -123,15 +121,15 @@ func Test_signer_Sign(t *testing.T) { keyResolverFunc: func(ctx context.Context, kid string) (interface{}, error) { return nil, fmt.Errorf("test") }, + alg: AlgorithmRSAPSSSHA512, }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -144,15 +142,15 @@ func Test_signer_Sign(t *testing.T) { pub, _, _ := ed25519.GenerateKey(rand.Reader) return pub, nil }, + alg: AlgorithmRSAPSSSHA512, }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target", "x-not-exist"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target", "x-not-exist"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -165,15 +163,15 @@ func Test_signer_Sign(t *testing.T) { _, priv, _ := ed25519.GenerateKey(rand.Reader) return priv, nil }, + alg: AlgorithmEdDSAEd25519BLAKE2B512, }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: uint64(time.Now().Unix()) + 1000, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target", "*expires", "x-custom-header"}, + ID: "sig1", + KeyID: "test", + Expires: uint64(time.Now().Unix()) + 1000, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target", "@expires", "x-custom-header"}, }, r: func() *http.Request { req := httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook?key=1", bytes.NewBufferString("{}")) @@ -190,15 +188,36 @@ func Test_signer_Sign(t *testing.T) { priv, _ := rsa.GenerateKey(rand.Reader, 2048) return priv, nil }, + alg: AlgorithmRSAPSSSHA512, }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, + }, + r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), + }, + wantErr: false, + }, + { + name: "rsassa-pkcs", + fields: fields{ + keyResolverFunc: func(ctx context.Context, kid string) (interface{}, error) { + priv, _ := rsa.GenerateKey(rand.Reader, 2048) + return priv, nil + }, + alg: AlgorithmRSAV15SHA256, + }, + args: args{ + sigMeta: &SignatureInput{ + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -211,15 +230,15 @@ func Test_signer_Sign(t *testing.T) { priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) return priv, nil }, + alg: AlgorithmECDSAP256SHA256, }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -233,15 +252,15 @@ func Test_signer_Sign(t *testing.T) { io.ReadFull(rand.Reader, secret[:]) return secret[:], nil }, + alg: AlgorithmHMACSHA256, }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -253,15 +272,15 @@ func Test_signer_Sign(t *testing.T) { keyResolverFunc: func(ctx context.Context, kid string) (interface{}, error) { return uint64(0), nil }, + alg: AlgorithmECDSAP256SHA256, }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -271,6 +290,7 @@ func Test_signer_Sign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := &signer{ + alg: tt.fields.alg, keyResolverFunc: tt.fields.keyResolverFunc, } _, err := s.Sign(tt.args.ctx, tt.args.sigMeta, tt.args.r) diff --git a/verifier.go b/verifier.go index 9f4adbf..cf532cb 100644 --- a/verifier.go +++ b/verifier.go @@ -60,8 +60,13 @@ func (v *verifier) Verify(ctx context.Context, sigMeta *SignatureInput, signatur return false, ErrExpiredSignature } - // Only 'hs2019' algorithms are supported - if sigMeta.Algorithm != AlgorithmHS2019 { + switch sigMeta.Algorithm { + case AlgorithmRSAPSSSHA512: + case AlgorithmRSAV15SHA256: + case AlgorithmHMACSHA256: + case AlgorithmECDSAP256SHA256: + case AlgorithmEdDSAEd25519BLAKE2B512: + default: return false, ErrNotSupportedSignature } diff --git a/verifier_test.go b/verifier_test.go index b8e7aea..17362a1 100644 --- a/verifier_test.go +++ b/verifier_test.go @@ -58,12 +58,11 @@ func Test_verifier_Verify(t *testing.T) { name: "nil request", args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, }, wantErr: true, @@ -72,12 +71,11 @@ func Test_verifier_Verify(t *testing.T) { name: "sigInput expired", args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 1, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 1, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -92,7 +90,7 @@ func Test_verifier_Verify(t *testing.T) { KeyID: "test", Expires: 0, Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -107,12 +105,11 @@ func Test_verifier_Verify(t *testing.T) { }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -127,12 +124,11 @@ func Test_verifier_Verify(t *testing.T) { }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -148,12 +144,11 @@ func Test_verifier_Verify(t *testing.T) { }, args: args{ sigMeta: &SignatureInput{ - ID: "sig1", - Algorithm: AlgorithmHS2019, - KeyID: "test", - Expires: 0, - Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target", "x-not-exist"}, + ID: "sig1", + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target", "x-not-exist"}, }, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), }, @@ -170,11 +165,11 @@ func Test_verifier_Verify(t *testing.T) { args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Algorithm: AlgorithmHS2019, + Algorithm: AlgorithmEdDSAEd25519BLAKE2B512, KeyID: "test", Expires: uint64(time.Now().Unix()) + 1000, Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target", "*expires", "x-custom-header"}, + Headers: []string{"@created", "@request-target", "@expires", "x-custom-header"}, }, signature: []byte{}, r: func() *http.Request { @@ -197,11 +192,34 @@ func Test_verifier_Verify(t *testing.T) { args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Algorithm: AlgorithmHS2019, + Algorithm: AlgorithmRSAPSSSHA512, + KeyID: "test", + Expires: 0, + Created: uint64(time.Now().Unix()), + Headers: []string{"@created", "@request-target"}, + }, + signature: []byte{}, + r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), + }, + wantErr: false, + want: false, + }, + { + name: "rsassa-pkcs", + fields: fields{ + keyResolverFunc: func(ctx context.Context, kid string) (interface{}, error) { + priv, _ := rsa.GenerateKey(rand.Reader, 2048) + return &priv.PublicKey, nil + }, + }, + args: args{ + sigMeta: &SignatureInput{ + ID: "sig1", + Algorithm: AlgorithmRSAV15SHA256, KeyID: "test", Expires: 0, Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + Headers: []string{"@created", "@request-target"}, }, signature: []byte{}, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), @@ -220,11 +238,11 @@ func Test_verifier_Verify(t *testing.T) { args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Algorithm: AlgorithmHS2019, + Algorithm: AlgorithmECDSAP256SHA256, KeyID: "test", Expires: 0, Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + Headers: []string{"@created", "@request-target"}, }, signature: []byte{}, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), @@ -244,11 +262,11 @@ func Test_verifier_Verify(t *testing.T) { args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Algorithm: AlgorithmHS2019, + Algorithm: AlgorithmHMACSHA256, KeyID: "test", Expires: 0, Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + Headers: []string{"@created", "@request-target"}, }, signature: []byte{}, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), @@ -266,11 +284,11 @@ func Test_verifier_Verify(t *testing.T) { args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Algorithm: AlgorithmHS2019, + Algorithm: AlgorithmHMACSHA256, KeyID: "test", Expires: 0, Created: uint64(time.Now().Unix()), - Headers: []string{"*created", "*request-target"}, + Headers: []string{"@created", "@request-target"}, }, signature: []byte{}, r: httptest.NewRequest(http.MethodPost, "http://localhost:8080/api/v1/webhook", bytes.NewBufferString("{}")), From fe7c23658f545f1f06fe543fdc5aedfff91757e9 Mon Sep 17 00:00:00 2001 From: Thibault NORMAND Date: Fri, 25 Jun 2021 16:30:41 +0200 Subject: [PATCH 2/2] feat(doc): add RoundTripper example. --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++- api.go | 4 ++-- httpsig.go | 2 +- signer_test.go | 2 +- verifier.go | 2 +- verifier_test.go | 2 +- 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 214bd7a..53bae6c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ the draft specification [draft-ietf-httpbis-message-signatures](https://www.ietf * `rsa-v1_5-sha256` (equiv. JWA RS256) * `hmac-sha256` (equiv. JWA HS256) * `ecdsa-p256-sha256` '(equiv. JWA ES256) - * `eddsa-ed25519-blake2b512` (not in the standard) (equiv JWA EdDSA) + * `eddsa-ed25519-sha512` (not in the standard) (equiv JWA EdDSA) ## Protocol @@ -120,3 +120,50 @@ for _, si := range inputs { } } ``` + +Using a custom `RoundTripper` + +```go +type SignerTransport struct { + http.RoundTripper + Signer httpsig.Signer + KeyID string +} + +func (ct *SignerTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Prepare a signature-input + si := httpsig.SignatureInput{ + ID: "sig1", + KeyID: ct.KeyID, + Headers: []string{"@request-target", "host", "Authorization", "Digest"}, + Created: uint64(time.Now().Unix()), + Nonce: uniuri.NewLen(32), + } + + // Generate the signature + sig, err := ct.Signer.Sign(req.Context(), si, r) + if err != nil { + return nil, fmt.Errorf("unable to sign the request: %w", err) + } + + // Prepare signature set + signSet := &httpsig.SignatureSet{} + signSet.Add(si.ID(), sig) + + // Assign to request headers. + req.Header.Set("Signature-Input", si.String()) + req.Header.Set("Signature", signSet.String()) + + // Delegate to parent RoundTripper + return ct.RoundTripper.RoundTrip(req) +} + +// Create a HTTP client with custom transport. +url := "http://localhost:8200/api/v1/resource" +tr := &SignerTransport{ + Signer: signer, + KeyID: "client-public-keyid", +} +client := &http.Client{Transport: tr} +resp, err := client.Get(url) +``` diff --git a/api.go b/api.go index b189b00..e485eb0 100644 --- a/api.go +++ b/api.go @@ -35,8 +35,8 @@ const ( AlgorithmHMACSHA256 Algorithm = "hmac-sha256" // AlgorithmECDSAP256SHA256 represents signature algorithm using ECDA P-256 curve with SHA-256 AlgorithmECDSAP256SHA256 Algorithm = "ecdsa-p256-sha256" - // AlgorithmEdDSAEd25519BLAKE512 represents signature algorithm using EdDSA Ed25519 curve with BLAKE2B-512 - AlgorithmEdDSAEd25519BLAKE2B512 Algorithm = "eddsa-ed25519-blake2b512" + // AlgorithmEdDSAEd25519SHA512 represents signature algorithm using EdDSA Ed25519 curve with SHA-512 + AlgorithmEdDSAEd25519SHA512 Algorithm = "eddsa-ed25519-sha512" ) // Verifier describes signature verification implementation contract. diff --git a/httpsig.go b/httpsig.go index 3e529ac..d46b676 100644 --- a/httpsig.go +++ b/httpsig.go @@ -64,7 +64,7 @@ func ParseSignatureInput(input string) ([]*SignatureInput, error) { case AlgorithmRSAV15SHA256: case AlgorithmHMACSHA256: case AlgorithmECDSAP256SHA256: - case AlgorithmEdDSAEd25519BLAKE2B512: + case AlgorithmEdDSAEd25519SHA512: default: // Skip invalid signature algorithm continue diff --git a/signer_test.go b/signer_test.go index ac5134e..1df3703 100644 --- a/signer_test.go +++ b/signer_test.go @@ -163,7 +163,7 @@ func Test_signer_Sign(t *testing.T) { _, priv, _ := ed25519.GenerateKey(rand.Reader) return priv, nil }, - alg: AlgorithmEdDSAEd25519BLAKE2B512, + alg: AlgorithmEdDSAEd25519SHA512, }, args: args{ sigMeta: &SignatureInput{ diff --git a/verifier.go b/verifier.go index cf532cb..7874f6b 100644 --- a/verifier.go +++ b/verifier.go @@ -65,7 +65,7 @@ func (v *verifier) Verify(ctx context.Context, sigMeta *SignatureInput, signatur case AlgorithmRSAV15SHA256: case AlgorithmHMACSHA256: case AlgorithmECDSAP256SHA256: - case AlgorithmEdDSAEd25519BLAKE2B512: + case AlgorithmEdDSAEd25519SHA512: default: return false, ErrNotSupportedSignature } diff --git a/verifier_test.go b/verifier_test.go index 17362a1..ee01bb2 100644 --- a/verifier_test.go +++ b/verifier_test.go @@ -165,7 +165,7 @@ func Test_verifier_Verify(t *testing.T) { args: args{ sigMeta: &SignatureInput{ ID: "sig1", - Algorithm: AlgorithmEdDSAEd25519BLAKE2B512, + Algorithm: AlgorithmEdDSAEd25519SHA512, KeyID: "test", Expires: uint64(time.Now().Unix()) + 1000, Created: uint64(time.Now().Unix()),