Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is an implementation of HTTP Message Signatures per RFC 9421. The library provides functionality for signing and verifying HTTP requests and responses using various cryptographic algorithms.

## Development Commands

### Testing
- `go test` - Run all tests
- `go test ./...` - Run tests recursively for all packages
- `go test -v` - Run tests with verbose output
- `go test -run TestName` - Run specific test
- `go test -fuzz FuzzName` - Run fuzz tests (see testdata/fuzz/ for existing fuzz test data)

### Building and Linting
- `go build` - Build the package
- `go vet` - Run go vet for static analysis
- `go fmt ./...` - Format all Go files
- `go mod tidy` - Clean up module dependencies
- `go mod verify` - Verify module integrity

### Coverage
- `go test -cover` - Run tests with coverage
- `go test -coverprofile=coverage.out && go tool cover -html=coverage.out` - Generate HTML coverage report

## Architecture Overview

### Core Components

1. **Signing Pipeline** (`sign.go`, `signatures.go`)
- `Signer` struct orchestrates request/response signing
- `SigningProfile` defines what fields and metadata to include
- `SigningKey` holds cryptographic material and metadata
- Supports asymmetric (RSA, ECDSA, Ed25519) and symmetric (HMAC) algorithms

2. **Accept-Signature Support** (`accept.go`)
- `AcceptSignature` struct for parsing Accept-Signature headers
- `ParseAcceptSignature()` function for client-server signature negotiation
- Support for signature profile requirements from clients

3. **Verification Pipeline** (`verify.go`)
- `Verifier` struct handles signature verification
- `VerifyProfile` enforces security requirements beyond basic signature validation
- `KeyFetcher` interface for key retrieval by keyID or other metadata
- Time-based validation for created/expires metadata with configurable tolerances

4. **Core Engine** (`base.go`)
- `calculateSignatureBase()` generates the canonical string for signing/verification
- `componentID` represents signature components (headers or derived values like @method, @target-uri)
- `httpMessage` abstraction handles both requests and responses uniformly

5. **HTTP Integration** (`http.go`)
- `NewHTTPClient()` creates signing/verifying HTTP clients
- `NewHandler()` wraps http.Handler for automatic request verification
- `transport` type implements http.RoundTripper for transparent signing/verification

6. **Content Integrity** (`digest.go`)
- Automatic Content-Digest header calculation when signing
- Support for SHA-256 and SHA-512 digests
- Body preservation during digest calculation

### Key Utilities

