httpsign

package module
v0.1.6 Latest Latest
Warning

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

Go to latest
Published: Feb 7, 2022 License: Apache-2.0 Imports: 22 Imported by: 2

README

HTTP Message Signatures, implementing draft-ietf-httpbis-message-signatures-08.

This is a nearly feature-complete implementation of draft -08, including all test vectors.

Notes and Missing Features

  • The Accept-Signature header.
  • Inclusion of Signature and Signature-Input as trailers is optional and is not yet implemented.
  • Extracting derived components from the "related request". See related issue.
  • In responses, when using the "wrapped handler" feature, the Content-Type header is only signed if set explicitly by the server. This is different, but arguably more secure, than the normal net.http behavior.
  • One of the -08 test cases is broken.

Go Reference Test GoReportCard example

Documentation

Overview

Package httpsign signs HTTP requests and responses as defined in draft-ietf-httpbis-message-signatures. See https://www.ietf.org/archive/id/draft-ietf-httpbis-message-signatures-07.html.

For client-side message signing and verification, use the Client wrapper. Alternatively, use SignRequest, VerifyResponse directly, but this is more complicated. For server-side operation, WrapHandler installs a wrapper around a normal HTTP message handler.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetRequestSignature added in v0.1.3

func GetRequestSignature(req *http.Request, signatureName string) (string, error)

GetRequestSignature returns the base64-encoded signature, parsed from a signed request. This is useful for the request-response feature.

func RequestDetails added in v0.1.1

func RequestDetails(signatureName string, req *http.Request) (keyID, alg string, err error)

RequestDetails parses a signed request and returns the key ID and optionally the algorithm used in the given signature.

func ResponseDetails added in v0.1.1

func ResponseDetails(signatureName string, res *http.Response) (keyID, alg string, err error)

ResponseDetails parses a signed response and returns the key ID and optionally the algorithm used in the given signature.

func SignRequest

func SignRequest(signatureName string, signer Signer, req *http.Request) (signatureInputHeader, signature string, err error)

SignRequest signs an HTTP request. Returns the Signature-Input and the Signature header values.

Example
package main

import (
	"bufio"
	"bytes"
	"fmt"
	"github.com/yaronf/httpsign"
	"net/http"
	"strings"
)

func main() {
	config := httpsign.NewSignConfig().SignCreated(false).SetNonce("BADCAB") // SignCreated should be "true" to protect against replay attacks
	fields := httpsign.Headers("@authority", "Date", "@method")
	signer, _ := httpsign.NewHMACSHA256Signer("my-shared-secret", bytes.Repeat([]byte{0x77}, 64), config, fields)
	reqStr := `GET /foo HTTP/1.1
Host: example.org
Date: Tue, 20 Apr 2021 02:07:55 GMT
Cache-Control: max-age=60

`
	req, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(reqStr)))
	signatureInput, signature, _ := httpsign.SignRequest("sig77", *signer, req)
	fmt.Printf("Signature-Input: %s\n", signatureInput)
	fmt.Printf("Signature:       %s", signature)
}
Output:

Signature-Input: sig77=("@authority" "date" "@method");nonce="BADCAB";alg="hmac-sha256";keyid="my-shared-secret"
Signature:       sig77=:BBxhfE6GoDVcohZvc+pT448u7GAK7EjJYTu+i26YZW0=:

func SignResponse

func SignResponse(signatureName string, signer Signer, res *http.Response) (signatureInput, signature string, err error)

SignResponse signs an HTTP response. Returns the Signature-Input and the Signature header values.

func VerifyRequest

func VerifyRequest(signatureName string, verifier Verifier, req *http.Request) error

VerifyRequest verifies a signed HTTP request. Returns an error if verification failed for any reason, otherwise nil.

Example
package main

import (
	"bufio"
	"bytes"
	"fmt"
	"github.com/yaronf/httpsign"
	"net/http"
	"strings"
)

