///|
/// Verification outcome for request signatures and timestamp checks.
pub(all) enum VerificationStatus {
Verified
MissingSignature
InvalidSignature
ExpiredTimestamp
Unsupported
} derive(Eq, Debug)
///|
/// Details returned by a verifier.
pub(all) struct VerificationResult {
status : VerificationStatus
reason : String
} derive(Eq, Debug)
///|
/// Stable text used by diagnostics and examples.
pub fn VerificationStatus::to_string(self : VerificationStatus) -> String {
match self {
Verified => "Verified"
MissingSignature => "MissingSignature"
InvalidSignature => "InvalidSignature"
ExpiredTimestamp => "ExpiredTimestamp"
Unsupported => "Unsupported"
}
}
///|
/// Construct a verification result.
pub fn VerificationResult::VerificationResult(
status : VerificationStatus,
reason : StringView,
) -> VerificationResult {
{ status, reason: reason.to_owned() }
}
///|
/// Signature verifier configuration.
pub(all) struct SignatureVerifier {
algorithm : String
header_name : String
secret_hint : String
} derive(Eq, Debug)
///|
/// Construct a verifier descriptor.
pub fn SignatureVerifier::SignatureVerifier(
algorithm : StringView,
header_name : StringView,
secret_hint : StringView,
) -> SignatureVerifier {
{
algorithm: algorithm.to_owned(),
header_name: header_name.to_owned(),
secret_hint: secret_hint.to_owned(),
}
}
///|
/// Check whether the required signature header exists.
pub fn SignatureVerifier::verify_presence(
self : SignatureVerifier,
request : HookRequest,
) -> VerificationResult {
match request.header(self.header_name) {
Some(value) =>
if value == "" {
VerificationResult(InvalidSignature, "signature header is empty")
} else {
VerificationResult(Verified, "signature header is present")
}
None => VerificationResult(MissingSignature, "signature header is missing")
}
}
///|
/// Verify an HMAC-SHA256 signature against the unmodified request body.
/// The comparison is constant-time and accepts the conventional `sha256=` prefix.
pub fn SignatureVerifier::verify_hmac_sha256(
self : SignatureVerifier,
request : HookRequest,
secret : String,
) -> VerificationResult {
match request.header(self.header_name) {
None => VerificationResult(MissingSignature, "signature header is missing")
Some(actual) =>
if secret == "" {
VerificationResult(Unsupported, "verification secret is empty")
} else {
let expected = "sha256=" + hmac_sha256_hex(secret, request.body)
if constant_time_equal(actual.to_lower(), expected) {
VerificationResult(Verified, "HMAC-SHA256 signature is valid")
} else {
VerificationResult(InvalidSignature, "HMAC-SHA256 signature mismatch")
}
}
}
}
///|
/// Verify a GitHub `X-Hub-Signature-256` header using the exact raw body.
pub fn verify_github_signature(
request : HookRequest,
secret : String,
) -> VerificationResult {
github_sha256_verifier().verify_hmac_sha256(request, secret)
}
///|
/// Standard GitHub HMAC-SHA256 verifier descriptor.
pub fn github_sha256_verifier() -> SignatureVerifier {
SignatureVerifier(
"hmac-sha256", "x-hub-signature-256", "GITHUB_WEBHOOK_SECRET",
)
}
///|
/// Standard GitLab token verifier descriptor.
pub fn gitlab_token_verifier() -> SignatureVerifier {
SignatureVerifier("token", "x-gitlab-token", "GITLAB_WEBHOOK_TOKEN")
}
///|
/// Standard Stripe signature verifier descriptor.
pub fn stripe_signature_verifier() -> SignatureVerifier {
SignatureVerifier("stripe-v1", "stripe-signature", "STRIPE_WEBHOOK_SECRET")
}