- **keyman/** - In-memory key storage implementation
- **keyutil/** - PEM key file reading utilities with support for various formats
- **sigtest/** - Test helpers for signature testing

### Error Handling

The library uses a structured error system (`sigerrors.go`) with specific error types:
- `ErrNoSigMissingSignature` - Missing signature headers
- `ErrSigVerification` - Signature verification failures
- `ErrSigProfile` - Profile validation failures
- `ErrSigKeyFetch` - Key retrieval failures

### Algorithm Support

**Asymmetric:**
- RSA-PSS-SHA512 (`rsa-pss-sha512`)
- RSA v1.5 SHA256 (`rsa-v1_5-sha256`)
- ECDSA P-256 SHA256 (`ecdsa-p256-sha256`)
- ECDSA P-384 SHA384 (`ecdsa-p384-sha384`)
- Ed25519 (`ed25519`)

**Symmetric:**
- HMAC-SHA256 (`hmac-sha256`)

### Security Defaults

`DefaultVerifyProfile` provides secure defaults:
- Requires Content-Digest, @method, @target-uri fields
- Mandates 'created' and 'keyid' metadata
- 5-minute signature validity window
- Prohibits algorithm metadata (must be derived from key)
- Allows only secure algorithms (ECDSA, Ed25519, HMAC)

## Testing Approach

The codebase uses standard Go testing with:
- RFC test vectors in `testdata/` directory
- Fuzz testing for signature parsing
- Round-trip tests validating sign→verify cycles
- Comprehensive algorithm coverage

## Important Implementation Notes

- Signature base calculation follows RFC 9421 exactly
- Multi-value headers are not yet supported (will return error)
- Content-Digest is automatically calculated when included in signature fields
- Request body is preserved during signing/verification by reading and reconstructing
- Context keys are used to pass verification results through HTTP middleware
80 changes: 80 additions & 0 deletions fz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ package httpsig
import (
"bufio"
"bytes"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

"github.com/remitly-oss/httpsig-go/sigtest"
Expand Down Expand Up @@ -158,6 +162,14 @@ func FuzzExtractSignatures(f *testing.F) {
SignatureHeader: "sig-b24=(\"@status\" \"content-type\" \"content-digest\" \"content-length\");created=1618884473;keyid=\"test-key-ecc-p256\"",
SignatureInputHeader: "sig-b24=:wNmSUAhwb5LxtOtOpNa6W5xj067m5hFrj0XQ4fvpaCLx0NKocgPquLgyahnzDnDAUy5eCdlYUEkLIj+32oiasw==:",
},
{"sig1=:dGVzdA==:", "sig1=(\"@method\", \"@target-uri\");created=1618884473"},
{"sig1=:invalid-base64:", "sig1=(\"@method\");created=1618884473"},
{"sig1", "sig1=()"},
{"sig1=:dGVzdA==:, sig2=:dGVzdA==:", "sig1=(\"@method\"), sig2=(\"@path\")"},
{"sig1=invalid", "sig1=(\"@method\")"},
{"sig1=:dGVzdA==:", "sig1=invalid"},
{"=:dGVzdA==:", "=(\"@method\")"},
{"sig1=:dGVzdA==:", "sig1=("},
}

for _, tc := range testcases {
Expand Down Expand Up @@ -198,3 +210,71 @@ func FuzzExtractSignatures(f *testing.F) {
}
})
}

// FuzzComponentIDParsing tests component identifier parsing edge cases
func FuzzComponentIDParsing(f *testing.F) {
testcases := []struct {
name string
parameters map[string]string
}{
{"@method", nil},
{"@target-uri", nil},
{"@query-param", map[string]string{"name": "test"}},
{"content-digest", nil},
{"", nil},
{"@invalid-component", nil},
{"header-with-unicode-😀", nil},
{"@query-param", map[string]string{"name": ""}},
{"@query-param", map[string]string{"": "value"}},
}

for _, tc := range testcases {
params := ""
for k, v := range tc.parameters {
params += fmt.Sprintf(";%s=%s", k, v)
}
f.Add(tc.name, params)
}

f.Fuzz(func(t *testing.T, name, paramStr string) {
// Create a SignedField with fuzzed input
field := SignedField{Name: name}

// Parse parameters string into map
if paramStr != "" {
field.Parameters = make(map[string]any)
pairs := strings.Split(paramStr, ";")
for _, pair := range pairs {
if kv := strings.SplitN(pair, "=", 2); len(kv) == 2 {
field.Parameters[kv[0]] = kv[1]
}
}
}

// Convert to componentID - should not panic
cID := field.componentID()

// Try to get signature name and value
_, err := cID.signatureName()
if err != nil {
var sigErr *SignatureError
if !errors.As(err, &sigErr) {
t.Errorf("Expected SignatureError, got: %T", err)
}
}

req := httptest.NewRequest("GET",
"http://example.com/test?param=value", nil)
req.Header.Set("Content-Digest",
"sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:")
msg := httpMessage{Req: req}

_, err = cID.signatureValue(msg)
if err != nil {
var sigErr *SignatureError
if !errors.As(err, &sigErr) {
t.Errorf("Expected SignatureError, got: %T", err)
}
}
})
}
10 changes: 8 additions & 2 deletions roundtrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ func TestRoundTrip(t *testing.T) {
PubKey: keyutil.MustReadPublicKeyFile("testdata/test-key-rsa-pss.pub"),
},
}),
Profile: createVerifyProfile("tst-rsa-pss"),
Profile: httpsig.VerifyProfile{
SignatureLabel: "tst-rsa-pss",
AllowedAlgorithms: []httpsig.Algorithm{httpsig.Algo_RSA_PSS_SHA512},
},
},
{
Name: "RSA-v15",
Expand All @@ -62,7 +65,10 @@ func TestRoundTrip(t *testing.T) {
PubKey: keyutil.MustReadPublicKeyFile("testdata/key-rsa-v15.pub"),
},
}),
Profile: createVerifyProfile("tst-rsa-pss"),
Profile: httpsig.VerifyProfile{
SignatureLabel: "tst-rsa-pss",
AllowedAlgorithms: []httpsig.Algorithm{httpsig.Algo_RSA_v1_5_sha256},
},
},
{
Name: "HMAC_SHA256",
Expand Down
5 changes: 3 additions & 2 deletions spec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestSpecVerify(t *testing.T) {
for _, tc := range cases {
t.Run(tc.Name, func(t *testing.T) {
if tc.Skip {
t.Skip(fmt.Sprintf("Skipping test %s", tc.Name))
t.Skipf("Skipping test %s", tc.Name)
}

hrrtxt, err := os.Open(fmt.Sprintf("testdata/%s", tc.SignedRequestOrResonseFile))
Expand All @@ -96,7 +96,8 @@ func TestSpecVerify(t *testing.T) {
requiredKeyID: tc.Key.KeyID,
key: tc.Key,
}, VerifyProfile{
SignatureLabel: fmt.Sprintf("sig-%s", tc.Name),
SignatureLabel: fmt.Sprintf("sig-%s", tc.Name),
DisableTimeEnforcement: true,
})

var verifyErr error
Expand Down
Loading
Loading