pub mod derivation;
pub mod imported;
pub mod internal;
pub mod paths;
pub mod seed_store;
pub mod seeds;
pub mod wrapping;
use affinidi_tdk::secrets_resolver::secrets::Secret;
use chrono::Utc;
use ed25519_dalek::SigningKey;
use multibase::Base;
use vti_common::slip10::{DerivationPath, ExtendedSigningKey};
use vti_common::store::KeyspaceHandle;
pub use vta_sdk::keys::{KeyOrigin, KeyRecord, KeyStatus, KeyType};
pub fn encode_private_multibase(key_type: &KeyType, raw_bytes: &[u8]) -> String {
let codec = key_type.multicodec_private();
let mut buf = Vec::with_capacity(codec.len() + raw_bytes.len());
buf.extend_from_slice(codec);
buf.extend_from_slice(raw_bytes);
multibase::encode(Base::Base58Btc, &buf)
}
pub fn encode_public_multibase(key_type: &KeyType, raw_bytes: &[u8]) -> String {
let codec = key_type.multicodec_public();
let mut buf = Vec::with_capacity(codec.len() + raw_bytes.len());
buf.extend_from_slice(codec);
buf.extend_from_slice(raw_bytes);
multibase::encode(Base::Base58Btc, &buf)
}
pub fn store_key(key_id: &str) -> String {
format!("key:{key_id}")
}
pub use vta_sdk::did_key::ed25519_multibase_pubkey;
#[allow(clippy::too_many_arguments)]
pub async fn save_key_record(
keys_ks: &KeyspaceHandle,
key_id: &str,
derivation_path: &str,
key_type: KeyType,
public_key: &str,
label: &str,
context_id: Option<&str>,
seed_id: Option<u32>,
) -> Result<(), Box<dyn std::error::Error>> {
let now = Utc::now();
let record = KeyRecord {
key_id: key_id.to_string(),
derivation_path: derivation_path.to_string(),
key_type,
status: KeyStatus::Active,
public_key: public_key.to_string(),
label: Some(label.to_string()),
context_id: context_id.map(String::from),
seed_id,
exportable: None,
origin: KeyOrigin::Derived,
created_at: now,
updated_at: now,
};
keys_ks.insert(store_key(key_id), &record).await?;
Ok(())
}
pub async fn derive_and_store_did_key(
seed: &[u8],
base: &str,
context_id: &str,
label: &str,
keys_ks: &KeyspaceHandle,
seed_id: Option<u32>,
) -> Result<(String, String), Box<dyn std::error::Error>> {
let dk_path = paths::allocate_path(keys_ks, base)
.await
.map_err(|e| format!("{e}"))?;
let root = ExtendedSigningKey::from_seed(seed)
.map_err(|e| format!("Failed to create BIP-32 root key: {e}"))?;
let derivation_path: DerivationPath = dk_path
.parse()
.map_err(|e| format!("Invalid derivation path: {e}"))?;
let dk_derived = root
.derive(&derivation_path)
.map_err(|e| format!("Key derivation failed: {e}"))?;
let signing_key = SigningKey::from_bytes(dk_derived.signing_key.as_bytes());
let public_key = signing_key.verifying_key().to_bytes();
let multibase_pubkey = ed25519_multibase_pubkey(&public_key);
let did = format!("did:key:{multibase_pubkey}");
let key_id = format!("{did}#{multibase_pubkey}");
let private_key_multibase =
encode_private_multibase(&KeyType::Ed25519, dk_derived.signing_key.as_bytes());
save_key_record(
keys_ks,
&key_id,
&dk_path,
KeyType::Ed25519,
&multibase_pubkey,
label,
Some(context_id),
seed_id,
)
.await?;
Ok((did, private_key_multibase))
}
#[allow(dead_code)]
pub struct DerivedEntityKeys {
pub signing_secret: Secret,
pub signing_path: String,
pub signing_pub: String,
pub signing_priv: String,
pub signing_label: String,
pub signing_key_type: KeyType,
pub ka_secret: Secret,
pub ka_path: String,
pub ka_pub: String,
pub ka_priv: String,
pub ka_label: String,
pub ka_key_type: KeyType,
pub additional_signing: Vec<DerivedSlotKey>,
}
#[derive(Debug, Clone)]
pub struct DerivedSlotKey {
pub slot: String,
pub secret: Secret,
pub path: String,
pub public_multibase: String,
pub private_multibase: String,
pub label: String,
pub key_type: KeyType,
}
pub struct PreRotationKeyData {
pub path: String,
pub public_key: String,
pub label: String,
}
pub struct DerivedSealedTransferKey {
pub path: String,
pub public_key: String,
pub private_key: String,
pub label: String,
}
pub async fn derive_sealed_transfer_key(
seed: &[u8],
base: &str,
label: &str,
keys_ks: &KeyspaceHandle,
) -> Result<DerivedSealedTransferKey, Box<dyn std::error::Error>> {
let path = paths::allocate_path(keys_ks, base)
.await
.map_err(|e| format!("{e}"))?;
let root = ExtendedSigningKey::from_seed(seed)
.map_err(|e| format!("Failed to create BIP-32 root key: {e}"))?;
let derived = root
.derive(
&path
.parse::<DerivationPath>()
.map_err(|e| format!("Invalid derivation path: {e}"))?,
)
.map_err(|e| format!("Key derivation failed: {e}"))?;
let secret = Secret::generate_ed25519(None, Some(derived.signing_key.as_bytes()));
let public_key = secret
.get_public_keymultibase()
.map_err(|e| format!("{e}"))?;
let private_key = encode_private_multibase(&KeyType::Ed25519, derived.signing_key.as_bytes());
Ok(DerivedSealedTransferKey {
path,
public_key,
private_key,
label: label.to_string(),
})
}
pub async fn save_sealed_transfer_key_record(
did: &str,
derived: &DerivedSealedTransferKey,
keys_ks: &KeyspaceHandle,
context_id: Option<&str>,
seed_id: Option<u32>,
) -> Result<(), Box<dyn std::error::Error>> {
save_key_record(
keys_ks,
&format!("{did}#sealed-transfer-0"),
&derived.path,
KeyType::Ed25519,
&derived.public_key,
&derived.label,
context_id,
seed_id,
)
.await
}
pub async fn derive_entity_keys_with_preference(
seed: &[u8],
base: &str,
signing_label: &str,
ka_label: &str,
keys_ks: &KeyspaceHandle,
signing_preference: &[KeyType],
) -> Result<DerivedEntityKeys, Box<dyn std::error::Error>> {
let mut derived = derive_entity_keys(seed, base, signing_label, ka_label, keys_ks).await?;
for candidate in signing_preference {
match candidate {
KeyType::Ed25519 => return Ok(derived),
KeyType::MlDsa44 | KeyType::MlDsa65 => {
use crate::derivation::Bip32Extension;
let root = ExtendedSigningKey::from_seed(seed)
.map_err(|e| format!("Failed to create BIP-32 root key: {e}"))?;
let secret = match candidate {
KeyType::MlDsa44 => root.derive_ml_dsa_44(&derived.signing_path),
_ => root.derive_ml_dsa_65(&derived.signing_path),
}
.map_err(|e| format!("{candidate:?} derivation failed: {e}"))?;
derived.signing_pub = secret
.get_public_keymultibase()
.map_err(|e| format!("{e}"))?;
derived.signing_priv = secret
.get_private_keymultibase()
.map_err(|e| format!("{e}"))?;
derived.signing_secret = secret;
derived.signing_key_type = candidate.clone();
return Ok(derived);
}
_ => continue,
}
}
Err(format!(
"no signing algorithm in {signing_preference:?} can be minted by this build; the \
template's preference list must name at least one supported algorithm"
)
.into())
}
pub async fn derive_entity_keys(
seed: &[u8],
base: &str,
signing_label: &str,
ka_label: &str,
keys_ks: &KeyspaceHandle,
) -> Result<DerivedEntityKeys, Box<dyn std::error::Error>> {
let signing_path = paths::allocate_path(keys_ks, base)
.await
.map_err(|e| format!("{e}"))?;
let ka_path = paths::allocate_path(keys_ks, base)
.await
.map_err(|e| format!("{e}"))?;
let root = ExtendedSigningKey::from_seed(seed)
.map_err(|e| format!("Failed to create BIP-32 root key: {e}"))?;
let signing_derived = root
.derive(
&signing_path
.parse::<DerivationPath>()
.map_err(|e| format!("Invalid derivation path: {e}"))?,
)
.map_err(|e| format!("Key derivation failed: {e}"))?;
let signing_priv =
encode_private_multibase(&KeyType::Ed25519, signing_derived.signing_key.as_bytes());
let signing_secret =
Secret::generate_ed25519(None, Some(signing_derived.signing_key.as_bytes()));
let signing_pub = signing_secret
.get_public_keymultibase()
.map_err(|e| format!("{e}"))?;
let ka_derived = root
.derive(
&ka_path
.parse::<DerivationPath>()
.map_err(|e| format!("Invalid derivation path: {e}"))?,
)
.map_err(|e| format!("Key derivation failed: {e}"))?;
let ka_priv = encode_private_multibase(&KeyType::Ed25519, ka_derived.signing_key.as_bytes());
let ka_secret = Secret::generate_ed25519(None, Some(ka_derived.signing_key.as_bytes()));
let ka_secret = ka_secret
.to_x25519()
.map_err(|e| format!("X25519 conversion failed: {e}"))?;
let ka_pub = ka_secret
.get_public_keymultibase()
.map_err(|e| format!("{e}"))?;
Ok(DerivedEntityKeys {
signing_secret,
signing_path,
signing_pub,
signing_priv,
signing_label: signing_label.to_string(),
signing_key_type: KeyType::Ed25519,
ka_secret,
ka_path,
ka_pub,
ka_priv,
ka_label: ka_label.to_string(),
ka_key_type: KeyType::X25519,
additional_signing: Vec::new(),
})
}
pub async fn derive_additional_signing_key(
seed: &[u8],
base: &str,
slot: &str,
label: &str,
keys_ks: &KeyspaceHandle,
preference: &[KeyType],
) -> Result<DerivedSlotKey, Box<dyn std::error::Error>> {
use crate::derivation::Bip32Extension;
let path = paths::allocate_path(keys_ks, base)
.await
.map_err(|e| format!("{e}"))?;
let root = ExtendedSigningKey::from_seed(seed)
.map_err(|e| format!("Failed to create BIP-32 root key: {e}"))?;
for candidate in preference {
let secret = match candidate {
KeyType::Ed25519 => root.derive_ed25519(&path),
KeyType::MlDsa44 => root.derive_ml_dsa_44(&path),
KeyType::MlDsa65 => root.derive_ml_dsa_65(&path),
_ => continue,
}
.map_err(|e| format!("slot '{slot}': {candidate:?} derivation failed: {e}"))?;
return Ok(DerivedSlotKey {
slot: slot.to_string(),
public_multibase: secret
.get_public_keymultibase()
.map_err(|e| format!("{e}"))?,
private_multibase: secret
.get_private_keymultibase()
.map_err(|e| format!("{e}"))?,
secret,
path,
label: label.to_string(),
key_type: candidate.clone(),
});
}
Err(format!(
"key slot '{slot}' names no signing algorithm this build can mint (asked for \
{preference:?}); the template's preference list must include at least one \
supported algorithm"
)
.into())
}
pub async fn save_entity_key_records(
did: &str,
derived: &DerivedEntityKeys,
keys_ks: &KeyspaceHandle,
context_id: Option<&str>,
seed_id: Option<u32>,
) -> Result<(), Box<dyn std::error::Error>> {
save_entity_key_records_with_ids(
&format!("{did}#key-0"),
&format!("{did}#key-1"),
derived,
keys_ks,
context_id,
seed_id,
)
.await
}
pub async fn save_entity_key_records_with_ids(
signing_vm_id: &str,
ka_vm_id: &str,
derived: &DerivedEntityKeys,
keys_ks: &KeyspaceHandle,
context_id: Option<&str>,
seed_id: Option<u32>,
) -> Result<(), Box<dyn std::error::Error>> {
save_key_record(
keys_ks,
signing_vm_id,
&derived.signing_path,
derived.signing_key_type.clone(),
&derived.signing_pub,
signing_vm_id,
context_id,
seed_id,
)
.await?;
save_key_record(
keys_ks,
ka_vm_id,
&derived.ka_path,
derived.ka_key_type.clone(),
&derived.ka_pub,
ka_vm_id,
context_id,
seed_id,
)
.await?;
Ok(())
}
pub async fn save_additional_signing_key_record(
vm_id: &str,
key: &DerivedSlotKey,
keys_ks: &KeyspaceHandle,
context_id: Option<&str>,
seed_id: Option<u32>,
) -> Result<(), Box<dyn std::error::Error>> {
save_key_record(
keys_ks,
vm_id,
&key.path,
key.key_type.clone(),
&key.public_multibase,
vm_id,
context_id,
seed_id,
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::derivation::Bip32Extension;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
fn test_seed() -> Vec<u8> {
vec![
7, 26, 142, 230, 65, 85, 188, 182, 29, 129, 52, 229, 217, 159, 243, 182, 73, 89, 196,
246, 58, 28, 100, 144, 187, 21, 157, 39, 4, 188, 154, 180,
]
}
fn temp_store() -> (Store, tempfile::TempDir) {
let dir = tempfile::tempdir().expect("failed to create temp dir");
let config = StoreConfig {
data_dir: dir.path().to_path_buf(),
};
let store = Store::open(&config).expect("failed to open store");
(store, dir)
}
#[tokio::test]
async fn a_preference_list_mints_its_first_choice_and_says_so() {
let seed = test_seed();
let (store, _dir) = temp_store();
let ks = store.keyspace(vta_keyspaces::KEYS).expect("keyspace");
let pq = derive_entity_keys_with_preference(
&seed,
"m/26'/1'/0'",
"signing",
"ka",
&ks,
&[KeyType::MlDsa44, KeyType::Ed25519],
)
.await
.expect("ML-DSA-44 is the first choice and this build can mint it");
assert_eq!(pq.signing_key_type, KeyType::MlDsa44);
assert_eq!(
pq.ka_key_type,
KeyType::X25519,
"the key-agreement slot takes no preference — X25519 is the only \
algorithm that can serve it in a DID document"
);
let (_b, bytes) = multibase::decode(&pq.signing_pub).expect("valid multibase");
assert!(
bytes.len() > 1000,
"signing_key_type says ML-DSA-44 but the key is {} bytes — the label \
and the key have come apart, which is the exact defect this carries \
the type to prevent",
bytes.len(),
);
}
#[tokio::test]
async fn a_preference_list_falls_back_and_records_what_it_took() {
let seed = test_seed();
let (store, _dir) = temp_store();
let ks = store.keyspace(vta_keyspaces::KEYS).expect("keyspace");
let classical = derive_entity_keys_with_preference(
&seed,
"m/26'/1'/1'",
"signing",
"ka",
&ks,
&[KeyType::X25519, KeyType::Ed25519],
)
.await
.expect("an unusable first choice falls through to the next");
assert_eq!(classical.signing_key_type, KeyType::Ed25519);
}
#[tokio::test]
async fn an_unsatisfiable_preference_is_an_error_not_a_downgrade() {
let seed = test_seed();
let (store, _dir) = temp_store();
let ks = store.keyspace(vta_keyspaces::KEYS).expect("keyspace");
let err = derive_entity_keys_with_preference(
&seed,
"m/26'/1'/2'",
"signing",
"ka",
&ks,
&[KeyType::X25519],
)
.await
.err()
.expect("a list naming no signing algorithm cannot be satisfied");
assert!(
err.to_string().contains("no signing algorithm"),
"the error must say the list could not be satisfied: {err}"
);
}
#[tokio::test]
async fn an_additional_slot_is_minted_at_its_own_path() {
let seed = test_seed();
let (store, _dir) = temp_store();
let ks = store.keyspace(vta_keyspaces::KEYS).expect("keyspace");
let primary = derive_entity_keys(&seed, "m/26'/3'/0'", "signing", "ka", &ks)
.await
.expect("primary");
let extra = derive_additional_signing_key(
&seed,
"m/26'/3'/0'",
"pq-signing",
"post-quantum signing",
&ks,
&[KeyType::MlDsa44, KeyType::Ed25519],
)
.await
.expect("ML-DSA-44 is mintable in this build");
assert_eq!(extra.slot, "pq-signing");
assert_eq!(extra.key_type, KeyType::MlDsa44);
assert_ne!(
extra.path, primary.signing_path,
"an extra slot must not share the primary's derivation path"
);
let (_b, bytes) = multibase::decode(&extra.public_multibase).expect("valid multibase");
assert!(
bytes.len() > 1000,
"key_type says ML-DSA-44 but the key is {} bytes",
bytes.len()
);
}
#[tokio::test]
async fn an_additional_slot_falls_back_and_refuses_an_empty_list() {
let seed = test_seed();
let (store, _dir) = temp_store();
let ks = store.keyspace(vta_keyspaces::KEYS).expect("keyspace");
let fell_back = derive_additional_signing_key(
&seed,
"m/26'/3'/1'",
"extra",
"extra",
&ks,
&[KeyType::X25519, KeyType::Ed25519],
)
.await
.expect("an unusable first choice falls through");
assert_eq!(fell_back.key_type, KeyType::Ed25519);
let err = derive_additional_signing_key(
&seed,
"m/26'/3'/2'",
"extra",
"extra",
&ks,
&[KeyType::X25519],
)
.await
.expect_err("a list naming no signing algorithm cannot be satisfied");
assert!(
err.to_string().contains("extra"),
"the error must name the slot that could not be satisfied: {err}"
);
}
#[tokio::test]
async fn a_saved_record_names_the_algorithm_the_key_is() {
let seed = test_seed();
let (store, _dir) = temp_store();
let ks = store.keyspace(vta_keyspaces::KEYS).expect("keyspace");
let did = "did:example:pq";
let derived = derive_entity_keys_with_preference(
&seed,
"m/26'/4'/0'",
"signing",
"ka",
&ks,
&[KeyType::MlDsa44],
)
.await
.expect("derive");
save_entity_key_records(did, &derived, &ks, Some("vta"), Some(0))
.await
.expect("save");
let record: KeyRecord = ks
.get(format!("key:{did}#key-0"))
.await
.expect("read")
.expect("a record was stored for #key-0");
assert_eq!(
record.key_type,
KeyType::MlDsa44,
"the record must carry the minted algorithm, not a literal"
);
}
#[tokio::test]
async fn test_create_store_recover_cycle() {
let seed = test_seed();
let (store, _dir) = temp_store();
let keys_ks = store.keyspace(vta_keyspaces::KEYS).unwrap();
let did = "did:webvh:abc123:example.com:vta";
let derived = derive_entity_keys(&seed, "m/44'/0'", "signing", "key-agreement", &keys_ks)
.await
.unwrap();
save_entity_key_records(did, &derived, &keys_ks, Some("vta"), Some(0))
.await
.unwrap();
let created_signing_pub = derived.signing_pub.clone();
let created_ka_pub = derived.ka_pub.clone();
let signing_record: KeyRecord = keys_ks
.get(store_key(&format!("{did}#key-0")))
.await
.unwrap()
.expect("signing key record not found");
let ka_record: KeyRecord = keys_ks
.get(store_key(&format!("{did}#key-1")))
.await
.unwrap()
.expect("KA key record not found");
assert_eq!(signing_record.key_type, KeyType::Ed25519);
assert_eq!(ka_record.key_type, KeyType::X25519);
assert_eq!(signing_record.seed_id, Some(0));
let root = ExtendedSigningKey::from_seed(&seed).unwrap();
let recovered_signing = root
.derive_ed25519(&signing_record.derivation_path)
.unwrap();
let recovered_ka = root.derive_x25519(&ka_record.derivation_path).unwrap();
let recovered_signing_pub = recovered_signing.get_public_keymultibase().unwrap();
let recovered_ka_pub = recovered_ka.get_public_keymultibase().unwrap();
assert_eq!(
signing_record.public_key, recovered_signing_pub,
"stored signing public key does not match recovered key"
);
assert_eq!(
ka_record.public_key, recovered_ka_pub,
"stored KA public key does not match recovered key"
);
assert_eq!(
created_signing_pub, recovered_signing_pub,
"created signing public key does not match recovered key — \
DID document would have wrong signing key"
);
assert_eq!(
created_ka_pub, recovered_ka_pub,
"created KA public key does not match recovered key — \
DID document would have wrong key-agreement key, \
DIDComm encryption/decryption will fail"
);
}
#[tokio::test]
async fn test_key_records_survive_store_reopen() {
let seed = test_seed();
let dir = tempfile::tempdir().unwrap();
let did = "did:webvh:abc123:example.com:vta";
{
let config = StoreConfig {
data_dir: dir.path().to_path_buf(),
};
let store = Store::open(&config).unwrap();
let keys_ks = store.keyspace(vta_keyspaces::KEYS).unwrap();
let derived = derive_entity_keys(&seed, "m/44'/0'", "signing", "ka", &keys_ks)
.await
.unwrap();
save_entity_key_records(did, &derived, &keys_ks, Some("vta"), Some(0))
.await
.unwrap();
store.persist().await.unwrap();
}
{
let config = StoreConfig {
data_dir: dir.path().to_path_buf(),
};
let store = Store::open(&config).unwrap();
let keys_ks = store.keyspace(vta_keyspaces::KEYS).unwrap();
let signing: KeyRecord = keys_ks
.get(store_key(&format!("{did}#key-0")))
.await
.unwrap()
.expect("signing key not found after reopen");
let ka: KeyRecord = keys_ks
.get(store_key(&format!("{did}#key-1")))
.await
.unwrap()
.expect("KA key not found after reopen");
let root = ExtendedSigningKey::from_seed(&seed).unwrap();
let recovered_sign_pub = root
.derive_ed25519(&signing.derivation_path)
.unwrap()
.get_public_keymultibase()
.unwrap();
let recovered_ka_pub = root
.derive_x25519(&ka.derivation_path)
.unwrap()
.get_public_keymultibase()
.unwrap();
assert_eq!(signing.public_key, recovered_sign_pub);
assert_eq!(ka.public_key, recovered_ka_pub);
}
}
#[tokio::test]
async fn test_path_allocation_produces_unique_keys() {
let seed = test_seed();
let (store, _dir) = temp_store();
let keys_ks = store.keyspace(vta_keyspaces::KEYS).unwrap();
let base = "m/44'/0'";
let mut pub_keys = Vec::new();
for _ in 0..5 {
let path = paths::allocate_path(&keys_ks, base).await.unwrap();
let root = ExtendedSigningKey::from_seed(&seed).unwrap();
let secret = root.derive_ed25519(&path).unwrap();
pub_keys.push(secret.get_public_keymultibase().unwrap());
}
for i in 0..pub_keys.len() {
for j in (i + 1)..pub_keys.len() {
assert_ne!(
pub_keys[i], pub_keys[j],
"path allocation produced duplicate keys at indices {i} and {j}"
);
}
}
}
#[tokio::test]
async fn test_hex_seed_roundtrip() {
let seed = test_seed();
let path = "m/44'/0'/0'";
let hex_seed = hex::encode(&seed);
let recovered_seed = hex::decode(&hex_seed).unwrap();
let root_original = ExtendedSigningKey::from_seed(&seed).unwrap();
let root_recovered = ExtendedSigningKey::from_seed(&recovered_seed).unwrap();
let sign_orig = root_original.derive_ed25519(path).unwrap();
let sign_recv = root_recovered.derive_ed25519(path).unwrap();
assert_eq!(
sign_orig.get_public_keymultibase().unwrap(),
sign_recv.get_public_keymultibase().unwrap(),
"hex-encoded seed round-trip produced different keys"
);
let ka_orig = root_original.derive_x25519(path).unwrap();
let ka_recv = root_recovered.derive_x25519(path).unwrap();
assert_eq!(
ka_orig.get_public_keymultibase().unwrap(),
ka_recv.get_public_keymultibase().unwrap(),
"hex-encoded seed round-trip produced different X25519 keys"
);
}
}