use vta_sdk::secure_store::{self, SecureStore};
use vta_sdk::session::SessionStore;
use zeroize::Zeroizing;
pub(crate) fn claim_keyring_key(slug: &str) -> String {
format!("bootstrap-claim:{slug}")
}
pub(crate) struct ClaimKey {
pub did: String,
pub seed: Zeroizing<[u8; 32]>,
}
fn require_os_keyring() -> Result<(), Box<dyn std::error::Error>> {
if !cfg!(feature = "keyring") {
return Err(
"the bootstrap claim key is kept only in the OS keyring, and this build \
has no `keyring` feature compiled in. Rebuild with it."
.into(),
);
}
match secure_store::override_from_env() {
Ok(Some(SecureStore::File)) => Err(format!(
"{}=file would write the bootstrap claim key to a plaintext file. It is kept only \
in the OS keyring — unset {} for this command.",
secure_store::OVERRIDE_ENV,
secure_store::OVERRIDE_ENV
)
.into()),
Ok(_) => Ok(()),
Err(msg) => Err(msg.into()),
}
}
pub async fn run_create(slug: &str) -> Result<(), Box<dyn std::error::Error>> {
require_os_keyring()?;
let did = create(&crate::auth::store(), slug)?;
eprintln!("Bootstrap claim DID created for '{slug}'. Its private key is in the OS keyring.");
eprintln!();
eprintln!("Supply this DID when you create the VTA. Only its holder can claim admin:");
println!("{did}");
eprintln!();
eprintln!("Once the VTA is up, from this machine:");
eprintln!(" pnm bootstrap connect --slug {slug} --vta-did <did> --expect-pcr0 <hex>");
Ok(())
}
pub async fn run_show(slug: &str) -> Result<(), Box<dyn std::error::Error>> {
require_os_keyring()?;
let key = load(&crate::auth::store(), slug)?;
println!("{}", key.did);
Ok(())
}
pub async fn run_discard(slug: &str) -> Result<(), Box<dyn std::error::Error>> {
require_os_keyring()?;
let store = crate::auth::store();
let key = load(&store, slug)?;
discard(&store, slug);
eprintln!(
"Discarded bootstrap claim DID {} for '{slug}'. A VTA registered with it can no longer \
be claimed from this machine.",
key.did
);
Ok(())
}
pub(crate) fn load_for_connect(slug: &str) -> Result<ClaimKey, Box<dyn std::error::Error>> {
require_os_keyring()?;
load(&crate::auth::store(), slug)
}
pub(crate) fn discard_after_claim(slug: &str) {
discard(&crate::auth::store(), slug);
}
fn create(store: &SessionStore, slug: &str) -> Result<String, Box<dyn std::error::Error>> {
let keyring_key = claim_keyring_key(slug);
if store.has_session(&keyring_key) {
return Err(format!(
"a bootstrap claim DID already exists for '{slug}'. Print it with \
`pnm bootstrap claim-did show --slug {slug}`, or remove it with \
`pnm bootstrap claim-did discard --slug {slug}` if its VTA is gone."
)
.into());
}
let (did, private_key_multibase) =
vta_cli_common::local_keygen::generate_unbound_admin_did_key();
store.store_pending_vta_binding(&keyring_key, &did, &private_key_multibase)?;
match store.loaded_session(&keyring_key) {
Some(info) if info.client_did == did => Ok(did),
_ => Err(
"the OS keyring accepted the claim key but did not return it on read-back; \
nothing was registered — fix the keyring and retry"
.into(),
),
}
}
fn load(store: &SessionStore, slug: &str) -> Result<ClaimKey, Box<dyn std::error::Error>> {
let info = store.loaded_session(&claim_keyring_key(slug)).ok_or_else(
|| -> Box<dyn std::error::Error> {
format!(
"no bootstrap claim DID for '{slug}' in the OS keyring. A TEE VTA can only be \
claimed with the DID registered when it was created — run \
`pnm bootstrap claim-did create --slug {slug}` before creating the VTA, and \
pass the same --slug to connect."
)
.into()
},
)?;
let seed = Zeroizing::new(crate::auth::session_seed(&info)?);
let public = ed25519_dalek::SigningKey::from_bytes(&seed)
.verifying_key()
.to_bytes();
if affinidi_crypto::did_key::ed25519_pub_to_did_key(&public) != info.client_did {
return Err(format!(
"the bootstrap claim key for '{slug}' does not match its stored DID; refusing to use it"
)
.into());
}
Ok(ClaimKey {
did: info.client_did,
seed,
})
}
fn discard(store: &SessionStore, slug: &str) {
store.logout(&claim_keyring_key(slug));
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::sync::Mutex;
use vta_sdk::session::{SessionBackend, SessionStore};
#[derive(Default)]
struct MemoryBackend(Mutex<HashMap<String, String>>);
impl SessionBackend for MemoryBackend {
fn load(&self, key: &str) -> Option<String> {
self.0.lock().unwrap().get(key).cloned()
}
fn save(&self, key: &str, value: &str) -> Result<(), Box<dyn std::error::Error>> {
self.0.lock().unwrap().insert(key.into(), value.into());
Ok(())
}
fn clear(&self, key: &str) {
self.0.lock().unwrap().remove(key);
}
}
fn store() -> SessionStore {
SessionStore::with_backend(Box::new(MemoryBackend::default()))
}
#[test]
fn create_then_load_returns_matching_key() {
let store = store();
let did = super::create(&store, "acme").unwrap();
assert!(did.starts_with("did:key:z6Mk"));
let key = super::load(&store, "acme").unwrap();
assert_eq!(key.did, did);
let signed =
vta_sdk::sealed_transfer::BootstrapRequest::new_signed_claim(&key.seed, [3; 16], None);
assert_eq!(signed.client_did, did);
signed.verify_claim_signature().unwrap();
}
#[test]
fn create_refuses_to_replace_an_existing_claim() {
let store = store();
let first = super::create(&store, "acme").unwrap();
let err = super::create(&store, "acme").unwrap_err().to_string();
assert!(err.contains("claim-did show"), "{err}");
assert_eq!(super::load(&store, "acme").unwrap().did, first);
}
#[test]
fn claim_is_kept_apart_from_the_vta_session() {
let store = store();
super::create(&store, "acme").unwrap();
assert!(!store.has_session(&crate::config::vta_keyring_key("acme")));
}
#[test]
fn discard_removes_the_key() {
let store = store();
super::create(&store, "acme").unwrap();
super::discard(&store, "acme");
let err = super::load(&store, "acme").err().unwrap().to_string();
assert!(err.contains("claim-did create"), "{err}");
}
}