diff --git a/algorithms.go b/algorithms.go index dfde090..eb490ff 100644 --- a/algorithms.go +++ b/algorithms.go @@ -6,7 +6,9 @@ import ( "crypto/rsa" "crypto/sha256" "crypto/sha512" + "crypto/ecdsa" "crypto/subtle" // Use should trigger great care + "encoding/asn1" "errors" "fmt" "golang.org/x/crypto/blake2b" @@ -16,11 +18,13 @@ import ( "hash" "io" "strings" + "math/big" ) const ( hmacPrefix = "hmac" rsaPrefix = "rsa" + ecdsaPrefix = "ecdsa" md4String = "md4" md5String = "md5" sha1String = "sha1" @@ -210,6 +214,76 @@ func (r *rsaAlgorithm) String() string { return fmt.Sprintf("%s-%s", rsaPrefix, hashToDef[r.kind].name) } +var _ signer = &ecdsaAlgorithm{} + +type ecdsaAlgorithm struct { + hash.Hash + kind crypto.Hash +} + +func (r *ecdsaAlgorithm) setSig(b []byte) error { + n, err := r.Write(b) + if err != nil { + r.Reset() + return err + } else if n != len(b) { + r.Reset() + return fmt.Errorf("could only write %d of %d bytes of signature to hash", n, len(b)) + } + return nil +} + +type ECDSASignature struct { + R, S *big.Int +} + +func (r *ecdsaAlgorithm) Sign(rand io.Reader, p crypto.PrivateKey, sig []byte) ([]byte, error) { + defer r.Reset() + if err := r.setSig(sig); err != nil { + return nil, err + } + ecdsaK, ok := p.(*ecdsa.PrivateKey) + if !ok { + return nil, errors.New("crypto.PrivateKey is not *ecdsa.PrivateKey") + } + R, S, err := ecdsa.Sign(rand, ecdsaK, r.Sum(nil)) + if err != nil { + return nil, err + } + + signature := ECDSASignature {R: R, S: S} + bytes, err := asn1.Marshal(signature) + + return bytes, err +} + +func (r *ecdsaAlgorithm) Verify(pub crypto.PublicKey, toHash, signature []byte) error { + defer r.Reset() + ecdsaK, ok := pub.(*ecdsa.PublicKey) + if !ok { + return errors.New("crypto.PublicKey is not *ecdsa.PublicKey") + } + if err := r.setSig(toHash); err != nil { + return err + } + + sig := new(ECDSASignature) + _, err := asn1.Unmarshal(signature, sig) + if err != nil { + return err + } + + if ecdsa.Verify(ecdsaK, r.Sum(nil), sig.R, sig.S) { + return nil + } else { + return errors.New("Invalid signature") + } +} + +func (r *ecdsaAlgorithm) String() string { + return fmt.Sprintf("%s-%s", ecdsaPrefix, hashToDef[r.kind].name) +} + var _ macer = &blakeMacAlgorithm{} type blakeMacAlgorithm struct { @@ -315,14 +389,26 @@ func newAlgorithm(algo string, key []byte) (hash.Hash, crypto.Hash, error) { // signerFromString is an internally public method constructor func signerFromString(s string) (signer, error) { s = strings.ToLower(s) - if !strings.HasPrefix(s, rsaPrefix) { + isEcdsa := false + var algo string = "" + if strings.HasPrefix(s, ecdsaPrefix) { + algo = strings.TrimPrefix(s, ecdsaPrefix + "-") + isEcdsa = true + } else if strings.HasPrefix(s, rsaPrefix) { + algo = strings.TrimPrefix(s, rsaPrefix+"-") + } else { return nil, fmt.Errorf("no signer matching %q", s) - } - algo := strings.TrimPrefix(s, rsaPrefix+"-") + } hash, cHash, err := newAlgorithm(algo, nil) if err != nil { return nil, err } + if isEcdsa { + return &ecdsaAlgorithm { + Hash: hash, + kind: cHash, + }, nil + } return &rsaAlgorithm{ Hash: hash, kind: cHash, diff --git a/digest.go b/digest.go index bf9e3a9..40caf8d 100644 --- a/digest.go +++ b/digest.go @@ -14,7 +14,7 @@ type DigestAlgorithm string const ( DigestSha256 DigestAlgorithm = "SHA-256" - DigestSha512 = "SHA-512" + DigestSha512 DigestAlgorithm = "SHA-512" ) var digestToDef = map[DigestAlgorithm]crypto.Hash{ diff --git a/go.mod b/go.mod index 2dbb7f7..ed03218 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,6 @@ -module github.com/go-fed/httpsig +module github.com/blacktemplar/httpsig + +go 1.14 require ( golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 diff --git a/go.modverify b/go.modverify deleted file mode 100644 index c13a726..0000000 --- a/go.modverify +++ /dev/null @@ -1,2 +0,0 @@ -golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo= -golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43 h1:PvnWIWTbA7gsEBkKjt0HV9hckYfcqYv8s/ju7ArZ0do= diff --git a/httpsig.go b/httpsig.go index 6908738..07eff0e 100644 --- a/httpsig.go +++ b/httpsig.go @@ -8,6 +8,7 @@ package httpsig import ( + "time" "crypto" "fmt" "net/http" @@ -45,6 +46,13 @@ const ( RSA_SHA384 Algorithm = rsaPrefix + "-" + sha384String RSA_SHA512 Algorithm = rsaPrefix + "-" + sha512String RSA_RIPEMD160 Algorithm = rsaPrefix + "-" + ripemd160String + // 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 + // 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 @@ -147,15 +155,15 @@ type Signer interface { // // An error is returned if an unknown or a known cryptographically insecure // Algorithm is provided. -func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme) (Signer, Algorithm, error) { +func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, Algorithm, error) { for _, pref := range prefs { - s, err := newSigner(pref, dAlgo, headers, scheme) + s, err := newSigner(pref, dAlgo, headers, scheme, expiresIn) if err != nil { continue } return s, pref, err } - s, err := newSigner(defaultAlgorithm, dAlgo, headers, scheme) + s, err := newSigner(defaultAlgorithm, dAlgo, headers, scheme, expiresIn) return s, defaultAlgorithm, err } @@ -184,6 +192,12 @@ type Verifier interface { // using an RSA based algorithm, then the public key is expected to be // of type *rsa.PublicKey. Verify(pKey crypto.PublicKey, algo Algorithm) error + // Gets the parsed Signature + Signature() string + // Gets the parsed Created value or 0 if no created value was given + Created() int64 + // Gets the parsed Expires value or 0 if no expires value was given + Expires() int64 } const ( @@ -201,20 +215,27 @@ func NewVerifier(r *http.Request) (Verifier, error) { if _, hasHostHeader := h[hostHeader]; len(r.Host) > 0 && !hasHostHeader { h[hostHeader] = []string{r.Host} } - return newVerifier(h, func(h http.Header, toInclude []string) (string, error) { - return signatureString(h, toInclude, addRequestTarget(r)) + return newVerifier(h, func(h http.Header, toInclude []string, created int64, expires int64) (string, error) { + return signatureString(h, toInclude, addRequestTarget(r), created, expires) }) } // NewResponseVerifier verifies the given response. It returns errors under the // same conditions as NewVerifier. func NewResponseVerifier(r *http.Response) (Verifier, error) { - return newVerifier(r.Header, func(h http.Header, toInclude []string) (string, error) { - return signatureString(h, toInclude, requestTargetNotPermitted) + return newVerifier(r.Header, func(h http.Header, toInclude []string, created int64, expires int64) (string, error) { + return signatureString(h, toInclude, requestTargetNotPermitted, created, expires) }) } -func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme) (Signer, error) { +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() + expires = created + expiresIn + } + s, err := signerFromString(string(algo)) if err == nil { a := &asymmSigner{ @@ -223,6 +244,8 @@ func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme S headers: headers, targetHeader: scheme, prefix: scheme.authScheme(), + created: created, + expires: expires, } return a, nil } @@ -236,6 +259,9 @@ func newSigner(algo Algorithm, dAlgo DigestAlgorithm, headers []string, scheme S headers: headers, targetHeader: scheme, prefix: scheme.authScheme(), + created: created, + expires: expires, + } return c, nil } diff --git a/signing.go b/signing.go index fe1ee50..000d8a6 100644 --- a/signing.go +++ b/signing.go @@ -1,6 +1,7 @@ package httpsig import ( + "strconv" "bytes" "crypto" "crypto/rand" @@ -25,6 +26,8 @@ const ( // RequestTarget specifies to include the http request method and // entire URI in the signature. Pass it as a header to NewSigner. RequestTarget = "(request-target)" + createdKey = "created" + expiresKey = "expires" dateHeader = "date" // Signature String Construction @@ -45,6 +48,8 @@ type macSigner struct { headers []string targetHeader SignatureScheme prefix string + created int64 + expires int64 } func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error { @@ -62,7 +67,7 @@ func (m *macSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http if err != nil { return err } - setSignatureHeader(r.Header, string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers) + setSignatureHeader(r.Header, string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires) return nil } @@ -81,7 +86,7 @@ func (m *macSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r http if err != nil { return err } - setSignatureHeader(r.Header(), string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers) + setSignatureHeader(r.Header(), string(m.targetHeader), m.prefix, pubKeyId, m.m.String(), enc, m.headers, m.created, m.expires) return nil } @@ -99,11 +104,11 @@ func (m *macSigner) signSignature(pKey crypto.PrivateKey, s string) (string, err } func (m *macSigner) signatureString(r *http.Request) (string, error) { - return signatureString(r.Header, m.headers, addRequestTarget(r)) + return signatureString(r.Header, m.headers, addRequestTarget(r), m.created, m.expires) } func (m *macSigner) signatureStringResponse(r http.ResponseWriter) (string, error) { - return signatureString(r.Header(), m.headers, requestTargetNotPermitted) + return signatureString(r.Header(), m.headers, requestTargetNotPermitted, m.created, m.expires) } var _ Signer = &asymmSigner{} @@ -115,6 +120,8 @@ type asymmSigner struct { headers []string targetHeader SignatureScheme prefix string + created int64 + expires int64 } func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *http.Request, body []byte) error { @@ -132,7 +139,7 @@ func (a *asymmSigner) SignRequest(pKey crypto.PrivateKey, pubKeyId string, r *ht if err != nil { return err } - setSignatureHeader(r.Header, string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers) + setSignatureHeader(r.Header, string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires) return nil } @@ -151,7 +158,7 @@ func (a *asymmSigner) SignResponse(pKey crypto.PrivateKey, pubKeyId string, r ht if err != nil { return err } - setSignatureHeader(r.Header(), string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers) + setSignatureHeader(r.Header(), string(a.targetHeader), a.prefix, pubKeyId, a.s.String(), enc, a.headers, a.created, a.expires) return nil } @@ -165,14 +172,14 @@ func (a *asymmSigner) signSignature(pKey crypto.PrivateKey, s string) (string, e } func (a *asymmSigner) signatureString(r *http.Request) (string, error) { - return signatureString(r.Header, a.headers, addRequestTarget(r)) + return signatureString(r.Header, a.headers, addRequestTarget(r), a.created, a.expires) } func (a *asymmSigner) signatureStringResponse(r http.ResponseWriter) (string, error) { - return signatureString(r.Header(), a.headers, requestTargetNotPermitted) + return signatureString(r.Header(), a.headers, requestTargetNotPermitted, a.created, a.expires) } -func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc string, headers []string) { +func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc string, headers []string, created int64, expires int64) { if len(headers) == 0 { headers = defaultHeaders } @@ -195,6 +202,34 @@ func setSignatureHeader(h http.Header, targetHeader, prefix, pubKeyId, algo, enc b.WriteString("hs2019") //real algorithm is hidden, see newest version of spec draft b.WriteString(parameterValueDelimiter) b.WriteString(parameterSeparater) + + hasCreated := false + hasExpires := false + for _, h := range headers { + val := strings.ToLower(h) + if val == "(" + createdKey + ")" { + hasCreated = true + } else if val == "(" + expiresKey + ")" { + hasExpires = true + } + } + + // Created + if hasCreated == true { + b.WriteString(createdKey) + b.WriteString(parameterKVSeparater) + b.WriteString(strconv.FormatInt(created, 10)) + b.WriteString(parameterSeparater) + } + + // Expires + if hasExpires == true { + b.WriteString(expiresKey) + b.WriteString(parameterKVSeparater) + b.WriteString(strconv.FormatInt(expires, 10)) + b.WriteString(parameterSeparater) + } + // Headers b.WriteString(headersParameter) b.WriteString(parameterKVSeparater) @@ -237,7 +272,7 @@ func addRequestTarget(r *http.Request) func(b *bytes.Buffer) error { } } -func signatureString(values http.Header, include []string, requestTargetFn func(b *bytes.Buffer) error) (string, error) { +func signatureString(values http.Header, include []string, requestTargetFn func(b *bytes.Buffer) error, created int64, expires int64) (string, error) { if len(include) == 0 { include = defaultHeaders } @@ -249,6 +284,20 @@ func signatureString(values http.Header, include []string, requestTargetFn func( if err != nil { return "", err } + } else if i == "(" + expiresKey + ")" { + if expires == 0 { + return "", fmt.Errorf("mssing expires value") + } + b.WriteString(i) + b.WriteString(headerFieldDelimiter) + b.WriteString(strconv.FormatInt(expires, 10)) + } else if i == "(" + createdKey + ")" { + if created == 0 { + return "", fmt.Errorf("mssing created value") + } + b.WriteString(i) + b.WriteString(headerFieldDelimiter) + b.WriteString(strconv.FormatInt(created, 10)) } else { hv, ok := values[textproto.CanonicalMIMEHeaderKey(i)] if !ok { diff --git a/verifying.go b/verifying.go index a70d4f7..754bded 100644 --- a/verifying.go +++ b/verifying.go @@ -1,11 +1,14 @@ package httpsig import ( + "strconv" "crypto" "encoding/base64" "fmt" "net/http" "strings" + "time" + "errors" ) var _ Verifier = &verifier{} @@ -14,16 +17,44 @@ type verifier struct { header http.Header kId string signature string + created int64 + expires int64 headers []string - sigStringFn func(http.Header, []string) (string, error) + sigStringFn func(http.Header, []string, int64, int64) (string, error) } -func newVerifier(h http.Header, sigStringFn func(http.Header, []string) (string, error)) (*verifier, error) { +func (v *verifier) Signature() string { + return v.signature +} + +func (v *verifier) Created() int64 { + return v.created +} + +func (v *verifier) Expires() int64 { + return v.expires +} + +func newVerifier(h http.Header, sigStringFn func(http.Header, []string, int64, int64) (string, error)) (*verifier, error) { scheme, s, err := getSignatureScheme(h) if err != nil { return nil, err } - kId, sig, headers, 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 + 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 + now := time.Now().Unix() + if now - expires > 10 { + return nil, errors.New("signature expired") + } + } if err != nil { return nil, err } @@ -31,6 +62,8 @@ func newVerifier(h http.Header, sigStringFn func(http.Header, []string) (string, header: h, kId: kId, signature: sig, + created: created, + expires: expires, headers: headers, sigStringFn: sigStringFn, }, nil @@ -57,7 +90,7 @@ func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) error { if !ok { return fmt.Errorf("public key for MAC verifying must be of type []byte") } - signature, err := v.sigStringFn(v.header, v.headers) + signature, err := v.sigStringFn(v.header, v.headers, v.created, v.expires) if err != nil { return err } @@ -75,7 +108,7 @@ func (v *verifier) macVerify(m macer, pKey crypto.PublicKey) error { } func (v *verifier) asymmVerify(s signer, pKey crypto.PublicKey) error { - toHash, err := v.sigStringFn(v.header, v.headers) + toHash, err := v.sigStringFn(v.header, v.headers, v.created, v.expires) if err != nil { return err } @@ -116,7 +149,7 @@ func getSignatureScheme(h http.Header) (scheme SignatureScheme, val string, err } } -func getSignatureComponents(scheme SignatureScheme, s string) (kId, sig string, headers []string, 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) } @@ -132,6 +165,16 @@ func getSignatureComponents(scheme SignatureScheme, s string) (kId, sig string, switch k { case keyIdParameter: kId = v + case createdKey: + created, err = strconv.ParseInt(v, 10, 64) + if err != nil { + return + } + case expiresKey: + expires, err = strconv.ParseInt(v, 10, 64) + if err != nil { + return + } case algorithmParameter: // Deprecated, ignore case headersParameter: