use std::sync::Arc;
use affinidi_data_integrity::DataIntegrityProof;
use affinidi_data_integrity::signer::Signer;
use affinidi_secrets_resolver::secrets::KeyType;
use vta_sdk::protocols::key_management::sign::{SignAlgorithm, SigningDomain};
use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
use vti_secrets::SeedStore;
use crate::auth::AuthClaims;
pub struct SigningContext<'a> {
pub keys_ks: &'a KeyspaceHandle,
pub imported_ks: &'a KeyspaceHandle,
pub internal_ks: &'a KeyspaceHandle,
pub contexts_ks: &'a KeyspaceHandle,
pub acl_ks: &'a KeyspaceHandle,
pub seed_store: &'a Arc<dyn SeedStore>,
pub audit: &'a vta_audit::SharedAuditSink,
pub auth: &'a AuthClaims,
}
pub struct RoomKeySigner<'a> {
ctx: SigningContext<'a>,
key_id: String,
verification_method: String,
}
impl<'a> RoomKeySigner<'a> {
pub fn new(ctx: SigningContext<'a>, key_id: impl Into<String>, room_did: &str) -> Self {
Self {
ctx,
key_id: key_id.into(),
verification_method: format!("{room_did}#key-1"),
}
}
}
#[async_trait::async_trait]
impl Signer for RoomKeySigner<'_> {
fn key_type(&self) -> KeyType {
KeyType::Ed25519
}
fn verification_method(&self) -> &str {
&self.verification_method
}
async fn sign(
&self,
data: &[u8],
) -> Result<Vec<u8>, affinidi_data_integrity::DataIntegrityError> {
let result = crate::operations::keys::sign_payload(
self.ctx.keys_ks,
self.ctx.imported_ks,
self.ctx.internal_ks,
self.ctx.contexts_ks,
self.ctx.acl_ks,
self.ctx.seed_store,
self.ctx.audit,
self.ctx.auth,
&self.key_id,
data,
&SignAlgorithm::EdDSA,
SigningDomain::ProtocolDefined,
"rooms/owner",
)
.await
.map_err(affinidi_data_integrity::DataIntegrityError::signing)?;
use base64::Engine as _;
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(&result.signature)
.map_err(affinidi_data_integrity::DataIntegrityError::signing)
}
}
pub struct VtaKeySigner<'a> {
inner: RoomKeySigner<'a>,
}
impl<'a> VtaKeySigner<'a> {
pub fn new(
ctx: SigningContext<'a>,
key_id: impl Into<String>,
verification_method: impl Into<String>,
) -> Self {
Self {
inner: RoomKeySigner {
ctx,
key_id: key_id.into(),
verification_method: verification_method.into(),
},
}
}
}
#[async_trait::async_trait]
impl Signer for VtaKeySigner<'_> {
fn key_type(&self) -> KeyType {
self.inner.key_type()
}
fn verification_method(&self) -> &str {
self.inner.verification_method()
}
async fn sign(
&self,
data: &[u8],
) -> Result<Vec<u8>, affinidi_data_integrity::DataIntegrityError> {
self.inner.sign(data).await
}
}
pub async fn sign_as_room(
ctx: SigningContext<'_>,
key_id: &str,
room_did: &str,
credential: &mut dtg_credentials::DTGCredential,
) -> Result<(String, String), AppError> {
let signer = RoomKeySigner::new(ctx, key_id, room_did);
let proof = DataIntegrityProof::sign(
&*credential,
&signer,
affinidi_data_integrity::SignOptions::new(),
)
.await
.map_err(|e| {
let mut cause = String::new();
let mut src: Option<&(dyn std::error::Error + 'static)> = std::error::Error::source(&e);
while let Some(inner) = src {
cause = format!("{cause}: {inner}");
src = inner.source();
}
AppError::Internal(format!("sign as room `{room_did}`: {e}{cause}"))
})?;
credential.credential_mut().proof = Some(proof);
let id = credential.id().unwrap_or_default().to_string();
let serialised = serde_json::to_string(credential)
.map_err(|e| AppError::Internal(format!("serialise the credential: {e}")))?;
Ok((serialised, id))
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[tokio::test]
async fn a_credential_signed_through_the_oracle_carries_the_rooms_proof() {
let ts = crate::test_support::open_test_store().await;
let seed: std::sync::Arc<dyn SeedStore> =
std::sync::Arc::new(crate::test_support::TestSeedStore(vec![7u8; 32]));
let auth = crate::test_support::super_admin_claims();
let ctx = || SigningContext {
keys_ks: &ts.keys_ks,
imported_ks: &ts.imported_ks,
internal_ks: &ts.internal_ks,
contexts_ks: &ts.contexts_ks,
acl_ks: &ts.acl_ks,
seed_store: &seed,
audit: &ts.audit,
auth: &auth,
};
let key_id = "room-northwind-signing";
let created = crate::operations::keys::create_key(
&ts.keys_ks,
&ts.internal_ks,
&ts.contexts_ks,
&seed,
&ts.audit,
&auth,
crate::operations::keys::CreateKeyParams {
key_type: vta_keys::KeyType::Ed25519,
internal: true,
derivation_path: None,
key_id: Some(key_id.into()),
mnemonic: None,
label: None,
context_id: None,
},
"test",
)
.await
.expect("mint the room's key");
let public = vta_sdk::did_key::decode_ed25519_public_key_multibase(&created.public_key)
.expect("the minted public key");
let room_did = format!(
"did:key:{}",
vta_sdk::did_key::ed25519_multibase_pubkey(&public)
);
let mut vic = dtg_credentials::DTGCredential::new_vic(
room_did.clone(),
"did:key:zInvitee".into(),
Utc::now(),
None,
)
.with_id("urn:uuid:11111111-1111-4111-8111-111111111111");
let (serialised, id) = sign_as_room(ctx(), key_id, &room_did, &mut vic)
.await
.expect("sign as the room");
assert_eq!(id, "urn:uuid:11111111-1111-4111-8111-111111111111");
let parsed: serde_json::Value = serde_json::from_str(&serialised).expect("parse");
assert_eq!(
parsed["proof"]["verificationMethod"],
serde_json::json!(format!("{room_did}#key-1")),
"the proof must name the room's own verification method"
);
assert_eq!(parsed["proof"]["cryptosuite"], "eddsa-jcs-2022");
assert!(
parsed["proof"]["proofValue"]
.as_str()
.is_some_and(|v| !v.is_empty()),
"a proof without a value is not a signature"
);
}
#[tokio::test]
async fn a_key_the_caller_may_not_name_is_refused() {
let ts = crate::test_support::open_test_store().await;
let seed: std::sync::Arc<dyn SeedStore> =
std::sync::Arc::new(crate::test_support::TestSeedStore(vec![7u8; 32]));
let auth = crate::test_support::super_admin_claims();
let ctx = || SigningContext {
keys_ks: &ts.keys_ks,
imported_ks: &ts.imported_ks,
internal_ks: &ts.internal_ks,
contexts_ks: &ts.contexts_ks,
acl_ks: &ts.acl_ks,
seed_store: &seed,
audit: &ts.audit,
auth: &auth,
};
let err = sign_as_room(
ctx(),
"a-key-that-does-not-exist",
"did:key:zRoom",
&mut dtg_credentials::DTGCredential::new_vic(
"did:key:zRoom".into(),
"did:key:zInvitee".into(),
Utc::now(),
None,
),
)
.await
.expect_err("an unknown key must not sign");
assert!(
format!("{err}").contains("not found"),
"expected the oracle's own refusal, got: {err}"
);
}
}