func main() {
	config := httpsign.NewVerifyConfig().SetVerifyCreated(false) // for testing only
	verifier, _ := httpsign.NewHMACSHA256Verifier("my-shared-secret", bytes.Repeat([]byte{0x77}, 64), config,
		httpsign.Headers("@authority", "Date", "@method"))
	reqStr := `GET /foo HTTP/1.1
Host: example.org
Date: Tue, 20 Apr 2021 02:07:55 GMT
Cache-Control: max-age=60
Signature-Input: sig77=("@authority" "date" "@method");alg="hmac-sha256";keyid="my-shared-secret"
Signature:       sig77=:3e9KqLP62NHfHY5OMG4036+U6tvBowZF35ALzTjpsf0=:

`
	req, _ := http.ReadRequest(bufio.NewReader(strings.NewReader(reqStr)))
	err := httpsign.VerifyRequest("sig77", *verifier, req)
	fmt.Printf("verified: %t", err == nil)
}
Output:

verified: true

func VerifyResponse

func VerifyResponse(signatureName string, verifier Verifier, res *http.Response) (err error)

VerifyResponse verifies a signed HTTP response. Returns an error if verification failed for any reason, otherwise nil.

func WrapHandler

func WrapHandler(h http.Handler, config *HandlerConfig) http.Handler

WrapHandler wraps a server's HTTP request handler so that the incoming request is verified and the response is signed. Both operations are optional. If config is nil, the default configuration is applied: requests are verified and responses are signed. Note: unlike the standard net.http behavior, if you want the "Content-Type" header to be signed, you should specify it explicitly.

Example (ClientSigns)
package main

import (
	"bufio"
	"bytes"
	"fmt"
	"github.com/yaronf/httpsign"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
)

func main() {
	// Callback to let the server locate its verifying key and configuration
	fetchVerifier := func(r *http.Request) (string, *httpsign.Verifier) {
		sigName := "sig1"
		verifier, _ := httpsign.NewHMACSHA256Verifier("key", bytes.Repeat([]byte{0x99}, 64), nil,
			httpsign.Headers("@method"))
		return sigName, verifier
	}

	// The basic handler (HTTP server) that gets wrapped
	simpleHandler := func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
		w.Header().Set("bar", "baz")
		fmt.Fprintln(w, "Hey client, your message verified just fine")
	}

	// Configure the wrapper and set it up
	config := httpsign.NewHandlerConfig().SetSignResponse(false).SetFetchVerifier(fetchVerifier)
	ts := httptest.NewServer(httpsign.WrapHandler(http.HandlerFunc(simpleHandler), config))
	defer ts.Close()

	// HTTP client code, with a signer
	signer, _ := httpsign.NewHMACSHA256Signer("key", bytes.Repeat([]byte{0x99}, 64), nil,
		*httpsign.NewFields().AddHeader("content-type").AddQueryParam("pet").AddHeader("@method"))

	client := httpsign.NewDefaultClient("sig1", signer, nil, nil)
	body := `{"hello": "world"}`
	host := ts.URL // test server
	path := "/foo?param=value&pet=dog"
	res, _ := client.Post(host+path, "application/json", bufio.NewReader(strings.NewReader(body)))

	serverText, _ := io.ReadAll(res.Body)
	res.Body.Close()

	fmt.Println("Status: ", res.Status)
	fmt.Println("Server sent: ", string(serverText))
}
Output:

Status:  200 OK
Server sent:  Hey client, your message verified just fine
Example (ServerSigns)
package main

import (
	"bytes"
	"fmt"
	"github.com/yaronf/httpsign"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
)

func main() {
	// Callback to let the server locate its signing key and configuration
	fetchSigner := func(res http.Response, r *http.Request) (string, *httpsign.Signer) {
		sigName := "sig1"
		signer, _ := httpsign.NewHMACSHA256Signer("key", bytes.Repeat([]byte{0}, 64), nil,
			httpsign.Headers("@status", "bar", "date", "content-type"))
		return sigName, signer
	}

	simpleHandler := func(w http.ResponseWriter, r *http.Request) { // this handler gets wrapped
		w.WriteHeader(200)
		w.Header().Set("bar", "some text here") // note: a single word in the header value would be interpreted is a trivial dictionary!
		w.Header().Set("Content-Type", "text/plain")
		fmt.Fprintln(w, "Hello, client")
	}

	// Configure the wrapper and set it up
	config := httpsign.NewHandlerConfig().SetVerifyRequest(false).SetFetchSigner(fetchSigner)
	ts := httptest.NewServer(httpsign.WrapHandler(http.HandlerFunc(simpleHandler), config))
	defer ts.Close()

	// HTTP client code
	verifier, _ := httpsign.NewHMACSHA256Verifier("key", bytes.Repeat([]byte{0}, 64), httpsign.NewVerifyConfig(), *httpsign.NewFields())
	client := httpsign.NewDefaultClient("sig1", nil, verifier, nil)
	res, err := client.Get(ts.URL)
	if err != nil {
		log.Fatal(err)
	}
	serverText, err := io.ReadAll(res.Body)
	if err != nil {
		log.Fatal(err)
	}
	res.Body.Close()

	fmt.Println("Server sent: ", string(serverText))
}
Output:

Server sent:  Hello, client

Types

type Client added in v0.1.1

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

Client represents an HTTP client that optionally signs requests and optionally verifies responses. The Signer may be nil to avoid signing. Similarly, if both Verifier and fetchVerifier are nil, no verification takes place. The fetchVerifier callback allows to generate a Verifier based on the particular response. Either Verifier or fetchVerifier may be specified, but not both. The client embeds an http.Client, which in most cases can be http.DefaultClient.

func NewClient added in v0.1.1

func NewClient(sigName string, signer *Signer, verifier *Verifier, fetchVerifier func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier), client http.Client) *Client

NewClient constructs a new client, with the flexibility of including a custom http.Client.

func NewDefaultClient added in v0.1.1

func NewDefaultClient(sigName string, signer *Signer, verifier *Verifier, fetchVerifier func(res *http.Response, req *http.Request) (sigName string, verifier *Verifier)) *Client

NewDefaultClient constructs a new client, based on the http.DefaultClient.

func (*Client) Do added in v0.1.1

func (c *Client) Do(req *http.Request) (*http.Response, error)

Do sends an http.Request, with optional signing and/or verification. Errors may be produced by any of these operations.

func (*Client) Get added in v0.1.1

func (c *Client) Get(url string) (res *http.Response, err error)

Get sends an HTTP GET, a wrapper for Do.

Example
package main

import (
	"bytes"
	"fmt"
	"github.com/yaronf/httpsign"
	"io"
	"net/http"
	"net/http/httptest"
)

func main() {
	// Set up a test server
	simpleHandler := func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
		w.Header().Set("Content-Type", "text/plain")
		_, _ = fmt.Fprintf(w, "Hey client, you sent a signature with these parameters: %s\n",
			r.Header.Get("Signature-Input"))
	}
	ts := httptest.NewServer(http.HandlerFunc(simpleHandler))
	defer ts.Close()

	// Client code starts here
	// Create a signer and a wrapped HTTP client (we set SignCreated to false to make the response deterministic,
	// don't do that in production.)
	signer, _ := httpsign.NewHMACSHA256Signer("key1", bytes.Repeat([]byte{1}, 64),
		httpsign.NewSignConfig().SignCreated(false), httpsign.Headers("@method"))
	client := httpsign.NewDefaultClient("sig22", signer, nil, nil) // sign, don't verify

	// Send an HTTP GET, get response -- signing and verification happen behind the scenes
	res, _ := client.Get(ts.URL)

	// Read the response
	serverText, _ := io.ReadAll(res.Body)
	_ = res.Body.Close()

	fmt.Println("Server sent: ", string(serverText))
}
Output:

Server sent:  Hey client, you sent a signature with these parameters: sig22=("@method");alg="hmac-sha256";keyid="key1"

func (*Client) Head added in v0.1.1

func (c *Client) Head(url string) (res *http.Response, err error)

Head sends an HTTP HEAD, a wrapper for Do.

func (*Client) Post added in v0.1.1

func (c *Client) Post(url, contentType string, body io.Reader) (res *http.Response, err error)

Post sends an HTTP POST, a wrapper for Do.

type Fields

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

Fields is a list of fields to be signed. To initialize, use Headers or for more complex cases, NewFields followed by a chain of Add... methods.

func Headers added in v0.1.5

func Headers(hs ...string) Fields

Headers is a simple way to generate a Fields list, where only simple header names and derived headers are needed.

func NewFields

func NewFields() *Fields

NewFields return an empty list of fields

func (*Fields) AddDictHeader

func (fs *Fields) AddDictHeader(hdr, key string) *Fields

AddDictHeader indicates that out of a header structured as a dictionary, a specific key value is signed/verified

func (*Fields) AddHeader

func (fs *Fields) AddHeader(hdr string) *Fields

AddHeader appends a bare header name, e.g. "cache-control"

func (*Fields) AddHeaders added in v0.1.5

func (fs *Fields) AddHeaders(hs ...string) *Fields

AddHeaders adds a list of simple or derived header names

func (*Fields) AddQueryParam

func (fs *Fields) AddQueryParam(qp string) *Fields

AddQueryParam indicates a request for a specific query parameter to be signed

func (*Fields) AddStructuredField added in v0.1.5

func (fs *Fields) AddStructuredField(hdr string) *Fields

AddStructuredField indicates that a header should be interpreted as a structured field, per RFC 8941

type HandlerConfig

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

HandlerConfig contains additional configuration for the HTTP message handler wrapper.

func NewHandlerConfig

func NewHandlerConfig() *HandlerConfig

NewHandlerConfig generates a default configuration. When verification or respectively, signing is required, the respective "fetch" callback must be supplied.

func (*HandlerConfig) SetFetchSigner

func (h *HandlerConfig) SetFetchSigner(f func(res http.Response, r *http.Request) (sigName string, signer *Signer)) *HandlerConfig

SetFetchSigner defines a callback that looks at the incoming request and the response, just before it is sent, and provides a Signer structure. In the simplest case, the signature name is a constant, and the key ID and key value are fetched based on the sender's identity. To simplify this logic, it is recommended to use the request's ctx (Context) member to store this information. If a Signer cannot be determined, the function should return Signer as nil.

func (*HandlerConfig) SetFetchVerifier

func (h *HandlerConfig) SetFetchVerifier(f func(r *http.Request) (sigName string, verifier *Verifier)) *HandlerConfig

SetFetchVerifier defines a callback that looks at the incoming request and provides a Verifier structure. In the simplest case, the signature name is a constant, and the key ID and key value are fetched based on the sender's identity, which in turn is gleaned from a header or query parameter. If a Verifier cannot be determined, the function should return Verifier as nil.

func (*HandlerConfig) SetReqNotVerified

func (h *HandlerConfig) SetReqNotVerified(f func(w http.ResponseWriter, r *http.Request, err error)) *HandlerConfig

SetReqNotVerified defines a callback to be called when a request fails to verify. The default callback sends a 401 status code with a generic error message.

func (*HandlerConfig) SetSignResponse

func (h *HandlerConfig) SetSignResponse(b bool) *HandlerConfig

SetSignResponse indicates that all HTTP responses must be signed.

func (*HandlerConfig) SetVerifyRequest

func (h *HandlerConfig) SetVerifyRequest(b bool) *HandlerConfig

SetVerifyRequest indicates that all incoming requests for this handler must be verified.

type SignConfig

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

SignConfig contains additional configuration for the signer.

func NewSignConfig

func NewSignConfig() *SignConfig

NewSignConfig generates a default configuration.

func (*SignConfig) SetExpires added in v0.1.1

func (c *SignConfig) SetExpires(expires int64) *SignConfig

