httpsig

package module
v0.0.0-...-6cb9b82 Latest Latest
Warning

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

Go to latest
Published: Jan 16, 2025 License: MIT Imports: 23 Imported by: 0

README

CI Codecov Go Report Card Go Reference

Introduction

httpsig is a library that facilitates the signing and verification of HTTP requests in compliance with the RFC 9421 HTTP Message Signatures standard.

Standalone Signing and Verification

To sign an HTTP request, first, create a Signer instance using your preferred key and signing algorithm:

// Create a signer.
signer, err := httpsig.NewSigner( 
    // specify a key
    httpsig.Key{KeyID: "key1", Key: privKey, Algorithm: httpsig.EcdsaP256Sha256}, 
    // specify the required options 
    // duration for which the signature should be valid
    httpsig.WithTTL(5 * time.Second), 
    // which components should be protected by a signature 
    httpsig.WithComponents("@authority", "@method", "x-my-fancy-header"), 
    // a tag for your specific application
    httpsig.WithTag("myapp"),
)
// error handling goes here

// Create a request
req, err := http.NewRequestWithContext(context.Background(), "GET", "https://some-url.com", nil)
// error handling goes here

// Sign the request
header, err := signer.Sign(httpsig.MessageFromRequest(req))
// error handling goes here

// Add the signature to the request
req.Header = header

To verify a response, create a Verifier using your preferred key and signing algorithm:

// Receive a response from the server
resp, err := client.Post("https://some-url.com", "application/json", &buf)
// error handling

// Create a verifier
verifier, err := httpsig.NewVerifier(
    // specify a key resolver to resolve the key used by the client
    keyResolver,
    // specify the required options
    // to detect and mitigate replay attacks
    httpsig.WithNonceChecker(nonceChecker),
    // which components are expected to be protected by a signature
    httpsig.WithRequiredComponents("@authority", "@method", "x-my-fancy-header"),
    // validity time skew
    httpsig.WithValidityTolerance(5 * time.Second),
    // how old a signature is allowed to be
    httpsig.WithMaxAge(30 * time.Second),
    // whether to validate all signatures present in the message
    httpsig.WithValidateAllSignatures(),
)
// error handling

// Verify the response
err := verifier.Verify(httpsig.MessageFromResponse(resp))
// error handling

If you need to validate a signature created by a specific application (identified by a tag), use the WithRequiredTag option when creating the verifier. This option allows you to specify a tag along with the same options as the NewVerifier function. Here’s an example:

verifier, err := httpsig.NewVerifier(
    // specify a key resolver to resolve the key used by the client
    keyResolver,
    // specify the required options
    // to detect and mitigate replay attacks
    httpsig.WithNonceChecker(nonceChecker), 
    httpsig.WithRequiredTag(
        // tag of the signature
        "myapp",
        // which components are expected to be protected by a signature
        httpsig.WithRequiredComponents("@authority", "@method", "x-my-fancy-header"),
        // validity time skew
        httpsig.WithValidityTolerance(5 * time.Second),
        // how old a signature is allowed to be
        httpsig.WithMaxAge(30 * time.Second),
    ), 
)
// error handling goes here

err = verifier.Verify(msg)
// error handling goes here

While the examples demonstrate signing a request and verifying a response, you can also verify requests and sign responses. Both the Verifier.Verify() and Signer.Sign() methods require a Message object, which can be created for requests and responses on both client and server sides using the following functions:

  • MessageFromRequest - creates a Message from an http.Request. Can be used for outbound (client-side) and inbound (server-side) requests.
  • MessageFromResponse - creates a Message from an http.Response. Can be used for inbound (client-side) responses from a server.
  • MessageForResponse - creates a Message from an outbound (server-side) response.

