use vectorpin::attestation::DOMAIN_TAG;
use vectorpin::{
signer::PinOptions, AttestationError, Pin, Signer, Verifier, VerifyError, VerifyOptions,
};
fn v2_signer(kid: &str) -> Signer {
Signer::generate(kid.into()).expect("test signer generate")
}
fn small_vec() -> Vec<f32> {
(0..8).map(|i| (i as f32) * 0.1).collect()
}
#[test]
fn domain_tag_is_exactly_13_bytes() {
assert_eq!(DOMAIN_TAG.len(), 13);
assert_eq!(DOMAIN_TAG, b"vectorpin/v2\x00");
}
#[test]
fn canonical_bytes_start_with_domain_tag() {
let signer = v2_signer("k1");
let v = small_vec();
let pin = signer.pin("hello", "m", v.as_slice()).unwrap();
let canonical = pin.header.canonicalize();
assert!(canonical.starts_with(DOMAIN_TAG));
}
#[test]
fn kid_is_in_signed_bytes() {
let signer = v2_signer("kid-a");
let v = small_vec();
let pin = signer.pin("hello", "m", v.as_slice()).unwrap();
let mut tampered = pin.clone();
tampered.header.kid = "kid-b".into();
let mut verifier = Verifier::new();
verifier
.add_key("kid-b", signer.public_key_bytes())
.unwrap();
let err = verifier.verify_signature(&tampered).unwrap_err();
assert_eq!(err, VerifyError::SignatureInvalid);
}
#[test]
fn v_is_in_signed_bytes() {
let signer = v2_signer("k1");
let v = small_vec();
let pin = signer.pin("hello", "m", v.as_slice()).unwrap();
let mut tampered = pin.clone();
tampered.header.v = 99;
let mut verifier = Verifier::new();
verifier.add_key("k1", signer.public_key_bytes()).unwrap();
let err = verifier.verify_signature(&tampered).unwrap_err();
assert!(matches!(err, VerifyError::UnsupportedVersion(99)));
}
#[test]
fn signer_rejects_nan_in_vector() {
let signer = v2_signer("k1");
let v: Vec<f32> = vec![1.0, f32::NAN, 3.0];
let err = signer.pin("x", "m", v.as_slice()).unwrap_err();
assert!(matches!(err, vectorpin::SignerError::InvalidVector(_)));
}
#[test]
fn signer_rejects_pos_inf() {
let signer = v2_signer("k1");
let v: Vec<f64> = vec![1.0, f64::INFINITY, 3.0];
let err = signer.pin("x", "m", v.as_slice()).unwrap_err();
assert!(matches!(err, vectorpin::SignerError::InvalidVector(_)));
}
#[test]
fn pos_zero_and_neg_zero_distinct() {
let signer = v2_signer("k1");
let p: Vec<f32> = vec![0.0, 1.0];
let n: Vec<f32> = vec![-0.0, 1.0];
let pin_p = signer.pin("x", "m", p.as_slice()).unwrap();
let pin_n = signer.pin("x", "m", n.as_slice()).unwrap();
assert_ne!(pin_p.header.vec_hash, pin_n.header.vec_hash);
}
#[test]
fn parser_rejects_nfd_model_field() {
let pin_json = r#"{"kid":"k","model":"café","sig":"AA","source_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000","ts":"2026-05-05T12:00:00Z","v":2,"vec_dim":1,"vec_dtype":"f32","vec_hash":"sha256:0000000000000000000000000000000000000000000000000000000000000000"}"#;
let err = Pin::from_json(pin_json).unwrap_err();
assert!(
matches!(err, AttestationError::NotNfc(_))
|| matches!(err, AttestationError::InvalidField { .. }),
"got {err:?}"
);
}
#[test]
fn parser_rejects_control_char_in_string_field() {
let bad = "kid\u{0007}".to_string();
let signer = v2_signer("k");
let v = small_vec();
let pin = signer.pin("x", "m", v.as_slice()).unwrap();
let mut value: serde_json::Value = serde_json::from_str(&pin.to_json()).unwrap();
value["kid"] = serde_json::Value::String(bad);
let err = Pin::from_value(value).unwrap_err();
assert!(
matches!(err, AttestationError::ControlChar(_)),
"got {err:?}"
);
}
#[test]
fn parser_rejects_bidi_override() {
let bad = "kid\u{202E}".to_string(); let signer = v2_signer("k");
let v = small_vec();
let pin = signer.pin("x", "m", v.as_slice()).unwrap();
let mut value: serde_json::Value = serde_json::from_str(&pin.to_json()).unwrap();
value["kid"] = serde_json::Value::String(bad);
let err = Pin::from_value(value).unwrap_err();
assert!(
matches!(err, AttestationError::BidiOverride(_)),
"got {err:?}"
);
}
#[test]
fn parser_rejects_ts_with_fractional_seconds() {
let signer = v2_signer("k");
let v = small_vec();
let pin = signer.pin("x", "m", v.as_slice()).unwrap();
let mut value: serde_json::Value = serde_json::from_str(&pin.to_json()).unwrap();
value["ts"] = serde_json::Value::String("2026-05-05T12:00:00.123Z".to_string());
let err = Pin::from_value(value).unwrap_err();
assert!(
matches!(err, AttestationError::BadTimestamp(_)),
"got {err:?}"
);
}
#[test]
fn parser_rejects_ts_with_offset() {
let signer = v2_signer("k");
let v = small_vec();
let pin = signer.pin("x", "m", v.as_slice()).unwrap();
let mut value: serde_json::Value = serde_json::from_str(&pin.to_json()).unwrap();
value["ts"] = serde_json::Value::String("2026-05-05T12:00:00+00:00".to_string());
let err = Pin::from_value(value).unwrap_err();
assert!(
matches!(err, AttestationError::BadTimestamp(_)),
"got {err:?}"
);
}
#[test]
fn parser_rejects_unknown_top_level_field() {
let signer = v2_signer("k");
let v = small_vec();
let pin = signer.pin("x", "m", v.as_slice()).unwrap();
let mut value: serde_json::Value = serde_json::from_str(&pin.to_json()).unwrap();
value["bogus"] = serde_json::Value::String("x".into());
let err = Pin::from_value(value).unwrap_err();
assert!(
matches!(err, AttestationError::UnknownTopLevelField(ref s) if s == "bogus"),
"got {err:?}"
);
}
#[test]
fn parser_rejects_non_string_extra_value() {
let signer = v2_signer("k");
let v = small_vec();
let opts = PinOptions {
extra: [("region".to_string(), "us-east".to_string())]
.into_iter()
.collect(),
..PinOptions::default()
};
let pin = signer
.pin_with_options("x", "m", v.as_slice(), opts)
.unwrap();
let mut value: serde_json::Value = serde_json::from_str(&pin.to_json()).unwrap();
value["extra"]["region"] = serde_json::json!(5);
let err = Pin::from_value(value).unwrap_err();
assert!(
matches!(err, AttestationError::InvalidField { field: "extra", .. }),
"got {err:?}"
);
}
#[test]
fn parser_rejects_oversize_pin_json() {
let oversize = "x".repeat(vectorpin::attestation::MAX_PIN_JSON_BYTES + 1);
let err = Pin::from_json(&oversize).unwrap_err();
assert!(
matches!(err, AttestationError::SizeLimit { .. }),
"got {err:?}"
);
}
#[test]
fn parser_rejects_sig_wrong_length() {
let signer = v2_signer("k");
let v = small_vec();
let pin = signer.pin("x", "m", v.as_slice()).unwrap();
let mut value: serde_json::Value = serde_json::from_str(&pin.to_json()).unwrap();
value["sig"] = serde_json::Value::String("AAAA".to_string());
let err = Pin::from_value(value).unwrap_err();
assert!(
matches!(err, AttestationError::InvalidField { field: "sig", .. }),
"got {err:?}"
);
}
#[test]
fn parser_rejects_vec_dim_zero() {
let signer = v2_signer("k");
let v = small_vec();
let pin = signer.pin("x", "m", v.as_slice()).unwrap();
let mut value: serde_json::Value = serde_json::from_str(&pin.to_json()).unwrap();
value["vec_dim"] = serde_json::json!(0);
let err = Pin::from_value(value).unwrap_err();
assert!(
matches!(
err,
AttestationError::InvalidField {
field: "vec_dim",
..
}
),
"got {err:?}"
);
}
#[test]
fn verify_nan_vector_rejected_as_parse_error() {
let signer = v2_signer("k");
let v = small_vec();
let pin = signer.pin("x", "m", v.as_slice()).unwrap();
let mut verifier = Verifier::new();
verifier
.add_key(signer.key_id(), signer.public_key_bytes())
.unwrap();
let mut nan_vec = v.clone();
nan_vec[0] = f32::NAN;
let err = verifier
.verify(
&pin,
VerifyOptions {
vector: Some(vectorpin::hash::VectorRef::F32(&nan_vec)),
..VerifyOptions::default()
},
)
.unwrap_err();
assert!(matches!(err, VerifyError::ParseError(_)), "got {err:?}");
}
#[test]
fn round_trip_with_extra_and_model_hash() {
let signer = v2_signer("k");
let v = small_vec();
let mut extra = std::collections::BTreeMap::new();
extra.insert("vectorpin.record_id".to_string(), "rec-1".to_string());
let opts = PinOptions {
model_hash: Some(format!("sha256:{}", "a".repeat(64))),
extra,
..PinOptions::default()
};
let pin = signer
.pin_with_options("hi", "m", v.as_slice(), opts)
.unwrap();
let json = pin.to_json();
let parsed = Pin::from_json(&json).unwrap();
assert_eq!(parsed, pin);
let mut verifier = Verifier::new();
verifier
.add_key(signer.key_id(), signer.public_key_bytes())
.unwrap();
verifier
.verify(
&parsed,
VerifyOptions {
expected_record_id: Some("rec-1"),
..VerifyOptions::default()
},
)
.expect("valid record_id matches");
}