httpsign

package
v3.0.0+incompatible Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 15, 2019 License: Apache-2.0 Imports: 15 Imported by: 0

README

httpsign

library for signing and authenticating HTTP requests between web services.

Overview

An keyed-hash message authentication code (HMAC) is used to provide integrity and authenticity of a message between web services. The following elements are input into the HMAC. Only the items in bold are required to be passed in by the user, the other elements are either optional or build by httpsign for you.

  • Shared secret, a randomly generated number from a CSPRNG.
  • Timestamp in epoch time (number of seconds since January 1, 1970 UTC).
  • Nonce, a randomly generated number from a CSPRNG.
  • Request body.
  • Optionally the HTTP Verb and HTTP Request URI.
  • Optionally an additional headers to sign.

Each request element is delimited with the character | and each request element is preceded by its length. A simple example with only the required parameters:

shared_secret = '042DAD12E0BE4625AC0B2C3F7172DBA8'
timestamp     = '1330837567'
nonce         = '000102030405060708090a0b0c0d0e0f'
request_body  = '{"hello": "world"}'

signature     = HMAC('042DAD12E0BE4625AC0B2C3F7172DBA8',
   '10|1330837567|32|000102030405060708090a0b0c0d0e0f|18|{"hello": "world"}')

The timestamp, nonce, signature, and signature version are set as headers for the HTTP request to be signed. They are then verified on the receiving side by running the same algorithm and verifying that the signatures match.

Note: By default the service can securely handle authenticating 5,000 requests per second. If you need to authenticate more, increase the capacity of the nonce cache when initializing the package.

Examples

Signing a Request

import (
    "github.com/mailgun/holster/httpsign"
    "github.com/mailgun/holster"
    "github.com/mailgun/holster/httpsign"
    "github.com/mailgun/holster/random"
    "github.com/mailgun/holster/secret"
)
// For consistency during tests, OMIT THIS LINE IN PRODUCTION
secret.RandomProvider = &random.FakeRNG{}

// Create a new randomly generated key
key, err := secret.NewKey()
// Store the key on disk for retrieval later
fd, err := os.Create("/tmp/test-secret.key")
if err != nil {
    panic(err)
}
fd.Write([]byte(secret.KeyToEncodedString(key)))
fd.Close()

auths, err := httpsign.New(&httpsign.Config{
    // Our pre-generated shared key
    KeyPath: "/tmp/test-secret.key",
    // Optionally include headers in the signed request
    HeadersToSign: []string{"X-Mailgun-Header"},
    // Optionally include the HTTP Verb and URI in the signed request
    SignVerbAndURI: true,
    // For consistency during tests, OMIT THESE 2 LINES IN PRODUCTION
    Clock:  &holster.FrozenClock{CurrentTime: time.Unix(1330837567, 0)},
    Random: &random.FakeRNG{},
})
if err != nil {
    panic(err)
}

// Build new request
r, _ := http.NewRequest("POST", "", strings.NewReader(`{"hello":"world"}`))
// Add our custom header that is included in the signature
r.Header.Set("X-Mailgun-Header", "nyan-cat")

// Sign the request
err = auths.SignRequest(r)
if err != nil {
    panic(err)
}

// Preform the request
// client := &http.Client{}
// response, _ := client.Do(r)

fmt.Printf("%s: %s\n", httpsign.XMailgunNonce, r.Header.Get(httpsign.XMailgunNonce))
fmt.Printf("%s: %s\n", httpsign.XMailgunTimestamp, r.Header.Get(httpsign.XMailgunTimestamp))
fmt.Printf("%s: %s\n", httpsign.XMailgunSignature, r.Header.Get(httpsign.XMailgunSignature))
fmt.Printf("%s: %s\n", httpsign.XMailgunSignatureVersion, r.Header.Get(httpsign.XMailgunSignatureVersion))

// Output: X-Mailgun-Nonce: 000102030405060708090a0b0c0d0e0f
// X-Mailgun-Timestamp: 1330837567
// X-Mailgun-Signature: 33f589de065a81b671c9728e7c6b6fecfb94324cb10472f33dc1f78b2a9e4fee
// X-Mailgun-Signature-Version: 2

Authenticating a Request

import (
    "github.com/mailgun/holster"
    "github.com/mailgun/holster/httpsign"
    "github.com/mailgun/holster/random"
    "github.com/mailgun/holster/secret"
)

// For consistency during tests, OMIT THIS LINE IN PRODUCTION
secret.RandomProvider = &random.FakeRNG{}

// Create a new randomly generated key
key, err := secret.NewKey()
// Store the key on disk for retrieval later
fd, err := os.Create("/tmp/test-secret.key")
if err != nil {
    panic(err)
}
fd.Write([]byte(secret.KeyToEncodedString(key)))
fd.Close()

// When authenticating a request, the config must match that of the signing code
auths, err := httpsign.New(&httpsign.Config{
    // Our pre-generated shared key
    KeyPath: "/tmp/test-secret.key",
    // Include headers in the signed request
    HeadersToSign: []string{"X-Mailgun-Header"},
    // Include the HTTP Verb and URI in the signed request
    SignVerbAndURI: true,
    // For consistency during tests, OMIT THESE 2 LINES IN PRODUCTION
    Clock:  &holster.FrozenClock{CurrentTime: time.Unix(1330837567, 0)},
    Random: &random.FakeRNG{},
})
if err != nil {
    panic(err)
}

// Pretend we received a new signed request
r, _ := http.NewRequest("POST", "", strings.NewReader(`{"hello":"world"}`))
// Add our custom header that is included in the signature
r.Header.Set("X-Mailgun-Header", "nyan-cat")

// These are the fields set by the client signing the request
r.Header.Set("X-Mailgun-Nonce", "000102030405060708090a0b0c0d0e0f")
r.Header.Set("X-Mailgun-Timestamp", "1330837567")
r.Header.Set("X-Mailgun-Signature", "33f589de065a81b671c9728e7c6b6fecfb94324cb10472f33dc1f78b2a9e4fee")
r.Header.Set("X-Mailgun-Signature-Version", "2")

// Verify the request
err = auths.AuthenticateRequest(r)
if err != nil {
    panic(err)
}

fmt.Printf("Request Verified\n")

// Output: Request Verified

Documentation

Overview

Provides tools for signing and authenticating HTTP requests between web services

An keyed-hash message authentication code (HMAC) is used to provide integrity and authenticity of a message between web services. The following elements are input into the HMAC. Only the items in bold are required to be passed in by the user, the other elements are either optional or build by httpsign for you.

Each request element is delimited with the character `|` and each request element is preceded by its length. A simple example with only the required parameters:

// Randomly generated number from a CSPRNG.
shared_secret = '042DAD12E0BE4625AC0B2C3F7172DBA8'
// Epoch time (number of seconds since January 1, 1970 UTC).
timestamp     = '1330837567'
// Randomly generated number from a CSPRNG.
nonce         = '000102030405060708090a0b0c0d0e0f'
// Request body
request_body  = '{"hello": "world"}'
// Optionally the HTTP Verb and HTTP Request URI.
// Optionally an additional headers to sign.

signature     = HMAC('042DAD12E0BE4625AC0B2C3F7172DBA8',
  '10|1330837567|32|000102030405060708090a0b0c0d0e0f|18|{"hello": "world"}')

The timestamp, nonce, signature, and signature version are set as headers for the HTTP request to be signed. They are then verified on the receiving side by running the same algorithm and verifying that the signatures match.

Note: By default the service can securely handle authenticating 5,000 requests per second. If you need to authenticate more, increase the capacity of the nonce cache when initializing the package.

Index

Examples

Constants

View Source
const CacheCapacity = 5000 * CacheTimeout

5,000 msg/sec * 100 sec = 500,000 elements

View Source
const CacheTimeout = 100

100 sec

View Source
const MaxSkewSec = 5

5 sec

