diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..341e47c --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,104 @@ +# Configuration file for golangci-lint linter. +# This will be automatically picked up when golangci-lint is invoked. +# For all config options, see https://golangci-lint.run/usage/configuration/#config-file +# +# For GoToSocial we mostly take the default linters, but we add a few to catch style issues as well. + +# options for analysis running +run: + # include test files or not, default is true + tests: false + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 5m + +linters: + # enable some extra linters, see here for the list: https://golangci-lint.run/usage/linters/ + enable: + - gocritic + - gofmt + - goheader + - gosec + - nilerr + - revive + +# https://golangci-lint.run/usage/linters/#linters-configuration +linters-settings: + # https://golangci-lint.run/usage/linters/#goheader + goheader: + template: |- + httpsig + Copyright (C) GoToSocial Authors admin@gotosocial.org + Copyright (C) go-fed + SPDX-License-Identifier: BSD-3-Clause + + BSD 3-Clause License + + Copyright (c) 2018, go-fed + Copyright (c) 2024, GoToSocial Authors + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + # https://golangci-lint.run/usage/linters/#govet + govet: + disable: + - composites + # https://golangci-lint.run/usage/linters/#revive + revive: + rules: + # Enable most default rules. + # See: https://github.com/mgechev/revive/blob/master/defaults.toml + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: dot-imports + - name: error-naming + - name: error-return + - name: error-strings + - name: exported + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: package-comments + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unreachable-code + # Disable below rules. + - name: redefines-builtin-id + disabled: true # This one is just annoying. + - name: unused-parameter + disabled: true # We often pass parameters to fulfil interfaces. + # https://golangci-lint.run/usage/linters/#staticcheck + staticcheck: + # Enable all checks, but disable SA1012: nil context passing. + # See: https://staticcheck.io/docs/configuration/options/#checks + checks: ["all", "-SA1012"] diff --git a/algorithms.go b/algorithms.go index 9595941..d8684a1 100644 --- a/algorithms.go +++ b/algorithms.go @@ -1,3 +1,39 @@ +// httpsig +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// Copyright (C) go-fed +// SPDX-License-Identifier: BSD-3-Clause +// +// BSD 3-Clause License +// +// Copyright (c) 2018, go-fed +// Copyright (c) 2024, GoToSocial Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + package httpsig import ( @@ -5,10 +41,9 @@ import ( "crypto/ecdsa" "crypto/hmac" "crypto/rsa" - "crypto/sha1" "crypto/sha256" "crypto/sha512" - "crypto/subtle" // Use should trigger great care + "crypto/subtle" "encoding/asn1" "errors" "fmt" @@ -20,25 +55,21 @@ 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" ) +//nolint:revive // Underscores aid legibility const ( hmacPrefix = "hmac" rsaPrefix = "rsa" 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" @@ -68,26 +99,20 @@ 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 }}, crypto.SHA3_512: {sha3_512String, func(key []byte) (hash.Hash, error) { return sha3.New512(), nil }}, crypto.SHA512_224: {sha512_224String, func(key []byte) (hash.Hash, error) { return sha512.New512_224(), nil }}, crypto.SHA512_256: {sha512_256String, func(key []byte) (hash.Hash, error) { return sha512.New512_256(), nil }}, - crypto.BLAKE2s_256: {blake2s_256String, func(key []byte) (hash.Hash, error) { return blake2s.New256(key) }}, - crypto.BLAKE2b_256: {blake2b_256String, func(key []byte) (hash.Hash, error) { return blake2b.New256(key) }}, - crypto.BLAKE2b_384: {blake2b_384String, func(key []byte) (hash.Hash, error) { return blake2b.New384(key) }}, - crypto.BLAKE2b_512: {blake2b_512String, func(key []byte) (hash.Hash, error) { return blake2b.New512(key) }}, + crypto.BLAKE2s_256: {blake2s_256String, blake2s.New256}, + crypto.BLAKE2b_256: {blake2b_256String, blake2b.New256}, + crypto.BLAKE2b_384: {blake2b_384String, blake2b.New384}, + crypto.BLAKE2b_512: {blake2b_512String, blake2b.New512}, } var stringToHash map[string]crypto.Hash @@ -148,6 +173,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 } @@ -265,7 +293,7 @@ func (r *ed25519Algorithm) Verify(pub crypto.PublicKey, toHash, signature []byte } func (r *ed25519Algorithm) String() string { - return fmt.Sprintf("%s", ed25519Prefix) + return ed25519Prefix } var _ signer = &ecdsaAlgorithm{} @@ -329,9 +357,8 @@ func (r *ecdsaAlgorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) if ecdsa.Verify(ecdsaK, r.Sum(nil), sig.R, sig.S) { return nil - } else { - return errors.New("Invalid signature") } + return errors.New("Invalid signature") } func (r *ecdsaAlgorithm) String() string { @@ -371,7 +398,7 @@ func (r *blakeMacAlgorithm) Equal(sig, actualMAC, key []byte) (bool, error) { } func (r *blakeMacAlgorithm) String() string { - return fmt.Sprintf("%s", hashToDef[r.kind].name) + return hashToDef[r.kind].name } func setSig(a hash.Hash, b []byte) error { @@ -386,9 +413,9 @@ func setSig(a hash.Hash, b []byte) error { return nil } -// IsSupportedHttpSigAlgorithm returns true if the string is supported by this +// IsSupportedAlgorithm returns true if the string is supported by this // library, is not a hash known to be weak, and is supported by the hardware. -func IsSupportedHttpSigAlgorithm(algo string) bool { +func IsSupportedAlgorithm(algo string) bool { a, err := isAvailable(algo) return a && err == nil } @@ -460,16 +487,17 @@ func signerFromString(s string) (signer, error) { s = strings.ToLower(s) isEcdsa := false isEd25519 := false - var algo string = "" - if strings.HasPrefix(s, ecdsaPrefix) { + var algo string + switch { + case strings.HasPrefix(s, ecdsaPrefix): algo = strings.TrimPrefix(s, ecdsaPrefix+"-") isEcdsa = true - } else if strings.HasPrefix(s, rsaPrefix) { + case strings.HasPrefix(s, rsaPrefix): algo = strings.TrimPrefix(s, rsaPrefix+"-") - } else if strings.HasPrefix(s, ed25519Prefix) { + case strings.HasPrefix(s, ed25519Prefix): isEd25519 = true algo = "sha512" - } else { + default: return nil, fmt.Errorf("no signer matching %q", s) } hash, cHash, err := newAlgorithm(algo, nil) @@ -526,7 +554,6 @@ func macerFromString(s string) (macer, error) { fn: hashFn, kind: cHash, }, nil - } else { - return nil, fmt.Errorf("no MACer matching %q", s) } + return nil, fmt.Errorf("no MACer matching %q", s) } diff --git a/algorithms_test.go b/algorithms_test.go index 5c3a3a3..773c197 100644 --- a/algorithms_test.go +++ b/algorithms_test.go @@ -11,6 +11,22 @@ import ( "testing" ) +//nolint:revive // Underscores aid legibility +const ( + // Just because you can glue things together, doesn't mean they will + // work. The following options are not supported. + rsa_SHA3_224 Algorithm = rsaPrefix + "-" + sha3_224String + rsa_SHA3_256 Algorithm = rsaPrefix + "-" + sha3_256String + rsa_SHA3_384 Algorithm = rsaPrefix + "-" + sha3_384String + rsa_SHA3_512 Algorithm = rsaPrefix + "-" + sha3_512String + rsa_SHA512_224 Algorithm = rsaPrefix + "-" + sha512_224String + rsa_SHA512_256 Algorithm = rsaPrefix + "-" + sha512_256String + rsa_BLAKE2S_256 Algorithm = rsaPrefix + "-" + blake2s_256String + rsa_BLAKE2B_256 Algorithm = rsaPrefix + "-" + blake2b_256String + rsa_BLAKE2B_384 Algorithm = rsaPrefix + "-" + blake2b_384String + rsa_BLAKE2B_512 Algorithm = rsaPrefix + "-" + blake2b_512String +) + func readFullFromCrypto(b []byte) error { n := len(b) t := 0 @@ -32,23 +48,22 @@ func TestIsAvailable(t *testing.T) { expectError bool }{ { - name: md4String, - algo: md4String, + name: "md4", + algo: "md4", expected: false, expectError: true, }, { - name: md5String, - algo: md5String, + name: "md5", + algo: "md5", 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: "sha1", + algo: "sha1", + expected: false, + expectError: true, }, { name: sha224String, @@ -75,16 +90,16 @@ func TestIsAvailable(t *testing.T) { expectError: false, }, { - name: md5sha1String, - algo: md5sha1String, + name: "md5sha1", + algo: "md5sha1", expected: false, expectError: true, }, { name: ripemd160String, algo: ripemd160String, - expected: true, - expectError: false, + expected: false, + expectError: true, }, { name: sha3_224String, @@ -285,9 +300,9 @@ func TestSignerFromString(t *testing.T) { expectKind: crypto.SHA512, }, { - name: "RSA_RIPEMD160", - input: RSA_RIPEMD160, - expectKind: crypto.RIPEMD160, + name: "RSA_RIPEMD160", + input: RSA_RIPEMD160, + expectError: true, }, { name: "rsa_SHA3_224", @@ -389,9 +404,9 @@ func TestMACerFromString(t *testing.T) { expectKind: crypto.SHA512, }, { - name: "HMAC_RIPEMD160", - input: HMAC_RIPEMD160, - expectKind: crypto.RIPEMD160, + name: "HMAC_RIPEMD160", + input: HMAC_RIPEMD160, + expectError: true, }, { name: "HMAC_SHA3_224", @@ -587,11 +602,6 @@ func TestSignerSigns(t *testing.T) { input: RSA_SHA512, inputCryptoHash: crypto.SHA512, }, - { - name: "RSA_RIPEMD160", - input: RSA_RIPEMD160, - inputCryptoHash: crypto.RIPEMD160, - }, { name: "rsa_SHA3_224", input: rsa_SHA3_224, @@ -738,11 +748,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) @@ -817,13 +822,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 +1014,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/digest.go b/digest.go index bf9e3a9..36fd62a 100644 --- a/digest.go +++ b/digest.go @@ -1,7 +1,42 @@ +// httpsig +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// Copyright (C) go-fed +// SPDX-License-Identifier: BSD-3-Clause +// +// BSD 3-Clause License +// +// Copyright (c) 2018, go-fed +// Copyright (c) 2024, GoToSocial Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + package httpsig import ( - "bytes" "crypto" "encoding/base64" "fmt" @@ -14,7 +49,7 @@ type DigestAlgorithm string const ( DigestSha256 DigestAlgorithm = "SHA-256" - DigestSha512 = "SHA-512" + DigestSha512 DigestAlgorithm = "SHA-512" ) var digestToDef = map[DigestAlgorithm]crypto.Hash{ @@ -22,7 +57,7 @@ var digestToDef = map[DigestAlgorithm]crypto.Hash{ DigestSha512: crypto.SHA512, } -// IsSupportedDigestAlgorithm returns true if hte string is supported by this +// IsSupportedDigestAlgorithm returns true if the string is supported by this // library, is not a hash known to be weak, and is supported by the hardware. func IsSupportedDigestAlgorithm(algo string) bool { uc := DigestAlgorithm(strings.ToUpper(algo)) @@ -33,11 +68,12 @@ func IsSupportedDigestAlgorithm(algo string) bool { func getHash(alg DigestAlgorithm) (h hash.Hash, toUse DigestAlgorithm, err error) { upper := DigestAlgorithm(strings.ToUpper(string(alg))) c, ok := digestToDef[upper] - if !ok { + switch { + case !ok: err = fmt.Errorf("unknown or unsupported Digest algorithm: %s", alg) - } else if !c.Available() { + case !c.Available(): err = fmt.Errorf("unavailable Digest algorithm: %s", alg) - } else { + default: h = c.New() toUse = upper } @@ -67,7 +103,7 @@ func addDigest(r *http.Request, algo DigestAlgorithm, b []byte) (err error) { fmt.Sprintf("%s%s%s", a, digestDelim, - base64.StdEncoding.EncodeToString(sum[:]))) + base64.StdEncoding.EncodeToString(sum))) return } @@ -89,32 +125,6 @@ func addDigestResponse(r http.ResponseWriter, algo DigestAlgorithm, b []byte) (e fmt.Sprintf("%s%s%s", a, digestDelim, - base64.StdEncoding.EncodeToString(sum[:]))) - return -} - -func verifyDigest(r *http.Request, body *bytes.Buffer) (err error) { - d := r.Header.Get(digestHeader) - if len(d) == 0 { - err = fmt.Errorf("cannot verify Digest: request has no Digest header") - return - } - elem := strings.SplitN(d, digestDelim, 2) - if len(elem) != 2 { - err = fmt.Errorf("cannot verify Digest: malformed Digest: %s", d) - return - } - var h hash.Hash - h, _, err = getHash(DigestAlgorithm(elem[0])) - if err != nil { - return - } - h.Write(body.Bytes()) - sum := h.Sum(nil) - encSum := base64.StdEncoding.EncodeToString(sum[:]) - if encSum != elem[1] { - err = fmt.Errorf("cannot verify Digest: header Digest does not match the digest of the request body") - return - } + base64.StdEncoding.EncodeToString(sum))) return } diff --git a/digest_test.go b/digest_test.go index 4ee4fa0..aa0d8d9 100644 --- a/digest_test.go +++ b/digest_test.go @@ -1,7 +1,6 @@ package httpsig import ( - "bytes" "net/http" "testing" ) @@ -78,86 +77,3 @@ func TestAddDigest(t *testing.T) { }) } } - -func TestVerifyDigest(t *testing.T) { - tests := []struct { - name string - r func() *http.Request - body []byte - expectError bool - }{ - { - name: "verify sha256", - r: func() *http.Request { - r, _ := http.NewRequest("POST", "example.com", nil) - r.Header.Set("Digest", "SHA-256=RYiuVuVdRpU+BWcNUUg3sf0EbJjQ9LDj9tUqR546hhk=") - return r - }, - body: []byte("johnny grab your gun"), - }, - { - name: "verify sha512", - r: func() *http.Request { - r, _ := http.NewRequest("POST", "example.com", nil) - r.Header.Set("Digest", "SHA-512=bM0eBRnZkuiOTsejYNb/UpvFozde+Do1ZqlXfRTS39aGmoEzoXBpjmIIuznPslc3kaprUtI/VXH8/5HsD+thGg==") - return r - }, - body: []byte("yours is the drill that will pierce the heavens"), - }, - { - name: "no digest header", - r: func() *http.Request { - r, _ := http.NewRequest("POST", "example.com", nil) - return r - }, - body: []byte("Yuji's gender is blue"), - expectError: true, - }, - { - name: "malformed digest", - r: func() *http.Request { - r, _ := http.NewRequest("POST", "example.com", nil) - r.Header.Set("Digest", "SHA-256am9obm55IGdyYWIgeW91ciBndW7jsMRCmPwcFJr79MiZb7kkJ65B5GSbk0yklZkbeFK4VQ==") - return r - }, - body: []byte("Tochee and Ozzie BFFs forever"), - expectError: true, - }, - { - name: "unsupported/unknown algo", - r: func() *http.Request { - r, _ := http.NewRequest("POST", "example.com", nil) - r.Header.Set("Digest", "MD5=poo") - return r - }, - body: []byte("what is a man? a miserable pile of secrets"), - expectError: true, - }, - { - name: "bad digest", - r: func() *http.Request { - r, _ := http.NewRequest("POST", "example.com", nil) - r.Header.Set("Digest", "SHA-256=bm9obm55IGdyYWIgeW91ciBndW7jsMRCmPwcFJr79MiZb7kkJ65B5GSbk0yklZkbeFK4VQ==") - return r - }, - body: []byte("johnny grab your gun"), - expectError: true, - }, - } - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - test := test - req := test.r() - buf := bytes.NewBuffer(test.body) - err := verifyDigest(req, buf) - gotErr := err != nil - if gotErr != test.expectError { - if test.expectError { - t.Fatalf("expected error, got: %s", err) - } else { - t.Fatalf("expected no error, got: %s", err) - } - } - }) - } -} diff --git a/httpsig.go b/httpsig.go index 8864da0..a7a3cf2 100644 --- a/httpsig.go +++ b/httpsig.go @@ -1,9 +1,45 @@ -// Implements HTTP request and response signing and verification. Supports the -// major MAC and asymmetric key signature algorithms. It has several safety -// restrictions: One, none of the widely known non-cryptographically safe +// httpsig +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// Copyright (C) go-fed +// SPDX-License-Identifier: BSD-3-Clause +// +// BSD 3-Clause License +// +// Copyright (c) 2018, go-fed +// Copyright (c) 2024, GoToSocial Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Package httpsig implements HTTP request and response signing and verification. +// Supports the major MAC and asymmetric key signature algorithms. It has several +// safety restrictions: One, none of the widely known non-cryptographically safe // algorithms are permitted; Two, the RSA SHA256 algorithms must be available in // the binary (and it should, barring export restrictions); Finally, the library -// assumes either the 'Authorizationn' or 'Signature' headers are to be set (but +// assumes either the 'Authorization' or 'Signature' headers are to be set (but // not both). package httpsig @@ -21,8 +57,8 @@ import ( // and responses. type Algorithm string +//nolint:revive // this is the most readable format for these constants const ( - // MAC-based algoirthms. HMAC_SHA224 Algorithm = hmacPrefix + "-" + sha224String HMAC_SHA256 Algorithm = hmacPrefix + "-" + sha256String HMAC_SHA384 Algorithm = hmacPrefix + "-" + sha384String @@ -43,7 +79,6 @@ 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 @@ -59,19 +94,6 @@ const ( // ED25519 algorithms // can only be SHA512 ED25519 Algorithm = ed25519Prefix - - // Just because you can glue things together, doesn't mean they will - // work. The following options are not supported. - rsa_SHA3_224 Algorithm = rsaPrefix + "-" + sha3_224String - rsa_SHA3_256 Algorithm = rsaPrefix + "-" + sha3_256String - rsa_SHA3_384 Algorithm = rsaPrefix + "-" + sha3_384String - rsa_SHA3_512 Algorithm = rsaPrefix + "-" + sha3_512String - rsa_SHA512_224 Algorithm = rsaPrefix + "-" + sha512_224String - rsa_SHA512_256 Algorithm = rsaPrefix + "-" + sha512_256String - rsa_BLAKE2S_256 Algorithm = rsaPrefix + "-" + blake2s_256String - rsa_BLAKE2B_256 Algorithm = rsaPrefix + "-" + blake2b_256String - rsa_BLAKE2B_384 Algorithm = rsaPrefix + "-" + blake2b_384String - rsa_BLAKE2B_512 Algorithm = rsaPrefix + "-" + blake2b_512String ) // HTTP Signatures can be applied to different HTTP headers, depending on the @@ -138,7 +160,7 @@ type Signer interface { // and if the signer is created to also sign the "Digest" header, the // HTTP Signature will then ensure both the Digest and body are not both // modified to maliciously represent different content. - SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error + SignRequest(pKey crypto.PrivateKey, pubKeyID string, r *http.Request, body []byte) error // SignResponse signs the response using a private key. The public key // id is used by the HTTP client to identify which key to use to verify // the signature. @@ -154,7 +176,7 @@ type Signer interface { // flight, and if the signer is created to also sign the "Digest" // header, the HTTP Signature will then ensure both the Digest and body // are not both modified to maliciously represent different content. - SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error + SignResponse(pKey crypto.PrivateKey, pubKeyID string, r http.ResponseWriter, body []byte) error } type SignerWithOptions interface { @@ -175,7 +197,7 @@ type SignerWithOptions interface { // and if the signer is created to also sign the "Digest" header, the // HTTP Signature will then ensure both the Digest and body are not both // modified to maliciously represent different content. - SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error + SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyID string, r *http.Request, body []byte, opts SignatureOption) error // SignResponseWithOptions signs the response using a private key. The public key // id is used by the HTTP client to identify which key to use to verify // the signature. @@ -191,7 +213,7 @@ type SignerWithOptions interface { // flight, and if the signer is created to also sign the "Digest" // header, the HTTP Signature will then ensure both the Digest and body // are not both modified to maliciously represent different content. - SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, opts SignatureOption) error + SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyID string, r http.ResponseWriter, body []byte, opts SignatureOption) error } // NewSigner creates a new Signer with the provided algorithm preferences to @@ -238,7 +260,7 @@ type SSHSigner interface { // and if the signer is created to also sign the "Digest" header, the // HTTP Signature will then ensure both the Digest and body are not both // modified to maliciously represent different content. - SignRequest(pubKeyId string, r *http.Request, body []byte) error + SignRequest(pubKeyID string, r *http.Request, body []byte) error // SignResponse signs the response using ssh.Signer. The public key // id is used by the HTTP client to identify which key to use to verify // the signature. @@ -249,10 +271,11 @@ type SSHSigner interface { // flight, and if the signer is created to also sign the "Digest" // header, the HTTP Signature will then ensure both the Digest and body // are not both modified to maliciously represent different content. - SignResponse(pubKeyId string, r http.ResponseWriter, body []byte) error + SignResponse(pubKeyID string, r http.ResponseWriter, body []byte) error } -// NewwSSHSigner creates a new Signer using the specified ssh.Signer +// NewSSHSigner creates a new Signer using the specified ssh.Signer. +// // At the moment only ed25519 ssh keys are supported. // The headers specified will be included into the HTTP signatures. // @@ -264,7 +287,7 @@ type SSHSigner interface { func NewSSHSigner(s ssh.Signer, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (SSHSigner, Algorithm, error) { sshAlgo := getSSHAlgorithm(s.PublicKey().Type()) if sshAlgo == "" { - return nil, "", fmt.Errorf("key type: %s not supported yet.", s.PublicKey().Type()) + return nil, "", fmt.Errorf("key type not supported: %s", s.PublicKey().Type()) } signer, err := newSSHSigner(s, sshAlgo, dAlgo, headers, scheme, expiresIn) @@ -276,13 +299,9 @@ func NewSSHSigner(s ssh.Signer, dAlgo DigestAlgorithm, headers []string, scheme } func getSSHAlgorithm(pkType string) Algorithm { - switch { - case strings.HasPrefix(pkType, sshPrefix+"-"+ed25519Prefix): + if strings.HasPrefix(pkType, sshPrefix+"-"+ed25519Prefix) { return ED25519 - case strings.HasPrefix(pkType, sshPrefix+"-"+rsaPrefix): - return RSA_SHA1 } - return "" } @@ -299,7 +318,7 @@ type Verifier interface { // // Note that the application is expected to determine the algorithm // used based on metadata or out-of-band information for this key id. - KeyId() string + KeyID() string // Verify accepts the public key specified by KeyId and returns an // error if verification fails or if the signature is malformed. The // algorithm must be the one used to create the signature in order to diff --git a/httpsig_test.go b/httpsig_test.go index 09b93ff..0ce86ee 100644 --- a/httpsig_test.go +++ b/httpsig_test.go @@ -315,7 +315,7 @@ func TestSignerRequest(t *testing.T) { if len(vals) != 1 { t.Fatalf("too many in header %s: %d", test.scheme, len(vals)) } - if p := toSignatureParameter(keyIdParameter, test.pubKeyId); !strings.Contains(vals[0], p) { + if p := toSignatureParameter(keyIDParameter, test.pubKeyId); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) } else if p := toSignatureParameter(algorithmParameter, string(test.expectedSignatureAlgorithm)); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) @@ -365,7 +365,7 @@ func TestSignerResponse(t *testing.T) { if len(vals) != 1 { t.Fatalf("too many in header %s: %d", test.scheme, len(vals)) } - if p := toSignatureParameter(keyIdParameter, test.pubKeyId); !strings.Contains(vals[0], p) { + if p := toSignatureParameter(keyIDParameter, test.pubKeyId); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) } else if p := toSignatureParameter(algorithmParameter, string(test.expectedSignatureAlgorithm)); !strings.Contains(vals[0], p) { t.Fatalf("%s\ndoes not contain\n%s", vals[0], p) @@ -509,8 +509,8 @@ func TestNewVerifier(t *testing.T) { if err != nil { t.Fatalf("%s", err) } - if v.KeyId() != test.pubKeyId { - t.Fatalf("got %s, want %s", v.KeyId(), test.pubKeyId) + if v.KeyID() != test.pubKeyId { + t.Fatalf("got %s, want %s", v.KeyID(), test.pubKeyId) } err = v.Verify(test.pubKey, test.expectedAlgorithm) if err != nil { @@ -546,8 +546,8 @@ func TestNewResponseVerifier(t *testing.T) { if err != nil { t.Fatalf("%s", err) } - if v.KeyId() != test.pubKeyId { - t.Fatalf("got %s, want %s", v.KeyId(), test.pubKeyId) + if v.KeyID() != test.pubKeyId { + t.Fatalf("got %s, want %s", v.KeyID(), test.pubKeyId) } err = v.Verify(test.pubKey, test.expectedAlgorithm) if err != nil { @@ -750,8 +750,8 @@ func Test_Verifying_HTTP_Messages_AppendixC(t *testing.T) { t.Fatalf("error creating verifier: %s", err) } - if "Test" != v.KeyId() { - t.Errorf("KeyId mismatch\nGot: %s\nWant: Test", v.KeyId()) + if "Test" != v.KeyID() { + t.Errorf("KeyID mismatch\nGot: %s\nWant: Test", v.KeyID()) } opts := SignatureOption{ @@ -808,8 +808,8 @@ func TestVerifyingEd25519(t *testing.T) { t.Fatalf("error creating verifier: %s", err) } - if "Test" != v.KeyId() { - t.Errorf("KeyId mismatch\nGot: %s\nWant: Test", v.KeyId()) + if "Test" != v.KeyID() { + t.Errorf("KeyID mismatch\nGot: %s\nWant: Test", v.KeyID()) } if err := v.Verify(testEd25519PublicKey, ED25519); err != nil { t.Errorf("Verification failure: %s", err) diff --git a/signing.go b/signing.go index a2fa38c..1f12bad 100644 --- a/signing.go +++ b/signing.go @@ -1,3 +1,39 @@ +// httpsig +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// Copyright (C) go-fed +// SPDX-License-Identifier: BSD-3-Clause +// +// BSD 3-Clause License +// +// Copyright (c) 2018, go-fed +// Copyright (c) 2024, GoToSocial Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + package httpsig import ( @@ -14,7 +50,7 @@ import ( const ( // Signature Parameters - keyIdParameter = "keyId" + keyIDParameter = "keyId" algorithmParameter = "algorithm" headersParameter = "headers" signatureParameter = "signature" @@ -43,7 +79,6 @@ var _ SignerWithOptions = &macSigner{} type macSigner struct { m macer - makeDigest bool dAlgo DigestAlgorithm headers []string targetHeader SignatureScheme @@ -52,11 +87,11 @@ type macSigner struct { expires int64 } -func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error { - return m.SignRequestWithOptions(pKey, pubKeyId, r, body, SignatureOption{}) +func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyID string, r *http.Request, body []byte) error { + return m.SignRequestWithOptions(pKey, pubKeyID, r, body, SignatureOption{}) } -func (m *macSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error { +func (m *macSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyID string, r *http.Request, body []byte, opts SignatureOption) error { if body != nil { err := addDigest(r, m.dAlgo, body) if err != nil { @@ -71,15 +106,15 @@ func (m *macSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId stri if err != nil { return err } - setSignatureHeader(r.Header, string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires) + setSignatureHeader(r.Header, string(m.targetHeader), m.prefix, pubKeyID, m.m.String(), enc, m.headers, m.created, m.expires) return nil } -func (m *macSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error { - return m.SignResponseWithOptions(pKey, pubKeyId, r, body, SignatureOption{}) +func (m *macSigner) SignResponse(pKey crypto.PrivateKey, pubKeyID string, r http.ResponseWriter, body []byte) error { + return m.SignResponseWithOptions(pKey, pubKeyID, r, body, SignatureOption{}) } -func (m *macSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, _ SignatureOption) error { +func (m *macSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyID string, r http.ResponseWriter, body []byte, _ SignatureOption) error { if body != nil { err := addDigestResponse(r, m.dAlgo, body) if err != nil { @@ -94,7 +129,7 @@ func (m *macSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId str if err != nil { return err } - setSignatureHeader(r.Header(), string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires) + setSignatureHeader(r.Header(), string(m.targetHeader), m.prefix, pubKeyID, m.m.String(), enc, m.headers, m.created, m.expires) return nil } @@ -123,7 +158,6 @@ var _ SignerWithOptions = &asymmSigner{} type asymmSigner struct { s signer - makeDigest bool dAlgo DigestAlgorithm headers []string targetHeader SignatureScheme @@ -132,11 +166,11 @@ type asymmSigner struct { expires int64 } -func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error { - return a.SignRequestWithOptions(pKey, pubKeyId, r, body, SignatureOption{}) +func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyID string, r *http.Request, body []byte) error { + return a.SignRequestWithOptions(pKey, pubKeyID, r, body, SignatureOption{}) } -func (a *asymmSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte, opts SignatureOption) error { +func (a *asymmSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyID string, r *http.Request, body []byte, opts SignatureOption) error { if body != nil { err := addDigest(r, a.dAlgo, body) if err != nil { @@ -151,15 +185,15 @@ func (a *asymmSigner) SignRequestWithOptions(pKey crypto.PrivateKey, pubKeyId st if err != nil { return err } - setSignatureHeader(r.Header, string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires) + setSignatureHeader(r.Header, string(a.targetHeader), a.prefix, pubKeyID, a.s.String(), enc, a.headers, a.created, a.expires) return nil } -func (a *asymmSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte) error { - return a.SignResponseWithOptions(pKey, pubKeyId, r, body, SignatureOption{}) +func (a *asymmSigner) SignResponse(pKey crypto.PrivateKey, pubKeyID string, r http.ResponseWriter, body []byte) error { + return a.SignResponseWithOptions(pKey, pubKeyID, r, body, SignatureOption{}) } -func (a *asymmSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId string, r http.ResponseWriter, body []byte, _ SignatureOption) error { +func (a *asymmSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyID string, r http.ResponseWriter, body []byte, _ SignatureOption) error { if body != nil { err := addDigestResponse(r, a.dAlgo, body) if err != nil { @@ -174,7 +208,7 @@ func (a *asymmSigner) SignResponseWithOptions(pKey crypto.PrivateKey, pubKeyId s if err != nil { return err } - setSignatureHeader(r.Header(), string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires) + setSignatureHeader(r.Header(), string(a.targetHeader), a.prefix, pubKeyID, a.s.String(), enc, a.headers, a.created, a.expires) return nil } @@ -201,35 +235,35 @@ type asymmSSHSigner struct { *asymmSigner } -func (a *asymmSSHSigner) SignRequest(pubKeyId string, r *http.Request, body []byte) error { - return a.asymmSigner.SignRequest(nil, pubKeyId, r, body) +func (a *asymmSSHSigner) SignRequest(pubKeyID string, r *http.Request, body []byte) error { + return a.asymmSigner.SignRequest(nil, pubKeyID, r, body) } -func (a *asymmSSHSigner) SignResponse(pubKeyId string, r http.ResponseWriter, body []byte) error { - return a.asymmSigner.SignResponse(nil, pubKeyId, r, body) +func (a *asymmSSHSigner) SignResponse(pubKeyID string, r http.ResponseWriter, body []byte) error { + return a.asymmSigner.SignResponse(nil, pubKeyID, r, body) } -func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc string, headers []string, created int64, expires int64) { +func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyID, algo, enc string, headers []string, created int64, expires int64) { if len(headers) == 0 { headers = defaultHeaders } var b bytes.Buffer - // KeyId + // KeyID b.WriteString(prefix) if len(prefix) > 0 { b.WriteString(prefixSeparater) } - b.WriteString(keyIdParameter) + b.WriteString(keyIDParameter) b.WriteString(parameterKVSeparater) b.WriteString(parameterValueDelimiter) - b.WriteString(pubKeyId) + b.WriteString(pubKeyID) b.WriteString(parameterValueDelimiter) b.WriteString(parameterSeparater) // Algorithm b.WriteString(algorithmParameter) b.WriteString(parameterKVSeparater) b.WriteString(parameterValueDelimiter) - b.WriteString("hs2019") //real algorithm is hidden, see newest version of spec draft + b.WriteString("hs2019") // real algorithm is hidden, see newest version of spec draft b.WriteString(parameterValueDelimiter) b.WriteString(parameterSeparater) @@ -245,7 +279,7 @@ func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc } // Created - if hasCreated == true { + if hasCreated { b.WriteString(createdKey) b.WriteString(parameterKVSeparater) b.WriteString(strconv.FormatInt(created, 10)) @@ -253,7 +287,7 @@ func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc } // Expires - if hasExpires == true { + if hasExpires { b.WriteString(expiresKey) b.WriteString(parameterKVSeparater) b.WriteString(strconv.FormatInt(expires, 10)) @@ -309,26 +343,27 @@ func signatureString(values http.Header, include []string, requestTargetFn func( var b bytes.Buffer for n, i := range include { i := strings.ToLower(i) - if i == RequestTarget { + switch { + case i == RequestTarget: err := requestTargetFn(&b) if err != nil { return "", err } - } else if i == "("+expiresKey+")" { + case i == "("+expiresKey+")": if expires == 0 { return "", fmt.Errorf("missing expires value") } b.WriteString(i) b.WriteString(headerFieldDelimiter) b.WriteString(strconv.FormatInt(expires, 10)) - } else if i == "("+createdKey+")" { + case i == "("+createdKey+")": if created == 0 { return "", fmt.Errorf("missing created value") } b.WriteString(i) b.WriteString(headerFieldDelimiter) b.WriteString(strconv.FormatInt(created, 10)) - } else { + default: hv, ok := values[textproto.CanonicalMIMEHeaderKey(i)] if !ok { return "", fmt.Errorf("missing header %q", i) diff --git a/verifying.go b/verifying.go index 52a142a..5c943e4 100644 --- a/verifying.go +++ b/verifying.go @@ -1,3 +1,39 @@ +// httpsig +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// Copyright (C) go-fed +// SPDX-License-Identifier: BSD-3-Clause +// +// BSD 3-Clause License +// +// Copyright (c) 2018, go-fed +// Copyright (c) 2024, GoToSocial Authors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + package httpsig import ( @@ -15,7 +51,7 @@ var _ VerifierWithOptions = &verifier{} type verifier struct { header http.Header - kId string + kID string signature string created int64 expires int64 @@ -28,16 +64,16 @@ func newVerifier(h http.Header, sigStringFn func(http.Header, []string, int64, i if err != nil { return nil, err } - kId, sig, headers, created, expires, err := getSignatureComponents(scheme, s) + kID, sig, headers, created, expires, err := getSignatureComponents(scheme, s) if created != 0 { - //check if created is not in the future, we assume a maximum clock offset of 10 seconds + // check if created is not in the future, we assume a maximum clock offset of 10 seconds now := time.Now().Unix() if created-now > 10 { return nil, errors.New("created is in the future") } } if expires != 0 { - //check if expires is in the past, we assume a maximum clock offset of 10 seconds + // check if expires is in the past, we assume a maximum clock offset of 10 seconds now := time.Now().Unix() if now-expires > 10 { return nil, errors.New("signature expired") @@ -48,7 +84,7 @@ func newVerifier(h http.Header, sigStringFn func(http.Header, []string, int64, i } return &verifier{ header: h, - kId: kId, + kID: kID, signature: sig, created: created, expires: expires, @@ -57,8 +93,8 @@ func newVerifier(h http.Header, sigStringFn func(http.Header, []string, int64, i }, nil } -func (v *verifier) KeyId() string { - return v.kId +func (v *verifier) KeyID() string { + return v.kID } func (v *verifier) Verify(pKey crypto.PublicKey, algo Algorithm) error { @@ -117,31 +153,32 @@ func (v *verifier) asymmVerify(s signer, pKey crypto.PublicKey, opts SignatureOp func getSignatureScheme(h http.Header) (scheme SignatureScheme, val string, err error) { s := h.Get(string(Signature)) - sigHasAll := strings.Contains(s, keyIdParameter) || + sigHasAll := strings.Contains(s, keyIDParameter) || strings.Contains(s, headersParameter) || strings.Contains(s, signatureParameter) a := h.Get(string(Authorization)) - authHasAll := strings.Contains(a, keyIdParameter) || + authHasAll := strings.Contains(a, keyIDParameter) || strings.Contains(a, headersParameter) || strings.Contains(a, signatureParameter) - if sigHasAll && authHasAll { + switch { + case sigHasAll && authHasAll: err = fmt.Errorf("both %q and %q have signature parameters", Signature, Authorization) return - } else if !sigHasAll && !authHasAll { + case !sigHasAll && !authHasAll: err = fmt.Errorf("neither %q nor %q have signature parameters", Signature, Authorization) return - } else if sigHasAll { + case sigHasAll: val = s scheme = Signature return - } else { // authHasAll + default: val = a scheme = Authorization return } } -func getSignatureComponents(scheme SignatureScheme, s string) (kId, sig string, headers []string, created int64, expires int64, err error) { +func getSignatureComponents(scheme SignatureScheme, s string) (kID, sig string, headers []string, created int64, expires int64, err error) { if as := scheme.authScheme(); len(as) > 0 { s = strings.TrimPrefix(s, as+prefixSeparater) } @@ -155,8 +192,8 @@ func getSignatureComponents(scheme SignatureScheme, s string) (kId, sig string, k := kv[0] v := strings.Trim(kv[1], parameterValueDelimiter) switch k { - case keyIdParameter: - kId = v + case keyIDParameter: + kID = v case createdKey: created, err = strconv.ParseInt(v, 10, 64) if err != nil { @@ -177,11 +214,12 @@ func getSignatureComponents(scheme SignatureScheme, s string) (kId, sig string, // Ignore unrecognized parameters } } - if len(kId) == 0 { - err = fmt.Errorf("missing %q parameter in http signature", keyIdParameter) - } else if len(sig) == 0 { + switch { + case len(kID) == 0: + err = fmt.Errorf("missing %q parameter in http signature", keyIDParameter) + case len(sig) == 0: err = fmt.Errorf("missing %q parameter in http signature", signatureParameter) - } else if len(headers) == 0 { // Optional + case len(headers) == 0: headers = defaultHeaders } return