use std::sync::Arc;
use affinidi_sd_jwt::error::SdJwtError;
use affinidi_secrets_resolver::secrets::Secret;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use ed25519_dalek::{Signature, Signer, SigningKey};
use ed25519_dalek_bip32::{DerivationPath, ExtendedSigningKey};
use serde_json::Value;
use vta_sdk::keys::{KeyOrigin, KeyRecord, KeyStatus, KeyType};
use zeroize::Zeroize;
use crate::auth::AuthClaims;
use crate::keys::seed_store::SeedStore;
use crate::keys::seeds::load_seed_bytes;
use crate::store::KeyspaceHandle;
use vti_common::error::AppError;
#[derive(Debug)]
pub struct HolderSdJwtSigner {
key: SigningKey,
kid: String,
}
impl affinidi_sd_jwt::signer::JwtSigner for HolderSdJwtSigner {
fn algorithm(&self) -> &str {
"EdDSA"
}
fn key_id(&self) -> Option<&str> {
Some(&self.kid)
}
fn sign_jwt(&self, header: &Value, payload: &Value) -> Result<String, SdJwtError> {
let h = URL_SAFE_NO_PAD.encode(
serde_json::to_vec(header).map_err(|e| SdJwtError::Verification(e.to_string()))?,
);
let p = URL_SAFE_NO_PAD.encode(
serde_json::to_vec(payload).map_err(|e| SdJwtError::Verification(e.to_string()))?,
);
let input = format!("{h}.{p}");
let sig: Signature = self.key.sign(input.as_bytes());
Ok(format!(
"{input}.{}",
URL_SAFE_NO_PAD.encode(sig.to_bytes())
))
}
}
#[derive(Debug)]
pub struct HolderKeys {
pub signer: HolderSdJwtSigner,
pub consent_secret: Secret,
}
pub async fn resolve_holder_keys(
keys_ks: &KeyspaceHandle,
seed_store: &Arc<dyn SeedStore>,
auth: &AuthClaims,
subject_did: &str,
) -> Result<HolderKeys, AppError> {
let multibase = subject_did.strip_prefix("did:key:").ok_or_else(|| {
AppError::Validation(format!("holder subject `{subject_did}` is not a did:key"))
})?;
let key_id = format!("{subject_did}#{multibase}");
let record: KeyRecord = keys_ks
.get(crate::keys::store_key(&key_id))
.await?
.ok_or_else(|| {
AppError::NotFound(format!(
"holder key for `{subject_did}` is not managed by this VTA"
))
})?;
if record.key_type != KeyType::Ed25519 {
return Err(AppError::Validation(format!(
"holder key `{key_id}` is not an Ed25519 key"
)));
}
if record.status != KeyStatus::Active {
return Err(AppError::Validation(format!(
"holder key `{key_id}` is not active"
)));
}
if record.origin != KeyOrigin::Derived {
return Err(AppError::Validation(
"imported holder keys are not supported for presentation yet".into(),
));
}
match &record.context_id {
Some(ctx) => auth.require_context(ctx)?,
None => auth.require_super_admin()?,
}
let mut seed = load_seed_bytes(keys_ks, &**seed_store, record.seed_id)
.await
.map_err(|e| AppError::Internal(format!("seed load: {e}")))?;
let bip32 = ExtendedSigningKey::from_seed(&seed)
.map_err(|e| AppError::Internal(format!("BIP-32 root key: {e}")))?;
seed.zeroize();
let path: DerivationPath = record.derivation_path.parse().map_err(|e| {
AppError::Internal(format!(
"invalid derivation path `{}`: {e}",
record.derivation_path
))
})?;
let derived = bip32
.derive(&path)
.map_err(|e| AppError::Internal(format!("derive: {e}")))?;
let signing_key = derived.signing_key;
let signer = HolderSdJwtSigner {
key: signing_key.clone(),
kid: key_id.clone(),
};
let mut consent_secret = Secret::generate_ed25519(Some(&key_id), Some(signing_key.as_bytes()));
consent_secret.id = key_id;
Ok(HolderKeys {
signer,
consent_secret,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::acl::Role;
use affinidi_sd_jwt::signer::JwtSigner;
use chrono::Utc;
use vti_common::config::StoreConfig;
use vti_common::store::Store;
fn admin_of(ctx: &str) -> AuthClaims {
AuthClaims {
role: Role::Admin,
allowed_contexts: vec![ctx.to_string()],
..Default::default()
}
}
fn super_admin() -> AuthClaims {
AuthClaims {
role: Role::Admin,
allowed_contexts: Vec::new(),
..Default::default()
}
}
async fn setup(
context: Option<&str>,
) -> (
tempfile::TempDir,
Store,
KeyspaceHandle,
Arc<dyn SeedStore>,
String,
) {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(&StoreConfig {
data_dir: dir.path().to_path_buf(),
})
.unwrap();
let keys_ks = store.keyspace(crate::keyspaces::KEYS).unwrap();
let seed = vec![42u8; 64];
let seed_store: Arc<dyn SeedStore> =
Arc::new(crate::test_support::TestSeedStore(seed.clone()));
let path = "m/26'/2'/0'/0'";
let bip32 = ExtendedSigningKey::from_seed(&seed).unwrap();
let derived = bip32
.derive(&path.parse::<DerivationPath>().unwrap())
.unwrap();
let subject_did = affinidi_crypto::did_key::ed25519_pub_to_did_key(
derived.signing_key.verifying_key().as_bytes(),
);
let multibase = subject_did.strip_prefix("did:key:").unwrap();
let key_id = format!("{subject_did}#{multibase}");
let record = KeyRecord {
key_id: key_id.clone(),
derivation_path: path.to_string(),
key_type: KeyType::Ed25519,
status: KeyStatus::Active,
public_key: multibase.to_string(),
label: None,
context_id: context.map(str::to_string),
seed_id: None,
origin: KeyOrigin::Derived,
created_at: Utc::now(),
updated_at: Utc::now(),
};
keys_ks
.insert(crate::keys::store_key(&key_id), &record)
.await
.unwrap();
(dir, store, keys_ks, seed_store, subject_did)
}
#[tokio::test]
async fn resolves_within_an_authorised_context() {
let (_d, _s, keys_ks, seed_store, subject_did) = setup(Some("acme")).await;
let keys = resolve_holder_keys(&keys_ks, &seed_store, &admin_of("acme"), &subject_did)
.await
.expect("resolve");
let multibase = subject_did.strip_prefix("did:key:").unwrap();
let key_id = format!("{subject_did}#{multibase}");
assert_eq!(keys.signer.key_id(), Some(key_id.as_str()));
assert_eq!(keys.consent_secret.id, key_id);
}
#[tokio::test]
async fn refuses_a_key_outside_the_callers_context() {
let (_d, _s, keys_ks, seed_store, subject_did) = setup(Some("acme")).await;
let err = resolve_holder_keys(&keys_ks, &seed_store, &admin_of("other"), &subject_did)
.await
.unwrap_err();
assert!(matches!(err, AppError::Forbidden(_)), "{err:?}");
}
#[tokio::test]
async fn parent_admin_resolves_a_descendant_context_key() {
let (_d, _s, keys_ks, seed_store, subject_did) = setup(Some("acme/eng")).await;
assert!(
resolve_holder_keys(&keys_ks, &seed_store, &admin_of("acme"), &subject_did)
.await
.is_ok()
);
}
#[tokio::test]
async fn unknown_subject_is_not_found() {
let (_d, _s, keys_ks, seed_store, _subject) = setup(Some("acme")).await;
let err = resolve_holder_keys(
&keys_ks,
&seed_store,
&super_admin(),
"did:key:zUnknownHolder",
)
.await
.unwrap_err();
assert!(matches!(err, AppError::NotFound(_)), "{err:?}");
}
}