SetExpires adds an "expires" parameter containing an expiration deadline, as Unix time. Default: 0 (do not add the parameter)

func (*SignConfig) SetNonce added in v0.1.1

func (c *SignConfig) SetNonce(nonce string) *SignConfig

SetNonce adds a "nonce" string parameter whose content should be unique per signed message. Default: empty string (do not add the parameter)

func (*SignConfig) SetRequestResponse

func (c *SignConfig) SetRequestResponse(name, signature string) *SignConfig

SetRequestResponse allows the server to indicate signature name and signature that it had received from a client and include it in the signature input.

func (*SignConfig) SignAlg

func (c *SignConfig) SignAlg(b bool) *SignConfig

SignAlg indicates that an "alg" signature parameters must be generated and signed (default: true).

func (*SignConfig) SignCreated

func (c *SignConfig) SignCreated(b bool) *SignConfig

SignCreated indicates that a "created" signature parameters must be generated and signed (default: true).

type Signer

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

Signer includes a cryptographic key and configuration of what needs to be signed.

func NewEd25519Signer added in v0.1.5

func NewEd25519Signer(keyID string, key *ed25519.PrivateKey, config *SignConfig, fields Fields) (*Signer, error)

NewEd25519Signer returns a new Signer structure. Key is an EdDSA Curve 25519 private key. Config may be nil for a default configuration.

func NewEd25519SignerFromSeed added in v0.1.5

func NewEd25519SignerFromSeed(keyID string, seed *[]byte, config *SignConfig, fields Fields) (*Signer, error)

NewEd25519SignerFromSeed returns a new Signer structure. Key is an EdDSA Curve 25519 private key, a 32 byte buffer according to RFC 8032. Config may be nil for a default configuration.

func NewHMACSHA256Signer

func NewHMACSHA256Signer(keyID string, key []byte, config *SignConfig, fields Fields) (*Signer, error)

NewHMACSHA256Signer returns a new Signer structure. Key must be at least 64 bytes long. Config may be nil for a default configuration.

func NewJWSSigner added in v0.1.4

func NewJWSSigner(alg jwa.SignatureAlgorithm, keyID string, key interface{}, config *SignConfig, fields Fields) (*Signer, error)

NewJWSSigner creates a generic signer for JWS algorithms, using the go-jwx package. The particular key type for each algorithm is documented in that package. Config may be nil for a default configuration.

func NewP256Signer

func NewP256Signer(keyID string, key *ecdsa.PrivateKey, config *SignConfig, fields Fields) (*Signer, error)

NewP256Signer returns a new Signer structure. Key is an elliptic curve P-256 private key. Config may be nil for a default configuration.

func NewRSAPSSSigner

func NewRSAPSSSigner(keyID string, key *rsa.PrivateKey, config *SignConfig, fields Fields) (*Signer, error)

NewRSAPSSSigner returns a new Signer structure. Key is an RSA private key. Config may be nil for a default configuration.

func NewRSASigner

func NewRSASigner(keyID string, key *rsa.PrivateKey, config *SignConfig, fields Fields) (*Signer, error)

NewRSASigner returns a new Signer structure. Key is an RSA private key. Config may be nil for a default configuration.

type Verifier

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

Verifier includes a cryptographic key (typically a public key) and configuration of what needs to be verified.

func NewEd25519Verifier added in v0.1.5

func NewEd25519Verifier(keyID string, key *ed25519.PublicKey, config *VerifyConfig, fields Fields) (*Verifier, error)

NewEd25519Verifier generates a new Verifier for EdDSA Curve 25519 signatures. Set config to nil for a default configuration. Fields is the list of required headers and fields, which may be empty (but this is typically insecure).

func NewHMACSHA256Verifier

func NewHMACSHA256Verifier(keyID string, key []byte, config *VerifyConfig, fields Fields) (*Verifier, error)

