Warning
This repository is archived and no longer maintained.
This module is created to provide a simple solution to sign and verify signature in HTTP messages according to the document:
https://tools.ietf.org/html/draft-ietf-httpbis-message-signatures-00
Since the current standard is still in draft mode and will have a few iterations (versions) before becoming stable, the project is going to maintain current and future versions.
To be compatible with ietf.org versioning the project will change only MINOR & PATCH versions, until document final release. A MINOR version will be equal to the draft version. A PATCH version will be used for bug fixes & improvements and will not break backward compatibility with IETF version.
For example:
| The Document version | Httpsignatures.go |
|---|---|
| draft-ietf-httpbis-message-signatures-00 | v0.0.1 |
| draft-ietf-httpbis-message-signatures-{MINOR} | v0.{MINOR}.0 |
| Final release | v1.0.0 |
To install the module:
go get github.com/igor-pavlenko/httpsignatures-go
To install a specific version, use:
go get github.com/igor-pavlenko/httpsignatures-go@v0.0.14
Don't forget: export GO111MODULE=on
package main
import (
"fmt""github.com/igor-pavlenko/httpsignatures-go""net/http""strings"
)
funcmain() {
constsKey="key1"// Don't put keys into code, neither push it in to git repo (this is just for example)secrets:=map[string]httpsignatures.Secret{
sKey: {
KeyID: sKey,
PublicKey: `-----BEGIN PUBLIC KEY----------END PUBLIC KEY-----`,
PrivateKey: `-----BEGIN RSA PRIVATE KEY----------END RSA PRIVATE KEY-----`,
Algorithm: "RSA-SHA256",
},
}
ss:=httpsignatures.NewSimpleSecretsStorage(secrets)
hs:=httpsignatures.NewHTTPSignatures(ss)
hs.SetDefaultSignatureHeaders([]string{"(created)", "digest", "(expires)", "(request-target)"})
r, _:=http.NewRequest(
"POST",
"https://example.com/foo?param=value&pet=dog",
strings.NewReader(`{"hello": "world"}`),
)
err:=hs.Sign(sKey, r)
iferr!=nil {
panic(err)
}
fmt.Println(r.Header.Get("Digest"))
fmt.Println(r.Header.Get("Signature"))
}package main
import (
"fmt""github.com/igor-pavlenko/httpsignatures-go""net/http""strings"
)
funcmain() {
constsKey="key1"// Don't put keys into code, neither push it in to git repo (this is just for example)secrets:=map[string]httpsignatures.Secret{
sKey: {
KeyID: sKey,
PublicKey: `-----BEGIN PUBLIC KEY----------END PUBLIC KEY-----`,
PrivateKey: `-----BEGIN RSA PRIVATE KEY----------END RSA PRIVATE KEY-----`,
Algorithm: "RSA-SHA256",
},
}
ss:=httpsignatures.NewSimpleSecretsStorage(secrets)
hs:=httpsignatures.NewHTTPSignatures(ss)
r, _:=http.NewRequest(
"POST",
"https://example.com/foo?param=value&pet=dog",
strings.NewReader(`{"hello": "world"}`),
)
r.Header.Set("Digest", "SHA-512=WZDPaVn/7XgHaAy8pmojAkGWoRx2UFChF41A2svX+TaPm+AbwAgBWnrIiYllu7BNNyealdVLvRwEmTHWXvJwew==")
r.Header.Set("Signature", `keyId="key1",algorithm="RSA-SHA256",created=1594222776,headers="(created) digest (request-target)",signature="HobdANH0pDuVm9ag0Zdy06+1wgPttgSqJIiBI0wmgILrJ3IlZ26KuHPGNTZs2N55SFHCpE1gLnmyKJwLF46hmgdElB7zFreYAGmNhukguoIiQ8slZnOjs2GtZ40kHa+7kO5mqT+i5GaRKwBtRiiFe3nEPxEmrugXEwj5j6DEvl8="`)
err:=hs.Verify(r)
iferr!=nil {
panic(err)
}
fmt.Println("Signature verified")
}If you have a lot of keys, you can get them from any external storage, for example: DB, Files, Vaults etc.
Just implement Secrets interface and inject it into httpsignatures.NewHTTPSignatures().
package main
import (
"fmt""github.com/igor-pavlenko/httpsignatures-go""io/ioutil""os""regexp"
)
// To create your own secrets storage implement the httpsignatures.Secrets interface// type Secrets interface {// Get(keyID string) (Secret, error)// }constalg="RSA-SHA512"// SimpleSecretsStorage local static secrets storagetypeFileSecretsStoragestruct {
dirstringstoragemap[string]httpsignatures.Secret
}
// Get get secret from local files by KeyIDfunc (sFileSecretsStorage) Get(keyIDstring) (httpsignatures.Secret, error) {
ifsecret, ok:=s.storage[keyID]; ok {
returnsecret, nil
}
validKeyID, err:=regexp.Match(`[a-zA-Z0-9]+`, []byte(keyID))
if!validKeyID {
return httpsignatures.Secret{}, &httpsignatures.SecretError{Message: "wrong keyID format allowed: [a-zA-Z0-9]+"}
}
publicKeyFile:=fmt.Sprintf("%s/%s.pub", s.dir, keyID)
publicKey, err:=s.readFile(publicKeyFile)
iferr!=nil {
return httpsignatures.Secret{}, &httpsignatures.SecretError{Message: "public key file not found", Err: err}
}
privateKeyFile:=fmt.Sprintf("%s/%s.key", s.dir, keyID)
privateKey, err:=s.readFile(privateKeyFile)
iferr!=nil {
return httpsignatures.Secret{}, &httpsignatures.SecretError{Message: "private key file not found", Err: err}
}
fmt.Println(privateKey, publicKey)
s.storage[keyID] = httpsignatures.Secret{
KeyID: keyID,
PublicKey: publicKey,
PrivateKey: privateKey,
Algorithm: alg,
}
returns.storage[keyID], nil
}
// Get key from filefunc (sFileSecretsStorage) readFile(fstring) (string, error) {
if!s.fileExists(f) {
return"", &httpsignatures.SecretError{Message: fmt.Sprintf("file '%s' not found", f)}
}
key, err:=ioutil.ReadFile(f)
iferr!=nil {
return"", &httpsignatures.SecretError{Message: fmt.Sprintf("read file error: '%s'", f), Err: err}
}
returnstring(key), nil
}
// Check if file existsfunc (sFileSecretsStorage) fileExists(fstring) bool {
i, err:=os.Stat(f)
ifos.IsNotExist(err) {
returnfalse
}
return!i.IsDir()
}
// NewSimpleSecretsStorage create new digestfuncNewFileSecretsStorage(dirstring) httpsignatures.Secrets {
iflen(dir) ==0 {
returnnil
}
s:=new(FileSecretsStorage)
s.dir=dirs.storage=make(map[string]httpsignatures.Secret)
returns
}
funcmain() {
hs:=httpsignatures.NewHTTPSignatures(NewFileSecretsStorage("/tmp"))
hs.SetDefaultExpiresSeconds(10)
}It's good practice to store private/public keys in secrets storage like AWS Secrets Manager, Vault by HashiCorp, or any other service. So you need to get keys by request.
Some use cases, service used to:
- validate incoming requests from other services (it needs only public keys)
- sign self outgoing requests (signed by itself. So it needs only self private key)
- sign outgoing requests on behalf of other services (it needs all private keys of served services)
- validate other service requests & sign self requests (it needs access to self private keys & only public keys of served services)
Keys should be stored as binary. Name pattern: "///<PrivateKey|PublicKey|Algorithm>". Where — environment (for example: prod, dev, sandbox, staging etc), — service identifier used as KeyID in requests, <PrivateKey|PublicKey|Algorithm> — key type, can be only PrivateKey, PublicKey, Algorithm.
aws secretsmanager create-secret --name "/dev/myServiceID/PrivateKey" \
--description "Private Key for service with keyID = myServiceID" \
--secret-binary file://private.key
aws secretsmanager create-secret --name "/dev/myServiceID/PublicKey" \
--description "Public Key for service with keyID = myServiceID" \
--secret-binary file://public.pub
# In case services use different signature algorithms, store it also in Secrets Manager
# If you have only one algorithm for all services, set it as a parameter (see below).
aws secretsmanager create-secret --name "/dev/myServiceID/Algorithm" \
--description "Algorithm for service with keyID = myServiceID" \
--secret-binary file://algorithm.txt
If you have only one algorithm for all services, set it as a parameter and do not store the algorithm name in Secrets Manager:
//...sm:=NewAwsSecretsManagerStorage("prod", secretsManager)
sm.SetAlgorithm("RSA-SHA512")
//...To validate incoming requests you need only PublicKey. PrivateKey & Algorithm can be omitted:
//...sm:=NewAwsSecretsManagerStorage("prod", secretsManager)
// Omit Algorithm sm.SetAlgorithm("RSA-SHA512")
// To skip private keys for all services, you have to define not empty map with "*" KeyID and set it to falsesm.SetRequiredPrivateKeys(map[string]bool{"*": false})
//...To sign outgoing requests you need only PrivateKey. PublicKey & Algorithm can be omitted:
//...sm:=NewAwsSecretsManagerStorage("prod", secretsManager)
// Omit Algorithm sm.SetAlgorithm("RSA-SHA512")
// To skip public keys for all services, you have to define not empty map with "*" KeyID and set it to falsesm.SetRequiredPublicKeys(map[string]bool{"*": false})
//...To sign self outgoing requests you need only PrivateKey. PublicKey & Algorithm can be omitted. To validate other services incoming requests you need only PublicKeys, PrivateKeys & Algorithms can be omitted:
//...sm:=NewAwsSecretsManagerStorage("prod", secretsManager)
// Omit Algorithm sm.SetAlgorithm("RSA-SHA512")
// Set required PrivateKey only for service with keyID = MyselfKeyID (current service).// You don't need PrivateKeys to validate incoming requests (and you don't have permissions to get PrivateKeys)sm.SetRequiredPrivateKeys(map[string]bool{"MyselfKeyID": true})
// You don't need self PublicKey, but PublicKeys of other services are required.sm.SetRequiredPublicKeys(map[string]bool{"MyselfKeyID": false})
//...You can set your custom signature hash algorithm by implementing the DigestHashAlgorithm interface.
package main
import (
"crypto/sha1""crypto/subtle""fmt""github.com/igor-pavlenko/httpsignatures-go"
)
// To create new digest algorithm, implement httpsignatures.DigestHashAlgorithm interface// type DigestHashAlgorithm interface {// Algorithm() string// Create(data []byte) ([]byte, error)// Verify(data []byte, digest []byte) error// }// Digest algorithm nameconstalgSha1Name="sha1"// algSha1 sha1 AlgorithmtypealgSha1struct{}
// Return algorithm namefunc (aalgSha1) Algorithm() string {
returnalgSha1Name
}
// Create hashfunc (aalgSha1) Create(data []byte) ([]byte, error) {
h:=sha1.New()
_, err:=h.Write(data)
iferr!=nil {
returnnil, &httpsignatures.CryptoError{Message: "error creating hash", Err: err}
}
returnh.Sum(nil), nil
}
// Verify hashfunc (aalgSha1) Verify(data []byte, digest []byte) error {
expected, err:=a.Create(data)
iferr!=nil {
returnerr
}
ifsubtle.ConstantTimeCompare(digest, expected) !=1 {
return&httpsignatures.CryptoError{Message: "wrong hash"}
}
returnnil
}
funcmain() {
hs:=httpsignatures.NewHTTPSignatures(httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{}))
// Add algorithm implementationhs.SetDigestAlgorithm(algSha1{})
// Set `algSha1Name` as default algorithm for digesterr:=hs.SetDefaultDigestAlgorithm(algSha1Name)
iferr!=nil {
fmt.Println(err)
}
}Choose one of supported digest hash algorithms with method SetDefaultDigestAlgorithm.
hs:=httpsignatures.NewHTTPSignatures(httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{}))
hs.SetDefaultDigestAlgorithm("MD5")If digest header set in signature headers — module will verify it. To disable verification use SetDefaultVerifyDigest
method.
hs:=httpsignatures.NewHTTPSignatures(httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{}))
hs.SetDefaultVerifyDigest(false)You can set your own custom signature hash algorithm by implementing the SignatureHashAlgorithm interface.
package main
import (
"crypto/hmac""crypto/sha1""github.com/igor-pavlenko/httpsignatures-go"
)
// To create your own signature hash algorithm, implement httpsignatures.SignatureHashAlgorithm interface// type SignatureHashAlgorithm interface {// Algorithm() string// Create(secret Secret, data []byte) ([]byte, error)// Verify(secret Secret, data []byte, signature []byte) error// }// Digest algorithm nameconstalgHmacSha1Name="HMAC-SHA1"// algHmacSha1 HMAC-SHA1 AlgorithmtypealgHmacSha1struct{}
// Return algorithm namefunc (aalgHmacSha1) Algorithm() string {
returnalgHmacSha1Name
}
// Create hashfunc (aalgHmacSha1) Create(secret httpsignatures.Secret, data []byte) ([]byte, error) {
iflen(secret.PrivateKey) ==0 {
returnnil, &httpsignatures.CryptoError{Message: "no private key found"}
}
mac:=hmac.New(sha1.New, []byte(secret.PrivateKey))
_, err:=mac.Write(data)
iferr!=nil {
returnnil, &httpsignatures.CryptoError{Message: "error creating signature", Err: err}
}
returnmac.Sum(nil), nil
}
// Verify hashfunc (aalgHmacSha1) Verify(secret httpsignatures.Secret, data []byte, signature []byte) error {
expected, err:=a.Create(secret, data)
iferr!=nil {
returnerr
}
if!hmac.Equal(signature, expected) {
return&httpsignatures.CryptoError{Message: "wrong signature"}
}
returnnil
}
funcmain() {
hs:=httpsignatures.NewHTTPSignatures(httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{}))
hs.SetSignatureHashAlgorithm(algHmacSha1{})
}By default, signature will expire in 30 seconds. You can set custom value for expiration using
SetDefaultExpiresSeconds method.
hs:=httpsignatures.NewHTTPSignatures(httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{}))
hs.SetDefaultExpiresSeconds(60)Default time gap is 10 seconds. To set custom time gap use SetDefaultTimeGap method.
hs:=httpsignatures.NewHTTPSignatures(httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{}))
hs.SetDefaultTimeGap(100)By default, headers used in signature: ["(created)"]. Use SetDefaultSignatureHeaders method to set custom headers
list.
hs:=httpsignatures.NewHTTPSignatures(httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{}))
hs.SetDefaultSignatureHeaders([]string{"(request-target)", "(created)", "(expires)", "date", "host", "digest"})- RSASSA-PSS with SHA256
- RSASSA-PSS with SHA512
- ECDSA with SHA256
- ECDSA with SHA512
- RSA-SHA256
- RSA-SHA512
- HMAC-SHA256
- HMAC-SHA512
- ED25519
- MD5
- SHA256
- SHA512
Look at examples & tests to find out how to work with lib.
- Gin plugin
- AwsSecretsManagerStorage plugin