vta-sdk 0.21.2

SDK for Verifiable Trust Agents operating in Verifiable Trust Communities
Documentation
//! Key + seed management methods on [`VtaClient`].

use super::{
    CreateKeyRequest, CreateKeyResponse, GetKeySecretResponse, ImportKeyRequest, ImportKeyResponse,
    InvalidateKeyResponse, ListKeysResponse, ListSeedsResponse, RenameKeyRequest,
    RenameKeyResponse, RotateSeedRequest, RotateSeedResponse, SignResponse, Transport, VtaClient,
    WrappingKeyResponse, encode_path_segment,
};
use crate::error::VtaError;
use crate::keys::{KeyRecord, KeyType};
use crate::protocols::key_management::derive_and_sign::DeriveAndSignResultBody;
use crate::protocols::key_management::derive_and_sign_document::DeriveAndSignDocumentResultBody;
use crate::protocols::key_management::sign::SignAlgorithm;
use crate::trust_tasks;

#[cfg(feature = "client")]
use crate::protocols::key_management;

#[cfg(feature = "client")]
impl VtaClient {
    // ── Key methods ─────────────────────────────────────────────────

    /// Create a key.
    ///
    /// Trust-task leg note: `spec/vta/keys/create/1.0` auto-generates the
    /// key id from the derivation path, so an explicit `req.key_id` only
    /// takes effect on the REST leg — exactly as it did on the legacy
    /// DIDComm message, which never carried `key_id` either.
    pub async fn create_key(&self, req: CreateKeyRequest) -> Result<CreateKeyResponse, VtaError> {
        // Built from the canonical body rather than a hand-rolled map: the map
        // spelled its members snake_case and carried `mnemonic` before the
        // registry had a member for it, so it was one rename away from
        // silently dropping the create-from-a-phrase path (see #884's
        // `update_acl`, the same failure with different members).
        let body = crate::protocols::key_management::create::CreateKeyBody {
            key_type: req.key_type.clone(),
            derivation_path: req.derivation_path.clone().unwrap_or_default(),
            mnemonic: req.mnemonic.clone(),
            label: req.label.clone(),
            context_id: req.context_id.clone(),
        };
        let wrapped: crate::protocols::key_management::create::CreateKeyResponseBody = self
            .rpc_tt(
                trust_tasks::TASK_KEYS_CREATE_0_1,
                serde_json::to_value(&body)?,
                30,
                |c, url| c.post(format!("{url}/keys")).json(&req),
            )
            .await?;
        let key = wrapped.key;
        Ok(CreateKeyResponse {
            key_id: key.key_id,
            key_type: key.key_type,
            derivation_path: key.derivation_path,
            public_key: key.public_key,
            status: key.status,
            label: key.label,
            created_at: key.created_at,
        })
    }

    /// Import an externally-created private key.
    ///
    /// A **sealed** or **JWE** carrier rides canonical `keys/import/0.1`, so it
    /// works over REST, DIDComm and TSP alike. A raw `private_key_multibase`
    /// stays on the legacy DIDComm message: the canonical task refuses
    /// cleartext, because one dispatcher serves all three transports and cannot
    /// establish that a given request travelled end to end — authcrypt can, and
    /// that is exactly what the legacy path has.
    pub async fn import_key(&self, req: ImportKeyRequest) -> Result<ImportKeyResponse, VtaError> {
        if req.private_key_multibase.is_some() {
            return self
                .rpc(
                    key_management::IMPORT_KEY,
                    serde_json::to_value(&req)?,
                    key_management::IMPORT_KEY_RESULT,
                    30,
                    |c, url| c.post(format!("{url}/keys/import")).json(&req),
                )
                .await;
        }
        let body = crate::protocols::key_management::import::ImportKeyBody {
            key_type: req.key_type.clone(),
            private_key_sealed: req.private_key_sealed.clone(),
            private_key_jwe: req.private_key_jwe.clone(),
            private_key_multibase: None,
            label: req.label.clone(),
            context_id: req.context_id.clone(),
        };
        let wrapped: crate::protocols::key_management::create::CreateKeyResponseBody = self
            .rpc_tt(
                trust_tasks::TASK_KEYS_IMPORT_0_1,
                serde_json::to_value(&body)?,
                30,
                |c, url| c.post(format!("{url}/keys/import")).json(&req),
            )
            .await?;
        Ok(ImportKeyResponse {
            key_id: wrapped.key.key_id,
            key_type: wrapped.key.key_type,
            public_key: wrapped.key.public_key,
            status: wrapped.key.status,
            label: wrapped.key.label,
            origin: wrapped.key.origin,
            created_at: wrapped.key.created_at,
        })
    }

