Skip to main content

cryptotensors/
key.rs

1// Copyright 2025-2026 aiyah-meloken
2// SPDX-License-Identifier: Apache-2.0
3//
4// This file is part of CryptoTensors, a derivative work based on safetensors.
5// This file is NEW and was not present in the original safetensors project.
6//
7// KeyMaterial is a pure data structure for JWK key storage.
8// Key loading is delegated to the registry module via KeyProvider implementations.
9
10use crate::cryptotensors::CryptoTensorsError;
11use crate::encryption::EncryptionAlgorithm;
12use crate::registry;
13use crate::signing::SignatureAlgorithm;
14use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
15use once_cell::sync::OnceCell;
16use ring::rand::{self, SecureRandom};
17use ring::signature::{Ed25519KeyPair, KeyPair};
18use serde::{de::Error, Deserialize, Deserializer, Serialize};
19
20/// Validation mode for Key Material
21///
22/// Different validation modes apply different rules:
23/// - `ForCreation`: Used when creating a new key (requires full key material + algorithm)
24/// - `FromHeader`: Used when loading from header (requires algorithm, key material can be empty)
25/// - `FromJwk`: Used when loading from JWK (requires key material, algorithm is optional)
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ValidateMode {
28    /// Validate for key creation (requires full key material and algorithm)
29    ForCreation,
30    /// Validate when loading from header (requires algorithm, key material can be empty)
31    FromHeader,
32    /// Validate when loading from JWK (requires key material, algorithm is optional)
33    FromJwk,
34}
35
36/// JSON Web Key (JWK) type
37#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum JwkKeyType {
40    /// Symmetric key type
41    Oct,
42    /// Asymmetric key type
43    Okp,
44}
45
46/// Serialize and deserialize OnceCell<Option<String>>
47mod key_material_serde {
48    use super::*;
49
50    pub fn deserialize<'de, D>(deserializer: D) -> Result<OnceCell<Option<String>>, D::Error>
51    where
52        D: Deserializer<'de>,
53    {
54        let value: Option<String> = Option::deserialize(deserializer)?;
55        let cell = OnceCell::new();
56        if let Some(v) = value {
57            cell.set(Some(v))
58                .map_err(|_| D::Error::custom("Failed to set OnceCell value"))?;
59        }
60        Ok(cell)
61    }
62}
63
64/// Key Material structure for managing cryptographic keys
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct KeyMaterial {
67    /// The key type (symmetric or asymmetric)
68    #[serde(rename = "kty")]
69    pub key_type: JwkKeyType,
70
71    /// The algorithm used for encryption or signing
72    pub alg: String,
73
74    /// Key identifier (kid)
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub kid: Option<String>,
77
78    /// The master key encoded in base64 for encryption
79    #[serde(skip_serializing, default)]
80    #[serde(with = "key_material_serde")]
81    pub k: OnceCell<Option<String>>,
82
83    /// The public key encoded in base64 for signing
84    #[serde(skip_serializing, default)]
85    #[serde(with = "key_material_serde")]
86    #[serde(rename = "x")]
87    pub x_pub: OnceCell<Option<String>>,
88
89    /// The private key encoded in base64 for signing
90    #[serde(skip_serializing, default)]
91    #[serde(with = "key_material_serde")]
92    #[serde(rename = "d")]
93    pub d_priv: OnceCell<Option<String>>,
94
95    /// JWK Set URL (jku)
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub jku: Option<String>,
98}
99
100impl KeyMaterial {
101    /// Create a new KeyMaterial
102    fn new_internal(
103        key_type: JwkKeyType,
104        alg: String,
105        kid: Option<String>,
106        jku: Option<String>,
107        k: Option<Vec<u8>>,
108        x_pub: Option<Vec<u8>>,
109        d_priv: Option<Vec<u8>>,
110    ) -> Result<Self, CryptoTensorsError> {
111        let key_material = Self {
112            key_type,
113            alg,
114            kid,
115            k: OnceCell::new(),
116            x_pub: OnceCell::new(),
117            d_priv: OnceCell::new(),
118            jku,
119        };
120
121        if let Some(k) = k {
122            key_material
123                .k
124                .set(Some(BASE64.encode(&k)))
125                .map_err(|_| CryptoTensorsError::KeyCreation("Failed to set key".to_string()))?;
126        }
127        if let Some(x_pub) = x_pub {
128            key_material
129                .x_pub
130                .set(Some(BASE64.encode(&x_pub)))
131                .map_err(|_| {
132                    CryptoTensorsError::KeyCreation("Failed to set public key".to_string())
133                })?;
134        }
135        if let Some(d_priv) = d_priv {
136            key_material
137                .d_priv
138                .set(Some(BASE64.encode(&d_priv)))
139                .map_err(|_| {
140                    CryptoTensorsError::KeyCreation("Failed to set private key".to_string())
141                })?;
142        }
143
144        Ok(key_material)
145    }
146
147    /// Load key from the registry
148    ///
149    /// This method retrieves the appropriate key from registered KeyProviders.
150    /// Users must register providers before calling this method.
151    ///
152    /// # Example
153    ///
154    /// ```no_run
155    /// use cryptotensors::key::KeyMaterial;
156    ///
157    /// fn example() -> Result<(), Box<dyn std::error::Error>> {
158    ///     // Set CRYPTOTENSOR_KEYS environment variable with JWK Set
159    ///     // EnvKeyProvider is automatically registered and enabled by default
160    ///     std::env::set_var("CRYPTOTENSOR_KEYS", r#"{
161    ///         "keys": [
162    ///             {
163    ///                 "kty": "oct",
164    ///                 "alg": "aes256gcm",
165    ///                 "kid": "my-enc-key",
166    ///                 "k": "dGVzdC1rZXktMzItYnl0ZXMtbG9uZy1lbmNyeXB0aW9u"
167    ///             },
168    ///             {
169    ///                 "kty": "okp",
170    ///                 "alg": "ed25519",
171    ///                 "kid": "my-sign-key",
172    ///                 "x": "dGVzdC1wdWJsaWMta2V5LTMyLWJ5dGVzLWxvbmctc2lnbmF0dXJl",
173    ///                 "d": "dGVzdC1wcml2YXRlLWtleS0zMi1ieXRlcy1sb25nLXNpZ25hdHVyZQ"
174    ///             }
175    ///         ]
176    ///     }"#);
177    ///
178    ///     // Create key material with kid to match the key in environment variable
179    ///     let key = KeyMaterial::new_enc_key(
180    ///         None,
181    ///         Some("aes256gcm".to_string()),
182    ///         Some("my-enc-key".to_string()),  // kid matches the key in CRYPTOTENSOR_KEYS
183    ///         None
184    ///     )?;
185    ///     key.load_key()?;  // Loads key from EnvKeyProvider
186    ///     Ok(())
187    /// }
188    /// ```
189    pub fn load_key(&self) -> Result<(), CryptoTensorsError> {
190        // Get key from registry based on key type
191        let jwk = match self.key_type {
192            JwkKeyType::Oct => registry::get_master_key(self.jku.as_deref(), self.kid.as_deref())?,
193            JwkKeyType::Okp => registry::get_verify_key(self.jku.as_deref(), self.kid.as_deref())?,
194        };
195
196        // Parse and validate the JWK
197        let key = Self::from_jwk(&jwk, false)?;
198
199        // Update this key material with the loaded key
200        self.update_from_key(&key)
201    }
202
203    /// Update this key material from another key material
204    pub(crate) fn update_from_key(&self, key: &KeyMaterial) -> Result<(), CryptoTensorsError> {
205        // Update key fields based on key type
206        match self.key_type {
207            JwkKeyType::Oct => {
208                if let Some(Some(k)) = key.k.get() {
209                    self.k
210                        .set(Some(k.clone()))
211                        .map_err(|_| CryptoTensorsError::MissingMasterKey)?;
212                } else {
213                    return Err(CryptoTensorsError::MissingMasterKey);
214                }
215            }
216            JwkKeyType::Okp => {
217                if let Some(Some(x_pub)) = key.x_pub.get() {
218                    self.x_pub
219                        .set(Some(x_pub.clone()))
220                        .map_err(|_| CryptoTensorsError::MissingVerificationKey)?;
221                } else {
222                    return Err(CryptoTensorsError::MissingVerificationKey);
223                }
224                // No need to validate private key as this is only used for verification
225            }
226        }
227
228        Ok(())
229    }
230
231    /// Validate the Key Material based on different scenarios
232    pub fn validate(&self, mode: ValidateMode) -> Result<(), CryptoTensorsError> {
233        // Validate key type
234        if self.key_type != JwkKeyType::Oct && self.key_type != JwkKeyType::Okp {
235            return Err(CryptoTensorsError::InvalidKey(
236                "Invalid key type".to_string(),
237            ));
238        }
239
240        // Validate algorithm based on mode
241        match mode {
242            ValidateMode::ForCreation | ValidateMode::FromHeader => {
243                if self.alg.is_empty() {
244                    return Err(CryptoTensorsError::InvalidAlgorithm(
245                        "Missing alg field".to_string(),
246                    ));
247                }
248            }
249            ValidateMode::FromJwk => {
250                // Algorithm is optional when loading from JWK
251            }
252        }
253
254        // Validate key existence based on mode and key type
255        match mode {
256            ValidateMode::ForCreation => {
257                match self.key_type {
258                    JwkKeyType::Oct => {
259                        if self.k.get().and_then(|k| k.as_ref()).is_none() {
260                            return Err(CryptoTensorsError::MissingMasterKey);
261                        }
262                    }
263                    JwkKeyType::Okp => {
264                        // For signing keys, both private and public keys are required
265                        // Private key is needed for signing, public key is needed for verification
266                        if self.d_priv.get().and_then(|k| k.as_ref()).is_none() {
267                            return Err(CryptoTensorsError::MissingSigningKey);
268                        }
269                        if self.x_pub.get().and_then(|k| k.as_ref()).is_none() {
270                            return Err(CryptoTensorsError::MissingVerificationKey);
271                        }
272                    }
273                }
274            }
275            ValidateMode::FromJwk => match self.key_type {
276                JwkKeyType::Oct => {
277                    if self.k.get().and_then(|k| k.as_ref()).is_none() {
278                        return Err(CryptoTensorsError::MissingMasterKey);
279                    }
280                }
281                JwkKeyType::Okp => {
282                    if self.x_pub.get().and_then(|k| k.as_ref()).is_none() {
283                        return Err(CryptoTensorsError::MissingVerificationKey);
284                    }
285                }
286            },
287            ValidateMode::FromHeader => {
288                // Keys are not required when loading from header (will be loaded from registry)
289            }
290        }
291
292        // Validate algorithm based on key type if algorithm is present
293        if !self.alg.is_empty() {
294            match self.key_type {
295                JwkKeyType::Oct => {
296                    if self.alg.parse::<EncryptionAlgorithm>().is_err() {
297                        return Err(CryptoTensorsError::InvalidAlgorithm(
298                            "Invalid encryption algorithm".to_string(),
299                        ));
300                    }
301                }
302                JwkKeyType::Okp => {
303                    if self.alg.parse::<SignatureAlgorithm>().is_err() {
304                        return Err(CryptoTensorsError::InvalidAlgorithm(
305                            "Invalid signature algorithm".to_string(),
306                        ));
307                    }
308                }
309            }
310        }
311
312        Ok(())
313    }
314
315    /// Create a new symmetric encryption key (kty=oct)
316    ///
317    /// # Arguments  
318    /// * `alg` - Optional algorithm name (default: "aes256gcm")
319    /// * `kid` - Optional key ID
320    /// * `jku` - Optional JWK URL
321    /// * `key_b64` - Optional base64-encoded key string
322    ///
323    /// # Returns
324    /// * `Ok(KeyMaterial)` - The generated key material
325    /// * `Err(CryptoTensorsError)` - If input is invalid
326    pub fn new_enc_key(
327        key_b64: Option<String>,
328        alg: Option<String>,
329        kid: Option<String>,
330        jku: Option<String>,
331    ) -> Result<Self, CryptoTensorsError> {
332        let alg = alg.unwrap_or_else(|| "aes256gcm".to_string());
333        let enc_alg = alg
334            .parse::<EncryptionAlgorithm>()
335            .map_err(|_| CryptoTensorsError::InvalidAlgorithm(alg.clone()))?;
336        let key_bytes = if let Some(ref b64_str) = key_b64 {
337            let bytes = BASE64.decode(b64_str).map_err(|e| {
338                CryptoTensorsError::InvalidKey(format!("Invalid base64 key: {}", e))
339            })?;
340            if bytes.len() != enc_alg.key_len() {
341                return Err(CryptoTensorsError::InvalidKeyLength {
342                    expected: enc_alg.key_len(),
343                    actual: bytes.len(),
344                });
345            }
346            bytes
347        } else {
348            // Generate random key
349            let mut key = vec![0u8; enc_alg.key_len()];
350            let rng = rand::SystemRandom::new();
351            rng.fill(&mut key)
352                .map_err(|e| CryptoTensorsError::RandomGeneration(e.to_string()))?;
353            key
354        };
355        KeyMaterial::new_internal(JwkKeyType::Oct, alg, kid, jku, Some(key_bytes), None, None)
356    }
357
358    /// Create a new signing key (kty=okp)
359    ///
360    /// # Arguments
361    /// * `alg` - Optional algorithm name (default: "ed25519")
362    /// * `kid` - Optional key ID
363    /// * `jku` - Optional JWK URL
364    /// * `public_b64` - Optional base64-encoded public key string
365    /// * `private_b64` - Optional base64-encoded private key string
366    ///
367    /// # Returns
368    /// * `Ok(KeyMaterial)` - The generated key material
369    /// * `Err(CryptoTensorsError)` - If input is invalid
370    pub fn new_sign_key(
371        public_b64: Option<String>,
372        private_b64: Option<String>,
373        alg: Option<String>,
374        kid: Option<String>,
375        jku: Option<String>,
376    ) -> Result<Self, CryptoTensorsError> {
377        let alg = alg.unwrap_or_else(|| "ed25519".to_string());
378        let sig_alg = alg
379            .parse::<SignatureAlgorithm>()
380            .map_err(|_| CryptoTensorsError::InvalidAlgorithm(alg.clone()))?;
381        match sig_alg {
382            SignatureAlgorithm::Ed25519 => {
383                let public = if let Some(pub_b64) = public_b64 {
384                    let pub_bytes = BASE64.decode(&pub_b64).map_err(|e| {
385                        CryptoTensorsError::InvalidKey(format!("Invalid base64 public key: {}", e))
386                    })?;
387                    if pub_bytes.len() != 32 {
388                        return Err(CryptoTensorsError::InvalidKeyLength {
389                            expected: 32,
390                            actual: pub_bytes.len(),
391                        });
392                    }
393                    Some(pub_bytes)
394                } else {
395                    None
396                };
397                let private = if let Some(priv_b64) = private_b64 {
398                    let priv_bytes = BASE64.decode(&priv_b64).map_err(|e| {
399                        CryptoTensorsError::InvalidKey(format!("Invalid base64 private key: {}", e))
400                    })?;
401                    if priv_bytes.len() != 32 {
402                        return Err(CryptoTensorsError::InvalidKeyLength {
403                            expected: 32,
404                            actual: priv_bytes.len(),
405                        });
406                    }
407                    Some(priv_bytes)
408                } else {
409                    None
410                };
411                // If both are None, generate new key pair
412                let (public, private) = if public.is_none() && private.is_none() {
413                    let rng = rand::SystemRandom::new();
414                    let mut private_key = [0u8; 32];
415                    rng.fill(&mut private_key)
416                        .map_err(|e| CryptoTensorsError::RandomGeneration(e.to_string()))?;
417                    let key_pair = Ed25519KeyPair::from_seed_unchecked(&private_key)
418                        .map_err(|e| CryptoTensorsError::KeyCreation(e.to_string()))?;
419                    (
420                        Some(key_pair.public_key().as_ref().to_vec()),
421                        Some(private_key.to_vec()),
422                    )
423                } else if public.is_some() && private.is_some() {
424                    // Try to verify the key pair
425                    Ed25519KeyPair::from_seed_and_public_key(
426                        private.clone().unwrap().as_slice(),
427                        public.clone().unwrap().as_slice(),
428                    )
429                    .map_err(|e| CryptoTensorsError::KeyCreation(e.to_string()))?;
430                    (public, private)
431                } else if private.is_some() && public.is_none() {
432                    // If only private key is provided, derive public key from it
433                    let private_key = private.clone().unwrap();
434                    let key_pair = Ed25519KeyPair::from_seed_unchecked(&private_key)
435                        .map_err(|e| CryptoTensorsError::KeyCreation(e.to_string()))?;
436                    (Some(key_pair.public_key().as_ref().to_vec()), private)
437                } else {
438                    // Only public key provided (verification-only key) - this is valid for verification
439                    (public, private)
440                };
441                KeyMaterial::new_internal(JwkKeyType::Okp, alg, kid, jku, None, public, private)
442            }
443        }
444    }
445
446    /// Convert this KeyMaterial to a JWK JSON string
447    ///
448    /// # Returns
449    /// * `Ok(String)` - JWK JSON string
450    /// * `Err(CryptoTensorsError)` - If serialization fails
451    pub fn to_jwk(&self) -> Result<String, CryptoTensorsError> {
452        // Only include JWK-relevant fields
453        #[derive(Serialize)]
454        struct JwkOut<'a> {
455            #[serde(rename = "kty")]
456            key_type: &'a JwkKeyType,
457            alg: &'a String,
458            #[serde(skip_serializing_if = "Option::is_none")]
459            kid: &'a Option<String>,
460            #[serde(skip_serializing_if = "Option::is_none")]
461            jku: &'a Option<String>,
462            #[serde(skip_serializing_if = "Option::is_none")]
463            k: Option<&'a String>,
464            #[serde(skip_serializing_if = "Option::is_none")]
465            #[serde(rename = "x")]
466            x_pub: Option<&'a String>,
467            #[serde(skip_serializing_if = "Option::is_none")]
468            #[serde(rename = "d")]
469            d_priv: Option<&'a String>,
470        }
471        let jwk = JwkOut {
472            key_type: &self.key_type,
473            alg: &self.alg,
474            kid: &self.kid,
475            jku: &self.jku,
476            k: self.k.get().and_then(|v| v.as_ref()),
477            x_pub: self.x_pub.get().and_then(|v| v.as_ref()),
478            d_priv: self.d_priv.get().and_then(|v| v.as_ref()),
479        };
480        serde_json::to_string(&jwk)
481            .map_err(|e| CryptoTensorsError::KeyCreation(format!("Failed to serialize JWK: {}", e)))
482    }
483
484    /// Get the master key as decoded bytes
485    ///
486    /// # Returns
487    /// * `Ok(Vec<u8>)` - The decoded master key bytes
488    /// * `Err(CryptoTensorsError::MissingMasterKey)` - If no master key is set
489    /// * `Err(CryptoTensorsError::InvalidKey)` - If base64 decoding fails
490    pub fn get_master_key_bytes(&self) -> Result<Vec<u8>, CryptoTensorsError> {
491        let k = self
492            .k
493            .get()
494            .and_then(|v| v.as_ref())
495            .ok_or(CryptoTensorsError::MissingMasterKey)?;
496        BASE64.decode(k).map_err(|e| {
497            CryptoTensorsError::InvalidKey(format!("Invalid base64 master key: {}", e))
498        })
499    }
500
501    /// Get the public key as decoded bytes
502    ///
503    /// # Returns
504    /// * `Ok(Vec<u8>)` - The decoded public key bytes
505    /// * `Err(CryptoTensorsError::MissingVerificationKey)` - If no public key is set
506    /// * `Err(CryptoTensorsError::InvalidKey)` - If base64 decoding fails
507    pub fn get_public_key_bytes(&self) -> Result<Vec<u8>, CryptoTensorsError> {
508        let x = self
509            .x_pub
510            .get()
511            .and_then(|v| v.as_ref())
512            .ok_or(CryptoTensorsError::MissingVerificationKey)?;
513        BASE64.decode(x).map_err(|e| {
514            CryptoTensorsError::InvalidKey(format!("Invalid base64 public key: {}", e))
515        })
516    }
517
518    /// Get the private key as decoded bytes
519    ///
520    /// # Returns
521    /// * `Ok(Vec<u8>)` - The decoded private key bytes
522    /// * `Err(CryptoTensorsError::MissingSigningKey)` - If no private key is set
523    /// * `Err(CryptoTensorsError::InvalidKey)` - If base64 decoding fails
524    pub fn get_private_key_bytes(&self) -> Result<Vec<u8>, CryptoTensorsError> {
525        let d = self
526            .d_priv
527            .get()
528            .and_then(|v| v.as_ref())
529            .ok_or(CryptoTensorsError::MissingSigningKey)?;
530        BASE64.decode(d).map_err(|e| {
531            CryptoTensorsError::InvalidKey(format!("Invalid base64 private key: {}", e))
532        })
533    }
534
535    /// Parse KeyMaterial from a serde_json::Value (header)
536    ///
537    /// # Arguments
538    /// * `header` - serde_json::Value containing key material
539    ///
540    /// # Returns
541    /// * `Ok<KeyMaterial>` - Key material
542    /// * `Err(CryptoTensorsError)` - If parsing or validation fails
543    pub fn from_header(header: &serde_json::Value) -> Result<Self, CryptoTensorsError> {
544        let key: KeyMaterial = serde_json::from_value(header.clone()).map_err(|e| {
545            CryptoTensorsError::InvalidKey(format!("Failed to parse key material: {}", e))
546        })?;
547        key.validate(ValidateMode::FromHeader)?;
548        Ok(key)
549    }
550
551    /// Create KeyMaterial from JWK string
552    ///
553    /// # Arguments
554    /// * `jwk_str` - JSON Web Key as a string
555    /// * `load_key` - Whether to immediately load the key
556    ///
557    /// # Returns
558    /// * `Ok(KeyMaterial)` - Successfully created key material
559    /// * `Err(CryptoTensorsError)` - If parsing or validation fails
560    pub fn from_jwk(
561        jwk_str: &serde_json::Value,
562        is_for_save: bool,
563    ) -> Result<Self, CryptoTensorsError> {
564        let key_material: KeyMaterial = serde_json::from_value(jwk_str.clone())
565            .map_err(|e| CryptoTensorsError::InvalidKey(format!("Failed to parse JWK: {}", e)))?;
566
567        // Requires symmetric key or private key for creation mode
568        // Requires symmetric key or public key for JWK loading mode
569        key_material.validate(if is_for_save {
570            ValidateMode::ForCreation
571        } else {
572            ValidateMode::FromJwk
573        })?;
574
575        Ok(key_material)
576    }
577
578    /// Create KeyMaterial from JWK JSON string
579    ///
580    /// # Arguments
581    /// * `jwk_json` - JSON string containing JWK
582    /// * `is_for_save` - Whether this key will be used for saving (requires full key material)
583    ///
584    /// # Returns
585    /// * `Ok(KeyMaterial)` - Successfully created key material
586    /// * `Err(CryptoTensorsError)` - If parsing fails
587    pub fn from_jwk_str(jwk_json: &str, is_for_save: bool) -> Result<Self, CryptoTensorsError> {
588        let jwk_value: serde_json::Value = serde_json::from_str(jwk_json)
589            .map_err(|e| CryptoTensorsError::InvalidKey(format!("Invalid JSON: {}", e)))?;
590        Self::from_jwk(&jwk_value, is_for_save)
591    }
592
593    /// Get kid (key identifier)
594    pub fn kid(&self) -> Option<&str> {
595        self.kid.as_deref()
596    }
597
598    /// Get jku (JWK Set URL)
599    pub fn jku(&self) -> Option<&str> {
600        self.jku.as_deref()
601    }
602
603    /// Set kid (key identifier)
604    pub fn set_kid(&mut self, kid: &str) {
605        self.kid = Some(kid.to_string());
606    }
607
608    /// Set jku (JWK Set URL)
609    pub fn set_jku(&mut self, jku: &str) {
610        self.jku = Some(jku.to_string());
611    }
612}
613
614/// Key role: which key to fetch and which Provider/Registry method to use.
615#[derive(Clone, Copy)]
616pub enum KeyRole {
617    /// Encryption master key; use `get_master_key`.
618    Master,
619    /// Signing private key; use `get_signing_key`.
620    Signing,
621    /// Verification public key; use `get_verify_key`.
622    Verify,
623}
624
625impl KeyRole {
626    fn missing_key_message(&self) -> &'static str {
627        match self {
628            KeyRole::Master => "encryption key required: provide enc_key or use registry (enc_kid)",
629            KeyRole::Signing => "signing key required: provide sign_key or use registry (sign_kid)",
630            KeyRole::Verify => {
631                "verification key required: provide sign_key or use registry (sign_kid)"
632            }
633        }
634    }
635}
636
637/// Parameters for a single key lookup. Built from config (serialize or deserialize) + optional header key.
638/// KeyProvider is only used via Registry registration; config never passes a provider.
639pub struct KeyLookupParams<'a> {
640    /// Directly provided key; takes precedence. When set, kid/jku are ignored.
641    pub direct: Option<&'a KeyMaterial>,
642    /// JWK Set URL for registry lookup (when no direct key).
643    pub jku: Option<&'a str>,
644    /// Key ID for registry lookup (when no direct key).
645    pub kid: Option<&'a str>,
646    /// Whether fallback to global registry is allowed.
647    pub registry_allowed: bool,
648}
649
650/// **Single source of truth for key load order.** Adjust resolution logic here only.
651/// Order: direct → registry (if allowed) → error. KeyProvider is used only via Registry registration.
652pub fn resolve_key(
653    role: KeyRole,
654    params: &KeyLookupParams<'_>,
655    for_creation: bool,
656) -> Result<KeyMaterial, CryptoTensorsError> {
657    if let Some(k) = params.direct {
658        return Ok(k.clone());
659    }
660    let jwk = if params.registry_allowed {
661        match role {
662            KeyRole::Master => registry::get_master_key(params.jku, params.kid)?,
663            KeyRole::Signing => registry::get_signing_key(params.jku, params.kid)?,
664            KeyRole::Verify => registry::get_verify_key(params.jku, params.kid)?,
665        }
666    } else {
667        return Err(CryptoTensorsError::KeyLoad(
668            role.missing_key_message().to_string(),
669        ));
670    };
671    KeyMaterial::from_jwk(&jwk, for_creation)
672}
673
674#[cfg(test)]
675mod tests {
676    use super::*;
677
678    #[test]
679    fn test_keymaterial_convenience_methods() {
680        // Test from_jwk_str
681        let jwk_json = r#"{"kty":"oct","alg":"aes256gcm","kid":"test","k":"dGVzdC1rZXktMzItYnl0ZXMtbG9uZy1lbmNyeXB0aW9u"}"#;
682        let key = KeyMaterial::from_jwk_str(jwk_json, false).unwrap();
683        assert_eq!(key.kid(), Some("test"));
684        assert_eq!(key.alg, "aes256gcm");
685
686        // Test setters
687        let mut key = KeyMaterial::new_enc_key(None, None, None, None).unwrap();
688        key.set_kid("new-kid");
689        key.set_jku("file:///new/path.jwk");
690        assert_eq!(key.kid(), Some("new-kid"));
691        assert_eq!(key.jku(), Some("file:///new/path.jwk"));
692    }
693}