Both the Signer and Verifier respect the "content-digest" component identifier as highlighted in the Security Considerations of the RFC. This is handled as follows:

  • On the Signer side, if the "content-digest" is configured to be included via the WithComponents option and the WithContentDigestAlgorithm option is not used, the implementation will calculate a message digest over the body using the sha-256 and sha-512 algorithms (the only supported algorithms according to RFC 9530). It will then create the "Content-Digest" header with the calculated values in addition to the signature-related headers. If the WithContentDigestAlgorithm option is used, the message digest will be calculated using the specified algorithm.
  • On the Verifier side, verification of the corresponding hash values is done by default with no additional configuration required. If the "Signature-Input" header value contains a "content-digest" component, the implementation expects the "Content-Digest" header to be present and uses the supplied algorithm names and values to calculate the digest over the body and compare these value to the received ones. If the "Content-Digest" header is missing, references unsupported hash algorithms (only sha-256 and sha-512 are supported), or there is a mismatch between the calculated and provided values, the message verification will fail with an error.

Signature Negotiation

The library not only supports signing and verifying HTTP messages but also facilitates signature negotiation, as defined in the RFC 9421 HTTP Message Signatures - Requesting Signatures, by utilizing the "Accept-Signature" header.

[!IMPORTANT]
While Chapter 5.2 - Processing an Accept-Signature of the RFC mandates that

... a target message MUST have the same label ...

this requirement conflicts with Chapter 7.2.5 - Signature Labels, which clearly states:

An intermediary is allowed to relabel an existing signature when processing the message. Therefore, applications should not rely on specific labels being present, and applications should not put semantic meaning on the labels themselves. Instead, additional signature parameters can be used to convey whatever additional meaning is required to be attached to, and covered by, the signature. In particular, the tag parameter can be used to define an application-specific value.

As a result, the current implementation does not enforce label consistency, even though you can specify them. The only reliable method to ensure effective signature negotiation is by utilizing the tag parameter, as also recommended in the statement above.

Requesting Signatures on the Client-Side from the Server

On the client side, you can request the server to sign the response by using the AcceptSignatureBuilder. This builder can be created with the NewAcceptSignature function, which accepts several options to specify parameters and components that you want the server to include in the response. Here’s an example:

// create a builder (all options are optional)
builder, err := httpsig.NewAcceptSignature(
    // specify which key and key algorithm the server should use for signing the response
    httpsig.WithExpectedKey(Key{KeyID: "foo", Algorithm: httpsig.EcdsaP256Sha256}), 
    // specify the NonceSource for the nonce to be added 
    httpsig.WithExpectedNonce(nonceSource),
    // specify which label should the server use when creating the response 
    httpsig.WithExpectedLabel("bar"),
    // specify which components should be covered by the signature 
    httpsig.WithExpectedComponents("@status", "content-digest;req", "content-digest"),
    // specify your content digest algorithm references 
    httpsig.WithContentDigestAlgorithmPreferences(httpsig.AlgorithmPreference{Algorithm: httpsig.Sha256, Preference: 2}),
    // specify which tag the server should use 
    httpsig.WithExpectedTag("awesome-app"),
    // specify whether you want the created time stamp to be included 
    httpsig.WithExpectedCreatedTimestamp(true),
    // specify whether you want the expires time stamp to be included 
    httpsig.WithExpectedExpiresTimestamp(true),
)
// error handling goes here

req := ... // create your request

err = builder.Build(req.Context(), req.Header)
// error handling goes here

When the above code executes, it will add an "Accept-Signature" header to the request with a value like: bar=("@status", "content-digest";req, "content-digest");keyid="foo";alg="ecdsa-p256-sha256";nonce="...";tag="awesome-app";created;expires.

Requesting Signatures on the Server-Side from the Client

Signature negotiation on the server side works differently from the client-side. Instead of using the AcceptSignatureBuilder, you specify the corresponding options when creating the Verifier. This ensures that there are no discrepancies between what the Verifier expects and what is included in the "Accept-Signature" response header if an expected signature is not present or required parameters/components are missing. Here’s an example, building on the earlier Verifier creation example:

