htmsig

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 18, 2025 License: MIT Imports: 13 Imported by: 0

README

htmsig - RFC 9421 HTTP Message Signatures for Go

Build Status Go Reference codecov.io

A complete Go implementation of RFC 9421: HTTP Message Signatures, providing cryptographic signing and verification for HTTP requests and responses.

Installation

go get github.com/lestrrat-go/htmsig

Quick Start

Client/Server Example

The easiest way to get started is using the http package for automatic signing and verification:

package htmsig_test

import (
  "bytes"
  "fmt"
  "io"
  "net/http"
  "net/http/httptest"
  "strings"
  "time"

  "github.com/lestrrat-go/htmsig/component"
  htmsighttp "github.com/lestrrat-go/htmsig/http"
)

func createApp(payload string, hmacKey []byte, clock htmsighttp.Clock) http.Handler {
  // Create a key resolver for verifying incoming requests
  keyResolver := htmsighttp.StaticKeyResolver(hmacKey)

  // Create reqVerifier for incoming requests
  reqVerifier := htmsighttp.NewVerifier(keyResolver)

  // Create response signer for outgoing responses
  responseSigner := htmsighttp.NewSigner(hmacKey, "server-key",
    htmsighttp.WithComponents(
      component.Method().WithParameter("req", true),    // ;req is required for response verification
      component.TargetURI().WithParameter("req", true), // ;req is required for response verification
      component.Status(),
      component.New("content-type"),
    ),
    htmsighttp.WithClock(clock))

  // Create the application handler
  app := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    _, _ = fmt.Fprint(w, payload)
  })

  // Wrap handler with both verification and signing. This will cause the
  // handler to both verify incoming requests and sign outgoing responses.
  wrappedHandler := htmsighttp.Wrap(
    app,
    htmsighttp.WithVerifier(reqVerifier),
    htmsighttp.WithSigner(responseSigner),
  )

  return wrappedHandler
}

// Example_client_server demonstrates client/server interaction
// with both request verification and response signing.
func Example_client_server() {
  const payload = `{"message": "Request verified and response signed"}`

  // Use HMAC key for deterministic signatures
  hmacKey := []byte("shared-hmac-secret")
  // Create fixed clock for deterministic timestamps
  fixedTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
  clock := htmsighttp.FixedClock(fixedTime)

  app := createApp(payload, hmacKey, clock)
  // Create test server
  server := httptest.NewServer(app)
  defer server.Close()

  { // Using a client with no signing abilities - the server will attempt to verify
    // the request, but since the client does not sign, it will fail.
    client := &http.Client{}
    resp, err := client.Get(server.URL + "/test")
    if err != nil {
      fmt.Printf("request failed: %v\n", err)
      return
    }
    defer resp.Body.Close() //nolint:errcheck

    // We will get a 401 Unauthorized response
    if resp.StatusCode != http.StatusUnauthorized {
      fmt.Printf("Expected status 401 Unauthorized, got %d\n", resp.StatusCode)
      return
    }
  }

  { // To make this work, we create a new client with signing/verification features
    // Create request signer
    requestSigner := htmsighttp.NewSigner(hmacKey, "client-key",
      htmsighttp.WithComponents(
        component.Method(),
        component.TargetURI(),
      ),
      htmsighttp.WithClock(clock))

    // Create response verifier
    responseVerifier := htmsighttp.NewVerifier(
      htmsighttp.StaticKeyResolver(hmacKey),
    )

    client := htmsighttp.NewClient(
      htmsighttp.WithSigner(requestSigner),
      htmsighttp.WithVerifier(responseVerifier),
    )

    resp, err := client.Get(server.URL + "/test")
    if err != nil {
      fmt.Printf("request failed: %v\n", err)
      return
    }
    defer resp.Body.Close() //nolint:errcheck

    buf, err := io.ReadAll(resp.Body)
    if err != nil {
      fmt.Printf("Failed to read response body: %v\n", err)
      return
    }

    if resp.StatusCode != http.StatusOK {
      fmt.Printf("Expected status 200, got %d\n", resp.StatusCode)
      return
    }

    if !bytes.Equal(buf, []byte(payload)) {
      fmt.Printf("Expected response body %q, got %q\n", payload, string(buf))
      return
    }

    sig := resp.Header.Get("Signature")
    if sig == "" {
      fmt.Printf("Expected response to have Signature header, but got empty\n")
      return
    }

    // Signature should start with "sig=:" and end with ":"
    if !strings.HasPrefix(sig, "sig=:") || !strings.HasSuffix(sig, ":") {
      fmt.Printf("Expected response signature format 'sig=:...:', got %q\n", sig)
      return
    }
  }
  // Output:
}

