Skip to content

common-fate/httpsig

Repository files navigation

httpsig

Go Reference

An implementation of RFC9421: HTTP Message Signatures in Go.

This library has support for the following features:

  • Supports all active algorithms for signing and verification, with pluggable interfaces for future additions.

  • Support for creating a signed content-digest field to protect the HTTP request body.

  • Protection against resource exhaustion when verifying the content-digest field.

  • Support for multiple HTTP request signatures.

  • Pluggable key directory for key material lookup.

  • Pluggable nonce storage backends to protect against replay attacks.

  • Safe-by-default middleware which strips unsigned HTTP headers and prevents unsigned HTTP request bodies from being read.

  • Client and server-side hooks to debug signing errors.

  • Support for server-side attributes associated with signing keys (see below for an example).

Usage

Client

package main

import (
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"log"
	"net/http/httputil"

	"github.com/common-fate/httpsig"
	"github.com/common-fate/httpsig/alg_ecdsa"
)

func main() {
	keyString := `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIFKbhfNZfpDsW43+0+JjUr9K+bTeuxopu653+hBaXGA7oAoGCCqGSM49
AwEHoUQDQgAEqIVYZVLCrPZHGHjP17CTW0/+D9Lfw0EkjqF7xB4FivAxzic30tMM
4GF+hR6Dxh71Z50VGGdldkkDXZCnTNnoXQ==
-----END EC PRIVATE KEY-----
`

	block, _ := pem.Decode([]byte(keyString))

	key, err := x509.ParseECPrivateKey(block.Bytes)
	if err != nil {
		log.Fatal(err)
	}
	client := httpsig.NewClient(httpsig.ClientOpts{
		Tag: "foo",
		Alg: alg_ecdsa.NewP256Signer(key),
	})

	res, err := client.Post("http://localhost:9091", "application/json", nil)
	if err != nil {
		log.Fatal(err)
	}

	resBytes, err := httputil.DumpResponse(res, true)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(resBytes))
}

Server

package main

import (
	"context"
	"crypto/ecdsa"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"log"
	"net/http"

	"github.com/common-fate/httpsig"
	"github.com/common-fate/httpsig/alg_ecdsa"
	"github.com/common-fate/httpsig/inmemory"
)

func main() {
	keyString := `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqIVYZVLCrPZHGHjP17CTW0/+D9Lf
w0EkjqF7xB4FivAxzic30tMM4GF+hR6Dxh71Z50VGGdldkkDXZCnTNnoXQ==
-----END PUBLIC KEY-----
`

	block, _ := pem.Decode([]byte(keyString))

	key, err := x509.ParsePKIXPublicKey(block.Bytes)
	if err != nil {
		log.Fatal(err)
	}
	ecKey := key.(*ecdsa.PublicKey)

	mux := http.NewServeMux()

	verifier := httpsig.Middleware(httpsig.MiddlewareOpts{
		NonceStorage: inmemory.NewNonceStorage(),
		KeyDirectory: alg_ecdsa.StaticKeyDirectory{
			Key: ecKey,
			Attributes: exampleAttributes{
				Username: "Alice",
			},
		},
		Tag:       "foo",
		Scheme:    "http",
		Authority: "localhost:9091",

		OnValidationError: func(ctx context.Context, err error) {
			fmt.Printf("validation error: %s\n", err)
		},

		OnDeriveSigningString: func(ctx context.Context, stringToSign string) {
			fmt.Printf("string to sign:\n%s\n\n", stringToSign)
		},
	})

	mux.Handle("/", verifier(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		attr := httpsig.AttributesFromContext(r.Context()).(exampleAttributes)
		msg := fmt.Sprintf("hello, %s!", attr.Username)
		w.Write([]byte(msg))
	})))

	err = http.ListenAndServe("localhost:9091", mux)
	if err != nil {
		log.Fatal(err)
	}
}

type exampleAttributes struct {
	Username string
}