Skip to main content

opentdf_protocol/
kas.rs

1//! KAS (Key Access Service) protocol types
2//!
3//! This module contains the data structures used in the KAS v2 rewrap protocol.
4//! These types define the request/response format for key unwrapping operations.
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9use crate::manifest::{KeyAccess, PolicyBinding};
10
11/// KAS client errors
12#[derive(Debug, Error)]
13pub enum KasError {
14    #[error("HTTP error: {status} - {message}")]
15    HttpError { status: u16, message: String },
16
17    #[error("Access denied for resource '{resource}': {reason}")]
18    AccessDenied { resource: String, reason: String },
19
20    #[error("Authentication failed: {reason}")]
21    AuthenticationFailed { reason: String },
22
23    #[error("Invalid response from KAS: {reason}")]
24    InvalidResponse {
25        reason: String,
26        expected: Option<String>,
27    },
28
29    #[error("Key unwrapping failed for algorithm '{algorithm}': {reason}")]
30    UnwrapError { algorithm: String, reason: String },
31
32    #[error("Cryptographic error: {operation} failed - {reason}")]
33    CryptoError { operation: String, reason: String },
34
35    #[error("JSON serialization error: {0}")]
36    SerializationError(#[from] serde_json::Error),
37
38    #[error("Base64 decode error: {0}")]
39    Base64Error(#[from] base64::DecodeError),
40
41    #[error("JWT error: {operation} - {reason}")]
42    JwtError { operation: String, reason: String },
43
44    #[error("PKCS8 error: {0}")]
45    Pkcs8Error(String),
46
47    #[error("HTTP request failed: {method} {url} - {reason}")]
48    RequestError {
49        method: String,
50        url: String,
51        reason: String,
52    },
53
54    #[error("Network timeout after {timeout_ms}ms")]
55    Timeout { timeout_ms: u64 },
56
57    #[error("Invalid KAS configuration: {reason}")]
58    ConfigError { reason: String },
59
60    #[error("Invalid KAS URL: {0}")]
61    InvalidUrl(String),
62}
63
64impl KasError {
65    /// Returns true if this error might be resolved by retrying the operation
66    pub fn is_retryable(&self) -> bool {
67        matches!(
68            self,
69            KasError::HttpError { status, .. } if *status >= 500 && *status < 600,
70        ) || matches!(self, KasError::Timeout { .. })
71    }
72
73    /// Returns a suggestion for how to fix this error, if available
74    pub fn suggestion(&self) -> Option<&str> {
75        match self {
76            KasError::AuthenticationFailed { .. } => {
77                Some("Verify OAuth token is valid and not expired")
78            }
79            KasError::AccessDenied { .. } => {
80                Some("Check that the user has permissions for this resource")
81            }
82            KasError::Timeout { .. } => {
83                Some("Check network connectivity or increase timeout value")
84            }
85            KasError::InvalidResponse { .. } => Some("Verify KAS server version compatibility"),
86            KasError::InvalidUrl(_) => {
87                Some("Use an HTTPS URL for the KAS endpoint (HTTP allowed only for localhost)")
88            }
89            _ => None,
90        }
91    }
92
93    /// Returns an error code for programmatic error handling
94    pub fn error_code(&self) -> &'static str {
95        match self {
96            KasError::HttpError { .. } => "HTTP_ERROR",
97            KasError::AccessDenied { .. } => "ACCESS_DENIED",
98            KasError::AuthenticationFailed { .. } => "AUTHENTICATION_FAILED",
99            KasError::InvalidResponse { .. } => "INVALID_RESPONSE",
100            KasError::UnwrapError { .. } => "UNWRAP_ERROR",
101            KasError::CryptoError { .. } => "CRYPTO_ERROR",
102            KasError::SerializationError(_) => "SERIALIZATION_ERROR",
103            KasError::Base64Error(_) => "BASE64_ERROR",
104            KasError::JwtError { .. } => "JWT_ERROR",
105            KasError::Pkcs8Error(_) => "PKCS8_ERROR",
106            KasError::RequestError { .. } => "REQUEST_ERROR",
107            KasError::Timeout { .. } => "TIMEOUT",
108            KasError::ConfigError { .. } => "CONFIG_ERROR",
109            KasError::InvalidUrl(_) => "INVALID_URL",
110        }
111    }
112}
113
114/// Unsigned rewrap request structure (before JWT signing)
115#[derive(Debug, Serialize, Deserialize)]
116pub struct UnsignedRewrapRequest {
117    #[serde(rename = "clientPublicKey")]
118    pub client_public_key: String,
119    pub requests: Vec<PolicyRequest>,
120}
121
122/// Individual policy request entry
123#[derive(Debug, Serialize, Deserialize)]
124pub struct PolicyRequest {
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub algorithm: Option<String>,
127    pub policy: Policy,
128    #[serde(rename = "keyAccessObjects")]
129    pub key_access_objects: Vec<KeyAccessObjectWrapper>,
130}
131
132/// Policy structure for KAS requests
133#[derive(Debug, Serialize, Deserialize)]
134pub struct Policy {
135    pub id: String,
136    pub body: String, // Base64-encoded policy JSON
137}
138
139/// Key access object wrapper
140#[derive(Debug, Serialize, Deserialize)]
141pub struct KeyAccessObjectWrapper {
142    #[serde(rename = "keyAccessObjectId")]
143    pub key_access_object_id: String,
144    #[serde(rename = "keyAccessObject")]
145    pub key_access_object: KeyAccessObject,
146}
147
148/// Key access object details
149#[derive(Debug, Serialize, Deserialize)]
150pub struct KeyAccessObject {
151    #[serde(rename = "type")]
152    pub key_type: String,
153    pub url: String,
154    pub protocol: String,
155    #[serde(rename = "wrappedKey")]
156    pub wrapped_key: String,
157    #[serde(rename = "policyBinding")]
158    pub policy_binding: KasPolicyBinding,
159    #[serde(rename = "encryptedMetadata", skip_serializing_if = "Option::is_none")]
160    pub encrypted_metadata: Option<String>,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub kid: Option<String>,
163    /// NanoTDF header bytes (base64 encoded) - used for NanoTDF rewrap requests
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub header: Option<String>,
166    /// Ephemeral public key for EC-wrapped keys (PEM format)
167    #[serde(rename = "ephemeralPublicKey", skip_serializing_if = "Option::is_none")]
168    pub ephemeral_public_key: Option<String>,
169}
170
171/// Algorithm types for KAS rewrap requests
172pub mod algorithm {
173    /// RSA-2048 algorithm for standard key wrapping
174    pub const RSA_2048: &str = "rsa:2048";
175    /// EC P-256 algorithm for ECIES key wrapping
176    pub const EC_P256: &str = "ec:secp256r1";
177    /// EC P-384 algorithm for ECIES key wrapping
178    pub const EC_P384: &str = "ec:secp384r1";
179    /// EC P-521 algorithm for ECIES key wrapping
180    pub const EC_P521: &str = "ec:secp521r1";
181}
182
183impl KeyAccessObject {
184    /// Detect the appropriate algorithm based on key type
185    pub fn detect_algorithm(&self) -> &'static str {
186        use crate::manifest::key_access_type;
187        if self.key_type == key_access_type::EC_WRAPPED {
188            algorithm::EC_P256 // Default to P-256 for EC wrapping
189        } else {
190            algorithm::RSA_2048
191        }
192    }
193
194    /// Check if this key access uses EC wrapping
195    pub fn is_ec_wrapped(&self) -> bool {
196        use crate::manifest::key_access_type;
197        self.key_type == key_access_type::EC_WRAPPED
198    }
199
200    /// Check if this key access uses RSA wrapping
201    pub fn is_rsa_wrapped(&self) -> bool {
202        use crate::manifest::key_access_type;
203        self.key_type == key_access_type::WRAPPED
204    }
205}
206
207/// Policy binding for KAS requests
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct KasPolicyBinding {
210    pub hash: String,
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub algorithm: Option<String>,
213}
214
215/// Signed rewrap request wrapper
216///
217/// The `signed_request_token` field contains a complete JWT token.
218/// Applications are responsible for creating this JWT externally.
219/// See the examples directory for JWT creation patterns.
220#[derive(Debug, Serialize, Deserialize)]
221pub struct SignedRewrapRequest {
222    #[serde(rename = "signedRequestToken")]
223    pub signed_request_token: String,
224}
225
226/// Rewrap response structure
227#[derive(Debug, Serialize, Deserialize)]
228#[non_exhaustive]
229pub struct RewrapResponse {
230    pub responses: Vec<PolicyRewrapResult>,
231    #[serde(rename = "sessionPublicKey")]
232    pub session_public_key: Option<String>,
233}
234
235/// Policy rewrap result
236#[derive(Debug, Serialize, Deserialize)]
237#[non_exhaustive]
238pub struct PolicyRewrapResult {
239    #[serde(rename = "policyId")]
240    pub policy_id: String,
241    pub results: Vec<KeyAccessRewrapResult>,
242}
243
244/// Individual key access rewrap result
245#[derive(Debug, Serialize, Deserialize)]
246#[non_exhaustive]
247pub struct KeyAccessRewrapResult {
248    #[serde(rename = "keyAccessObjectId")]
249    pub key_access_object_id: String,
250    pub status: String,
251    #[serde(rename = "kasWrappedKey")]
252    pub kas_wrapped_key: Option<String>,
253    #[serde(rename = "entityWrappedKey")]
254    pub entity_wrapped_key: Option<String>, // Legacy field
255    pub error: Option<String>,
256}
257
258// ============================================================================
259// Type Conversions between manifest types and KAS protocol types
260// ============================================================================
261
262impl From<PolicyBinding> for KasPolicyBinding {
263    fn from(pb: PolicyBinding) -> Self {
264        Self {
265            hash: pb.hash,
266            algorithm: Some(pb.alg),
267        }
268    }
269}
270
271impl From<KasPolicyBinding> for PolicyBinding {
272    fn from(kpb: KasPolicyBinding) -> Self {
273        Self {
274            alg: kpb.algorithm.unwrap_or_else(|| "HS256".to_string()),
275            hash: kpb.hash,
276        }
277    }
278}
279
280impl From<&KeyAccess> for KeyAccessObject {
281    fn from(ka: &KeyAccess) -> Self {
282        Self {
283            key_type: ka.access_type.clone(),
284            url: ka.url.clone(),
285            protocol: ka.protocol.clone(),
286            wrapped_key: ka.wrapped_key.clone(),
287            policy_binding: ka.policy_binding.clone().into(),
288            encrypted_metadata: ka.encrypted_metadata.clone(),
289            kid: ka.kid.clone(),
290            header: None,
291            ephemeral_public_key: ka.ephemeral_public_key.clone(),
292        }
293    }
294}
295
296impl From<KeyAccess> for KeyAccessObject {
297    fn from(ka: KeyAccess) -> Self {
298        Self {
299            key_type: ka.access_type,
300            url: ka.url,
301            protocol: ka.protocol,
302            wrapped_key: ka.wrapped_key,
303            policy_binding: ka.policy_binding.into(),
304            encrypted_metadata: ka.encrypted_metadata,
305            kid: ka.kid,
306            header: None,
307            ephemeral_public_key: ka.ephemeral_public_key,
308        }
309    }
310}
311
312impl From<&KeyAccessObject> for KeyAccess {
313    fn from(kao: &KeyAccessObject) -> Self {
314        Self {
315            access_type: kao.key_type.clone(),
316            url: kao.url.clone(),
317            kid: kao.kid.clone(),
318            // KeyAccessObject (KAS rewrap wire type) carries no split id.
319            sid: None,
320            protocol: kao.protocol.clone(),
321            wrapped_key: kao.wrapped_key.clone(),
322            policy_binding: kao.policy_binding.clone().into(),
323            encrypted_metadata: kao.encrypted_metadata.clone(),
324            schema_version: Some("1.0".to_string()),
325            ephemeral_public_key: kao.ephemeral_public_key.clone(),
326        }
327    }
328}
329
330impl From<KeyAccessObject> for KeyAccess {
331    fn from(kao: KeyAccessObject) -> Self {
332        Self {
333            access_type: kao.key_type,
334            url: kao.url,
335            kid: kao.kid,
336            // KeyAccessObject (KAS rewrap wire type) carries no split id.
337            sid: None,
338            protocol: kao.protocol,
339            wrapped_key: kao.wrapped_key,
340            policy_binding: kao.policy_binding.into(),
341            encrypted_metadata: kao.encrypted_metadata,
342            schema_version: Some("1.0".to_string()),
343            ephemeral_public_key: kao.ephemeral_public_key,
344        }
345    }
346}