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:

  1. Parse the Signature-Input header as an SFV dictionary to get the list of signatures.
  2. Parse the Signature header to get the raw signature bytes for each label.
  3. For each signature (or just the one matching requiredLabel): a. Extract the covered components and metadata from the inner list. b. Check that all requiredComponents are covered. c. Check time constraints (maxAge, maxClockSkew, rejectExpired). d. Resolve the key via the KeyProvider. e. Verify the alg parameter (if present) matches the resolved key's algorithm. f. Reconstruct the signature base from the message. g. Verify the cryptographic signature.
  4. 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-Input or Signature headers found
  • Malformed input: headers present but cannot be parsed as SFV
  • Invalid signature: signature present but cryptographic verification failed
  • Expired: signature is older than maxAge or past its expires time
  • Future-dated: signature created timestamp is more than maxClockSkew ahead of the current time
  • Algorithm mismatch: the alg parameter in the signature metadata doesn't match the resolved key's algorithm
  • Key not found: the KeyProvider returned no key for the given keyId

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.