    pub async fn list_keys(
        &self,
        offset: u64,
        limit: u64,
        status: Option<&str>,
        context_id: Option<&str>,
    ) -> Result<ListKeysResponse, VtaError> {
        self.rpc_tt(
            trust_tasks::TASK_KEYS_LIST_0_1,
            serde_json::to_value(crate::protocols::key_management::list::ListKeysBody {
                offset: Some(offset),
                limit: Some(limit),
                status: status
                    .map(str::to_string)
                    .and_then(|s| serde_json::from_value(serde_json::Value::String(s)).ok()),
                context_id: context_id.map(str::to_string),
            })?,
            30,
            |c, url| {
                let mut u = format!("{url}/keys?offset={offset}&limit={limit}");
                if let Some(s) = status {
                    u.push_str(&format!("&status={s}"));
                }
                if let Some(ctx) = context_id {
                    u.push_str(&format!("&context_id={ctx}"));
                }
                c.get(u)
            },
        )
        .await
    }

    pub async fn get_key(&self, key_id: &str) -> Result<KeyRecord, VtaError> {
        // Canonical `keys/show/0.1` answers `{ key }`, with `key: null` for a
        // key the maintainer does not hold — a successful answer, not an error.
        // This method promises a record, so absence becomes `NotFound` here
        // rather than a decode failure the caller cannot interpret.
        let wrapped: crate::protocols::key_management::get::GetKeyResponseBody = self
            .rpc_tt(
                trust_tasks::TASK_KEYS_SHOW_0_1,
                serde_json::json!({ "keyId": key_id }),
                30,
                |c, url| c.get(format!("{url}/keys/{}", encode_path_segment(key_id))),
            )
            .await?;
        wrapped
            .key
            .ok_or_else(|| VtaError::NotFound(format!("no key record for `{key_id}`")))
    }

    /// Export a key's secret material. The trust-task twin lives in the
    /// seeds slice (`spec/vta/seeds/export-mnemonic/1.0`) — same
    /// `{ key_id }` request and the same
    /// `operations::keys::get_key_secret` spine as the legacy message.
    pub async fn get_key_secret(&self, key_id: &str) -> Result<GetKeySecretResponse, VtaError> {
        self.rpc_tt(
            trust_tasks::TASK_SEEDS_EXPORT_MNEMONIC_1_0,
            serde_json::json!({ "key_id": key_id }),
            30,
            |c, url| c.get(format!("{url}/keys/{}/secret", encode_path_segment(key_id))),
        )
        .await
    }

    /// Sign a payload using a VTA-managed key.
    ///
    /// Sends the base64url-encoded payload to the VTA, which derives the key,
    /// signs in memory, and returns the signature. Key material never leaves VTA.
    pub async fn sign(
        &self,
        key_id: &str,
        payload: &[u8],
        algorithm: SignAlgorithm,
    ) -> Result<SignResponse, VtaError> {
        use base64::Engine;
        let payload_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(payload);
        self.rpc_tt(
            trust_tasks::TASK_KEYS_SIGN_0_1,
            serde_json::json!({
                "keyId": key_id,
                "payload": payload_b64,
                "algorithm": algorithm,
            }),
            30,
            |c, url| {
                c.post(format!("{url}/keys/{}/sign", encode_path_segment(key_id)))
                    .json(&serde_json::json!({
                        "payload": payload_b64,
                        "algorithm": algorithm,
                    }))
            },
        )
        .await
    }

    /// Ephemerally derive a key at `derivation_path` and sign `payload` —
    /// **without persisting a key record**. Admin-only on the VTA. Returns the
    /// derived public key + signature.
    ///
    /// This is how a client (e.g. a fleet manager whose fleet seed *is* this
    /// VTA's seed) acts as a derived child identity — e.g. a per-VTA super-admin
    /// at `m/26'/9'/<idx>'` — so the seed never leaves the VTA. REST:
    /// `POST /keys/derive-and-sign`; DIDComm: the `keys/derive-and-sign/1.0`
    /// trust task.
    pub async fn derive_and_sign(
        &self,
        key_type: KeyType,
        derivation_path: &str,
        payload: &[u8],
        algorithm: SignAlgorithm,
    ) -> Result<DeriveAndSignResultBody, VtaError> {
        use base64::Engine;
        let payload_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(payload);
        let body = serde_json::json!({
            "keyType": serde_json::to_value(&key_type)?,
            "derivationPath": derivation_path,
            "payload": payload_b64,
            "algorithm": algorithm,
        });
        self.rpc_tt(
            trust_tasks::TASK_KEYS_DERIVE_AND_SIGN_0_1,
            body.clone(),
            30,
            move |c, url| c.post(format!("{url}/keys/derive-and-sign")).json(&body),
        )
        .await
    }

