Swift implementation of RFC 9421 HTTP Message Signatures with the Signature-Key header extension.
- 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: "0.1.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