View Source
const XMailgunNonce = "X-Mailgun-Nonce"
View Source
const XMailgunSignature = "X-Mailgun-Signature"
View Source
const XMailgunSignatureVersion = "X-Mailgun-Signature-Version"
View Source
const XMailgunTimestamp = "X-Mailgun-Timestamp"

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// KeyPath is a path to a file that contains the key to sign requests. If
	// it is an empty string then the key should be provided in `KeyBytes`.
	KeyPath string
	// KeyBytes is a key that is used by lemma to sign requests. Ignored if
	// `KeyPath` is not an empty string.
	KeyBytes []byte
	// List of headers to sign
	HeadersToSign []string
	// Include the http verb and uri in request
	SignVerbAndURI bool
	// Capacity of the nonce cache
	NonceCacheCapacity int
	// Nonce cache timeout
	NonceCacheTimeout int
	// Clock to use when signing
	Clock holster.Clock
	// Random Number Generator to use when signing
	Random random.RandomProvider
	// Toggle emitting metrics or not
	EmitStats bool
	// Hostname of statsd server
	StatsdHost string
	// Port of statsd server
	StatsdPort int
	// Prefix to prepend to metrics
	StatsdPrefix string
	// Default: X-Mailgun-Nonce
	NonceHeaderName string
	// Default: X-Mailgun-Timestamp
	TimestampHeaderName string
	// Default: X-Mailgun-Signature
	SignatureHeaderName string
	// Default: X-Mailgun-Signature-Version
	SignatureVersionHeaderName string
}

Modify NonceCacheCapacity and NonceCacheTimeout if your service needs to authenticate more than 5,000 requests per second. For example, if you need to handle 10,000 requests per second and timeout after one minute, you may want to set NonceCacheTimeout to 60 and NonceCacheCapacity to 10000 * cacheTimeout = 600000.

type NonceCache

type NonceCache struct {
	sync.Mutex
	// contains filtered or unexported fields
}

func NewNonceCache

func NewNonceCache(capacity int, cacheTTL int, clock holster.Clock) *NonceCache

Return a new NonceCache. Allows you to control cache capacity and ttl

func (*NonceCache) InCache

func (n *NonceCache) InCache(nonce string) bool

InCache checks if a nonce is in the cache. If not, it adds it to the cache and returns false. Otherwise it returns true.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Represents a service that can be used to sign and authenticate requests.

func New

func New(config *Config) (*Service, error)

Return a new Service. Config can not be nil. If you need control over setting time and random providers, use NewWithProviders.

func NewWithProviders

func NewWithProviders(config *Config, clock holster.Clock,
	randomProvider random.RandomProvider) (*Service, error)

Returns a new Service. Provides control over time and random providers.

func (*Service) AuthenticateRequest

func (s *Service) AuthenticateRequest(r *http.Request) error

Authenticates HTTP request to ensure it was sent by an authorized sender.

Example
package main

import (
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/mailgun/holster"
	"github.com/mailgun/holster/httpsign"
	"github.com/mailgun/holster/random"
	"github.com/mailgun/holster/secret"
)

func main() {
	// For consistency during tests, OMIT THIS LINE IN PRODUCTION
	secret.RandomProvider = &random.FakeRNG{}

	// Create a new randomly generated key
	key, err := secret.NewKey()
	// Store the key on disk for retrieval later
	fd, err := os.Create("/tmp/test-secret.key")
	if err != nil {
		panic(err)
	}
	fd.Write([]byte(secret.KeyToEncodedString(key)))
	fd.Close()

	// When authenticating a request, the config must match that of the signing code
	auths, err := httpsign.New(&httpsign.Config{
		// Our pre-generated shared key
		KeyPath: "/tmp/test-secret.key",
		// Include headers in the signed request
		HeadersToSign: []string{"X-Mailgun-Header"},
		// Include the HTTP Verb and URI in the signed request
		SignVerbAndURI: true,
		// For consistency during tests, OMIT THESE 2 LINES IN PRODUCTION
		Clock:  &holster.FrozenClock{CurrentTime: time.Unix(1330837567, 0)},
		Random: &random.FakeRNG{},
	})
	if err != nil {
		panic(err)
	}

	// Pretend we received a new signed request
	r, _ := http.NewRequest("POST", "", strings.NewReader(`{"hello":"world"}`))
	// Add our custom header that is included in the signature
	r.Header.Set("X-Mailgun-Header", "nyan-cat")

	// These are the fields set by the client signing the request
	r.Header.Set("X-Mailgun-Nonce", "000102030405060708090a0b0c0d0e0f")
	r.Header.Set("X-Mailgun-Timestamp", "1330837567")
	r.Header.Set("X-Mailgun-Signature", "33f589de065a81b671c9728e7c6b6fecfb94324cb10472f33dc1f78b2a9e4fee")
	r.Header.Set("X-Mailgun-Signature-Version", "2")

	// Verify the request
	err = auths.AuthenticateRequest(r)
	if err != nil {
		panic(err)
	}

	fmt.Printf("Request Verified\n")

}
Output:

