Skip to content

Signing HTTP Messages with http-signatures Golang library

License

Notifications You must be signed in to change notification settings

igor-pavlenko/httpsignatures-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sign/Verify signature with http-signatures golang lib

Linter & Tests Codecov Go Report Card Quality Gate Status

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

Versions compatibility

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

Installation

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

Sign

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"))
}

Verify

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")
}

Settings

Custom Secrets Storage

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)
}

AWS Secrets Manager Storage

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)

How to store keys in Secrets Manager

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")
//...

Validate incoming requests

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})
//...

Sign self outgoing requests or sign outgoing requests on behalf of other services

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})
//...

Validate other service requests & sign self requests

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})
//...

Custom Digest hash algorithm

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)
	}
}

Default Digest algorithm

Choose one of supported digest hash algorithms with method SetDefaultDigestAlgorithm.

hs := httpsignatures.NewHTTPSignatures(httpsignatures.NewSimpleSecretsStorage(map[string]httpsignatures.Secret{}))
hs.SetDefaultDigestAlgorithm("MD5")

Disable/Enable verify Digest function

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)

Custom Signature hash algorithm

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{})
}

Default expires seconds

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 for expires/created time verification

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)

Default signature headers

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"})

Supported Signature hash algorithms

  • RSASSA-PSS with SHA256
  • RSASSA-PSS with SHA512
  • ECDSA with SHA256
  • ECDSA with SHA512
  • RSA-SHA256
  • RSA-SHA512
  • HMAC-SHA256
  • HMAC-SHA512
  • ED25519

Supported Digest hash algorithms

  • MD5
  • SHA256
  • SHA512

Examples

Look at examples & tests to find out how to work with lib.

Todo