///|
pub struct StableSignature {
value : String
} derive(Eq, Debug)
///|
pub fn StableSignature::new(value : String) -> StableSignature {
{ value, }
}
///|
fn utf8_bytes(value : String) -> Bytes {
@utf8.encode(value)
}
///|
pub fn sha256_hex(message : String) -> String {
let bytes = utf8_bytes(message)
@crypto.bytes_to_hex_string(@crypto.sha256(bytes))
}
///|
pub fn hmac_sha256_hex(secret : String, message : String) -> String {
let key = utf8_bytes(secret)
let payload = utf8_bytes(message)
@crypto.bytes_to_hex_string(
@crypto.hmac(@crypto.SHA256::new(), key.view(), payload.view()),
)
}
///|
/// Compatibility helper for deterministic audit and idempotency digests.
pub fn stable_sign(secret : String, canonical : String) -> StableSignature {
StableSignature::new(hmac_sha256_hex(secret, canonical))
}
///|
pub fn constant_time_equal(left : String, right : String) -> Bool {
let left_bytes = utf8_bytes(left)
let right_bytes = utf8_bytes(right)
let max_length = if left_bytes.length() > right_bytes.length() {
left_bytes.length()
} else {
right_bytes.length()
}
let mut difference = left_bytes.length() ^ right_bytes.length()
for i in 0.. String {
strip_prefix(trim_ascii(signature), prefix)
}
///|
fn github_signature(
request : WebhookRequest,
policy : VerificationPolicy,
) -> String {
"sha256=" + hmac_sha256_hex(policy.secret, request.body)
}
///|
fn generic_signature(
request : WebhookRequest,
policy : VerificationPolicy,
) -> String {
policy.provider.signature_prefix +
hmac_sha256_hex(policy.secret, canonical_base(request, policy.provider))
}
///|
fn feishu_signature(
request : WebhookRequest,
policy : VerificationPolicy,
) -> String {
let timestamp = request.header(policy.provider.timestamp_header).unwrap_or("")
let nonce = request.header("x-lark-request-nonce").unwrap_or("")
sha256_hex(timestamp + nonce + policy.secret + request.body)
}
///|
fn stripe_signature_for_timestamp(
request : WebhookRequest,
policy : VerificationPolicy,
timestamp : String,
) -> String {
hmac_sha256_hex(policy.secret, timestamp + "." + request.body)
}
///|
pub fn expected_signature(
request : WebhookRequest,
policy : VerificationPolicy,
) -> String {
match policy.provider.name {
"github" => github_signature(request, policy)
"feishu" => feishu_signature(request, policy)
"stripe" => {
let parsed = parse_stripe_signature(
request.signature(policy.provider.signature_header),
)
if parsed.timestamp_text == "" {
""
} else {
"v1=" +
stripe_signature_for_timestamp(request, policy, parsed.timestamp_text)
}
}
_ => generic_signature(request, policy)
}
}
///|
fn verify_stripe_signature(
request : WebhookRequest,
policy : VerificationPolicy,
) -> Bool {
let parsed = parse_stripe_signature(
request.signature(policy.provider.signature_header),
)
if parsed.timestamp_text == "" || parsed.v1_signatures.length() == 0 {
return false
}
let expected = stripe_signature_for_timestamp(
request,
policy,
parsed.timestamp_text,
)
for signature in parsed.v1_signatures {
if constant_time_equal(signature, expected) {
return true
}
}
false
}
///|
pub fn verify_signature(
request : WebhookRequest,
policy : VerificationPolicy,
) -> Bool {
let actual = request.signature(policy.provider.signature_header)
if actual == "" {
false
} else if policy.provider.name == "stripe" {
verify_stripe_signature(request, policy)
} else {
constant_time_equal(trim_ascii(actual), expected_signature(request, policy))
}
}
///|
fn sign_stripe_request(
request : WebhookRequest,
policy : VerificationPolicy,
) -> WebhookRequest {
let timestamp = if request.received_at > 0 {
request.received_at.to_string()
} else {
policy.now.to_string()
}
let signature = stripe_signature_for_timestamp(request, policy, timestamp)
request.with_replaced_header(
policy.provider.signature_header,
"t=" + timestamp + ",v1=" + signature,
)
}
///|
pub fn sign_request(
request : WebhookRequest,
policy : VerificationPolicy,
) -> WebhookRequest {
if policy.provider.name == "stripe" {
sign_stripe_request(request, policy)
} else {
request.with_replaced_header(
policy.provider.signature_header,
expected_signature(request, policy),
)
}
}