Request Verified

func (*Service) AuthenticateRequestWithKey

func (s *Service) AuthenticateRequestWithKey(r *http.Request, secretKey []byte) (err error)

Authenticates HTTP request to ensure it was sent by an authorized sender. Checks message signature with the passed in key, not the one initialized with.

func (*Service) CheckTimestamp

func (s *Service) CheckTimestamp(timestampHeader string) (bool, error)

Parses a timestamp header and returns true if the timestamp is neither older than the TTL or is from the future.

func (*Service) SignRequest

func (s *Service) SignRequest(r *http.Request) error

Signs a given HTTP request with signature, nonce, and timestamp.

Example
package main

import (
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/mailgun/holster"
	"github.com/mailgun/holster/httpsign"
	"github.com/mailgun/holster/random"
	"github.com/mailgun/holster/secret"
)

func main() {
	// For consistency during tests, OMIT THIS LINE IN PRODUCTION
	secret.RandomProvider = &random.FakeRNG{}

	// Create a new randomly generated key
	key, err := secret.NewKey()
	// Store the key on disk for retrieval later
	fd, err := os.Create("/tmp/test-secret.key")
	if err != nil {
		panic(err)
	}
	fd.Write([]byte(secret.KeyToEncodedString(key)))
	fd.Close()

	auths, err := httpsign.New(&httpsign.Config{
		// Our pre-generated shared key
		KeyPath: "/tmp/test-secret.key",
		// Optionally include headers in the signed request
		HeadersToSign: []string{"X-Mailgun-Header"},
		// Optionally include the HTTP Verb and URI in the signed request
		SignVerbAndURI: true,
		// For consistency during tests, OMIT THESE 2 LINES IN PRODUCTION
		Clock:  &holster.FrozenClock{CurrentTime: time.Unix(1330837567, 0)},
		Random: &random.FakeRNG{},
	})
	if err != nil {
		panic(err)
	}

	// Build new request
	r, _ := http.NewRequest("POST", "", strings.NewReader(`{"hello":"world"}`))
	// Add our custom header that is included in the signature
	r.Header.Set("X-Mailgun-Header", "nyan-cat")

	// Sign the request
	err = auths.SignRequest(r)
	if err != nil {
		panic(err)
	}

	// Preform the request
	// client := &http.Client{}
	// response, _ := client.Do(r)

	fmt.Printf("%s: %s\n", httpsign.XMailgunNonce, r.Header.Get(httpsign.XMailgunNonce))
	fmt.Printf("%s: %s\n", httpsign.XMailgunTimestamp, r.Header.Get(httpsign.XMailgunTimestamp))
	fmt.Printf("%s: %s\n", httpsign.XMailgunSignature, r.Header.Get(httpsign.XMailgunSignature))
	fmt.Printf("%s: %s\n", httpsign.XMailgunSignatureVersion, r.Header.Get(httpsign.XMailgunSignatureVersion))

}
Output:

X-Mailgun-Nonce: 000102030405060708090a0b0c0d0e0f
X-Mailgun-Timestamp: 1330837567
X-Mailgun-Signature: 33f589de065a81b671c9728e7c6b6fecfb94324cb10472f33dc1f78b2a9e4fee
X-Mailgun-Signature-Version: 2

func (*Service) SignRequestWithKey

func (s *Service) SignRequestWithKey(r *http.Request, secretKey []byte) error

Signs a given HTTP request with signature, nonce, and timestamp. Signs the message with the passed in key not the one initialized with.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL