use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use vti_common::error::AppError;
use crate::store::KeyspaceHandle;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum WebvhKeyRole {
UpdateKey,
PreRotation,
Verification { fragment_id: u32 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebvhKeyHandle {
pub scid: String,
pub version_id: String,
pub hash: String,
pub public_key: String,
pub derivation_path: String,
pub seed_id: Option<u32>,
pub role: WebvhKeyRole,
pub label: String,
pub created_at: DateTime<Utc>,
}
const ACTIVE_PREFIX: &str = "webvh:";
const SUPERSEDED_PREFIX: &str = "superseded:webvh:";
fn storage_key(handle: &WebvhKeyHandle) -> String {
storage_key_for(&handle.scid, &handle.version_id, &handle.role, &handle.hash)
}
fn storage_key_for(scid: &str, version_id: &str, role: &WebvhKeyRole, hash: &str) -> String {
match role {
WebvhKeyRole::UpdateKey => {
format!("{ACTIVE_PREFIX}{scid}:{version_id}:{hash}")
}
WebvhKeyRole::PreRotation => {
format!("{ACTIVE_PREFIX}{scid}:{version_id}:pre-rotation:{hash}")
}
WebvhKeyRole::Verification { fragment_id } => {
format!("{ACTIVE_PREFIX}{scid}:{version_id}:vm:{fragment_id}:{hash}")
}
}
}
fn supersede_key_for(active_key: &str) -> String {
format!("{SUPERSEDED_PREFIX}{}", &active_key[ACTIVE_PREFIX.len()..])
}
pub async fn install(keys_ks: &KeyspaceHandle, handle: &WebvhKeyHandle) -> Result<(), AppError> {
keys_ks.insert(storage_key(handle), handle).await
}
#[cfg(test)]
pub async fn load_handle(
keys_ks: &KeyspaceHandle,
scid: &str,
version_id: &str,
role: WebvhKeyRole,
hash: &str,
) -> Result<Option<WebvhKeyHandle>, AppError> {
keys_ks
.get::<WebvhKeyHandle>(storage_key_for(scid, version_id, &role, hash))
.await
}
pub async fn find_handle_by_hash(
keys_ks: &KeyspaceHandle,
scid: &str,
hash: &str,
) -> Result<Option<WebvhKeyHandle>, AppError> {
let prefix = format!("{ACTIVE_PREFIX}{scid}:");
let raw_keys = keys_ks.prefix_keys(prefix.as_bytes().to_vec()).await?;
for raw in raw_keys {
let key = match std::str::from_utf8(&raw) {
Ok(s) => s,
Err(_) => continue,
};
if !key.ends_with(hash) {
continue;
}
if let Some(handle) = keys_ks.get::<WebvhKeyHandle>(raw).await? {
return Ok(Some(handle));
}
}
Ok(None)
}
pub async fn supersede_keys_for_version(
keys_ks: &KeyspaceHandle,
scid: &str,
version_id: &str,
) -> Result<(), AppError> {
let prefix = format!("{ACTIVE_PREFIX}{scid}:{version_id}:");
let raw_keys = keys_ks.prefix_keys(prefix.as_bytes().to_vec()).await?;
for raw in raw_keys {
let key = match std::str::from_utf8(&raw) {
Ok(s) => s.to_string(),
Err(_) => continue,
};
let handle: Option<WebvhKeyHandle> = keys_ks.get(raw.clone()).await?;
let Some(handle) = handle else {
continue;
};
let new_key = supersede_key_for(&key);
keys_ks.insert(new_key.into_bytes(), &handle).await?;
keys_ks.remove(raw).await?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
async fn test_keys_ks() -> KeyspaceHandle {
let dir = tempfile::tempdir().expect("tempdir");
let cfg = StoreConfig {
data_dir: dir.path().to_path_buf(),
};
std::mem::forget(dir);
let store = Store::open(&cfg).expect("open store");
store.keyspace(crate::keyspaces::KEYS).expect("keyspace")
}
fn handle(scid: &str, version_id: &str, role: WebvhKeyRole, hash: &str) -> WebvhKeyHandle {
WebvhKeyHandle {
scid: scid.into(),
version_id: version_id.into(),
hash: hash.into(),
public_key: format!("z6Mk{hash}Pub"),
derivation_path: "m/26'/0'/0'/0".into(),
seed_id: Some(1),
role,
label: "test".into(),
created_at: Utc::now(),
}
}
#[tokio::test]
async fn install_and_load_round_trip_for_each_role() {
let ks = test_keys_ks().await;
let scid = "Q123";
let v1 = "1-zVer";
let update_handle = handle(scid, v1, WebvhKeyRole::UpdateKey, "zHashUpdate");
let pre_handle = handle(scid, v1, WebvhKeyRole::PreRotation, "zHashPre");
let vm_handle = handle(
scid,
v1,
WebvhKeyRole::Verification { fragment_id: 7 },
"zHashVm",
);
install(&ks, &update_handle).await.unwrap();
install(&ks, &pre_handle).await.unwrap();
install(&ks, &vm_handle).await.unwrap();
let loaded = load_handle(&ks, scid, v1, WebvhKeyRole::UpdateKey, "zHashUpdate")
.await
.unwrap()
.expect("update key handle present");
assert_eq!(loaded.public_key, update_handle.public_key);
let loaded_pre = load_handle(&ks, scid, v1, WebvhKeyRole::PreRotation, "zHashPre")
.await
.unwrap()
.expect("pre-rotation handle present");
assert_eq!(loaded_pre.role, WebvhKeyRole::PreRotation);
let loaded_vm = load_handle(
&ks,
scid,
v1,
WebvhKeyRole::Verification { fragment_id: 7 },
"zHashVm",
)
.await
.unwrap()
.expect("vm handle present");
assert!(matches!(
loaded_vm.role,
WebvhKeyRole::Verification { fragment_id: 7 }
));
}
#[tokio::test]
async fn find_handle_by_hash_finds_across_roles_and_versions() {
let ks = test_keys_ks().await;
let scid = "Q123";
let pre_v1 = handle("Q123", "1-zV", WebvhKeyRole::PreRotation, "zHashShared");
let update_v2 = handle("Q123", "2-zV", WebvhKeyRole::UpdateKey, "zHashShared");
install(&ks, &pre_v1).await.unwrap();
install(&ks, &update_v2).await.unwrap();
let found = find_handle_by_hash(&ks, scid, "zHashShared")
.await
.unwrap()
.expect("should find handle by hash");
assert_eq!(found.hash, "zHashShared");
}
#[tokio::test]
async fn supersede_moves_all_roles_for_a_version() {
let ks = test_keys_ks().await;
let scid = "Q123";
let v1 = "1-zVer";
install(&ks, &handle(scid, v1, WebvhKeyRole::UpdateKey, "zU"))
.await
.unwrap();
install(&ks, &handle(scid, v1, WebvhKeyRole::PreRotation, "zP"))
.await
.unwrap();
install(
&ks,
&handle(
scid,
v1,
WebvhKeyRole::Verification { fragment_id: 0 },
"zV",
),
)
.await
.unwrap();
install(&ks, &handle(scid, "2-zVer", WebvhKeyRole::UpdateKey, "zU2"))
.await
.unwrap();
supersede_keys_for_version(&ks, scid, v1).await.unwrap();
assert!(
load_handle(&ks, scid, v1, WebvhKeyRole::UpdateKey, "zU")
.await
.unwrap()
.is_none()
);
assert!(
load_handle(&ks, scid, v1, WebvhKeyRole::PreRotation, "zP")
.await
.unwrap()
.is_none()
);
let superseded_key = format!("{SUPERSEDED_PREFIX}{scid}:{v1}:zU");
let restored: Option<WebvhKeyHandle> = ks.get(superseded_key.into_bytes()).await.unwrap();
assert!(restored.is_some(), "superseded entry should exist");
assert!(
load_handle(&ks, scid, "2-zVer", WebvhKeyRole::UpdateKey, "zU2")
.await
.unwrap()
.is_some()
);
}
#[tokio::test]
async fn supersede_is_idempotent() {
let ks = test_keys_ks().await;
let scid = "Q";
let v1 = "1-z";
install(&ks, &handle(scid, v1, WebvhKeyRole::UpdateKey, "zU"))
.await
.unwrap();
supersede_keys_for_version(&ks, scid, v1).await.unwrap();
supersede_keys_for_version(&ks, scid, v1).await.unwrap();
}
}