verifier, err := httpsig.NewVerifier(
    // specify a key resolver
    keyResolver,
    // specify the required options
    // to detect and mitigate replay attacks
    httpsig.WithNonceChecker(nonceChecker),
    httpsig.WithRequiredTag(
        // tag of the signature
        "myapp",
        // which components are expected to be protected by a signature
        httpsig.WithRequiredComponents("@authority", "@method", "x-my-fancy-header"),
        // validity time skew
        httpsig.WithValidityTolerance(5 * time.Second),
        // how old a signature is allowed to be
        httpsig.WithMaxAge(30 * time.Second),
        // if there is no signature tagged "myapp", or some of the required components or parameters
        // are not present, request a signature from the client
        httpsig.WithSignatureNegotiation(
            // specify which key and algorithm the client should use
            httpsig.WithRequestedKey(httpsig.Key{KeyID: "key1", Algorithm: httpsig.EcdsaP256Sha256}),
            // specify the source for the nonce, the client should use
            httpsig.WithRequestedNonce(nonceGetter),
            // specify the label for the signature, the client should use
            httpsig.WithRequestedLabel("bar"),
        ),
    ),
)
// error handling goes here

err = verifier.Verify(msg)

var missingSigErr *httpsig.NoApplicableSignatureError
if errors.As(err, &missingSigErr) {
    // if this error is returned, call Negotiate to update the http headers with the 
    // Accept-Signature header
    missingSigErr.Negotiate(resp.Header)
}
// further error handling

[!IMPORTANT]
The WithSignatureNegotiation option at the top level (outside the WithRequiredTag option) is mutually exclusive with the WithValidateAllSignatures option. However, you can still use WithSignatureNegotiation at the top level if you want to apply the same configuration for all expected tagged signatures, thereby simplifying your code.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrUnsupportedKeyType             = errors.New("unsupported key type/format")
	ErrUnsupportedAlgorithm           = errors.New("unknown/unsupported algorithm")
	ErrInvalidKeySize                 = errors.New("invalid key size")
	ErrNoKeyProvided                  = errors.New("no key provided")
	ErrInvalidSignature               = errors.New("invalid signature")
	ErrVerificationFailed             = errors.New("verification failed")
	ErrContentDigestMismatch          = errors.New("content digest mismatch")
	ErrMalformedData                  = errors.New("malformed data")
	ErrUnsupportedComponentIdentifier = errors.New("unsupported component identifier")
	ErrInvalidComponentIdentifier     = errors.New("invalid component identifier")
	ErrCanonicalization               = errors.New("failed to canonicalize component")
	ErrMalformedSignatureParameter    = errors.New("malformed signature parameter")
	ErrNoApplicableDigestFound        = errors.New("no applicable digest found")
	ErrVerifierCreation               = errors.New("verifier creation failed")
	ErrParameter                      = errors.New("parameter error")
	ErrValidity                       = errors.New("validity error")
	ErrMissingParameter               = errors.New("missing parameter error")
	ErrSignatureNegotiationError      = errors.New("signature negotiation error")
)

Functions

This section is empty.

Types

type AcceptSignatureBuilder

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

func NewAcceptSignature

func NewAcceptSignature(opts ...AcceptSignatureOption) (*AcceptSignatureBuilder, error)

func (*AcceptSignatureBuilder) Build

func (asb *AcceptSignatureBuilder) Build(ctx context.Context, header http.Header) error

type AcceptSignatureOption

type AcceptSignatureOption func(*AcceptSignatureBuilder) error

func WithContentDigestAlgorithmPreferences

func WithContentDigestAlgorithmPreferences(prefs ...AlgorithmPreference) AcceptSignatureOption

func WithExpectedComponents

func WithExpectedComponents(identifiers ...string) AcceptSignatureOption

func WithExpectedCreatedTimestamp

func WithExpectedCreatedTimestamp(flag bool) AcceptSignatureOption

func WithExpectedExpiresTimestamp

func WithExpectedExpiresTimestamp(flag bool) AcceptSignatureOption

func WithExpectedKey

func WithExpectedKey(key Key) AcceptSignatureOption

func WithExpectedLabel

func WithExpectedLabel(label string) AcceptSignatureOption

func WithExpectedNonce

func WithExpectedNonce(ng NonceGetter) AcceptSignatureOption

