// verifier.mbt — Verification of requests and responses (RFC 9421 §3.2).
//
// The verifier parses the Signature-Input and Signature fields, matches
// labels, checks policy (algorithm whitelist, time, required components),
// resolves keys, rebuilds the signature base, performs the cryptographic
// check, and enforces Content-Digest and nonce policies. Cheap format and
// policy checks always run before any cryptographic work.
///|
/// A signature that passed every check.
pub(all) struct VerifiedSignature {
label : String
keyid : String
algorithm : String
covered_components : Array[CoveredComponent]
created : Int64?
expires : Int64?
nonce : String?
tag : String?
}
///|
/// A signature that was rejected, with the reason.
pub(all) struct RejectedSignature {
label : String?
error : HsError
}
///|
/// The aggregate outcome of verifying all labels.
pub(all) struct VerificationReport {
verified : Array[VerifiedSignature]
rejected : Array[RejectedSignature]
}
///|
/// How multiple signatures must relate for the overall verification to
/// succeed.
pub(all) enum MultiSignaturePolicy {
/// At least one signature verifies.
AnyValid
/// Every label present must verify.
AllPresentValid
/// Only the named label must verify.
SpecificLabel(String)
}
///|
/// Verifies every signature on a request.
pub fn verify_request(
request : RequestContext,
signature_input : String,
signature : String,
resolver : InMemoryKeyResolver,
policy : VerificationPolicy,
clock : FixedClock,
nonce_store : InMemoryNonceStore,
limits : Limits,
multi : MultiSignaturePolicy,
) -> Result[VerificationReport, HsError] {
verify_target(
TargetRequest(request),
signature_input,
signature,
resolver,
policy,
clock,
nonce_store,
limits,
multi,
)
}
///|
/// Verifies every signature on a response.
pub fn verify_response(
response : ResponseContext,
signature_input : String,
signature : String,
resolver : InMemoryKeyResolver,
policy : VerificationPolicy,
clock : FixedClock,
nonce_store : InMemoryNonceStore,
limits : Limits,
multi : MultiSignaturePolicy,
) -> Result[VerificationReport, HsError] {
verify_target(
TargetResponse(response),
signature_input,
signature,
resolver,
policy,
clock,
nonce_store,
limits,
multi,
)
}
///|
/// Verifies every signature on any target message.
pub fn verify_target(
target : SignTarget,
signature_input : String,
signature : String,
resolver : InMemoryKeyResolver,
policy : VerificationPolicy,
clock : FixedClock,
nonce_store : InMemoryNonceStore,
limits : Limits,
multi : MultiSignaturePolicy,
) -> Result[VerificationReport, HsError] {
try {
let report = verify_all_raise(
target, signature_input, signature, resolver, policy, clock, nonce_store, limits,
multi,
)
Ok(report)
} catch {
e => Err(e)
}
}
///|
/// Internal: full verification flow, raising only on structural errors
/// (field-level failures) that make the whole message unverifiable.
fn verify_all_raise(
target : SignTarget,
signature_input : String,
signature : String,
resolver : InMemoryKeyResolver,
policy : VerificationPolicy,
clock : FixedClock,
nonce_store : InMemoryNonceStore,
limits : Limits,
multi : MultiSignaturePolicy,
) -> VerificationReport raise HsError {
limits.check_signature_field_size(signature_input.length(), "Signature-Input")
limits.check_signature_field_size(signature.length(), "Signature")
let input = match parse_signature_input(signature_input, limits) {
Ok(i) => i
Err(e) => raise e
}
let field = match parse_signature_field(signature, limits) {
Ok(f) => f
Err(e) => raise e
}
match validate_signature_input(input, limits) {
Ok(_) => ()
Err(e) => raise e
}
match validate_signature_labels(input, field) {
Ok(_) => ()
Err(e) => raise e
}
if input.entries.length() > limits.max_signature_count {
raise hs_error(PolicyValidation, TooManySignatures, "too many signatures")
}
let verified : Array[VerifiedSignature] = Array::new()
let rejected : Array[RejectedSignature] = Array::new()
for entry in input.entries {
let sig = match get_signature(field, entry.label) {
Ok(b) => b
Err(e) => {
rejected.push({ label: Some(entry.label), error: e })
continue
}
}
try {
let v = verify_one_label_raise(
target, entry, sig, resolver, policy, clock, nonce_store, limits,
)
verified.push(v)
} catch {
e => rejected.push({ label: Some(entry.label), error: e })
}
}
let ok = match multi {
AnyValid => !verified.is_empty()
AllPresentValid => verified.length() == input.entries.length()
SpecificLabel(label) => {
let mut found = false
for v in verified {
if v.label == label {
found = true
break
}
}
found
}
}
if !ok {
raise hs_error(
PolicyValidation,
SignatureMismatch,
"multi-signature policy not satisfied",
)
}
{ verified, rejected }
}
///|
/// Verifies a single label, returning its details on success.
pub fn verify_label(
target : SignTarget,
entry : SignatureInputEntry,
signature_bytes : Bytes,
resolver : InMemoryKeyResolver,
policy : VerificationPolicy,
clock : FixedClock,
nonce_store : InMemoryNonceStore,
limits : Limits,
) -> Result[VerifiedSignature, HsError] {
Ok(
verify_one_label_raise(
target, entry, signature_bytes, resolver, policy, clock, nonce_store, limits,
),
) catch {
e => Err(e)
}
}
///|
/// Internal: verifies one signature, raising on the first failure. Ordering
/// is deliberate: cheap format/policy checks precede key resolution and the
/// cryptographic verification.
fn verify_one_label_raise(
target : SignTarget,
entry : SignatureInputEntry,
signature_bytes : Bytes,
resolver : InMemoryKeyResolver,
policy : VerificationPolicy,
clock : FixedClock,
nonce_store : InMemoryNonceStore,
limits : Limits,
) -> VerifiedSignature raise HsError {
let p = entry.parameters
// 1. Structural parameter checks.
if p.created is Some(c) && p.expires is Some(e) {
if e < c {
raise hs_error(
PolicyValidation,
InvalidTimestamp,
"expires is earlier than created",
)
}
}
if policy.require_created && p.created is None {
raise hs_error(PolicyValidation, InvalidTimestamp, "created is required")
}
if policy.require_expires && p.expires is None {
raise hs_error(PolicyValidation, InvalidTimestamp, "expires is required")
}
if policy.require_keyid && p.keyid is None {
raise hs_error(PolicyValidation, MissingKeyId, "keyid is required")
}
if policy.reject_unknown_parameters && !p.extensions.is_empty() {
raise hs_error(
PolicyValidation,
UnsupportedComponentParameter,
"unknown signature parameters are not allowed",
)
}
if policy.expected_tag is Some(expected) {
let matches = match p.tag {
Some(t) => t == expected
None => false
}
if !matches {
raise hs_error(PolicyValidation, InvalidTag, "tag does not match policy")
}
}
// 2. Required components.
for wanted in policy.required_components {
if !components_cover(entry.covered_components, wanted) {
raise hs_error(
PolicyValidation,
MissingRequiredComponent,
"required component not covered: " + wanted.identifier(),
)
}
}
// 3. Key resolution.
let keyid = match p.keyid {
Some(k) => k
None => raise hs_error(PolicyValidation, MissingKeyId, "keyid is missing")
}
limits.check_keyid_length(keyid)
let record = match resolver.resolve(keyid) {
Ok(r) => r
Err(e) => raise e
}
let requested_alg = p.alg
let algorithm = match requested_alg {
Some(a) => a
None => record.algorithm
}
if !policy.allows_algorithm(algorithm) {
raise hs_error(
PolicyValidation,
AlgorithmNotAllowed,
"algorithm not allowed: " + algorithm,
)
}
if requested_alg is Some(a) {
if record.algorithm != a {
raise hs_error(
KeyResolution,
AlgorithmMismatch,
"key algorithm does not match requested algorithm",
)
}
}
// 4. Time checks.
let now = clock.now_unix_seconds()
let created = p.created
if created is Some(c) {
if c > now + policy.allowed_clock_skew_seconds {
raise hs_error(
PolicyValidation,
CreatedInFuture,
"signature created in the future",
)
}
// Compare with subtraction to avoid Int64 overflow for unbounded policies.
if c - now > policy.max_future_seconds {
raise hs_error(
PolicyValidation,
CreatedInFuture,
"signature created too far in the future",
)
}
if policy.max_signature_age_seconds is Some(limit) {
if now - c > limit {
raise hs_error(
PolicyValidation,
SignatureTooOld,
"signature is too old",
)
}
}
}
if p.expires is Some(e) {
if now > e + policy.allowed_clock_skew_seconds {
raise hs_error(
PolicyValidation,
SignatureExpired,
"signature has expired",
)
}
}
// 5. Build the signature base.
let base = match build_signature_base(target, entry, limits) {
Ok(b) => b
Err(e) => raise e
}
// 6. Cryptographic verification.
let provider = algorithm_provider(algorithm)
let valid = match
provider.verify(base.bytes, signature_bytes, record.material) {
Ok(v) => v
Err(e) => raise e
}
if !valid {
raise hs_error(
CryptographicVerification,
SignatureMismatch,
"signature does not match",
)
}
// 7. Nonce policy.
if p.nonce is Some(nonce) {
limits.check_nonce_length(nonce)
match nonce_store.check_and_store(keyid, nonce, p.expires) {
Ok(_) => ()
Err(e) => raise e
}
} else if policy.require_nonce {
raise hs_error(PolicyValidation, NonceRequired, "nonce is required")
}
// 8. Content-Digest binding.
if policy.require_content_digest || policy.require_content_digest_covered {
let headers = match target {
TargetRequest(req) => req.headers
TargetResponse(resp) => resp.headers
}
let body = match target {
TargetRequest(req) => req.body.unwrap_or(Bytes::new(0))
TargetResponse(resp) => resp.body.unwrap_or(Bytes::new(0))
}
let binding = RequireCoveredContentDigest::new(
entry.covered_components,
limits,
)
match binding.validate(headers, body) {
Ok(_) => ()
Err(e) => raise e
}
}
{
label: entry.label,
keyid,
algorithm,
covered_components: entry.covered_components,
created: p.created,
expires: p.expires,
nonce: p.nonce,
tag: p.tag,
}
}
///|
/// Convenience: an empty verification report.
pub fn empty_report() -> VerificationReport {
{ verified: Array::new(), rejected: Array::new() }
}