use crate::effects::EffectRow;
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
type HmacSha256 = Hmac<Sha256>;
pub const ED25519_SIG_PREFIX: &str = "ed25519:";
#[must_use]
pub fn sign_ed25519(payload: &str, signing_key: &ed25519_dalek::SigningKey) -> String {
use ed25519_dalek::Signer;
let sig = signing_key.sign(payload.as_bytes());
format!("{ED25519_SIG_PREFIX}{}", encode_hex(&sig.to_bytes()))
}
#[must_use]
pub fn verify_ed25519(
payload: &str,
signature: &str,
verifying_key: &ed25519_dalek::VerifyingKey,
) -> bool {
use ed25519_dalek::Verifier;
let Some(hex) = signature.strip_prefix(ED25519_SIG_PREFIX) else {
return false;
};
let Some(bytes) = decode_hex(hex) else {
return false;
};
let Ok(bytes): Result<[u8; 64], _> = bytes.try_into() else {
return false;
};
let sig = ed25519_dalek::Signature::from_bytes(&bytes);
verifying_key.verify(payload.as_bytes(), &sig).is_ok()
}
fn encode_hex(bytes: &[u8]) -> String {
use std::fmt::Write;
bytes.iter().fold(String::new(), |mut acc, b| {
let _ = write!(acc, "{b:02x}");
acc
})
}
fn decode_hex(hex: &str) -> Option<Vec<u8>> {
if hex.len() % 2 != 0 {
return None;
}
let bytes = hex.as_bytes();
let mut out = Vec::with_capacity(hex.len() / 2);
for chunk in bytes.chunks_exact(2) {
let hi = hex_val(chunk[0])?;
let lo = hex_val(chunk[1])?;
out.push((hi << 4) | lo);
}
Some(out)
}
const fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[must_use]
pub fn sign_hmac(payload: &str, key: &[u8]) -> Option<String> {
let Ok(mut mac) = HmacSha256::new_from_slice(key) else {
return None;
};
mac.update(payload.as_bytes());
Some(format!("{:x}", mac.finalize().into_bytes()))
}
#[must_use]
pub fn verify_hmac(payload: &str, signature: &str, key: &[u8]) -> bool {
if signature.is_empty() {
return false;
}
let Some(expected) = decode_hex(signature) else {
return false;
};
let Ok(mut mac) = HmacSha256::new_from_slice(key) else {
return false;
};
mac.update(payload.as_bytes());
mac.verify_slice(&expected).is_ok()
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ToolManifest {
pub tool_name: String,
pub version: String,
pub publisher: String,
pub description: String,
#[serde(default)]
pub effects: EffectSummary,
pub capabilities: Vec<String>,
pub trust_level: f32,
pub requires_human_review: bool,
pub created_at: i64,
#[serde(default)]
pub signature: String,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct EffectSummary {
#[serde(default)]
pub reads: Vec<String>,
#[serde(default)]
pub writes: Vec<String>,
#[serde(default)]
pub spawns: bool,
}
impl EffectSummary {
#[must_use]
pub fn from_effect_row(effects: &EffectRow) -> Self {
Self {
reads: effects.reads.iter().map(|r| format!("{r:?}")).collect(),
writes: effects.writes.iter().map(|r| format!("{r:?}")).collect(),
spawns: effects.spawns,
}
}
#[must_use]
pub fn has_destructive_effects(&self) -> bool {
!self.writes.is_empty() || self.spawns
}
}
impl ToolManifest {
#[must_use]
pub fn new(tool_name: &str, version: &str, publisher: &str, description: &str) -> Self {
Self {
tool_name: tool_name.to_string(),
version: version.to_string(),
publisher: publisher.to_string(),
description: description.to_string(),
effects: EffectSummary::default(),
capabilities: Vec::new(),
trust_level: 0.5,
requires_human_review: false,
created_at: chrono::Utc::now().timestamp(),
signature: String::new(),
}
}
#[must_use]
pub fn with_effects(mut self, effects: EffectSummary) -> Self {
self.effects = effects;
self
}
#[must_use]
pub fn with_capabilities(mut self, caps: Vec<String>) -> Self {
self.capabilities = caps;
self
}
#[must_use]
pub const fn with_trust(mut self, trust: f32) -> Self {
self.trust_level = trust.clamp(0.0, 1.0);
self
}
#[must_use]
pub const fn require_review(mut self) -> Self {
self.requires_human_review = true;
self
}
#[must_use]
pub fn signing_payload(&self) -> String {
let without_sig = Self {
signature: String::new(),
..self.clone()
};
serde_json::to_string(&without_sig).unwrap_or_default()
}
#[must_use]
pub fn sign(mut self, key: &[u8]) -> Self {
let payload = self.signing_payload();
if let Some(sig) = sign_hmac(&payload, key) {
self.signature = sig;
}
self
}
#[must_use]
pub fn verify_signature(&self, key: &[u8]) -> bool {
verify_hmac(&self.signing_payload(), &self.signature, key)
}
#[must_use]
pub fn signature_scheme(&self) -> &'static str {
if self.signature.starts_with(ED25519_SIG_PREFIX) {
"ed25519"
} else if self.signature.is_empty() {
"none"
} else {
"hmac-sha256"
}
}
#[must_use]
pub fn sign_ed25519(mut self, signing_key: &ed25519_dalek::SigningKey) -> Self {
self.signature = sign_ed25519(&self.signing_payload(), signing_key);
self
}
#[must_use]
pub fn verify_signature_ed25519(&self, verifying_key: &ed25519_dalek::VerifyingKey) -> bool {
verify_ed25519(&self.signing_payload(), &self.signature, verifying_key)
}
#[must_use]
pub fn has_capability(&self, cap: &str) -> bool {
self.capabilities.iter().any(|c| c == cap)
}
#[must_use]
pub fn is_destructive(&self) -> bool {
self.effects.has_destructive_effects()
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TrustScope {
pub name: String,
pub min_trust: f32,
#[serde(default)]
pub allowed_tools: Vec<String>,
#[serde(default)]
pub denied_tools: Vec<String>,
pub allow_destructive: bool,
pub allow_network: bool,
pub allow_filesystem_write: bool,
pub require_review: bool,
pub max_calls_per_minute: u32,
}
impl Default for TrustScope {
fn default() -> Self {
Self {
name: "default".into(),
min_trust: 0.5,
allowed_tools: Vec::new(),
denied_tools: Vec::new(),
allow_destructive: false,
allow_network: false,
allow_filesystem_write: false,
require_review: false,
max_calls_per_minute: 60,
}
}
}
impl TrustScope {
#[must_use]
pub fn strict() -> Self {
Self {
name: "strict".into(),
min_trust: 0.8,
allowed_tools: vec!["memory.search".into(), "memory.recall".into()],
denied_tools: vec!["file.".into(), "process.".into(), "network.".into()],
allow_destructive: false,
allow_network: false,
allow_filesystem_write: false,
require_review: true,
max_calls_per_minute: 10,
}
}
#[must_use]
pub fn permissive() -> Self {
Self {
name: "permissive".into(),
min_trust: 0.3,
allow_destructive: true,
allow_network: true,
allow_filesystem_write: true,
require_review: false,
max_calls_per_minute: 200,
..Self::default()
}
}
#[must_use]
pub fn is_tool_allowed(&self, manifest: &ToolManifest) -> bool {
if manifest.trust_level < self.min_trust {
return false;
}
if self
.denied_tools
.iter()
.any(|p| manifest.tool_name.starts_with(p))
{
return false;
}
if !self.allowed_tools.is_empty()
&& !self
.allowed_tools
.iter()
.any(|p| manifest.tool_name.starts_with(p))
{
return false;
}
if manifest.is_destructive() && !self.allow_destructive {
return false;
}
if manifest.has_capability("network_access") && !self.allow_network {
return false;
}
if manifest.has_capability("filesystem_write") && !self.allow_filesystem_write {
return false;
}
if self.require_review && !manifest.requires_human_review {
}
true
}
}
pub struct ToolAttestationRegistry {
manifests: ahash::AHashMap<String, ToolManifest>,
signing_key: Vec<u8>,
ed25519_key: Option<ed25519_dalek::VerifyingKey>,
external_scope: TrustScope,
trusted_publishers: Vec<String>,
}
impl ToolAttestationRegistry {
#[must_use]
pub fn new(signing_key: Vec<u8>) -> Self {
Self {
manifests: ahash::AHashMap::new(),
signing_key,
ed25519_key: None,
external_scope: TrustScope::default(),
trusted_publishers: vec!["whitemagic-core".into()],
}
}
#[must_use]
pub fn new_ed25519(verifying_key: ed25519_dalek::VerifyingKey) -> Self {
Self {
manifests: ahash::AHashMap::new(),
signing_key: Vec::new(),
ed25519_key: Some(verifying_key),
external_scope: TrustScope::default(),
trusted_publishers: vec!["whitemagic-core".into()],
}
}
#[must_use]
pub const fn with_ed25519_key(mut self, verifying_key: ed25519_dalek::VerifyingKey) -> Self {
self.ed25519_key = Some(verifying_key);
self
}
#[must_use]
pub fn with_legacy_hmac_key(mut self, key: Vec<u8>) -> Self {
self.signing_key = key;
self
}
#[must_use]
fn verify_manifest(&self, manifest: &ToolManifest) -> bool {
match manifest.signature_scheme() {
"ed25519" => self
.ed25519_key
.as_ref()
.is_some_and(|key| manifest.verify_signature_ed25519(key)),
"hmac-sha256" => {
!self.signing_key.is_empty() && manifest.verify_signature(&self.signing_key)
}
_ => false,
}
}
#[must_use]
pub fn with_external_scope(mut self, scope: TrustScope) -> Self {
self.external_scope = scope;
self
}
pub fn trust_publisher(&mut self, publisher: &str) {
if !self.trusted_publishers.contains(&publisher.to_string()) {
self.trusted_publishers.push(publisher.to_string());
}
}
pub fn register(&mut self, manifest: ToolManifest) -> bool {
if !self.verify_manifest(&manifest) {
return false;
}
if !self.trusted_publishers.contains(&manifest.publisher) {
return false;
}
self.manifests.insert(manifest.tool_name.clone(), manifest);
true
}
pub fn register_unsigned(&mut self, manifest: ToolManifest) -> bool {
if !self.trusted_publishers.contains(&manifest.publisher) {
return false;
}
self.manifests.insert(manifest.tool_name.clone(), manifest);
true
}
#[must_use]
pub fn get(&self, tool_name: &str) -> Option<&ToolManifest> {
self.manifests.get(tool_name)
}
#[must_use]
pub fn is_tool_allowed(&self, tool_name: &str) -> bool {
let Some(manifest) = self.manifests.get(tool_name) else {
return false; };
if manifest.publisher == "whitemagic-core" {
return true;
}
self.external_scope.is_tool_allowed(manifest)
}
#[must_use]
pub fn verify_provenance(&self, manifest: &ToolManifest) -> bool {
self.verify_manifest(manifest) && self.trusted_publishers.contains(&manifest.publisher)
}
#[must_use]
pub fn registered_tools(&self) -> Vec<String> {
self.manifests.keys().cloned().collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.manifests.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.manifests.is_empty()
}
}
#[must_use]
pub fn manifest_hash(manifest: &ToolManifest) -> String {
let payload = manifest.signing_payload();
let mut hasher = Sha256::new();
hasher.update(payload.as_bytes());
format!("{:x}", hasher.finalize())
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_KEY: &[u8] = b"test_signing_key_123";
fn make_manifest(name: &str, publisher: &str) -> ToolManifest {
ToolManifest::new(name, "1.0.0", publisher, "A test tool")
.with_trust(0.8)
.with_capabilities(vec!["read_only".into()])
}
#[test]
fn manifest_sign_and_verify() {
let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
assert!(
manifest.verify_signature(TEST_KEY),
"Signed manifest should verify"
);
}
#[test]
fn manifest_tamper_detected() {
let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
let tampered = ToolManifest {
description: "Tampered description".into(),
..manifest
};
assert!(
!tampered.verify_signature(TEST_KEY),
"Tampered manifest should fail verification"
);
}
#[test]
fn manifest_unsigned_fails_verification() {
let manifest = make_manifest("memory.search", "whitemagic-core");
assert!(!manifest.verify_signature(TEST_KEY));
}
#[test]
fn manifest_wrong_key_fails() {
let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
assert!(!manifest.verify_signature(b"wrong_key"));
}
#[test]
fn manifest_has_capability() {
let manifest = make_manifest("memory.search", "whitemagic-core")
.with_capabilities(vec!["read_only".into(), "search".into()]);
assert!(manifest.has_capability("read_only"));
assert!(manifest.has_capability("search"));
assert!(!manifest.has_capability("network_access"));
}
#[test]
fn manifest_destructive_detection() {
let manifest = ToolManifest::new("file.write", "1.0.0", "whitemagic-core", "Write file")
.with_effects(EffectSummary {
writes: vec!["Filesystem".into()],
..Default::default()
});
assert!(manifest.is_destructive());
let read_only = ToolManifest::new("memory.search", "1.0.0", "whitemagic-core", "Search")
.with_effects(EffectSummary {
reads: vec!["Galaxy".into()],
..Default::default()
});
assert!(!read_only.is_destructive());
}
#[test]
fn trust_scope_default_allows_trusted() {
let scope = TrustScope::default();
let manifest = make_manifest("memory.search", "whitemagic-core").with_trust(0.6);
assert!(scope.is_tool_allowed(&manifest));
}
#[test]
fn trust_scope_blocks_low_trust() {
let scope = TrustScope::default();
let manifest = make_manifest("memory.search", "external").with_trust(0.2);
assert!(!scope.is_tool_allowed(&manifest));
}
#[test]
fn trust_scope_strict_blocks_destructive() {
let scope = TrustScope::strict();
let manifest = ToolManifest::new("file.write", "1.0.0", "external", "Write file")
.with_trust(0.9)
.with_effects(EffectSummary {
writes: vec!["Filesystem".into()],
..Default::default()
});
assert!(!scope.is_tool_allowed(&manifest));
}
#[test]
fn trust_scope_strict_blocks_network() {
let scope = TrustScope::strict();
let manifest = ToolManifest::new("http.fetch", "1.0.0", "external", "Fetch URL")
.with_trust(0.9)
.with_capabilities(vec!["network_access".into()]);
assert!(!scope.is_tool_allowed(&manifest));
}
#[test]
fn trust_scope_denied_list_takes_precedence() {
let scope = TrustScope {
allowed_tools: vec!["memory.".into()],
denied_tools: vec!["memory.delete".into()],
..TrustScope::permissive()
};
let manifest = make_manifest("memory.delete", "external").with_trust(0.9);
assert!(!scope.is_tool_allowed(&manifest));
}
#[test]
fn registry_register_signed_manifest() {
let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
assert!(registry.register(manifest));
assert_eq!(registry.len(), 1);
}
#[test]
fn registry_rejects_invalid_signature() {
let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
let manifest = make_manifest("memory.search", "whitemagic-core").sign(b"wrong_key");
assert!(!registry.register(manifest));
assert_eq!(registry.len(), 0);
}
#[test]
fn registry_rejects_untrusted_publisher() {
let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
let manifest = make_manifest("memory.search", "untrusted").sign(TEST_KEY);
assert!(!registry.register(manifest));
}
#[test]
fn registry_allows_trusted_publisher() {
let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
registry.trust_publisher("external:acme");
let manifest = make_manifest("custom.tool", "external:acme").sign(TEST_KEY);
assert!(registry.register(manifest));
}
#[test]
fn registry_internal_tools_bypass_scope() {
let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec())
.with_external_scope(TrustScope::strict());
let manifest = ToolManifest::new("file.write", "1.0.0", "whitemagic-core", "Write")
.with_trust(0.5)
.with_effects(EffectSummary {
writes: vec!["Filesystem".into()],
..Default::default()
})
.sign(TEST_KEY);
registry.register(manifest);
assert!(registry.is_tool_allowed("file.write"));
}
#[test]
fn registry_external_tools_checked_against_scope() {
let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec())
.with_external_scope(TrustScope::strict());
registry.trust_publisher("external:acme");
let manifest = ToolManifest::new("file.write", "1.0.0", "external:acme", "Write")
.with_trust(0.9)
.with_effects(EffectSummary {
writes: vec!["Filesystem".into()],
..Default::default()
})
.sign(TEST_KEY);
registry.register(manifest);
assert!(!registry.is_tool_allowed("file.write"));
}
#[test]
fn registry_unknown_tool_not_allowed() {
let registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
assert!(!registry.is_tool_allowed("unknown.tool"));
}
#[test]
fn registry_verify_provenance() {
let registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
assert!(registry.verify_provenance(&manifest));
let untrusted = make_manifest("memory.search", "untrusted").sign(TEST_KEY);
assert!(!registry.verify_provenance(&untrusted));
}
#[test]
fn manifest_hash_deterministic() {
let m1 = make_manifest("memory.search", "whitemagic-core");
let m2 = make_manifest("memory.search", "whitemagic-core");
assert_eq!(manifest_hash(&m1), manifest_hash(&m2));
}
#[test]
fn manifest_hash_changes_with_content() {
let m1 = make_manifest("memory.search", "whitemagic-core");
let m2 = make_manifest("memory.search", "whitemagic-core").with_trust(0.9);
assert_ne!(manifest_hash(&m1), manifest_hash(&m2));
}
#[test]
fn registry_registered_tools_list() {
let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
registry.register(make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY));
registry.register(make_manifest("memory.recall", "whitemagic-core").sign(TEST_KEY));
let tools = registry.registered_tools();
assert_eq!(tools.len(), 2);
assert!(tools.contains(&"memory.search".to_string()));
assert!(tools.contains(&"memory.recall".to_string()));
}
#[test]
fn trust_scope_permissive_allows_most() {
let scope = TrustScope::permissive();
let manifest = ToolManifest::new("file.write", "1.0.0", "external", "Write")
.with_trust(0.5)
.with_effects(EffectSummary {
writes: vec!["Filesystem".into()],
..Default::default()
})
.with_capabilities(vec!["filesystem_write".into()]);
assert!(scope.is_tool_allowed(&manifest));
}
fn ed25519_key() -> ed25519_dalek::SigningKey {
ed25519_dalek::SigningKey::from_bytes(&[7u8; 32])
}
#[test]
fn manifest_sign_and_verify_ed25519() {
let key = ed25519_key();
let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
assert!(manifest.verify_signature_ed25519(&key.verifying_key()));
assert_eq!(manifest.signature_scheme(), "ed25519");
}
#[test]
fn manifest_ed25519_tamper_detected() {
let key = ed25519_key();
let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
let tampered = ToolManifest {
description: "Tampered".into(),
..manifest
};
assert!(!tampered.verify_signature_ed25519(&key.verifying_key()));
}
#[test]
fn manifest_ed25519_wrong_key_fails() {
let key = ed25519_key();
let other = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
assert!(!manifest.verify_signature_ed25519(&other.verifying_key()));
}
#[test]
fn manifest_ed25519_signature_not_valid_as_hmac() {
let key = ed25519_key();
let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
assert!(!manifest.verify_signature(TEST_KEY));
}
#[test]
fn manifest_hmac_signature_scheme_unchanged() {
let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
assert_eq!(manifest.signature_scheme(), "hmac-sha256");
assert!(manifest.verify_signature(TEST_KEY));
assert!(!manifest.verify_signature_ed25519(&ed25519_key().verifying_key()));
}
#[test]
fn unsigned_manifest_scheme_none() {
let manifest = make_manifest("memory.search", "whitemagic-core");
assert_eq!(manifest.signature_scheme(), "none");
}
#[test]
fn registry_ed25519_first_registers_ed25519_manifest() {
let key = ed25519_key();
let mut registry = ToolAttestationRegistry::new_ed25519(key.verifying_key());
let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
assert!(registry.register(manifest.clone()));
assert!(registry.verify_provenance(&manifest));
}
#[test]
fn registry_ed25519_first_rejects_legacy_hmac_without_key() {
let key = ed25519_key();
let mut registry = ToolAttestationRegistry::new_ed25519(key.verifying_key());
let manifest = make_manifest("memory.search", "whitemagic-core").sign(TEST_KEY);
assert!(
!registry.register(manifest),
"legacy HMAC must fail closed when no legacy key is configured"
);
assert_eq!(registry.len(), 0);
}
#[test]
fn registry_migration_window_accepts_both_schemes() {
let key = ed25519_key();
let mut registry = ToolAttestationRegistry::new_ed25519(key.verifying_key())
.with_legacy_hmac_key(TEST_KEY.to_vec());
let modern = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
let legacy = make_manifest("memory.recall", "whitemagic-core").sign(TEST_KEY);
assert!(registry.register(modern));
assert!(registry.register(legacy));
assert_eq!(registry.len(), 2);
}
#[test]
fn registry_ed25519_rejects_wrong_issuer_key() {
let key = ed25519_key();
let other = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
let mut registry = ToolAttestationRegistry::new_ed25519(other.verifying_key());
let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
assert!(!registry.register(manifest));
}
#[test]
fn registry_ed25519_rejects_tampered_manifest() {
let key = ed25519_key();
let mut registry = ToolAttestationRegistry::new_ed25519(key.verifying_key());
let tampered = ToolManifest {
description: "Tampered".into(),
..make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key)
};
assert!(!registry.register(tampered));
}
#[test]
fn registry_hmac_only_rejects_ed25519_manifest() {
let key = ed25519_key();
let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
let manifest = make_manifest("memory.search", "whitemagic-core").sign_ed25519(&key);
assert!(
!registry.register(manifest),
"an ed25519 manifest needs an ed25519 issuer key"
);
}
#[test]
fn registry_rejects_unsigned_manifest_fail_closed() {
let mut registry = ToolAttestationRegistry::new(TEST_KEY.to_vec());
let manifest = make_manifest("memory.search", "whitemagic-core");
assert!(!registry.register(manifest));
}
#[test]
fn verify_hmac_rejects_forgeries_and_malformed_hex() {
let good = sign_hmac("payload", TEST_KEY).unwrap();
assert!(verify_hmac("payload", &good, TEST_KEY));
assert!(!verify_hmac("payload", &good, b"wrong"));
assert!(!verify_hmac("payload!", &good, TEST_KEY));
assert!(!verify_hmac("payload", "", TEST_KEY));
assert!(!verify_hmac("payload", "zz", TEST_KEY));
}
}