diff --git a/README.md b/README.md index 3d4cb06..7bd87ed 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # httpsig +Forked from + > HTTP Signatures made simple [![Build Status][Build-Status-Image]][Build-Status-Url] [![Go Reference][Go-Reference-Image]][Go-Reference-Url] [![Go Report Card][Go-Report-Card-Image]][Go-Report-Card-Url] [![License][License-Image]][License-Url] [![Chat][Chat-Image]][Chat-Url] [![OpenCollective][OpenCollective-Image]][OpenCollective-Url] -`go get github.com/go-fed/httpsig` +`go get github.com/42wim/httpsig` Implementation of [HTTP Signatures](https://tools.ietf.org/html/draft-cavage-http-signatures). @@ -23,7 +25,7 @@ signing of hash schemes. Its goals are: ## How to use -`import "github.com/go-fed/httpsig"` +`import "github.com/42wim/httpsig"` ### Signing @@ -99,15 +101,15 @@ func verify(r *http.Request) error { constructed on a per-request or per-response basis it should not be a common restriction. -[Build-Status-Image]: https://travis-ci.org/go-fed/httpsig.svg?branch=master -[Build-Status-Url]: https://travis-ci.org/go-fed/httpsig -[Go-Reference-Image]: https://pkg.go.dev/badge/github.com/go-fed/httpsig -[Go-Reference-Url]: https://pkg.go.dev/github.com/go-fed/httpsig -[Go-Report-Card-Image]: https://goreportcard.com/badge/github.com/go-fed/httpsig -[Go-Report-Card-Url]: https://goreportcard.com/report/github.com/go-fed/httpsig -[License-Image]: https://img.shields.io/github/license/go-fed/httpsig?color=blue +[Build-Status-Image]: https://travis-ci.org/42wim/httpsig.svg?branch=master +[Build-Status-Url]: https://travis-ci.org/42wim/httpsig +[Go-Reference-Image]: https://pkg.go.dev/badge/github.com/42wim/httpsig +[Go-Reference-Url]: https://pkg.go.dev/github.com/42wim/httpsig +[Go-Report-Card-Image]: https://goreportcard.com/badge/github.com/42wim/httpsig +[Go-Report-Card-Url]: https://goreportcard.com/report/github.com/42wim/httpsig +[License-Image]: https://img.shields.io/github/license/42wim/httpsig?color=blue [License-Url]: https://opensource.org/licenses/BSD-3-Clause -[Chat-Image]: https://img.shields.io/matrix/go-fed:feneas.org?server_fqdn=matrix.org +[Chat-Image]: https://img.shields.io/matrix/42wim:feneas.org?server_fqdn=matrix.org [Chat-Url]: https://matrix.to/#/!BLOSvIyKTDLIVjRKSc:feneas.org?via=feneas.org&via=matrix.org -[OpenCollective-Image]: https://img.shields.io/opencollective/backers/go-fed-activitypub-labs -[OpenCollective-Url]: https://opencollective.com/go-fed-activitypub-labs +[OpenCollective-Image]: https://img.shields.io/opencollective/backers/42wim-activitypub-labs +[OpenCollective-Url]: https://opencollective.com/42wim-activitypub-labs diff --git a/algorithms.go b/algorithms.go index 9595941..2743eed 100644 --- a/algorithms.go +++ b/algorithms.go @@ -5,7 +5,6 @@ import ( "crypto/ecdsa" "crypto/hmac" "crypto/rsa" - "crypto/sha1" "crypto/sha256" "crypto/sha512" "crypto/subtle" // Use should trigger great care @@ -20,7 +19,6 @@ import ( "golang.org/x/crypto/blake2b" "golang.org/x/crypto/blake2s" "golang.org/x/crypto/ed25519" - "golang.org/x/crypto/ripemd160" "golang.org/x/crypto/sha3" "golang.org/x/crypto/ssh" ) @@ -31,15 +29,10 @@ const ( sshPrefix = "ssh" ecdsaPrefix = "ecdsa" ed25519Prefix = "ed25519" - md4String = "md4" - md5String = "md5" - sha1String = "sha1" sha224String = "sha224" sha256String = "sha256" sha384String = "sha384" sha512String = "sha512" - md5sha1String = "md5sha1" - ripemd160String = "ripemd160" sha3_224String = "sha3-224" sha3_256String = "sha3-256" sha3_384String = "sha3-384" @@ -68,16 +61,10 @@ var hashToDef = map[crypto.Hash]struct { // http://www.iana.org/assignments/signature-algorithms // // Note that the forbidden hashes have an invalid 'new' function. - crypto.MD4: {md4String, func(key []byte) (hash.Hash, error) { return nil, nil }}, - crypto.MD5: {md5String, func(key []byte) (hash.Hash, error) { return nil, nil }}, - // Temporarily enable SHA1 because of issue https://github.com/golang/go/issues/37278 - crypto.SHA1: {sha1String, func(key []byte) (hash.Hash, error) { return sha1.New(), nil }}, crypto.SHA224: {sha224String, func(key []byte) (hash.Hash, error) { return sha256.New224(), nil }}, crypto.SHA256: {sha256String, func(key []byte) (hash.Hash, error) { return sha256.New(), nil }}, crypto.SHA384: {sha384String, func(key []byte) (hash.Hash, error) { return sha512.New384(), nil }}, crypto.SHA512: {sha512String, func(key []byte) (hash.Hash, error) { return sha512.New(), nil }}, - crypto.MD5SHA1: {md5sha1String, func(key []byte) (hash.Hash, error) { return nil, nil }}, - crypto.RIPEMD160: {ripemd160String, func(key []byte) (hash.Hash, error) { return ripemd160.New(), nil }}, crypto.SHA3_224: {sha3_224String, func(key []byte) (hash.Hash, error) { return sha3.New224(), nil }}, crypto.SHA3_256: {sha3_256String, func(key []byte) (hash.Hash, error) { return sha3.New256(), nil }}, crypto.SHA3_384: {sha3_384String, func(key []byte) (hash.Hash, error) { return sha3.New384(), nil }}, @@ -112,16 +99,6 @@ func init() { } func isForbiddenHash(h crypto.Hash) bool { - switch h { - // Not actually cryptographically secure - case crypto.MD4: - fallthrough - case crypto.MD5: - fallthrough - case crypto.MD5SHA1: // shorthand for crypto/tls, not actually implemented - return true - } - // Still cryptographically secure return false } @@ -148,6 +125,9 @@ type hmacAlgorithm struct { func (h *hmacAlgorithm) Sign(sig, key []byte) ([]byte, error) { hs, err := h.fn(key) + if err != nil { + return nil, err + } if err = setSig(hs, sig); err != nil { return nil, err } @@ -192,11 +172,49 @@ func (r *rsaAlgorithm) setSig(b []byte) error { return nil } +// Code from https://github.com/cloudtools/ssh-cert-authority/pull/49/files +// This interface provides a way to reach the exported, but not accessible SignWithOpts() method +// in x/crypto/ssh/agent. Access to this is needed to sign with more secure signing algorithms +type agentKeyringSigner interface { + SignWithOpts(rand io.Reader, data []byte, opts crypto.SignerOpts) (*ssh.Signature, error) +} + +// A struct to wrap an SSH Signer with one that will switch to SHA256 Signatures. +// Replaces the call to Sign() with a call to SignWithOpts using HashFunc() algorithm. +type Sha256Signer struct { + ssh.Signer +} + +func (s Sha256Signer) HashFunc() crypto.Hash { + return crypto.SHA256 +} + +func (s Sha256Signer) Sign(rand io.Reader, data []byte) (*ssh.Signature, error) { + if aks, ok := s.Signer.(agentKeyringSigner); !ok { + return nil, fmt.Errorf("ssh: can't wrap a non ssh agentKeyringSigner") + } else { + return aks.SignWithOpts(rand, data, s) + } +} + func (r *rsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) { if r.sshSigner != nil { - sshsig, err := r.sshSigner.Sign(rand, sig) - if err != nil { - return nil, err + var ( + sshsig *ssh.Signature + err error + ) + // are we using an SSH Agent + if _, ok := r.sshSigner.(agentKeyringSigner); ok { + signer := Sha256Signer{r.sshSigner} + sshsig, err = signer.Sign(rand, sig) + if err != nil { + return nil, err + } + } else { + sshsig, err = r.sshSigner.(ssh.AlgorithmSigner).SignWithAlgorithm(rand, sig, ssh.SigAlgoRSASHA2256) + if err != nil { + return nil, err + } } return sshsig.Blob, nil @@ -272,7 +290,8 @@ var _ signer = &ecdsaAlgorithm{} type ecdsaAlgorithm struct { hash.Hash - kind crypto.Hash + kind crypto.Hash + sshSigner ssh.Signer } func (r *ecdsaAlgorithm) setSig(b []byte) error { @@ -292,6 +311,22 @@ type ECDSASignature struct { } func (r *ecdsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) { + if r.sshSigner != nil { + sshsig, err := r.sshSigner.Sign(rand, sig) + if err != nil { + return nil, err + } + // SSH wraps the (r, s) pair as two mpints, convert it to the ASN.1 + // encoding expected by Verify. + var blob struct { + R *big.Int + S *big.Int + } + if err := ssh.Unmarshal(sshsig.Blob, &blob); err != nil { + return nil, err + } + return asn1.Marshal(ECDSASignature{R: blob.R, S: blob.S}) + } defer r.Reset() if err := r.setSig(sig); err != nil { return nil, err @@ -442,6 +477,16 @@ func newAlgorithm(algo string, key []byte) (hash.Hash, crypto.Hash, error) { func signerFromSSHSigner(sshSigner ssh.Signer, s string) (signer, error) { switch { + case strings.HasPrefix(s, ecdsaPrefix): + hash, cHash, err := newAlgorithm(strings.TrimPrefix(s, ecdsaPrefix+"-"), nil) + if err != nil { + return nil, err + } + return &ecdsaAlgorithm{ + Hash: hash, + kind: cHash, + sshSigner: sshSigner, + }, nil case strings.HasPrefix(s, rsaPrefix): return &rsaAlgorithm{ sshSigner: sshSigner, diff --git a/algorithms_test.go b/algorithms_test.go index 5c3a3a3..1c54e11 100644 --- a/algorithms_test.go +++ b/algorithms_test.go @@ -2,6 +2,8 @@ package httpsig import ( "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/hmac" "crypto/rand" "crypto/rsa" @@ -31,25 +33,6 @@ func TestIsAvailable(t *testing.T) { expected bool expectError bool }{ - { - name: md4String, - algo: md4String, - expected: false, - expectError: true, - }, - { - name: md5String, - algo: md5String, - expected: false, - expectError: true, - }, - { - // TODO: Disable once https://github.com/golang/go/issues/37278 is fixed - name: sha1String, - algo: sha1String, - expected: true, - expectError: false, - }, { name: sha224String, algo: sha224String, @@ -74,18 +57,6 @@ func TestIsAvailable(t *testing.T) { expected: true, expectError: false, }, - { - name: md5sha1String, - algo: md5sha1String, - expected: false, - expectError: true, - }, - { - name: ripemd160String, - algo: ripemd160String, - expected: true, - expectError: false, - }, { name: sha3_224String, algo: sha3_224String, @@ -189,11 +160,6 @@ func TestSignerFromString(t *testing.T) { input: HMAC_SHA512, expectError: true, }, - { - name: "HMAC_RIPEMD160", - input: HMAC_RIPEMD160, - expectError: true, - }, { name: "HMAC_SHA3_224", input: HMAC_SHA3_224, @@ -284,11 +250,6 @@ func TestSignerFromString(t *testing.T) { input: RSA_SHA512, expectKind: crypto.SHA512, }, - { - name: "RSA_RIPEMD160", - input: RSA_RIPEMD160, - expectKind: crypto.RIPEMD160, - }, { name: "rsa_SHA3_224", input: rsa_SHA3_224, @@ -339,6 +300,26 @@ func TestSignerFromString(t *testing.T) { input: rsa_BLAKE2B_512, expectKind: crypto.BLAKE2b_512, }, + { + name: "ECDSA_SHA224", + input: ECDSA_SHA224, + expectKind: crypto.SHA224, + }, + { + name: "ECDSA_SHA256", + input: ECDSA_SHA256, + expectKind: crypto.SHA256, + }, + { + name: "ECDSA_SHA384", + input: ECDSA_SHA384, + expectKind: crypto.SHA384, + }, + { + name: "ECDSA_SHA512", + input: ECDSA_SHA512, + expectKind: crypto.SHA512, + }, } for _, test := range tests { s, err := signerFromString(string(test.input)) @@ -388,11 +369,6 @@ func TestMACerFromString(t *testing.T) { input: HMAC_SHA512, expectKind: crypto.SHA512, }, - { - name: "HMAC_RIPEMD160", - input: HMAC_RIPEMD160, - expectKind: crypto.RIPEMD160, - }, { name: "HMAC_SHA3_224", input: HMAC_SHA3_224, @@ -483,11 +459,6 @@ func TestMACerFromString(t *testing.T) { input: RSA_SHA512, expectError: true, }, - { - name: "RSA_RIPEMD160", - input: RSA_RIPEMD160, - expectError: true, - }, { name: "rsa_SHA3_224", input: rsa_SHA3_224, @@ -562,9 +533,10 @@ func TestMACerFromString(t *testing.T) { func TestSignerSigns(t *testing.T) { tests := []struct { - name string - input Algorithm - inputCryptoHash crypto.Hash + name string + input Algorithm + inputCryptoHash crypto.Hash + // Only the BLAKE2 hashes lack a PKCS#1 v1.5 DigestInfo prefix. expectRSAUnsupported bool }{ { @@ -588,45 +560,34 @@ func TestSignerSigns(t *testing.T) { inputCryptoHash: crypto.SHA512, }, { - name: "RSA_RIPEMD160", - input: RSA_RIPEMD160, - inputCryptoHash: crypto.RIPEMD160, - }, - { - name: "rsa_SHA3_224", - input: rsa_SHA3_224, - inputCryptoHash: crypto.SHA3_224, - expectRSAUnsupported: true, + name: "rsa_SHA3_224", + input: rsa_SHA3_224, + inputCryptoHash: crypto.SHA3_224, }, { - name: "rsa_SHA3_256", - input: rsa_SHA3_256, - inputCryptoHash: crypto.SHA3_256, - expectRSAUnsupported: true, + name: "rsa_SHA3_256", + input: rsa_SHA3_256, + inputCryptoHash: crypto.SHA3_256, }, { - name: "rsa_SHA3_384", - input: rsa_SHA3_384, - inputCryptoHash: crypto.SHA3_384, - expectRSAUnsupported: true, + name: "rsa_SHA3_384", + input: rsa_SHA3_384, + inputCryptoHash: crypto.SHA3_384, }, { - name: "rsa_SHA3_512", - input: rsa_SHA3_512, - inputCryptoHash: crypto.SHA3_512, - expectRSAUnsupported: true, + name: "rsa_SHA3_512", + input: rsa_SHA3_512, + inputCryptoHash: crypto.SHA3_512, }, { - name: "rsa_SHA512_224", - input: rsa_SHA512_224, - inputCryptoHash: crypto.SHA512_224, - expectRSAUnsupported: true, + name: "rsa_SHA512_224", + input: rsa_SHA512_224, + inputCryptoHash: crypto.SHA512_224, }, { - name: "rsa_SHA512_256", - input: rsa_SHA512_256, - inputCryptoHash: crypto.SHA512_256, - expectRSAUnsupported: true, + name: "rsa_SHA512_256", + input: rsa_SHA512_256, + inputCryptoHash: crypto.SHA512_256, }, { name: "rsa_BLAKE2S_256", @@ -738,11 +699,6 @@ func TestSignerVerifies(t *testing.T) { input: RSA_SHA512, inputCryptoHash: crypto.SHA512, }, - { - name: "RSA_RIPEMD160", - input: RSA_RIPEMD160, - inputCryptoHash: crypto.RIPEMD160, - }, } for _, test := range tests { privKey, err := rsa.GenerateKey(rand.Reader, 2048) @@ -781,6 +737,61 @@ func TestSignerVerifies(t *testing.T) { } } +// ECDSA signatures are not deterministic, so sign and verify are tested as a +// round trip instead of against a fixed signature. +func TestECDSASignerSignsAndVerifies(t *testing.T) { + tests := []struct { + name string + input Algorithm + curve elliptic.Curve + }{ + { + name: "ECDSA_SHA224", + input: ECDSA_SHA224, + curve: elliptic.P256(), + }, + { + name: "ECDSA_SHA256", + input: ECDSA_SHA256, + curve: elliptic.P256(), + }, + { + name: "ECDSA_SHA384", + input: ECDSA_SHA384, + curve: elliptic.P384(), + }, + { + name: "ECDSA_SHA512", + input: ECDSA_SHA512, + curve: elliptic.P521(), + }, + } + for _, test := range tests { + privKey, err := ecdsa.GenerateKey(test.curve, rand.Reader) + if err != nil { + t.Fatalf("%q: Failed setup: %s", test.name, err) + } + toHash := make([]byte, 65535) + n, err := doNotUseInProdCode.Read(toHash) + if n != len(toHash) { + t.Fatalf("%q: Failed setup: %d bytes != %d bytes", test.name, n, len(toHash)) + } else if err != nil { + t.Fatalf("%q: Failed setup: %s", test.name, err) + } + s, err := signerFromString(string(test.input)) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + signature, err := s.Sign(rand.Reader, privKey, toHash) + if err != nil { + t.Fatalf("%q: %s", test.name, err) + } + if err := s.Verify(privKey.Public(), toHash, signature); err != nil { + t.Fatalf("%q: %s", test.name, err) + } + } +} + func TestMACerSigns(t *testing.T) { tests := []struct { name string @@ -817,13 +828,6 @@ func TestMACerSigns(t *testing.T) { isHMAC: true, keySize: 128, }, - { - name: "HMAC_RIPEMD160", - input: HMAC_RIPEMD160, - inputCryptoHash: crypto.RIPEMD160, - isHMAC: true, - keySize: 128, - }, { name: "HMAC_SHA3_224", input: HMAC_SHA3_224, @@ -1016,13 +1020,6 @@ func TestMACerEquals(t *testing.T) { isHMAC: true, keySize: 128, }, - { - name: "HMAC_RIPEMD160", - input: HMAC_RIPEMD160, - inputCryptoHash: crypto.RIPEMD160, - isHMAC: true, - keySize: 128, - }, { name: "HMAC_SHA3_224", input: HMAC_SHA3_224, diff --git a/go.mod b/go.mod index a407692..ae100bb 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ -module github.com/go-fed/httpsig +module github.com/42wim/httpsig -require golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 +require golang.org/x/crypto v0.57.0 -go 1.13 +require golang.org/x/sys v0.48.0 // indirect + +go 1.26.0 diff --git a/go.sum b/go.sum index 8b7d318..c1de29d 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,11 @@ -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= diff --git a/httpsig.go b/httpsig.go index 17310c8..fcc4e0a 100644 --- a/httpsig.go +++ b/httpsig.go @@ -27,7 +27,6 @@ const ( HMAC_SHA256 Algorithm = hmacPrefix + "-" + sha256String HMAC_SHA384 Algorithm = hmacPrefix + "-" + sha384String HMAC_SHA512 Algorithm = hmacPrefix + "-" + sha512String - HMAC_RIPEMD160 Algorithm = hmacPrefix + "-" + ripemd160String HMAC_SHA3_224 Algorithm = hmacPrefix + "-" + sha3_224String HMAC_SHA3_256 Algorithm = hmacPrefix + "-" + sha3_256String HMAC_SHA3_384 Algorithm = hmacPrefix + "-" + sha3_384String @@ -43,19 +42,16 @@ const ( BLAKE2B_384 Algorithm = blake2b_384String BLAKE2B_512 Algorithm = blake2b_512String // RSA-based algorithms. - RSA_SHA1 Algorithm = rsaPrefix + "-" + sha1String RSA_SHA224 Algorithm = rsaPrefix + "-" + sha224String // RSA_SHA256 is the default algorithm. - RSA_SHA256 Algorithm = rsaPrefix + "-" + sha256String - RSA_SHA384 Algorithm = rsaPrefix + "-" + sha384String - RSA_SHA512 Algorithm = rsaPrefix + "-" + sha512String - RSA_RIPEMD160 Algorithm = rsaPrefix + "-" + ripemd160String + RSA_SHA256 Algorithm = rsaPrefix + "-" + sha256String + RSA_SHA384 Algorithm = rsaPrefix + "-" + sha384String + RSA_SHA512 Algorithm = rsaPrefix + "-" + sha512String // ECDSA algorithms - ECDSA_SHA224 Algorithm = ecdsaPrefix + "-" + sha224String - ECDSA_SHA256 Algorithm = ecdsaPrefix + "-" + sha256String - ECDSA_SHA384 Algorithm = ecdsaPrefix + "-" + sha384String - ECDSA_SHA512 Algorithm = ecdsaPrefix + "-" + sha512String - ECDSA_RIPEMD160 Algorithm = ecdsaPrefix + "-" + ripemd160String + ECDSA_SHA224 Algorithm = ecdsaPrefix + "-" + sha224String + ECDSA_SHA256 Algorithm = ecdsaPrefix + "-" + sha256String + ECDSA_SHA384 Algorithm = ecdsaPrefix + "-" + sha384String + ECDSA_SHA512 Algorithm = ecdsaPrefix + "-" + sha512String // ED25519 algorithms // can only be SHA512 ED25519 Algorithm = ed25519Prefix @@ -207,7 +203,8 @@ type SSHSigner interface { } // NewwSSHSigner creates a new Signer using the specified ssh.Signer -// At the moment only ed25519 ssh keys are supported. +// At the moment only ed25519, rsa and ecdsa-sha2-nistp256 ssh keys +// (including their OpenSSH certificate variants) are supported. // The headers specified will be included into the HTTP signatures. // // The Digest will also be calculated on a request's body using the provided @@ -234,7 +231,11 @@ func getSSHAlgorithm(pkType string) Algorithm { case strings.HasPrefix(pkType, sshPrefix+"-"+ed25519Prefix): return ED25519 case strings.HasPrefix(pkType, sshPrefix+"-"+rsaPrefix): - return RSA_SHA1 + return RSA_SHA256 + // Covers both ecdsa-sha2-nistp256 and + // ecdsa-sha2-nistp256-cert-v01@openssh.com. + case strings.HasPrefix(pkType, ssh.KeyAlgoECDSA256): + return ECDSA_SHA256 } return "" @@ -324,7 +325,6 @@ func newSSHSigner(sshSigner ssh.Signer, algo Algorithm, dAlgo DigestAlgorithm, h } func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, error) { - var expires, created int64 = 0, 0 if expiresIn != 0 { created = time.Now().Unix() diff --git a/httpsig_test.go b/httpsig_test.go index dca48e7..118d26e 100644 --- a/httpsig_test.go +++ b/httpsig_test.go @@ -3,6 +3,8 @@ package httpsig import ( "bytes" "crypto" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -19,6 +21,7 @@ import ( "testing" "golang.org/x/crypto/ed25519" + "golang.org/x/crypto/ssh" ) const ( @@ -63,6 +66,7 @@ type ed25519PubKey struct { var ( privKey *rsa.PrivateKey + privECDSAKey *ecdsa.PrivateKey macKey []byte tests []httpsigTest testSpecRSAPrivateKey *rsa.PrivateKey @@ -77,6 +81,10 @@ func init() { if err != nil { panic(err) } + privECDSAKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + panic(err) + } pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader) if err != nil { panic(err) @@ -139,6 +147,32 @@ func init() { expectedSignatureAlgorithm: "hs2019", expectedDigest: "SHA-256=07PJQngqg8+BlomdI6zM7ieOxhINWI+iivJxBDSm3Dg=", }, + { + name: "ecdsa signature", + prefs: []Algorithm{ECDSA_SHA256}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Signature, + privKey: privECDSAKey, + pubKey: privECDSAKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: ECDSA_SHA256, + expectedSignatureAlgorithm: "hs2019", + }, + { + name: "digest on ecdsa signature", + prefs: []Algorithm{ECDSA_SHA256}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + body: []byte("Last night as I lay dreaming This strangest kind of feeling Revealed its secret meaning And now I know..."), + scheme: Signature, + privKey: privECDSAKey, + pubKey: privECDSAKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: ECDSA_SHA256, + expectedSignatureAlgorithm: "hs2019", + expectedDigest: "SHA-256=07PJQngqg8+BlomdI6zM7ieOxhINWI+iivJxBDSm3Dg=", + }, { name: "hmac signature", prefs: []Algorithm{HMAC_SHA256}, @@ -189,6 +223,18 @@ func init() { expectedAlgorithm: ED25519, expectedSignatureAlgorithm: "hs2019", }, + { + name: "ecdsa authorization", + prefs: []Algorithm{ECDSA_SHA256}, + digestAlg: DigestSha256, + headers: []string{"Date", "Digest"}, + scheme: Authorization, + privKey: privECDSAKey, + pubKey: privECDSAKey.Public(), + pubKeyId: "pubKeyId", + expectedAlgorithm: ECDSA_SHA256, + expectedSignatureAlgorithm: "hs2019", + }, { name: "hmac authorization", prefs: []Algorithm{HMAC_SHA256}, @@ -390,6 +436,121 @@ func TestSignerResponse(t *testing.T) { } } +func TestGetSSHAlgorithm(t *testing.T) { + sshTests := []struct { + pkType string + expected Algorithm + }{ + {ssh.KeyAlgoED25519, ED25519}, + {ssh.CertAlgoED25519v01, ED25519}, + {ssh.KeyAlgoRSA, RSA_SHA256}, + {ssh.CertAlgoRSAv01, RSA_SHA256}, + {ssh.KeyAlgoECDSA256, ECDSA_SHA256}, + {ssh.CertAlgoECDSA256v01, ECDSA_SHA256}, + {ssh.KeyAlgoECDSA384, ""}, + {ssh.KeyAlgoECDSA521, ""}, + {"ssh-dss", ""}, + } + for _, test := range sshTests { + if got := getSSHAlgorithm(test.pkType); got != test.expected { + t.Errorf("%q: got %q, want %q", test.pkType, got, test.expected) + } + } +} + +func TestSSHSignerRequest(t *testing.T) { + pubEd25519Key, privEd25519Key, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("error generating ed25519 key: %s", err) + } + sshTests := []struct { + name string + privKey crypto.PrivateKey + pubKey crypto.PublicKey + expectedAlgorithm Algorithm + }{ + { + name: "rsa", + privKey: privKey, + pubKey: privKey.Public(), + expectedAlgorithm: RSA_SHA256, + }, + { + name: "ed25519", + privKey: privEd25519Key, + pubKey: pubEd25519Key, + expectedAlgorithm: ED25519, + }, + { + name: "ecdsa", + privKey: privECDSAKey, + pubKey: privECDSAKey.Public(), + expectedAlgorithm: ECDSA_SHA256, + }, + } + for _, test := range sshTests { + for _, useCert := range []bool{false, true} { + name := test.name + if useCert { + name += " certificate" + } + t.Run(name, func(t *testing.T) { + sshSigner, err := ssh.NewSignerFromKey(test.privKey) + if err != nil { + t.Fatalf("error creating ssh signer: %s", err) + } + if useCert { + sshSigner = newTestCertSigner(t, sshSigner) + } + s, algo, err := NewSSHSigner(sshSigner, DigestSha256, []string{"Date", "Digest"}, Signature, 0) + if err != nil { + t.Fatalf("error creating ssh http signer: %s", err) + } + if algo != test.expectedAlgorithm { + t.Fatalf("got %s, want %s", algo, test.expectedAlgorithm) + } + req, err := http.NewRequest(testMethod, testUrl, nil) + if err != nil { + t.Fatalf("error creating request: %s", err) + } + req.Header.Set("Date", testDate) + req.Header.Set("Digest", testDigest) + if err := s.SignRequest("pubKeyId", req, nil); err != nil { + t.Fatalf("error signing request: %s", err) + } + v, err := NewVerifier(req) + if err != nil { + t.Fatalf("error creating verifier: %s", err) + } + if v.KeyId() != "pubKeyId" { + t.Errorf("KeyId mismatch\nGot: %s\nWant: pubKeyId", v.KeyId()) + } + if err := v.Verify(test.pubKey, algo); err != nil { + t.Errorf("Verification failure: %s", err) + } + }) + } + } +} + +func newTestCertSigner(t *testing.T, s ssh.Signer) ssh.Signer { + t.Helper() + cert := &ssh.Certificate{ + Key: s.PublicKey(), + CertType: ssh.UserCert, + KeyId: "test", + ValidBefore: ssh.CertTimeInfinity, + } + if err := cert.SignCert(rand.Reader, s); err != nil { + t.Fatalf("error signing certificate: %s", err) + } + certSigner, err := ssh.NewCertSigner(cert, s) + if err != nil { + t.Fatalf("error creating certificate signer: %s", err) + } + return certSigner +} + func TestNewSignerRequestMissingHeaders(t *testing.T) { failingTests := []struct { name string