Swift implementation of RFC 9421 HTTP Message Signatures with the Signature-Key header extension.
Tracks draft-hardt-httpbis-signature-key-08.
| Line | Implements |
|---|---|
2.x |
-08 |
1.x |
-05 era, plus alg emission in 1.2.0 |
-08 is not wire compatible with earlier revisions in either direction, and the
protocol has no version negotiation, so both ends of a deployment move together.
See MIGRATING-2.0.md.
| Scheme | Status |
|---|---|
hwk |
full |
jkt-jwt |
full |
jwt |
assertion claims and cnf.jwk; the issuer's signature is the caller's to check |
jwks_uri |
parameters, discovery URL, and metadata issuer validation; the fetch is the caller's |
jwks |
parameters and URL; the fetch is the caller's |
self-jwt |
claims and JWT signature verification; the fetch is the caller's |
x509 |
not implemented |
This library performs no network I/O. Discovery schemes parse and expose what is needed, and validate what comes back; the caller owns the fetch and its egress admission.
- iOS 17.4+ / macOS 14+
- Swift 5.9+
- No external dependencies (uses CryptoKit and Security frameworks)
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/hellocoop/swift-httpsig.git", from: "2.0.0"),
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "HTTPMessageSignatures", package: "swift-httpsig"),
]
),
]import HTTPMessageSignatures
// Create a signing key (Secure Enclave on device, CryptoKit for testing)
let key = CryptoKitP256SigningKey()
// Create a signer with default components
let signer = HTTPMessageSigner(
key: key,
label: "sig",
components: ["@method", "@authority", "@path", "signature-key"]
)
// Sign a request (adds Signature-Input, Signature, and Signature-Key headers)
var request = URLRequest(url: URL(string: "https://wallet.hello.coop/api/v1/mobile/register")!)
request.httpMethod = "POST"
let signedRequest = try signer.sign(request)The @authority component in the signature base is derived from the request URL. When verifying on the server, do not use the Host header from the incoming request — reverse proxies (nginx, ALB, CloudFront) commonly rewrite the Host header to the internal upstream hostname.
Instead, use a server-side environment constant for the expected authority:
// Node.js server — CORRECT
const AUTHORITY = `${HOST}.${DOMAIN}` // e.g. "wallet.hello.coop"
// In signature base construction:
case '@authority':
value = AUTHORITY // NOT req.headers.host// Swift client — the URL already contains the correct authority
// No special handling needed; URLRequest.url.host() is used automaticallylet result = try HTTPMessageVerifier.verify(request: signedRequest)
// result.jwk - the public key that verified the signature
// result.parameters.created - when the signature was created
// result.components - which components were coveredlet key = try SecureEnclaveSigningKey()
// Persist the key handle for later use
let keyData = key.dataRepresentation
UserDefaults.standard.set(keyData, forKey: "deviceKey")
// Restore later
let restored = try SecureEnclaveSigningKey(dataRepresentation: keyData)let thumbprint = try JWKThumbprint.compute(key.publicKeyJWK)| Scheme | Format | Use Case |
|---|---|---|
hwk |
Inline JWK parameters | Device-bound keys |
jwt |
JWT with cnf.jwk |
Delegated/attested keys |
jwks_uri |
JWKS discovery URI | Server keys |
MIT