    /// Derive a key at `derivation_path` and attach an `eddsa-jcs-2022`
    /// Data-Integrity proof to `document`, signed **as the derived key** —
    /// persisting no key record. Admin-only. Returns the signer `did:key` + the
    /// signed document. This is how a fleet manager has its fleet VTA sign an
    /// auth document as a per-VTA super-admin without the seed leaving the VTA.
    pub async fn derive_and_sign_document(
        &self,
        key_type: KeyType,
        derivation_path: &str,
        document: serde_json::Value,
        proof_purpose: Option<&str>,
    ) -> Result<DeriveAndSignDocumentResultBody, VtaError> {
        let body = serde_json::json!({
            "keyType": serde_json::to_value(&key_type)?,
            "derivationPath": derivation_path,
            "document": document,
            "proofPurpose": proof_purpose,
        });
        self.rpc_tt(
            trust_tasks::TASK_KEYS_DERIVE_AND_SIGN_DOCUMENT_0_1,
            body.clone(),
            30,
            move |c, url| {
                c.post(format!("{url}/keys/derive-and-sign-document"))
                    .json(&body)
            },
        )
        .await
    }

    pub async fn invalidate_key(&self, key_id: &str) -> Result<InvalidateKeyResponse, VtaError> {
        self.rpc_tt(
            trust_tasks::TASK_KEYS_REVOKE_0_1,
            serde_json::json!({ "keyId": key_id }),
            30,
            |c, url| c.delete(format!("{url}/keys/{}", encode_path_segment(key_id))),
        )
        .await
    }

    pub async fn rename_key(
        &self,
        key_id: &str,
        new_key_id: &str,
    ) -> Result<RenameKeyResponse, VtaError> {
        self.rpc_tt(
            trust_tasks::TASK_KEYS_RENAME_0_1,
            serde_json::json!({ "keyId": key_id, "newKeyId": new_key_id }),
            30,
            |c, url| {
                c.patch(format!("{url}/keys/{}", encode_path_segment(key_id)))
                    .json(&RenameKeyRequest {
                        key_id: new_key_id.to_string(),
                    })
            },
        )
        .await
    }

    // ── Import key methods ──────────────────────────────────────────

    /// Fetch an ephemeral wrapping key for REST key import.
    pub async fn get_wrapping_key(&self) -> Result<WrappingKeyResponse, VtaError> {
        match &self.transport {
            Transport::Rest {
                client,
                base_url,
                auth,
            } => {
                Self::ensure_token_valid(client, base_url, auth).await?;
                let token = auth.lock().await.token.clone();
                let req = client.get(format!("{base_url}/keys/import/wrapping-key"));
                let resp = Self::with_auth_token(req, &token).send().await?;
                Self::handle_response(resp).await
            }
            #[cfg(feature = "session")]
            Transport::DIDComm { .. } => Err(VtaError::UnsupportedTransport(
                "wrapping key not needed for DIDComm transport".into(),
            )),
            #[cfg(feature = "tsp")]
            Transport::Tsp { .. } => Err(VtaError::UnsupportedTransport(
                "wrapping key not needed for TSP transport".into(),
            )),
        }
    }

    // ── Seed methods ────────────────────────────────────────────────

    pub async fn list_seeds(&self) -> Result<ListSeedsResponse, VtaError> {
        self.rpc_tt(
            trust_tasks::TASK_SEEDS_LIST_1_0,
            serde_json::json!({}),
            30,
            |c, url| c.get(format!("{url}/keys/seeds")),
        )
        .await
    }

    pub async fn rotate_seed(
        &self,
        mnemonic: Option<String>,
    ) -> Result<RotateSeedResponse, VtaError> {
        let body = RotateSeedRequest {
            mnemonic: mnemonic.clone(),
        };
        self.rpc_tt(
            trust_tasks::TASK_SEEDS_ROTATE_1_0,
            serde_json::json!({ "mnemonic": mnemonic }),
            30,
            |c, url| c.post(format!("{url}/keys/seeds/rotate")).json(&body),
        )
        .await
    }
}