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"
)
func main() {
const sKey = "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)
if err != 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"
)
func main() {
const sKey = "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)
if err != 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)
// }
const alg = "RSA-SHA512"
// SimpleSecretsStorage local static secrets storage
type FileSecretsStorage struct {
dir string
storage map[string]httpsignatures.Secret
}
// Get get secret from local files by KeyID
func (s FileSecretsStorage) Get(keyID string) (httpsignatures.Secret, error) {
if secret, ok := s.storage[keyID]; ok {
return secret, 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)
if err != 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)
if err != 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,
}
return s.storage[keyID], nil
}
// Get key from file
func (s FileSecretsStorage) readFile(f string) (string, error) {
if !s.fileExists(f) {
return "", &httpsignatures.SecretError{Message: fmt.Sprintf("file '%s' not found", f)}
}
key, err := ioutil.ReadFile(f)
if err != nil {
return "", &httpsignatures.SecretError{Message: fmt.Sprintf("read file error: '%s'", f), Err: err}
}
return string(key), nil
}
// Check if file exists
func (s FileSecretsStorage) fileExists(f string) bool {
i, err := os.Stat(f)
if os.IsNotExist(err) {
return false
}
return !i.IsDir()
}
// NewSimpleSecretsStorage create new digest
func NewFileSecretsStorage(dir string) httpsignatures.Secrets {
if len(dir) == 0 {
return nil
}
s := new(FileSecretsStorage)
s.dir = dir
s.storage = make(map[string]httpsignatures.Secret)
return s
}
func main() {
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 false
sm.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 false
sm.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 name
const algSha1Name = "sha1"
// algSha1 sha1 Algorithm
type algSha1 struct{}
// Return algorithm name
func (a algSha1) Algorithm() string {
return algSha1Name
}
// Create hash
func (a algSha1) Create(data []byte) ([]byte, error) {
h := sha1.New()
_, err := h.Write(data)
if err != nil {
return nil, &httpsignatures.CryptoError{Message: "error creating hash", Err: err}
}
return h.Sum(nil), nil
}
// Verify hash
func (a algSha1) Verify(data []byte, digest []byte) error {
expected, err := a.Create(data)
if err != nil {
return err
}
if subtle.ConstantTimeCompare(digest, expected) != 1 {
return &httpsignatures.CryptoError{Message: "wrong hash"}
}
return nil
}
func main() {
hs := httpsignatures.NewHTTPSignatures(httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{}))
// Add algorithm implementation
hs.SetDigestAlgorithm(algSha1{})
// Set `algSha1Name` as default algorithm for digest
err := hs.SetDefaultDigestAlgorithm(algSha1Name)
if err != 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 name
const algHmacSha1Name = "HMAC-SHA1"
// algHmacSha1 HMAC-SHA1 Algorithm
type algHmacSha1 struct{}
// Return algorithm name
func (a algHmacSha1) Algorithm() string {
return algHmacSha1Name
}
// Create hash
func (a algHmacSha1) Create(secret httpsignatures.Secret, data []byte) ([]byte, error) {
if len(secret.PrivateKey) == 0 {
return nil, &httpsignatures.CryptoError{Message: "no private key found"}
}
mac := hmac.New(sha1.New, []byte(secret.PrivateKey))
_, err := mac.Write(data)
if err != nil {
return nil, &httpsignatures.CryptoError{Message: "error creating signature", Err: err}
}
return mac.Sum(nil), nil
}
// Verify hash
func (a algHmacSha1) Verify(secret httpsignatures.Secret, data []byte, signature []byte) error {
expected, err := a.Create(secret, data)
if err != nil {
return err
}
if !hmac.Equal(signature, expected) {
return &httpsignatures.CryptoError{Message: "wrong signature"}
}
return nil
}
func main() {
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