func WithExpectedTag

func WithExpectedTag(tag string) AcceptSignatureOption

type AlgorithmPreference

type AlgorithmPreference struct {
	Algorithm  DigestAlgorithm
	Preference int
}

func (AlgorithmPreference) String

func (p AlgorithmPreference) String() string

type DigestAlgorithm

type DigestAlgorithm string

DigestAlgorithm is the digest algorithm to use. Available algorithms are: - SHA-256 (sha-256). - SHA-512 (sha-512).

const (
	Sha256 DigestAlgorithm = "sha-256"
	Sha512 DigestAlgorithm = "sha-512"
)

type Key

type Key struct {
	// KeyID is the identifier of the key.
	KeyID string
	// Algorithm is the cryptographic algorithm to use with the key.
	Algorithm SignatureAlgorithm
	// Key is the actual key material, like public, private or a secret key.
	Key any
}

Key is the key to use for signing or verifying.

func (Key) ResolveKey

func (k Key) ResolveKey(_ context.Context, _ string) (Key, error)

type KeyResolver

type KeyResolver interface {
	ResolveKey(ctx context.Context, keyID string) (Key, error)
}

KeyResolver is used to resolve a key id to a verifying key.

type Message

type Message struct {
	Context       context.Context //nolint: containedctx
	Method        string
	Authority     string
	URL           *url.URL
	Header        http.Header
	Body          func() (io.ReadCloser, error)
	RequestHeader http.Header
	RequestBody   func() (io.ReadCloser, error)
	StatusCode    int
	IsRequest     bool
}

Message is a representation of an HTTP request or response, containing the values needed to construct or validate a signature.

func MessageForResponse

func MessageForResponse(req *http.Request, respHeader http.Header, body []byte, respCode int) *Message

func MessageFromRequest

func MessageFromRequest(req *http.Request) *Message

func MessageFromResponse

func MessageFromResponse(rw *http.Response) *Message

type NoApplicableSignatureError

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

func (*NoApplicableSignatureError) Error

func (*NoApplicableSignatureError) Is

func (*NoApplicableSignatureError) Negotiate

func (e *NoApplicableSignatureError) Negotiate(header http.Header)

type NonceChecker

type NonceChecker interface {
	CheckNonce(ctx context.Context, nonce string) error
}

NonceChecker is responsible for the verification of the nonce received in a signature, e.g. to prevent replay attacks, or to verify that the nonce is the expected one, like if requested using the Accept-Signature header.

type NonceCheckerFunc

type NonceCheckerFunc func(ctx context.Context, nonce string) error

func (NonceCheckerFunc) GetNonce

func (nc NonceCheckerFunc) GetNonce(ctx context.Context, nonce string) error

type NonceGetter

type NonceGetter interface {
	GetNonce(ctx context.Context) (string, error)
}

NonceGetter represents a source of random nonces to go into resulting objects.

type NonceGetterFunc

type NonceGetterFunc func(ctx context.Context) (string, error)

func (NonceGetterFunc) GetNonce

func (ng NonceGetterFunc) GetNonce(ctx context.Context) (string, error)

type SignatureAlgorithm

type SignatureAlgorithm string

SignatureAlgorithm is the signature algorithm to use. Available algorithms are: - RSASSA-PKCS1-v1_5 using SHA-256 (rsa-v1_5-sha256). - RSASSA-PSS using SHA-512 (rsa-pss-sha512). - ECDSA using curve P-256 DSS and SHA-256 (ecdsa-p256-sha256). - ECDSA using curve P-384 DSS and SHA-384 (ecdsa-p384-sha384). - EdDSA using curve edwards25519 (ed25519). - HMAC using SHA-256 (hmac-sha256).

