use affinidi_messaging_didcomm::Message;
use affinidi_secrets_resolver::secrets::Secret;
use affinidi_tdk::messaging::ATM;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use ed25519_dalek::{Signer, SigningKey};
use serde_json::json;
use uuid::Uuid;
use crate::error::AppError;
use crate::keys::seed_store::SeedStore;
use crate::operations::internal_authority::InternalAuthority;
use crate::store::KeyspaceHandle;
use vta_sdk::did_key::decode_private_key_multibase;
pub mod proxy_login;
pub mod release;
pub mod sign_trust_task;
pub mod upsert;
pub const PROXY_LOGIN_INNER_MSG_TYPE: &str =
"https://openvtc.org/vault/proxy-login/session-envelope/1.0";
pub fn secret_kind_label(kind: vti_common::vault::SecretKind) -> &'static str {
use vti_common::vault::SecretKind;
match kind {
SecretKind::Password => "password",
SecretKind::Passkey => "passkey",
SecretKind::OauthTokens => "oauth-tokens",
SecretKind::DidSelfIssued => "did-self-issued",
SecretKind::DidcommPeer => "didcomm-peer",
SecretKind::BearerToken => "bearer-token",
SecretKind::SshKey => "ssh-key",
SecretKind::Custom => "custom",
}
}
pub const RELEASE_INNER_MSG_TYPE: &str = "https://openvtc.org/vault/release/secret-envelope/1.0";
pub async fn authcrypt_to_holder(
atm: &ATM,
vta_did: &str,
holder_did: &str,
inner_type: &str,
body: serde_json::Value,
) -> Result<String, AppError> {
let msg = Message::build(Uuid::new_v4().to_string(), inner_type.to_string(), body)
.from(vta_did.to_string())
.to(holder_did.to_string())
.finalize();
let (jwe, _metadata) = atm
.pack_encrypted(&msg, holder_did, Some(vta_did), Some(vta_did))
.await
.map_err(|e| AppError::Internal(format!("pack_encrypted failed: {e}")))?;
Ok(jwe)
}
#[cfg(feature = "webvh")]
pub mod password_post;
const PROXY_LOGIN_CHANNEL: &str = "vault-proxy-login-internal";
const SIGN_TRUST_TASK_CHANNEL: &str = "vault-sign-trust-task-internal";
pub const PROXY_LOGIN_ID_TOKEN_TTL_SECS: u64 = 300;
pub async fn load_signing_key_by_id(
keys_ks: &KeyspaceHandle,
imported_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
audit_ks: &KeyspaceHandle,
key_id: &str,
) -> Result<SigningKey, AppError> {
let authority = InternalAuthority::new("vault-proxy-login");
let resp = crate::operations::keys::get_key_secret_internal(
keys_ks,
imported_ks,
seed_store,
audit_ks,
authority,
key_id,
PROXY_LOGIN_CHANNEL,
)
.await?;
let seed: [u8; 32] = decode_private_key_multibase(&resp.private_key_multibase)
.map_err(|e| AppError::Internal(format!("decode signing-key seed for {key_id}: {e}")))?;
Ok(SigningKey::from_bytes(&seed))
}
pub async fn load_signing_secret_by_id(
keys_ks: &KeyspaceHandle,
imported_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
audit_ks: &KeyspaceHandle,
key_id: &str,
) -> Result<Secret, AppError> {
let authority = InternalAuthority::new("vault-sign-trust-task");
let resp = crate::operations::keys::get_key_secret_internal(
keys_ks,
imported_ks,
seed_store,
audit_ks,
authority,
key_id,
SIGN_TRUST_TASK_CHANNEL,
)
.await?;
let mut secret = Secret::from_multibase(&resp.private_key_multibase, None)
.map_err(|e| AppError::Internal(format!("construct Secret for {key_id}: {e}")))?;
secret.id = key_id.to_string();
Ok(secret)
}
pub fn build_siop_id_token(
siop_did: &str,
signing_key_id: &str,
audience: &str,
nonce: Option<&str>,
iat: u64,
ttl_secs: u64,
signing_key: &SigningKey,
) -> Result<String, AppError> {
let header = json!({
"alg": "EdDSA",
"typ": "JWT",
"kid": signing_key_id,
});
let nonce_claim = match nonce {
Some(n) => n.to_string(),
None => uuid::Uuid::new_v4().to_string(),
};
let payload = json!({
"iss": siop_did,
"sub": siop_did,
"aud": audience,
"nonce": nonce_claim,
"iat": iat,
"exp": iat.saturating_add(ttl_secs),
});
let header_b64 = URL_SAFE_NO_PAD.encode(
serde_json::to_vec(&header)
.map_err(|e| AppError::Internal(format!("serialize SIOP header: {e}")))?,
);
let payload_b64 = URL_SAFE_NO_PAD.encode(
serde_json::to_vec(&payload)
.map_err(|e| AppError::Internal(format!("serialize SIOP payload: {e}")))?,
);
let signing_input = format!("{header_b64}.{payload_b64}");
let signature = signing_key.sign(signing_input.as_bytes());
let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
Ok(format!("{signing_input}.{sig_b64}"))
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::{Signature, Verifier, VerifyingKey};
#[test]
fn siop_id_token_round_trip_verifies_against_signing_key() {
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let verifying_key: VerifyingKey = (&signing_key).into();
let siop_did = "did:webvh:Q1:proxy.example:persona-work";
let kid = format!("{siop_did}#key-0");
let audience = "did:web:rp.example";
let iat = 1_700_000_000u64;
let ttl = 300;
let jws = build_siop_id_token(siop_did, &kid, audience, None, iat, ttl, &signing_key)
.expect("build SIOP id_token");
let parts: Vec<&str> = jws.split('.').collect();
assert_eq!(parts.len(), 3, "compact JWS = 3 parts");
let signing_input = format!("{}.{}", parts[0], parts[1]);
let sig_bytes = URL_SAFE_NO_PAD.decode(parts[2]).expect("sig decode");
let signature = Signature::from_slice(&sig_bytes).expect("sig parse");
verifying_key
.verify(signing_input.as_bytes(), &signature)
.expect("signature verifies against the signing key's public half");
let header_json: serde_json::Value =
serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0]).expect("header decode"))
.expect("header parse");
assert_eq!(header_json["alg"], "EdDSA");
assert_eq!(header_json["typ"], "JWT");
assert_eq!(header_json["kid"], kid);
let payload_json: serde_json::Value =
serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).expect("payload decode"))
.expect("payload parse");
assert_eq!(payload_json["iss"], siop_did);
assert_eq!(payload_json["sub"], siop_did);
assert_eq!(payload_json["aud"], audience);
assert_eq!(payload_json["iat"], iat);
assert_eq!(payload_json["exp"], iat + ttl);
assert!(
payload_json["nonce"]
.as_str()
.map(|n| !n.is_empty())
.unwrap_or(false),
"nonce is server-generated and non-empty"
);
}
#[test]
fn siop_id_token_different_signing_key_fails_verification() {
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let wrong_key: VerifyingKey = (&SigningKey::from_bytes(&[99u8; 32])).into();
let jws = build_siop_id_token(
"did:webvh:foo",
"did:webvh:foo#key-0",
"did:web:rp.example",
None,
1_700_000_000,
300,
&signing_key,
)
.unwrap();
let parts: Vec<&str> = jws.split('.').collect();
let signing_input = format!("{}.{}", parts[0], parts[1]);
let sig = Signature::from_slice(&URL_SAFE_NO_PAD.decode(parts[2]).unwrap()).unwrap();
assert!(
wrong_key.verify(signing_input.as_bytes(), &sig).is_err(),
"verification must fail against an unrelated public key"
);
}
#[test]
fn siop_id_token_embeds_caller_nonce_verbatim() {
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let nonce = "rp-challenge_5e3f-AB cd~!@#$%^&*()";
let jws = build_siop_id_token(
"did:webvh:foo",
"did:webvh:foo#key-0",
"did:web:rp.example",
Some(nonce),
1_700_000_000,
300,
&signing_key,
)
.expect("build SIOP id_token with caller nonce");
let parts: Vec<&str> = jws.split('.').collect();
let payload_json: serde_json::Value =
serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
assert_eq!(
payload_json["nonce"], nonce,
"caller-supplied nonce must appear verbatim in the id_token's nonce claim"
);
}
#[test]
fn siop_id_token_generates_uuid_nonce_when_none_supplied() {
let signing_key = SigningKey::from_bytes(&[42u8; 32]);
let jws = build_siop_id_token(
"did:webvh:foo",
"did:webvh:foo#key-0",
"did:web:rp.example",
None,
1_700_000_000,
300,
&signing_key,
)
.unwrap();
let parts: Vec<&str> = jws.split('.').collect();
let payload_json: serde_json::Value =
serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
let n = payload_json["nonce"].as_str().expect("nonce is a string");
assert_eq!(n.len(), 36, "fallback nonce is a UUIDv4 (36 chars)");
assert!(
uuid::Uuid::parse_str(n).is_ok(),
"fallback nonce parses as a UUID"
);
}
}