source: examples/client_server_example_test.go

The baove example shows how to use this module for http.Handlers, but you can certainly do more low-level processing manually. Please read the documentation for htmsig package, the input package, and component package for more details.

Supported Algorithms

Algorithm RFC 9421 Name Description
RSA-PSS with SHA-512 rsa-pss-sha512 Recommended RSA algorithm
RSA PKCS#1 v1.5 with SHA-256 rsa-v1_5-sha256 Legacy RSA algorithm
ECDSA with P-256 and SHA-256 ecdsa-p256-sha256 NIST P-256 curve
ECDSA with P-384 and SHA-384 ecdsa-p384-sha384 NIST P-384 curve
Ed25519 ed25519 Edwards curve signature
HMAC with SHA-256 hmac-sha256 Symmetric key algorithm

Advanced Usage

Custom Component Selection

You can specify exactly which parts of the HTTP message to include in signatures:

  • Derived Components: @method, @target-uri, @authority, @scheme, @request-target, @path, @query, @status
  • HTTP Fields: Any HTTP header (e.g., content-type, date, authorization)
  • Signature Parameters: created, expires, keyid, alg, nonce, tag

License

This project is licensed under the MIT License - see the LICENSE file for details.

References

Documentation

Index

Constants

View Source
const (
	SignatureInputHeader = "Signature-Input"
	SignatureHeader      = "Signature"
)
View Source
const (
	AlgorithmRSAPSSSHA512    = "rsa-pss-sha512"    // Section 3.3.1
	AlgorithmRSAV15SHA256    = "rsa-v1_5-sha256"   // Section 3.3.2
	AlgorithmHMACSHA256      = "hmac-sha256"       // Section 3.3.3
	AlgorithmECDSAP256SHA256 = "ecdsa-p256-sha256" // Section 3.3.4
	AlgorithmECDSAP384SHA384 = "ecdsa-p384-sha384" // Section 3.3.5
	AlgorithmEd25519         = "ed25519"           // Section 3.3.6
)

RFC 9421 Algorithm Names (Section 6.2.2 Initial Contents)

Variables

This section is empty.

Functions

func SignRequest

func SignRequest(ctx context.Context, headers http.Header, inputValue *input.Value, key any) error

SignRequest signs an HTTP request using the provided headers. Request information must be provided in context using component.WithRequestInfo.

func SignResponse

func SignResponse(ctx context.Context, headers http.Header, inputValue *input.Value, key any) error

SignResponse signs an HTTP response using the provided headers. Response information must be provided in context using component.WithResponseInfo.

func VerifyRequest

func VerifyRequest(ctx context.Context, headers http.Header, keyOrResolver any, options ...VerifyOption) error

VerifyRequest verifies HTTP request signatures using the provided headers. Request information must be provided in context using component.WithRequestInfo.

func VerifyResponse

func VerifyResponse(ctx context.Context, headers http.Header, keyOrResolver any, options ...VerifyOption) error

VerifyResponse verifies HTTP response signatures using the provided headers. Response information must be provided in context using component.WithResponseInfo.

Types

type Clock

type Clock interface {
	Now() time.Time
}

Clock provides the current time for timestamp operations.

type KeyResolver

type KeyResolver interface {
	ResolveKey(keyID string) (any, error)
}

KeyResolver interface allows resolving cryptographic keys by their ID

type Option

type Option = option.Interface

type SystemClock

type SystemClock struct{}

SystemClock uses the system time.

func (SystemClock) Now

func (SystemClock) Now() time.Time

type VerifyOption

type VerifyOption interface {
	Option
	// contains filtered or unexported methods
}

VerifyOption configures signature verification behavior

func WithClock

func WithClock(clock Clock) VerifyOption

WithClock returns a VerifyOption that sets the clock for expiration checking

func WithValidateExpires

func WithValidateExpires(validate bool) VerifyOption

WithValidateExpires returns a VerifyOption that enables or disables expiration validation

Directories

Path Synopsis
Package http provides HTTP handlers and clients that implement RFC 9421 HTTP Message Signatures.
Package http provides HTTP handlers and clients that implement RFC 9421 HTTP Message Signatures.

Jump to

Keyboard shortcuts

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