const (
	RsaPkcs1v15Sha256 SignatureAlgorithm = "rsa-v1_5-sha256"
	RsaPkcs1v15Sha384 SignatureAlgorithm = "rsa-v1_5-sha384"
	RsaPkcs1v15Sha512 SignatureAlgorithm = "rsa-v1_5-sha512"
	RsaPssSha256      SignatureAlgorithm = "rsa-pss-sha256"
	RsaPssSha384      SignatureAlgorithm = "rsa-pss-sha384"
	RsaPssSha512      SignatureAlgorithm = "rsa-pss-sha512"
	EcdsaP256Sha256   SignatureAlgorithm = "ecdsa-p256-sha256"
	EcdsaP384Sha384   SignatureAlgorithm = "ecdsa-p384-sha384"
	EcdsaP521Sha512   SignatureAlgorithm = "ecdsa-p521-sha512"
	Ed25519           SignatureAlgorithm = "ed25519"
	HmacSha256        SignatureAlgorithm = "hmac-sha256"
	HmacSha384        SignatureAlgorithm = "hmac-sha384"
	HmacSha512        SignatureAlgorithm = "hmac-sha512"
)

type SignatureNegotiationOption

type SignatureNegotiationOption func(sno *sigNegotiationOpts)

func WithRequestedContentDigestAlgorithmPreferences

func WithRequestedContentDigestAlgorithmPreferences(prefs ...AlgorithmPreference) SignatureNegotiationOption

func WithRequestedKey

func WithRequestedKey(key Key) SignatureNegotiationOption

func WithRequestedLabel

func WithRequestedLabel(label string) SignatureNegotiationOption

func WithRequestedNonce

func WithRequestedNonce(ng NonceGetter) SignatureNegotiationOption

type SignatureParameter

type SignatureParameter string
const (
	KeyID   SignatureParameter = "keyid"
	Alg     SignatureParameter = "alg"
	Created SignatureParameter = "created"
	Expires SignatureParameter = "expires"
	Nonce   SignatureParameter = "nonce"
	Tag     SignatureParameter = "tag"
)

type Signer

type Signer interface {
	Sign(msg *Message) (http.Header, error)
}

func NewSigner

func NewSigner(key Key, opts ...SignerOption) (Signer, error)

NewSigner creates a new signer with the given options.

type SignerOption

type SignerOption func(s *signer) error

func WithComponents

func WithComponents(identifiers ...string) SignerOption

WithComponents sets the HTTP fields / derived component names to be included in signing.

func WithContentDigestAlgorithm

func WithContentDigestAlgorithm(alg DigestAlgorithm) SignerOption

func WithLabel

func WithLabel(label string) SignerOption

WithLabel sets the label of the signature in the Signature-Input and Signature headers.

func WithNonce

func WithNonce(ng NonceGetter) SignerOption

func WithTTL

func WithTTL(ttl time.Duration) SignerOption

func WithTag

func WithTag(tag string) SignerOption

type Verifier

type Verifier interface {
	Verify(msg *Message) error
}

func NewVerifier

func NewVerifier(resolver KeyResolver, opts ...VerifierOption) (Verifier, error)

NewVerifier creates a new verifier with the given options.

type VerifierOption

type VerifierOption func(v *verifier, e *expectations, f bool) error

func WithCreatedTimestampRequired

func WithCreatedTimestampRequired(flag bool) VerifierOption

func WithExpiredTimestampRequired

func WithExpiredTimestampRequired(flag bool) VerifierOption

func WithMaxAge

func WithMaxAge(d time.Duration) VerifierOption

func WithNonceChecker

func WithNonceChecker(checker NonceChecker) VerifierOption

func WithRequiredComponents

func WithRequiredComponents(identifiers ...string) VerifierOption

WithRequiredComponents sets the HTTP fields / derived component names to be included in signing.

func WithRequiredTag

func WithRequiredTag(tag string, opts ...VerifierOption) VerifierOption

func WithSignatureNegotiation

func WithSignatureNegotiation(opts ...SignatureNegotiationOption) VerifierOption

func WithValidateAllSignatures

func WithValidateAllSignatures() VerifierOption

func WithValidityTolerance

func WithValidityTolerance(d time.Duration) VerifierOption

WithValidityTolerance sets the clock tolerance for verifying created and expires times.

Jump to

Keyboard shortcuts

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