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)
letkey=CryptoKitP256SigningKey()
// Create a signer with default components
letsigner=HTTPMessageSigner(
key: key,
label:"sig",
components:["@method","@authority","@path","signature-key"])
// Sign a request (adds Signature-Input, Signature, and Signature-Key headers)
varrequest=URLRequest(url:URL(string:"https://wallet.hello.coop/api/v1/mobile/register")!)
request.httpMethod ="POST"letsignedRequest=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 — CORRECTconstAUTHORITY=`${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 automaticallyletresult=tryHTTPMessageVerifier.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 coveredletkey=trySecureEnclaveSigningKey()
// Persist the key handle for later use
letkeyData= key.dataRepresentation
UserDefaults.standard.set(keyData, forKey:"deviceKey")
// Restore later
letrestored=trySecureEnclaveSigningKey(dataRepresentation: keyData)letthumbprint=tryJWKThumbprint.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