use std::fs;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Utc};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
use serde::Deserialize;
pub const RESTORE_INTENT_KIND: &str = "cortex_restore_intent";
pub const RESTORE_TAKEOVER_KIND: &str = "cortex_restore_lock_takeover";
pub const RESTORE_INTENT_SCHEMA_VERSION: u16 = 1;
pub const RESTORE_INTENT_PRINCIPAL_NOT_BOUND_INVARIANT: &str =
"restore.intent.operator_principal_id.not_bound_to_verifying_key";
pub const OPERATOR_PRINCIPAL_PREFIX: &str = "operator:";
pub const OPERATOR_PRINCIPAL_FINGERPRINT_HEX_LEN: usize = 64;
#[must_use]
pub fn derive_operator_principal_id(key_bytes: &[u8; 32]) -> String {
let digest = blake3::hash(key_bytes);
let hex = digest.to_hex().to_string();
debug_assert_eq!(hex.len(), OPERATOR_PRINCIPAL_FINGERPRINT_HEX_LEN);
format!("{OPERATOR_PRINCIPAL_PREFIX}{hex}")
}
#[derive(Debug)]
pub enum IntentError {
Io {
field: &'static str,
path: PathBuf,
message: String,
},
Malformed {
path: PathBuf,
message: String,
},
KindMismatch {
expected: &'static str,
found: String,
},
SchemaMismatch {
expected: u16,
found: u16,
},
OutsideValidity {
now: DateTime<Utc>,
not_before: DateTime<Utc>,
not_after: DateTime<Utc>,
},
DeploymentMismatch {
payload: String,
expected: String,
},
ActivePathMismatch {
field: &'static str,
payload: PathBuf,
expected: PathBuf,
},
ManifestDigestMismatch {
payload: String,
computed: String,
},
StagedDigestMismatch {
field: &'static str,
payload: String,
computed: String,
},
Signature {
reason: String,
path: PathBuf,
},
BadSignature,
KeyMismatch {
payload_principal: String,
derived_principal: String,
},
}
impl std::fmt::Display for IntentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io {
field,
path,
message,
} => write!(
f,
"restore intent: cannot read `{field}` at `{}`: {message}",
path.display()
),
Self::Malformed { path, message } => write!(
f,
"restore intent: payload `{}` is malformed JSON: {message}",
path.display()
),
Self::KindMismatch { expected, found } => write!(
f,
"restore intent: payload kind `{found}` does not match expected `{expected}`",
),
Self::SchemaMismatch { expected, found } => write!(
f,
"restore intent: payload schema_version {found} does not match expected {expected}",
),
Self::OutsideValidity {
now,
not_before,
not_after,
} => write!(
f,
"restore intent: now={now} is outside validity window [{not_before}, {not_after}]",
),
Self::DeploymentMismatch { payload, expected } => write!(
f,
"restore intent: payload deployment_id `{payload}` does not match runtime `{expected}`",
),
Self::ActivePathMismatch {
field,
payload,
expected,
} => write!(
f,
"restore intent: payload `{field}` `{}` does not match runtime path `{}`",
payload.display(),
expected.display(),
),
Self::ManifestDigestMismatch { payload, computed } => write!(
f,
"restore intent: backup_manifest_blake3 mismatch: payload={payload}, computed={computed}",
),
Self::StagedDigestMismatch {
field,
payload,
computed,
} => write!(
f,
"restore intent: staged `{field}` digest mismatch: payload={payload}, computed={computed}",
),
Self::Signature { reason, path } => write!(
f,
"restore intent: signature failure: {reason} (path `{}`)",
path.display()
),
Self::BadSignature => write!(f, "restore intent: Ed25519 signature verification failed"),
Self::KeyMismatch {
payload_principal,
derived_principal,
} => write!(
f,
"restore intent: payload operator_principal_id `{payload_principal}` is not bound to the supplied verifying key (expected deterministic derivation `{derived_principal}`; invariant={RESTORE_INTENT_PRINCIPAL_NOT_BOUND_INVARIANT})",
),
}
}
}
impl std::error::Error for IntentError {}
#[derive(Debug, Deserialize)]
pub struct RestoreIntentPayload {
pub kind: String,
pub schema_version: u16,
pub deployment_id: String,
pub active_db_path: PathBuf,
pub active_event_log_path: PathBuf,
pub backup_manifest_blake3: String,
pub staged_sqlite_blake3: String,
pub staged_jsonl_blake3: String,
pub operator_principal_id: String,
pub not_before: DateTime<Utc>,
pub not_after: DateTime<Utc>,
pub p_n_schema_version: u16,
}
#[derive(Debug, Deserialize)]
pub struct RestoreLockTakeoverPayload {
pub kind: String,
pub schema_version: u16,
pub deployment_id: String,
pub operator_principal_id: String,
pub stale_pid: u32,
pub stale_acquired_at: DateTime<Utc>,
pub justification: String,
pub not_before: DateTime<Utc>,
pub not_after: DateTime<Utc>,
}
#[derive(Debug)]
pub struct VerifiedRestoreIntent {
pub payload: RestoreIntentPayload,
#[allow(dead_code)]
pub canonical_bytes: Vec<u8>,
pub canonical_blake3: String,
}
#[derive(Debug)]
pub struct VerifiedTakeoverAttestation {
pub payload: RestoreLockTakeoverPayload,
#[allow(dead_code)]
pub canonical_bytes: Vec<u8>,
pub canonical_blake3: String,
}
#[derive(Debug)]
pub struct ExpectedIntent<'a> {
pub deployment_id: &'a str,
pub active_db_path: &'a Path,
pub active_event_log_path: &'a Path,
pub backup_manifest_blake3: &'a str,
pub staged_sqlite_blake3: &'a str,
pub staged_jsonl_blake3: &'a str,
pub now: DateTime<Utc>,
pub verifying_key: VerifyingKey,
#[allow(dead_code)]
pub verifying_key_fingerprint: &'a str,
}
pub fn verify_restore_intent(
intent_path: &Path,
signature_path: &Path,
expected: &ExpectedIntent<'_>,
) -> Result<VerifiedRestoreIntent, IntentError> {
let canonical_bytes = read_canonical_bytes(intent_path, "restore_intent")?;
let payload: RestoreIntentPayload =
serde_json::from_slice(&canonical_bytes).map_err(|err| IntentError::Malformed {
path: intent_path.to_path_buf(),
message: err.to_string(),
})?;
if payload.kind != RESTORE_INTENT_KIND {
return Err(IntentError::KindMismatch {
expected: RESTORE_INTENT_KIND,
found: payload.kind,
});
}
if payload.schema_version != RESTORE_INTENT_SCHEMA_VERSION
|| payload.p_n_schema_version != RESTORE_INTENT_SCHEMA_VERSION
{
return Err(IntentError::SchemaMismatch {
expected: RESTORE_INTENT_SCHEMA_VERSION,
found: payload.schema_version,
});
}
enforce_validity_window(expected.now, payload.not_before, payload.not_after)?;
enforce_deployment(&payload.deployment_id, expected.deployment_id)?;
enforce_active_path(
"active_db_path",
&payload.active_db_path,
expected.active_db_path,
)?;
enforce_active_path(
"active_event_log_path",
&payload.active_event_log_path,
expected.active_event_log_path,
)?;
enforce_digest(
"backup_manifest_blake3",
&payload.backup_manifest_blake3,
expected.backup_manifest_blake3,
|payload, computed| IntentError::ManifestDigestMismatch {
payload: payload.to_string(),
computed: computed.to_string(),
},
)?;
enforce_digest(
"staged_sqlite_blake3",
&payload.staged_sqlite_blake3,
expected.staged_sqlite_blake3,
|payload, computed| IntentError::StagedDigestMismatch {
field: "staged_sqlite_blake3",
payload: payload.to_string(),
computed: computed.to_string(),
},
)?;
enforce_digest(
"staged_jsonl_blake3",
&payload.staged_jsonl_blake3,
expected.staged_jsonl_blake3,
|payload, computed| IntentError::StagedDigestMismatch {
field: "staged_jsonl_blake3",
payload: payload.to_string(),
computed: computed.to_string(),
},
)?;
verify_detached_signature(
signature_path,
&canonical_bytes,
&expected.verifying_key,
&payload.operator_principal_id,
)?;
let canonical_blake3 = blake3_hex(&canonical_bytes);
Ok(VerifiedRestoreIntent {
payload,
canonical_bytes,
canonical_blake3,
})
}
#[derive(Debug)]
pub struct ExpectedTakeover<'a> {
pub deployment_id: &'a str,
pub stale_pid: u32,
pub stale_acquired_at: DateTime<Utc>,
pub now: DateTime<Utc>,
pub verifying_key: VerifyingKey,
#[allow(dead_code)]
pub verifying_key_fingerprint: &'a str,
}
pub fn verify_takeover_attestation(
attestation_path: &Path,
signature_path: &Path,
expected: &ExpectedTakeover<'_>,
) -> Result<VerifiedTakeoverAttestation, IntentError> {
let canonical_bytes = read_canonical_bytes(attestation_path, "takeover_attestation")?;
let payload: RestoreLockTakeoverPayload =
serde_json::from_slice(&canonical_bytes).map_err(|err| IntentError::Malformed {
path: attestation_path.to_path_buf(),
message: err.to_string(),
})?;
if payload.kind != RESTORE_TAKEOVER_KIND {
return Err(IntentError::KindMismatch {
expected: RESTORE_TAKEOVER_KIND,
found: payload.kind,
});
}
if payload.schema_version != RESTORE_INTENT_SCHEMA_VERSION {
return Err(IntentError::SchemaMismatch {
expected: RESTORE_INTENT_SCHEMA_VERSION,
found: payload.schema_version,
});
}
enforce_validity_window(expected.now, payload.not_before, payload.not_after)?;
enforce_deployment(&payload.deployment_id, expected.deployment_id)?;
if payload.stale_pid != expected.stale_pid {
return Err(IntentError::Malformed {
path: attestation_path.to_path_buf(),
message: format!(
"takeover attestation stale_pid={} does not match observed marker pid={}",
payload.stale_pid, expected.stale_pid,
),
});
}
if payload.stale_acquired_at != expected.stale_acquired_at {
return Err(IntentError::Malformed {
path: attestation_path.to_path_buf(),
message: format!(
"takeover attestation stale_acquired_at={} does not match observed marker acquired_at={}",
payload.stale_acquired_at, expected.stale_acquired_at,
),
});
}
if payload.justification.trim().is_empty() {
return Err(IntentError::Malformed {
path: attestation_path.to_path_buf(),
message: "takeover attestation justification is empty".to_string(),
});
}
verify_detached_signature(
signature_path,
&canonical_bytes,
&expected.verifying_key,
&payload.operator_principal_id,
)?;
let canonical_blake3 = blake3_hex(&canonical_bytes);
Ok(VerifiedTakeoverAttestation {
payload,
canonical_bytes,
canonical_blake3,
})
}
fn read_canonical_bytes(path: &Path, field: &'static str) -> Result<Vec<u8>, IntentError> {
fs::read(path).map_err(|err| IntentError::Io {
field,
path: path.to_path_buf(),
message: err.to_string(),
})
}
fn enforce_validity_window(
now: DateTime<Utc>,
not_before: DateTime<Utc>,
not_after: DateTime<Utc>,
) -> Result<(), IntentError> {
if not_after < not_before {
return Err(IntentError::OutsideValidity {
now,
not_before,
not_after,
});
}
if now < not_before || now > not_after {
return Err(IntentError::OutsideValidity {
now,
not_before,
not_after,
});
}
Ok(())
}
fn enforce_deployment(payload_id: &str, expected_id: &str) -> Result<(), IntentError> {
if payload_id == expected_id {
Ok(())
} else {
Err(IntentError::DeploymentMismatch {
payload: payload_id.to_string(),
expected: expected_id.to_string(),
})
}
}
fn enforce_active_path(
field: &'static str,
payload_path: &Path,
expected_path: &Path,
) -> Result<(), IntentError> {
let payload_canon = payload_path
.canonicalize()
.unwrap_or_else(|_| payload_path.to_path_buf());
let expected_canon = expected_path
.canonicalize()
.unwrap_or_else(|_| expected_path.to_path_buf());
if payload_canon == expected_canon {
Ok(())
} else {
Err(IntentError::ActivePathMismatch {
field,
payload: payload_canon,
expected: expected_canon,
})
}
}
fn enforce_digest(
_field: &'static str,
payload_digest: &str,
computed_digest: &str,
err: impl FnOnce(&str, &str) -> IntentError,
) -> Result<(), IntentError> {
if payload_digest == computed_digest {
Ok(())
} else {
Err(err(payload_digest, computed_digest))
}
}
fn verify_detached_signature(
signature_path: &Path,
canonical_bytes: &[u8],
verifying_key: &VerifyingKey,
payload_principal: &str,
) -> Result<(), IntentError> {
let key_bytes = verifying_key.to_bytes();
let derived_principal = derive_operator_principal_id(&key_bytes);
if payload_principal != derived_principal {
return Err(IntentError::KeyMismatch {
payload_principal: payload_principal.to_string(),
derived_principal,
});
}
let sig_bytes = fs::read(signature_path).map_err(|err| IntentError::Signature {
reason: format!("cannot read detached signature: {err}"),
path: signature_path.to_path_buf(),
})?;
let sig_array: [u8; 64] =
sig_bytes
.as_slice()
.try_into()
.map_err(|_| IntentError::Signature {
reason: format!(
"detached signature must be exactly 64 bytes (Ed25519), got {}",
sig_bytes.len()
),
path: signature_path.to_path_buf(),
})?;
let signature = Signature::from_bytes(&sig_array);
verifying_key
.verify(canonical_bytes, &signature)
.map_err(|_| IntentError::BadSignature)?;
Ok(())
}
fn blake3_hex(bytes: &[u8]) -> String {
format!("blake3:{}", blake3::hash(bytes).to_hex())
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signer, SigningKey};
fn now() -> DateTime<Utc> {
Utc::now()
}
fn fixture_signing_key() -> SigningKey {
SigningKey::from_bytes(&[7u8; 32])
}
fn fixture_principal_id() -> String {
let vk = fixture_signing_key().verifying_key();
derive_operator_principal_id(&vk.to_bytes())
}
fn sign_into(dir: &Path, payload_json: &serde_json::Value) -> (PathBuf, PathBuf, VerifyingKey) {
let intent_path = dir.join("RESTORE_INTENT.json");
let sig_path = dir.join("RESTORE_INTENT.sig");
let bytes = serde_json::to_vec_pretty(payload_json).unwrap();
fs::write(&intent_path, &bytes).unwrap();
let signing_key = fixture_signing_key();
let signature = signing_key.sign(&bytes);
fs::write(&sig_path, signature.to_bytes()).unwrap();
(intent_path, sig_path, signing_key.verifying_key())
}
#[test]
fn verify_restore_intent_happy_path() {
let dir = tempfile::tempdir().unwrap();
let active_db = dir.path().join("cortex.db");
let active_jsonl = dir.path().join("events.jsonl");
fs::write(&active_db, b"db").unwrap();
fs::write(&active_jsonl, b"jsonl").unwrap();
let principal_id = fixture_principal_id();
let not_before = now() - chrono::Duration::seconds(60);
let not_after = now() + chrono::Duration::seconds(60);
let payload = serde_json::json!({
"kind": RESTORE_INTENT_KIND,
"schema_version": RESTORE_INTENT_SCHEMA_VERSION,
"deployment_id": "dep-1",
"active_db_path": active_db,
"active_event_log_path": active_jsonl,
"backup_manifest_blake3": "blake3:aa",
"staged_sqlite_blake3": "blake3:bb",
"staged_jsonl_blake3": "blake3:cc",
"operator_principal_id": principal_id,
"not_before": not_before.to_rfc3339(),
"not_after": not_after.to_rfc3339(),
"p_n_schema_version": RESTORE_INTENT_SCHEMA_VERSION,
});
let (intent_path, sig_path, verifying_key) = sign_into(dir.path(), &payload);
let expected = ExpectedIntent {
deployment_id: "dep-1",
active_db_path: &active_db,
active_event_log_path: &active_jsonl,
backup_manifest_blake3: "blake3:aa",
staged_sqlite_blake3: "blake3:bb",
staged_jsonl_blake3: "blake3:cc",
now: now(),
verifying_key,
verifying_key_fingerprint: "fp",
};
let verified = verify_restore_intent(&intent_path, &sig_path, &expected).unwrap();
assert_eq!(
verified.payload.operator_principal_id,
fixture_principal_id()
);
assert!(verified.canonical_blake3.starts_with("blake3:"));
}
#[test]
fn verify_restore_intent_rejects_wrong_deployment() {
let dir = tempfile::tempdir().unwrap();
let active_db = dir.path().join("cortex.db");
let active_jsonl = dir.path().join("events.jsonl");
fs::write(&active_db, b"db").unwrap();
fs::write(&active_jsonl, b"jsonl").unwrap();
let not_before = now() - chrono::Duration::seconds(60);
let not_after = now() + chrono::Duration::seconds(60);
let payload = serde_json::json!({
"kind": RESTORE_INTENT_KIND,
"schema_version": RESTORE_INTENT_SCHEMA_VERSION,
"deployment_id": "dep-other",
"active_db_path": active_db,
"active_event_log_path": active_jsonl,
"backup_manifest_blake3": "blake3:aa",
"staged_sqlite_blake3": "blake3:bb",
"staged_jsonl_blake3": "blake3:cc",
"operator_principal_id": fixture_principal_id(),
"not_before": not_before.to_rfc3339(),
"not_after": not_after.to_rfc3339(),
"p_n_schema_version": RESTORE_INTENT_SCHEMA_VERSION,
});
let (intent_path, sig_path, verifying_key) = sign_into(dir.path(), &payload);
let expected = ExpectedIntent {
deployment_id: "dep-1",
active_db_path: &active_db,
active_event_log_path: &active_jsonl,
backup_manifest_blake3: "blake3:aa",
staged_sqlite_blake3: "blake3:bb",
staged_jsonl_blake3: "blake3:cc",
now: now(),
verifying_key,
verifying_key_fingerprint: "fp",
};
match verify_restore_intent(&intent_path, &sig_path, &expected) {
Err(IntentError::DeploymentMismatch { payload, expected }) => {
assert_eq!(payload, "dep-other");
assert_eq!(expected, "dep-1");
}
other => panic!("expected DeploymentMismatch, got {other:?}"),
}
}
#[test]
fn verify_restore_intent_rejects_expired() {
let dir = tempfile::tempdir().unwrap();
let active_db = dir.path().join("cortex.db");
let active_jsonl = dir.path().join("events.jsonl");
fs::write(&active_db, b"db").unwrap();
fs::write(&active_jsonl, b"jsonl").unwrap();
let not_before = now() - chrono::Duration::seconds(120);
let not_after = now() - chrono::Duration::seconds(60);
let payload = serde_json::json!({
"kind": RESTORE_INTENT_KIND,
"schema_version": RESTORE_INTENT_SCHEMA_VERSION,
"deployment_id": "dep-1",
"active_db_path": active_db,
"active_event_log_path": active_jsonl,
"backup_manifest_blake3": "blake3:aa",
"staged_sqlite_blake3": "blake3:bb",
"staged_jsonl_blake3": "blake3:cc",
"operator_principal_id": fixture_principal_id(),
"not_before": not_before.to_rfc3339(),
"not_after": not_after.to_rfc3339(),
"p_n_schema_version": RESTORE_INTENT_SCHEMA_VERSION,
});
let (intent_path, sig_path, verifying_key) = sign_into(dir.path(), &payload);
let expected = ExpectedIntent {
deployment_id: "dep-1",
active_db_path: &active_db,
active_event_log_path: &active_jsonl,
backup_manifest_blake3: "blake3:aa",
staged_sqlite_blake3: "blake3:bb",
staged_jsonl_blake3: "blake3:cc",
now: now(),
verifying_key,
verifying_key_fingerprint: "fp",
};
match verify_restore_intent(&intent_path, &sig_path, &expected) {
Err(IntentError::OutsideValidity { .. }) => {}
other => panic!("expected OutsideValidity, got {other:?}"),
}
}
#[test]
fn verify_restore_intent_rejects_tampered_payload() {
let dir = tempfile::tempdir().unwrap();
let active_db = dir.path().join("cortex.db");
let active_jsonl = dir.path().join("events.jsonl");
fs::write(&active_db, b"db").unwrap();
fs::write(&active_jsonl, b"jsonl").unwrap();
let not_before = now() - chrono::Duration::seconds(60);
let not_after = now() + chrono::Duration::seconds(60);
let payload = serde_json::json!({
"kind": RESTORE_INTENT_KIND,
"schema_version": RESTORE_INTENT_SCHEMA_VERSION,
"deployment_id": "dep-1",
"active_db_path": active_db,
"active_event_log_path": active_jsonl,
"backup_manifest_blake3": "blake3:aa",
"staged_sqlite_blake3": "blake3:bb",
"staged_jsonl_blake3": "blake3:cc",
"operator_principal_id": fixture_principal_id(),
"not_before": not_before.to_rfc3339(),
"not_after": not_after.to_rfc3339(),
"p_n_schema_version": RESTORE_INTENT_SCHEMA_VERSION,
});
let (intent_path, sig_path, verifying_key) = sign_into(dir.path(), &payload);
let mut bytes = fs::read(&intent_path).unwrap();
bytes[0] = bytes[0].wrapping_add(1);
fs::write(&intent_path, &bytes).unwrap();
let expected = ExpectedIntent {
deployment_id: "dep-1",
active_db_path: &active_db,
active_event_log_path: &active_jsonl,
backup_manifest_blake3: "blake3:aa",
staged_sqlite_blake3: "blake3:bb",
staged_jsonl_blake3: "blake3:cc",
now: now(),
verifying_key,
verifying_key_fingerprint: "fp",
};
match verify_restore_intent(&intent_path, &sig_path, &expected) {
Err(IntentError::BadSignature) | Err(IntentError::Malformed { .. }) => {}
other => panic!("expected BadSignature or Malformed, got {other:?}"),
}
}
#[test]
fn verify_restore_intent_refuses_principal_not_bound_to_key() {
let dir = tempfile::tempdir().unwrap();
let active_db = dir.path().join("cortex.db");
let active_jsonl = dir.path().join("events.jsonl");
fs::write(&active_db, b"db").unwrap();
fs::write(&active_jsonl, b"jsonl").unwrap();
let not_before = now() - chrono::Duration::seconds(60);
let not_after = now() + chrono::Duration::seconds(60);
let forged_principal = "operator:trusted-incident-responder";
let derived_principal = fixture_principal_id();
assert_ne!(
forged_principal, derived_principal,
"test premise: forged principal must differ from the derived one",
);
let payload = serde_json::json!({
"kind": RESTORE_INTENT_KIND,
"schema_version": RESTORE_INTENT_SCHEMA_VERSION,
"deployment_id": "dep-1",
"active_db_path": active_db,
"active_event_log_path": active_jsonl,
"backup_manifest_blake3": "blake3:aa",
"staged_sqlite_blake3": "blake3:bb",
"staged_jsonl_blake3": "blake3:cc",
"operator_principal_id": forged_principal,
"not_before": not_before.to_rfc3339(),
"not_after": not_after.to_rfc3339(),
"p_n_schema_version": RESTORE_INTENT_SCHEMA_VERSION,
});
let (intent_path, sig_path, verifying_key) = sign_into(dir.path(), &payload);
let expected = ExpectedIntent {
deployment_id: "dep-1",
active_db_path: &active_db,
active_event_log_path: &active_jsonl,
backup_manifest_blake3: "blake3:aa",
staged_sqlite_blake3: "blake3:bb",
staged_jsonl_blake3: "blake3:cc",
now: now(),
verifying_key,
verifying_key_fingerprint: "fp",
};
match verify_restore_intent(&intent_path, &sig_path, &expected) {
Err(IntentError::KeyMismatch {
payload_principal,
derived_principal: observed_derived,
}) => {
assert_eq!(payload_principal, forged_principal);
assert_eq!(observed_derived, derived_principal);
let rendered = IntentError::KeyMismatch {
payload_principal,
derived_principal: observed_derived,
}
.to_string();
assert!(
rendered.contains(RESTORE_INTENT_PRINCIPAL_NOT_BOUND_INVARIANT),
"Display impl must surface the stable invariant; got: {rendered}",
);
}
other => {
panic!("expected KeyMismatch (structural binding fail-closed); got {other:?}",)
}
}
}
#[test]
fn derive_operator_principal_id_is_deterministic_and_prefixed() {
let vk = fixture_signing_key().verifying_key();
let principal = derive_operator_principal_id(&vk.to_bytes());
assert!(principal.starts_with(OPERATOR_PRINCIPAL_PREFIX));
assert_eq!(principal, derive_operator_principal_id(&vk.to_bytes()));
let hex_part = principal.strip_prefix(OPERATOR_PRINCIPAL_PREFIX).unwrap();
assert_eq!(hex_part.len(), OPERATOR_PRINCIPAL_FINGERPRINT_HEX_LEN);
assert!(hex_part.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn operator_principal_fingerprint_hex_len_is_full_blake3_digest() {
assert_eq!(OPERATOR_PRINCIPAL_FINGERPRINT_HEX_LEN, 64);
let vk = fixture_signing_key().verifying_key();
let principal = derive_operator_principal_id(&vk.to_bytes());
let hex_part = principal.strip_prefix(OPERATOR_PRINCIPAL_PREFIX).unwrap();
let expected_hex = blake3::hash(&vk.to_bytes()).to_hex().to_string();
assert_eq!(hex_part, expected_hex.as_str());
assert_eq!(hex_part.len(), 64);
}
}