Skip to main content

vta_sdk/protocols/key_management/
import.rs

1//! `keys/import/0.1` — hand an externally-created private key to the VTA.
2
3use serde::{Deserialize, Serialize};
4
5use crate::keys::KeyType;
6
7/// Request payload for canonical `keys/import/0.1`.
8///
9/// Exactly one carrier member conveys the key. The choice between them is a
10/// **confidentiality decision, not a formatting one**: `private_key_sealed`
11/// and `private_key_jwe` encrypt the material to the VTA, so it is opaque to
12/// every intermediary and to the transport itself, while
13/// `private_key_multibase` is cleartext and safe only where the transport is
14/// end-to-end confidential.
15///
16/// The trust-task dispatcher **refuses the cleartext carrier outright**, because
17/// one dispatcher serves REST, DIDComm and TSP and cannot tell which one carried
18/// a given request. The legacy `key-management/1.0/import-key` DIDComm message
19/// still accepts it, where authcrypt has already established that guarantee.
20#[derive(Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
23pub struct ImportKeyBody {
24    #[serde(alias = "key_type")]
25    pub key_type: KeyType,
26    /// Armored sealed-transfer bundle carrying the private key, encrypted to
27    /// the VTA. The carrier to prefer.
28    #[serde(
29        default,
30        alias = "private_key_sealed",
31        skip_serializing_if = "Option::is_none"
32    )]
33    pub private_key_sealed: Option<String>,
34    /// JWE compact serialization of the private key, encrypted to the VTA.
35    #[serde(
36        default,
37        alias = "private_key_jwe",
38        skip_serializing_if = "Option::is_none"
39    )]
40    pub private_key_jwe: Option<String>,
41    /// Raw multibase-encoded private key — **cleartext**. Refused by the
42    /// trust-task dispatcher; see the type's documentation.
43    #[serde(
44        default,
45        alias = "private_key_multibase",
46        skip_serializing_if = "Option::is_none"
47    )]
48    pub private_key_multibase: Option<String>,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub label: Option<String>,
51    #[serde(default, alias = "context_id", skip_serializing_if = "Option::is_none")]
52    pub context_id: Option<String>,
53}
54
55// Manual Debug — every carrier field is (or decrypts to) the private key being
56// imported. Redact via `{:?}` so no tracing call site or panic-with-debug can
57// leak it. Serialize is unchanged.
58impl std::fmt::Debug for ImportKeyBody {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.debug_struct("ImportKeyBody")
61            .field("key_type", &self.key_type)
62            .field(
63                "private_key_sealed",
64                &self.private_key_sealed.as_ref().map(|_| "<redacted>"),
65            )
66            .field(
67                "private_key_jwe",
68                &self.private_key_jwe.as_ref().map(|_| "<redacted>"),
69            )
70            .field(
71                "private_key_multibase",
72                &self.private_key_multibase.as_ref().map(|_| "<redacted>"),
73            )
74            .field("label", &self.label)
75            .field("context_id", &self.context_id)
76            .finish()
77    }
78}
79
80/// `keys/import/0.1` response — the realized record under `key`, exactly as
81/// `keys/create` answers.
82pub use super::create::CreateKeyResponseBody as ImportKeyResponseBody;
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    /// The canonical wire form is camelCase; the pre-fold snake_case spellings
89    /// keep deserializing so a producer written against them is not broken by
90    /// the fold.
91    #[test]
92    fn accepts_both_spellings_on_intake() {
93        let canonical = serde_json::json!({
94            "keyType": "ed25519",
95            "privateKeySealed": "-----BEGIN SEALED TRANSFER-----",
96            "contextId": "app"
97        });
98        let body: ImportKeyBody = serde_json::from_value(canonical).unwrap();
99        assert_eq!(body.context_id.as_deref(), Some("app"));
100        assert!(body.private_key_sealed.is_some());
101
102        let legacy = serde_json::json!({
103            "key_type": "ed25519",
104            "private_key_sealed": "-----BEGIN SEALED TRANSFER-----",
105            "context_id": "app"
106        });
107        let body: ImportKeyBody = serde_json::from_value(legacy).unwrap();
108        assert_eq!(body.context_id.as_deref(), Some("app"));
109    }
110
111    /// Emission is canonical regardless of which spelling came in.
112    #[test]
113    fn emits_camel_case() {
114        let body = ImportKeyBody {
115            key_type: KeyType::Ed25519,
116            private_key_sealed: Some("sealed".into()),
117            private_key_jwe: None,
118            private_key_multibase: None,
119            label: None,
120            context_id: Some("app".into()),
121        };
122        let v = serde_json::to_value(&body).unwrap();
123        assert!(v.get("keyType").is_some(), "{v}");
124        assert!(v.get("privateKeySealed").is_some(), "{v}");
125        assert!(v.get("key_type").is_none(), "{v}");
126    }
127
128    /// The redacting Debug is the only thing standing between a private key and
129    /// a log line, so it is pinned rather than assumed.
130    #[test]
131    fn debug_redacts_every_carrier() {
132        let body = ImportKeyBody {
133            key_type: KeyType::Ed25519,
134            private_key_sealed: Some("SEALED-SECRET".into()),
135            private_key_jwe: Some("JWE-SECRET".into()),
136            private_key_multibase: Some("z-MULTIBASE-SECRET".into()),
137            label: None,
138            context_id: None,
139        };
140        let rendered = format!("{body:?}");
141        assert!(!rendered.contains("SEALED-SECRET"), "{rendered}");
142        assert!(!rendered.contains("JWE-SECRET"), "{rendered}");
143        assert!(!rendered.contains("MULTIBASE-SECRET"), "{rendered}");
144        assert!(rendered.contains("<redacted>"), "{rendered}");
145    }
146}