NewHMACSHA256Verifier generates a new Verifier for HMAC-SHA256 signatures. Set config to nil for a default configuration. Fields is the list of required headers and fields, which may be empty (but this is typically insecure).

func NewJWSVerifier added in v0.1.4

func NewJWSVerifier(alg jwa.SignatureAlgorithm, key interface{}, keyID string, config *VerifyConfig, fields Fields) (*Verifier, error)

NewJWSVerifier creates a generic verifier for JWS algorithms, using the go-jwx package. The particular key type for each algorithm is documented in that package. Set config to nil for a default configuration. Fields is the list of required headers and fields, which may be empty (but this is typically insecure).

func NewP256Verifier

func NewP256Verifier(keyID string, key *ecdsa.PublicKey, config *VerifyConfig, fields Fields) (*Verifier, error)

NewP256Verifier generates a new Verifier for ECDSA (P-256) signatures. Set config to nil for a default configuration. Fields is the list of required headers and fields, which may be empty (but this is typically insecure).

func NewRSAPSSVerifier

func NewRSAPSSVerifier(keyID string, key *rsa.PublicKey, config *VerifyConfig, fields Fields) (*Verifier, error)

NewRSAPSSVerifier generates a new Verifier for RSA-PSS signatures. Set config to nil for a default configuration. Fields is the list of required headers and fields, which may be empty (but this is typically insecure).

func NewRSAVerifier

func NewRSAVerifier(keyID string, key *rsa.PublicKey, config *VerifyConfig, fields Fields) (*Verifier, error)

NewRSAVerifier generates a new Verifier for RSA signatures. Set config to nil for a default configuration. Fields is the list of required headers and fields, which may be empty (but this is typically insecure).

type VerifyConfig

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

VerifyConfig contains additional configuration for the verifier.

func NewVerifyConfig

func NewVerifyConfig() *VerifyConfig

NewVerifyConfig generates a default configuration.

func (*VerifyConfig) SetAllowedAlgs

func (v *VerifyConfig) SetAllowedAlgs(allowedAlgs []string) *VerifyConfig

SetAllowedAlgs defines the allowed values of the "alg" parameter. This is useful if the actual algorithm used in verification is taken from the message - not a recommended practice. Default: an empty list, signifying all values are accepted.

func (*VerifyConfig) SetNotNewerThan

func (v *VerifyConfig) SetNotNewerThan(notNewerThan time.Duration) *VerifyConfig

SetNotNewerThan sets the window for messages that appear to be newer than the current time, which can only happen if clocks are out of sync. Default: 1,000 ms.

func (*VerifyConfig) SetNotOlderThan

func (v *VerifyConfig) SetNotOlderThan(notOlderThan time.Duration) *VerifyConfig

SetNotOlderThan sets the window for messages that are older than the current time, because of network latency. Default: 10,000 ms.

func (*VerifyConfig) SetRejectExpired added in v0.1.1

func (v *VerifyConfig) SetRejectExpired(rejectExpired bool) *VerifyConfig

SetRejectExpired indicates that expired messages (according to the "expires" parameter) must fail verification. Default: true.

func (*VerifyConfig) SetRequestResponse added in v0.1.3

func (v *VerifyConfig) SetRequestResponse(name, signature string) *VerifyConfig

SetRequestResponse allows the server to indicate signature name and signature that it had received from a client and include it in the signature input. Here this is configured on the client side when verifying the response.

func (*VerifyConfig) SetVerifyCreated

func (v *VerifyConfig) SetVerifyCreated(verifyCreated bool) *VerifyConfig

SetVerifyCreated indicates that the "created" parameter must be within some time window, defined by NotNewerThan and NotOlderThan. Default: true.

func (*VerifyConfig) SetVerifyKeyID added in v0.1.6

func (v *VerifyConfig) SetVerifyKeyID(verify bool) *VerifyConfig

SetVerifyKeyID defines how to verify the keyid parameter, if one exists. If this value is set, the signature verifies only if the value is the same as was specified in the Verifier structure. Default: true

Jump to

Keyboard shortcuts

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