use crate::client::http::KeygenResponse;
use crate::clock::Clock;
use crate::crypto::{
digest::verify_digest,
freshness::check_date_freshness,
signing::build_signing_string,
verify::{decode_public_key, parse_signature_header, verify_ed25519},
};
use crate::GatewardenError;
pub fn verify_response(
response: &KeygenResponse,
public_key_hex: &str,
clock: &dyn Clock,
) -> Result<(), GatewardenError> {
let signature_header = response
.signature
.as_ref()
.ok_or(GatewardenError::SignatureMissing)?;
let date_header = response
.date
.as_ref()
.ok_or(GatewardenError::SignatureMissing)?;
verify_digest(&response.body, response.digest.as_deref())?;
let parsed_sig = parse_signature_header(signature_header)?;
let verifying_key = decode_public_key(public_key_hex)?;
let signing_string = build_signing_string(
"post",
&response.request_path,
&response.host,
date_header,
response.digest.as_deref(),
);
verify_ed25519(&parsed_sig.signature, &signing_string, &verifying_key)?;
check_date_freshness(date_header, clock)?;
Ok(())
}
pub fn verify_response_signature_only(
response: &KeygenResponse,
public_key_hex: &str,
) -> Result<(), GatewardenError> {
let signature_header = response
.signature
.as_ref()
.ok_or(GatewardenError::SignatureMissing)?;
let date_header = response
.date
.as_ref()
.ok_or(GatewardenError::SignatureMissing)?;
verify_digest(&response.body, response.digest.as_deref())?;
let parsed_sig = parse_signature_header(signature_header)?;
let verifying_key = decode_public_key(public_key_hex)?;
let signing_string = build_signing_string(
"post",
&response.request_path,
&response.host,
date_header,
response.digest.as_deref(),
);
verify_ed25519(&parsed_sig.signature, &signing_string, &verifying_key)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::clock::MockClock;
use crate::crypto::digest::format_digest_header;
use base64::{engine::general_purpose::STANDARD, Engine};
use chrono::{TimeZone, Utc};
use ed25519_dalek::{Signer, SigningKey};
const TEST_SIGNING_SEED_BYTES: [u8; 32] = [
0x9d, 0x61, 0xb1, 0x9d, 0xef, 0xfd, 0x5a, 0x60, 0xba, 0x84, 0x4a, 0xf4, 0x92, 0xec, 0x2c,
0xc4, 0x44, 0x49, 0xc5, 0x69, 0x7b, 0x32, 0x69, 0x19, 0x70, 0x3b, 0xac, 0x03, 0x1c, 0xae,
0x7f, 0x60,
];
const TEST_VERIFY_KEY_HEX: &str =
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a";
fn get_test_signing_key() -> SigningKey {
SigningKey::from_bytes(&TEST_SIGNING_SEED_BYTES)
}
fn sign_test_data(signing_string: &str) -> String {
let signing_key = get_test_signing_key();
let signature = signing_key.sign(signing_string.as_bytes());
STANDARD.encode(signature.to_bytes())
}
fn create_test_response(body: &str, date: &str, host: &str, path: &str) -> KeygenResponse {
let body_bytes = body.as_bytes().to_vec();
let digest = format_digest_header(&body_bytes);
let signing_string = build_signing_string("post", path, host, date, Some(&digest));
let signature_b64 = sign_test_data(&signing_string);
let signature_header = format!(r#"algorithm="ed25519", signature="{}""#, signature_b64);
KeygenResponse {
status: 200,
date: Some(date.to_string()),
signature: Some(signature_header),
digest: Some(digest),
body: body_bytes,
request_path: path.to_string(),
host: host.to_string(),
}
}
#[test]
fn test_verify_response_valid() {
let clock = MockClock::new(Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap());
let response = create_test_response(
r#"{"data":{"valid":true}}"#,
"Wed, 15 Jan 2025 12:00:00 GMT",
"api.keygen.sh",
"/v1/accounts/test/licenses/actions/validate-key",
);
let result = verify_response(&response, TEST_VERIFY_KEY_HEX, &clock);
assert!(result.is_ok());
}
#[test]
fn test_verify_response_missing_signature() {
let clock = MockClock::new(Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap());
let mut response = create_test_response(
r#"{"data":{"valid":true}}"#,
"Wed, 15 Jan 2025 12:00:00 GMT",
"api.keygen.sh",
"/v1/accounts/test/licenses/actions/validate-key",
);
response.signature = None;
let result = verify_response(&response, TEST_VERIFY_KEY_HEX, &clock);
assert!(matches!(result, Err(GatewardenError::SignatureMissing)));
}
#[test]
fn test_verify_response_missing_date() {
let clock = MockClock::new(Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap());
let mut response = create_test_response(
r#"{"data":{"valid":true}}"#,
"Wed, 15 Jan 2025 12:00:00 GMT",
"api.keygen.sh",
"/v1/accounts/test/licenses/actions/validate-key",
);
response.date = None;
let result = verify_response(&response, TEST_VERIFY_KEY_HEX, &clock);
assert!(matches!(result, Err(GatewardenError::SignatureMissing)));
}
#[test]
fn test_verify_response_digest_mismatch() {
let clock = MockClock::new(Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap());
let mut response = create_test_response(
r#"{"data":{"valid":true}}"#,
"Wed, 15 Jan 2025 12:00:00 GMT",
"api.keygen.sh",
"/v1/accounts/test/licenses/actions/validate-key",
);
response.body = b"tampered body".to_vec();
let result = verify_response(&response, TEST_VERIFY_KEY_HEX, &clock);
assert!(matches!(result, Err(GatewardenError::DigestMismatch)));
}
#[test]
fn test_verify_response_invalid_signature() {
let clock = MockClock::new(Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap());
let mut response = create_test_response(
r#"{"data":{"valid":true}}"#,
"Wed, 15 Jan 2025 12:00:00 GMT",
"api.keygen.sh",
"/v1/accounts/test/licenses/actions/validate-key",
);
let wrong_sig = STANDARD.encode([0u8; 64]);
response.signature = Some(format!(r#"algorithm="ed25519", signature="{}""#, wrong_sig));
let result = verify_response(&response, TEST_VERIFY_KEY_HEX, &clock);
assert!(matches!(result, Err(GatewardenError::SignatureInvalid)));
}
#[test]
fn test_verify_response_stale() {
let clock = MockClock::new(Utc.with_ymd_and_hms(2025, 1, 15, 12, 10, 0).unwrap()); let response = create_test_response(
r#"{"data":{"valid":true}}"#,
"Wed, 15 Jan 2025 12:00:00 GMT",
"api.keygen.sh",
"/v1/accounts/test/licenses/actions/validate-key",
);
let result = verify_response(&response, TEST_VERIFY_KEY_HEX, &clock);
assert!(matches!(
result,
Err(GatewardenError::ResponseTooOld { .. })
));
}
#[test]
fn test_verify_response_future() {
let clock = MockClock::new(Utc.with_ymd_and_hms(2025, 1, 15, 11, 58, 0).unwrap()); let response = create_test_response(
r#"{"data":{"valid":true}}"#,
"Wed, 15 Jan 2025 12:00:00 GMT",
"api.keygen.sh",
"/v1/accounts/test/licenses/actions/validate-key",
);
let result = verify_response(&response, TEST_VERIFY_KEY_HEX, &clock);
assert!(matches!(result, Err(GatewardenError::ResponseFromFuture)));
}
#[test]
fn test_verify_response_signature_only_valid() {
let response = create_test_response(
r#"{"data":{"valid":true}}"#,
"Wed, 15 Jan 2025 12:00:00 GMT",
"api.keygen.sh",
"/v1/accounts/test/licenses/actions/validate-key",
);
let result = verify_response_signature_only(&response, TEST_VERIFY_KEY_HEX);
assert!(result.is_ok());
}
#[test]
fn test_verify_response_no_digest() {
let clock = MockClock::new(Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap());
let body = r#"{"data":{"valid":true}}"#;
let date = "Wed, 15 Jan 2025 12:00:00 GMT";
let host = "api.keygen.sh";
let path = "/v1/accounts/test/licenses/actions/validate-key";
let signing_string = build_signing_string("post", path, host, date, None);
let signature_b64 = sign_test_data(&signing_string);
let signature_header = format!(r#"algorithm="ed25519", signature="{}""#, signature_b64);
let response = KeygenResponse {
status: 200,
date: Some(date.to_string()),
signature: Some(signature_header),
digest: None,
body: body.as_bytes().to_vec(),
request_path: path.to_string(),
host: host.to_string(),
};
let result = verify_response(&response, TEST_VERIFY_KEY_HEX, &clock);
assert!(result.is_ok());
}
#[test]
fn test_verify_fails_closed_missing_both() {
let clock = MockClock::new(Utc.with_ymd_and_hms(2025, 1, 15, 12, 0, 0).unwrap());
let response = KeygenResponse {
status: 200,
date: None,
signature: None,
digest: None,
body: b"{}".to_vec(),
request_path: "/test".to_string(),
host: "api.keygen.sh".to_string(),
};
let result = verify_response(&response, TEST_VERIFY_KEY_HEX, &clock);
assert!(matches!(result, Err(GatewardenError::SignatureMissing)));
}
}