Verifying Signatures
This guide covers verifying HTTP Message Signatures in all five languages.
Step 1: Implement a KeyProvider
The KeyProvider resolves a key ID (from the signature metadata) to a VerifyingKey. This is where you look up keys from your database, JWKS endpoint, configuration file, or wherever you store them. The auto-detect constructors infer the algorithm from the public key type, so a single provider can handle multiple algorithms.
// Go: KeyProvider is a function type. Auto-detect handles mixed key types.
provider := func(keyID string, alg httpsig.Algorithm) (httpsig.VerifyingKey, error) {
pub, ok := keyRegistry[keyID]
if !ok {
return nil, fmt.Errorf("unknown key: %s", keyID)
}
return httpsig.NewVerifyingKeyFromPublic(keyID, pub)
}
import type { KeyProvider } from '@zourzouvillys/httpsig';
import { newVerifyingKey } from '@zourzouvillys/httpsig';
const provider: KeyProvider = async (keyId, algorithm) => {
const publicKey = await loadPublicKey(keyId);
if (!publicKey) throw new Error(`unknown key: ${keyId}`);
return newVerifyingKey(keyId, publicKey); // auto-detects algorithm
};
// Java: KeyProvider is a @FunctionalInterface. Auto-detect from JCA key type.
KeyProvider provider = (keyId, algorithm) -> {
PublicKey pub = keyStore.get(keyId);
if (pub == null) return null;
return Keys.verifyingKey(keyId, pub); // auto-detects algorithm
};
// Swift: implement the KeyProvider protocol
struct MyKeyProvider: KeyProvider {
let keys: [String: Curve25519.Signing.PublicKey]
func resolve(keyId: String, algorithm: Algorithm?) throws -> (any VerifyingKey)? {
guard let pub = keys[keyId] else { return nil }
return Ed25519VerifyingKey(keyId: keyId, publicKey: pub)
}
}
// Kotlin: KeyProvider is a fun interface (SAM). Auto-detect from JCA key type.
val provider = KeyProvider { keyId, _ ->
val pub = keyStore[keyId] ?: return@KeyProvider null
Keys.verifyingKey(keyId, pub) // auto-detects algorithm
}
Step 2: Configure Verification Options
Verification options let you enforce policies on incoming signatures: which components must be covered, how old signatures can be, and whether expired signatures are rejected.
opts := &httpsig.VerifyOptions{
RequiredComponents: []httpsig.ComponentIdentifier{
httpsig.Component("@method"),
httpsig.Component("@authority"),
},
MaxAge: 5 * time.Minute,
MaxClockSkew: 30 * time.Second, // reject signatures created >30s in the future
RejectExpired: httpsig.BoolPtr(true),
}
import type { VerifyOptions } from '@zourzouvillys/httpsig';
import { component } from '@zourzouvillys/httpsig';
const opts: VerifyOptions = {
requiredComponents: [component('@method'), component('@authority')],
maxAgeMs: 5 * 60 * 1000,
maxClockSkewMs: 30_000, // reject signatures created >30s in the future
rejectExpired: true,
};
var opts = new Verifier.VerifyOptions(
List.of(
ComponentIdentifier.of("@method"),
ComponentIdentifier.of("@authority")
),
Duration.ofMinutes(5),
Duration.ofSeconds(30), // reject signatures created >30s in the future
true,
null,
null
);
let opts = VerifyOptions(
requiredComponents: [.init("@method"), .init("@authority")],
maxAge: 300,
maxClockSkew: 30 // reject signatures created >30s in the future
)
val opts = Verifier.VerifyOptions(
requiredComponents = listOf(
ComponentIdentifier.of("@method"),
ComponentIdentifier.of("@authority"),
),
maxAge = Duration.ofMinutes(5),
maxClockSkew = Duration.ofSeconds(30), // reject signatures created >30s in the future
rejectExpired = true,
)
Step 3: Verify
result, err := httpsig.VerifyMessage(msg, provider, opts, nil)
if err != nil {
// signature invalid, missing, or expired
return err
}
fmt.Printf("Verified: label=%s, keyId=%s, algorithm=%s\n",
result.Label, result.KeyID, result.Algorithm)
import { verifyMessage } from '@zourzouvillys/httpsig';
try {
const result = await verifyMessage(msg, provider, opts);
console.log(`Verified: label=${result.label}, keyId=${result.keyId}`);
} catch (err) {
// MissingSignatureError, InvalidSignatureError, or MalformedInputError
console.error('Verification failed:', err);
}
try {
Verifier.VerifyResult result = Verifier.verify(httpMessage, provider, opts, null);
System.out.println("Verified: " + result.label() + " by " + result.keyId());
} catch (HttpSigException e) {
// signature invalid or missing
}
do {
let result = try Verifier.verify(msg: httpMessage, provider: keyProvider, options: opts)
print("Verified: label=\(result.label), keyId=\(result.keyId)")
} catch {
// HttpSigError: missingSignature, invalidSignature, signatureExpired, etc.
print("Verification failed: \(error)")
}
try {
val result = Verifier.verify(httpMessage, provider, opts)
println("Verified: label=${result.label}, keyId=${result.keyId}")
} catch (e: HttpSigException) {
println("Verification failed: ${e.message}")
}
Replay Protection with Nonce Checking
The nonceChecker option in VerifyOptions lets you plug in custom replay detection. When a signature includes a nonce parameter, the verifier calls your checker after cryptographic verification succeeds. If the checker rejects the nonce (e.g., it was already seen), verification fails.
opts := &httpsig.VerifyOptions{
RequiredComponents: []httpsig.ComponentIdentifier{
httpsig.Component("@method"),
httpsig.Component("@authority"),
},
NonceChecker: func(ctx context.Context, nonce, keyID string, alg httpsig.Algorithm) error {
if seen, _ := nonceStore.Exists(ctx, nonce); seen {
return fmt.Errorf("nonce already used: %s", nonce)
}
nonceStore.Record(ctx, nonce)
return nil
},
}
const opts: VerifyOptions = {
requiredComponents: [component('@method'), component('@authority')],
nonceChecker: async (nonce, keyId, algorithm) => {
if (await nonceStore.exists(nonce)) {
throw new Error(`nonce already used: ${nonce}`);
}
await nonceStore.record(nonce);
},
};
var opts = Verifier.VerifyOptions.builder()
.requiredComponents(List.of(
ComponentIdentifier.of("@method"),
ComponentIdentifier.of("@authority")))
.nonceChecker((nonce, keyId, algorithm) -> {
if (nonceStore.exists(nonce)) {
throw new HttpSigException("nonce already used: " + nonce);
}
nonceStore.record(nonce);
})
.build();
let opts = VerifyOptions(
requiredComponents: [.init("@method"), .init("@authority")],
nonceChecker: { nonce, keyId, algorithm in
guard !nonceStore.exists(nonce) else {
throw HttpSigError.invalidSignature("nonce already used: \(nonce)")
}
nonceStore.record(nonce)
}
)
val opts = Verifier.VerifyOptions(
requiredComponents = listOf(
ComponentIdentifier.of("@method"),
ComponentIdentifier.of("@authority"),
),
nonceChecker = { nonce, keyId, algorithm ->
if (nonceStore.exists(nonce)) {
throw HttpSigException("nonce already used: $nonce")
}
nonceStore.record(nonce)
},
)
The nonce value is also available in VerifyResult.nonce after successful verification, so you can inspect it even without a checker.
Verification Process
Under the hood, verifyMessage performs these steps:
- Parse the
Signature-Inputheader as an SFV dictionary to get the list of signatures. - Parse the
Signatureheader to get the raw signature bytes for each label. - For each signature (or just the one matching
requiredLabel): a. Extract the covered components and metadata from the inner list. b. Check that allrequiredComponentsare covered. c. Check time constraints (maxAge,maxClockSkew,rejectExpired). d. Resolve the key via theKeyProvider. e. Verify thealgparameter (if present) matches the resolved key's algorithm. f. Reconstruct the signature base from the message. g. Verify the cryptographic signature. - Return the first signature that passes all checks, or an error if none do.
Error Handling
All implementations distinguish between these error cases:
- Missing signature: no
Signature-InputorSignatureheaders found - Malformed input: headers present but cannot be parsed as SFV
- Invalid signature: signature present but cryptographic verification failed
- Expired: signature is older than
maxAgeor past itsexpirestime - Future-dated: signature
createdtimestamp is more thanmaxClockSkewahead of the current time - Algorithm mismatch: the
algparameter in the signature metadata doesn't match the resolved key's algorithm - Key not found: the
KeyProviderreturned no key for the givenkeyId
Verifying Response Signatures
To verify a response signature that includes request-bound components (;req), pass the original request as the reqMsg parameter:
result, err := httpsig.VerifyMessage(responseMsg, provider, opts, requestMsg)
This allows the verifier to extract @method;req, @authority;req, and other request components from the original request when reconstructing the signature base.