Skip to main content

affinidi_secrets_resolver/
secrets.rs

1/*!
2Handles Secrets - mainly used for internal representation and for saving to files (should always be encrypted)
3
4*/
5
6#[cfg(feature = "slh-dsa")]
7use crate::multicodec::SLH_DSA_SHA2_128S_PUB;
8#[cfg(feature = "ml-dsa")]
9use crate::multicodec::{
10    ML_DSA_44_PRIV_SEED, ML_DSA_44_PUB, ML_DSA_65_PRIV_SEED, ML_DSA_65_PUB, ML_DSA_87_PRIV_SEED,
11    ML_DSA_87_PUB,
12};
13use crate::{
14    errors::{Result, SecretsResolverError},
15    multicodec::{
16        ED25519_PRIV, ED25519_PUB, MultiEncoded, MultiEncodedBuf, P256_PRIV, P256_PUB, P384_PRIV,
17        P384_PUB, P521_PRIV, SECP256K1_PRIV, SECP256K1_PUB, X25519_PRIV, X25519_PUB,
18    },
19};
20pub use affinidi_crypto::KeyType;
21use affinidi_crypto::{JWK, Params};
22use base58::ToBase58;
23use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
24use serde::{Deserialize, Serialize};
25use serde_json::{Value, json};
26use sha2::{Digest, Sha256};
27use tracing::warn;
28use unsigned_varint::encode as varint_encode;
29use x25519_dalek::{PublicKey, StaticSecret};
30use zeroize::{Zeroize, ZeroizeOnDrop};
31
32/// A Shadow inner struct that helps with deserializing
33/// Allows for post-processing of the JWK material
34#[derive(Deserialize)]
35struct SecretShadow {
36    id: String,
37    #[serde(rename = "type")]
38    type_: SecretType,
39    #[serde(flatten)]
40    secret_material: SecretMaterial,
41}
42
43/// Public Structure that manages everything to do with Keys and Secrets
44#[derive(Debug, Clone, Deserialize, Serialize, Zeroize, ZeroizeOnDrop)]
45#[serde(try_from = "SecretShadow")]
46pub struct Secret {
47    /// A key ID identifying a secret (private key).
48    pub id: String,
49
50    /// Must have the same semantics as type ('type' field) of the corresponding method in DID Doc containing a public key.
51    #[serde(rename = "type")]
52    pub type_: SecretType,
53
54    /// Value of the secret (private key)
55    #[serde(flatten)]
56    pub secret_material: SecretMaterial,
57
58    /// Performance cheat to hold private key material in a single field
59    #[serde(skip)]
60    pub(crate) private_bytes: Vec<u8>,
61
62    /// Performance cheat to hold public key material in a single field
63    #[serde(skip)]
64    pub(crate) public_bytes: Vec<u8>,
65
66    /// What crypto type is this secret
67    #[serde(skip)]
68    pub(crate) key_type: KeyType,
69}
70
71/// Converts the inner Secret Shadow to a public Shadow Struct
72/// Handles post-deserializing crypto functions to populate a full Secret Struct
73impl TryFrom<SecretShadow> for Secret {
74    type Error = SecretsResolverError;
75
76    fn try_from(shadow: SecretShadow) -> Result<Self> {
77        match shadow.secret_material {
78            SecretMaterial::JWK(jwk) => {
79                let mut secret = Secret::from_jwk(&jwk)?;
80                secret.id = shadow.id;
81                secret.type_ = shadow.type_;
82                Ok(secret)
83            }
84            SecretMaterial::PrivateKeyMultibase(private) => {
85                Secret::from_multibase(&private, Some(&shadow.id))
86            }
87            _ => Err(SecretsResolverError::KeyError(
88                "Unsupported secret material type".into(),
89            )),
90        }
91    }
92}
93
94/// Compresses a SEC1 uncompressed EC point (`0x04 || x || y`) to the
95/// multicodec-encoded compressed form (`parity || x`), wrapped with
96/// the given public-key codec. Returns `KeyError` if `public_bytes` is
97/// too short for the claimed curve instead of panicking.
98fn compress_ec_point(
99    public_bytes: &[u8],
100    compressed_len: usize,
101    codec: u64,
102) -> Result<MultiEncodedBuf> {
103    let coord_len = compressed_len - 1;
104    let required = 1 + 2 * coord_len; // 0x04 || x || y
105    if public_bytes.len() < required {
106        return Err(SecretsResolverError::KeyError(format!(
107            "public_bytes too short to compress: expected >= {required} bytes, got {}",
108            public_bytes.len()
109        )));
110    }
111    let last_y_byte = public_bytes[required - 1];
112    let parity: u8 = if last_y_byte.is_multiple_of(2) {
113        0x02
114    } else {
115        0x03
116    };
117    let mut compressed: Vec<u8> = Vec::with_capacity(compressed_len);
118    compressed.push(parity);
119    compressed.extend_from_slice(&public_bytes[1..=coord_len]);
120    Ok(MultiEncodedBuf::encode_bytes(codec, &compressed))
121}
122
123impl Secret {
124    /// Helper function to get raw bytes
125    fn convert_to_raw(input: &str) -> Result<Vec<u8>> {
126        BASE64_URL_SAFE_NO_PAD
127            .decode(input)
128            .map_err(|e| SecretsResolverError::KeyError(format!("Failed to decode base64url: {e}")))
129    }
130
131    /// Converts a JWK to a Secret
132    pub fn from_jwk(jwk: &JWK) -> Result<Self> {
133        match &jwk.params {
134            Params::EC(params) => {
135                let mut x = Secret::convert_to_raw(&params.x)?;
136                let mut y = Secret::convert_to_raw(&params.y)?;
137
138                x.append(&mut y);
139                Ok(Secret {
140                    id: jwk.key_id.as_ref().unwrap_or(&"".to_string()).to_string(),
141                    type_: SecretType::JsonWebKey2020,
142                    secret_material: SecretMaterial::JWK(jwk.to_owned()),
143                    private_bytes: Secret::convert_to_raw(params.d.as_ref().ok_or(
144                        SecretsResolverError::KeyError(
145                            "Must have secret key available".to_string(),
146                        ),
147                    )?)?,
148                    public_bytes: x,
149                    key_type: KeyType::try_from(params.curve.as_str())?,
150                })
151            }
152            Params::OKP(params) => Ok(Secret {
153                id: jwk.key_id.as_ref().unwrap_or(&"".to_string()).to_string(),
154                type_: SecretType::JsonWebKey2020,
155                secret_material: SecretMaterial::JWK(jwk.to_owned()),
156                private_bytes: Secret::convert_to_raw(params.d.as_ref().ok_or(
157                    SecretsResolverError::KeyError("Must have secret key available".to_string()),
158                )?)?,
159                public_bytes: Secret::convert_to_raw(&params.x)?,
160                key_type: KeyType::try_from(params.curve.as_str())?,
161            }),
162        }
163    }
164
165    /// Helper functions for converting between different types.
166    /// Create a new Secret from a JWK JSON string
167    /// Example:
168    /// ```ignore
169    /// use affinidi_secrets_resolver::secrets::{Secret, SecretMaterial, SecretType};
170    ///
171    ///
172    /// let key_id = "did:example:123#key-1";
173    /// let key_str = r#"{
174    ///    "crv": "Ed25519",
175    ///    "d": "LLWCf...dGpIqSFw",
176    ///    "kty": "OKP",
177    ///    "x": "Hn8T...ZExwQo"
178    ///  }"#;
179    ///
180    /// let secret = Secret::from_str(key_id, key_str)?;
181    /// ```
182    pub fn from_str(key_id: &str, jwk: &Value) -> Result<Self> {
183        let mut jwk: JWK = serde_json::from_value(jwk.to_owned())
184            .map_err(|e| SecretsResolverError::KeyError(format!("Failed to parse JWK: {e}")))?;
185
186        jwk.key_id = Some(key_id.to_string());
187        Self::from_jwk(&jwk)
188    }
189
190    /// Creates a secret from a multibase encoded key
191    /// Inputs:
192    /// private: multi-encoded private key string
193    /// specified)
194    /// kid: Optional Key ID (or random if not provided)
195    ///
196    pub fn from_multibase(private: &str, kid: Option<&str>) -> Result<Self> {
197        let private_bytes = multibase::decode(private).map_err(|e| {
198            SecretsResolverError::KeyError(format!("Failed to decode private key: {e}"))
199        })?;
200
201        let private_bytes = MultiEncoded::new(private_bytes.1.as_slice())?;
202
203        match private_bytes.codec() {
204            ED25519_PRIV => {
205                if private_bytes.data().len() != 32 {
206                    return Err(SecretsResolverError::KeyError(
207                        "Invalid ED25519 private key length".into(),
208                    ));
209                }
210                let mut pb: [u8; 32] = [0; 32];
211                pb.copy_from_slice(private_bytes.data());
212
213                let secret = Secret::generate_ed25519(kid, Some(&pb));
214                pb.zeroize();
215                Ok(secret)
216            }
217            X25519_PRIV => {
218                if private_bytes.data().len() != 32 {
219                    return Err(SecretsResolverError::KeyError(
220                        "Invalid X25519 private key length".into(),
221                    ));
222                }
223                let mut pb: [u8; 32] = [0; 32];
224                pb.copy_from_slice(private_bytes.data());
225
226                let secret = Secret::generate_x25519(kid, Some(&pb));
227                pb.zeroize();
228                secret
229            }
230            P256_PRIV => {
231                if private_bytes.data().len() != 32 {
232                    return Err(SecretsResolverError::KeyError(
233                        "Invalid P256 private key length".into(),
234                    ));
235                }
236
237                Secret::generate_p256(kid, Some(private_bytes.data()))
238            }
239            P384_PRIV => Secret::generate_p384(kid, Some(private_bytes.data())),
240            SECP256K1_PRIV => Secret::generate_secp256k1(kid, Some(private_bytes.data())),
241            #[cfg(feature = "ml-dsa")]
242            ML_DSA_44_PRIV_SEED => {
243                if private_bytes.data().len() != 32 {
244                    return Err(SecretsResolverError::KeyError(
245                        "Invalid ML-DSA-44 seed length".into(),
246                    ));
247                }
248                let mut pb: [u8; 32] = [0; 32];
249                pb.copy_from_slice(private_bytes.data());
250                let s = Secret::generate_ml_dsa_44(kid, Some(&pb));
251                pb.zeroize();
252                Ok(s)
253            }
254            #[cfg(feature = "ml-dsa")]
255            ML_DSA_65_PRIV_SEED => {
256                if private_bytes.data().len() != 32 {
257                    return Err(SecretsResolverError::KeyError(
258                        "Invalid ML-DSA-65 seed length".into(),
259                    ));
260                }
261                let mut pb: [u8; 32] = [0; 32];
262                pb.copy_from_slice(private_bytes.data());
263                let s = Secret::generate_ml_dsa_65(kid, Some(&pb));
264                pb.zeroize();
265                Ok(s)
266            }
267            #[cfg(feature = "ml-dsa")]
268            ML_DSA_87_PRIV_SEED => {
269                if private_bytes.data().len() != 32 {
270                    return Err(SecretsResolverError::KeyError(
271                        "Invalid ML-DSA-87 seed length".into(),
272                    ));
273                }
274                let mut pb: [u8; 32] = [0; 32];
275                pb.copy_from_slice(private_bytes.data());
276                let s = Secret::generate_ml_dsa_87(kid, Some(&pb));
277                pb.zeroize();
278                Ok(s)
279            }
280            _ => Err(SecretsResolverError::KeyError(
281                "Unsupported key type in from_multibase".into(),
282            )),
283        }
284    }
285
286    /// Decodes a multikey to raw bytes
287    pub fn decode_multikey(key: &str) -> Result<Vec<u8>> {
288        let bytes = multibase::decode(key).map_err(|e| {
289            SecretsResolverError::KeyError(format!("Failed to multibase.decode key: {e}"))
290        })?;
291        let bytes = MultiEncoded::new(bytes.1.as_slice()).map_err(|e| {
292            SecretsResolverError::KeyError(format!("Failed to load decoded key: {e}"))
293        })?;
294        Ok(bytes.data().to_vec())
295    }
296
297    /// Get the multibase (Base58btc) encoded public key
298    pub fn get_public_keymultibase(&self) -> Result<String> {
299        let encoded = match self.key_type {
300            KeyType::Ed25519 => MultiEncodedBuf::encode_bytes(ED25519_PUB, &self.public_bytes),
301            KeyType::X25519 => MultiEncodedBuf::encode_bytes(X25519_PUB, &self.public_bytes),
302            KeyType::P256 => compress_ec_point(&self.public_bytes, 33, P256_PUB)?,
303            KeyType::P384 => compress_ec_point(&self.public_bytes, 49, P384_PUB)?,
304            KeyType::P521 => {
305                return Err(SecretsResolverError::KeyError(
306                    "P-521 is not supported".to_string(),
307                ));
308            }
309            KeyType::Secp256k1 => compress_ec_point(&self.public_bytes, 33, SECP256K1_PUB)?,
310            #[cfg(feature = "ml-dsa")]
311            KeyType::MlDsa44 => MultiEncodedBuf::encode_bytes(ML_DSA_44_PUB, &self.public_bytes),
312            #[cfg(feature = "ml-dsa")]
313            KeyType::MlDsa65 => MultiEncodedBuf::encode_bytes(ML_DSA_65_PUB, &self.public_bytes),
314            #[cfg(feature = "ml-dsa")]
315            KeyType::MlDsa87 => MultiEncodedBuf::encode_bytes(ML_DSA_87_PUB, &self.public_bytes),
316            #[cfg(feature = "slh-dsa")]
317            KeyType::SlhDsaSha2_128s => {
318                MultiEncodedBuf::encode_bytes(SLH_DSA_SHA2_128S_PUB, &self.public_bytes)
319            }
320            _ => {
321                return Err(SecretsResolverError::KeyError(
322                    "Unsupported key type".into(),
323                ));
324            }
325        };
326        Ok(multibase::encode(
327            multibase::Base::Base58Btc,
328            encoded.into_bytes(),
329        ))
330    }
331
332    /// Generates a hash of the multikey - useful where you want to pre-rotate keys
333    /// but not disclose the actual public key itself!
334    pub fn get_public_keymultibase_hash(&self) -> Result<String> {
335        let key = self.get_public_keymultibase()?;
336
337        Secret::base58_hash_string(&key)
338    }
339
340    /// Will convert a string to a base58btc encoded multihash (SHA256) representation
341    /// `base58<multihash<multikey>>`
342    pub fn base58_hash_string(key: &str) -> Result<String> {
343        let hash = Sha256::digest(key.as_bytes());
344        // Multihash binary format: varint(code) || varint(length) || digest
345        // SHA-256 code = 0x12
346        let mut code_buf = varint_encode::u64_buffer();
347        let code_varint = varint_encode::u64(0x12, &mut code_buf);
348        let mut len_buf = varint_encode::u64_buffer();
349        let len_varint = varint_encode::u64(hash.len() as u64, &mut len_buf);
350        let mut bytes = Vec::with_capacity(code_varint.len() + len_varint.len() + hash.len());
351        bytes.extend_from_slice(code_varint);
352        bytes.extend_from_slice(len_varint);
353        bytes.extend_from_slice(&hash);
354        Ok(bytes.to_base58())
355    }
356
357    /// Get the multibase (Base58btc) encoded private key
358    pub fn get_private_keymultibase(&self) -> Result<String> {
359        let encoded = match self.key_type {
360            KeyType::Ed25519 => MultiEncodedBuf::encode_bytes(ED25519_PRIV, &self.private_bytes),
361            KeyType::X25519 => MultiEncodedBuf::encode_bytes(X25519_PRIV, &self.private_bytes),
362            KeyType::P256 => MultiEncodedBuf::encode_bytes(P256_PRIV, &self.private_bytes),
363            KeyType::P384 => MultiEncodedBuf::encode_bytes(P384_PRIV, &self.private_bytes),
364            KeyType::P521 => MultiEncodedBuf::encode_bytes(P521_PRIV, &self.private_bytes),
365            KeyType::Secp256k1 => {
366                MultiEncodedBuf::encode_bytes(SECP256K1_PRIV, &self.private_bytes)
367            }
368            #[cfg(feature = "ml-dsa")]
369            KeyType::MlDsa44 => {
370                MultiEncodedBuf::encode_bytes(ML_DSA_44_PRIV_SEED, &self.private_bytes)
371            }
372            #[cfg(feature = "ml-dsa")]
373            KeyType::MlDsa65 => {
374                MultiEncodedBuf::encode_bytes(ML_DSA_65_PRIV_SEED, &self.private_bytes)
375            }
376            #[cfg(feature = "ml-dsa")]
377            KeyType::MlDsa87 => {
378                MultiEncodedBuf::encode_bytes(ML_DSA_87_PRIV_SEED, &self.private_bytes)
379            }
380            #[cfg(feature = "slh-dsa")]
381            KeyType::SlhDsaSha2_128s => {
382                return Err(SecretsResolverError::KeyError(
383                    "SLH-DSA has no private-key multicodec registered; persist raw private_bytes \
384                     instead of encoding as multikey"
385                        .into(),
386                ));
387            }
388            _ => {
389                return Err(SecretsResolverError::KeyError(
390                    "Unsupported key type".into(),
391                ));
392            }
393        };
394        Ok(multibase::encode(
395            multibase::Base::Base58Btc,
396            encoded.into_bytes(),
397        ))
398    }
399
400    /// Get the public key bytes
401    pub fn get_public_bytes(&self) -> &[u8] {
402        self.public_bytes.as_slice()
403    }
404
405    /// Get the private key bytes
406    pub fn get_private_bytes(&self) -> &[u8] {
407        self.private_bytes.as_slice()
408    }
409
410    /// What crypto type is this secret
411    pub fn get_key_type(&self) -> KeyType {
412        self.key_type
413    }
414
415    pub fn to_x25519(&self) -> Result<Secret> {
416        if self.key_type != KeyType::Ed25519 {
417            warn!(
418                "Can only convert ED25519 to X25519! Current key type is {:#?}",
419                self.key_type
420            );
421            Err(SecretsResolverError::KeyError(format!(
422                "Can only convert ED25519 to X25519! Current key type is {:#?}",
423                self.key_type
424            )))
425        } else {
426            // Convert to X25519 Secret bytes
427            let x25519_secret = affinidi_crypto::ed25519::ed25519_private_to_x25519(
428                self.private_bytes.first_chunk::<32>().unwrap(),
429            );
430
431            let x25519_sk = StaticSecret::from(x25519_secret);
432            let x25519_pk = PublicKey::from(&x25519_sk);
433
434            let secret = BASE64_URL_SAFE_NO_PAD.encode(x25519_sk.as_bytes());
435            let public = BASE64_URL_SAFE_NO_PAD.encode(x25519_pk.as_bytes());
436
437            let jwk = json!({
438                "crv": "X25519",
439                "d": secret,
440                "kty": "OKP",
441                "x": public
442            });
443
444            Secret::from_str(&self.id, &jwk)
445        }
446    }
447}
448
449/// Must have the same semantics as type ('type' field) of the corresponding method in DID Doc containing a public key.
450#[derive(Debug, Clone, Deserialize, Serialize, Zeroize)]
451pub enum SecretType {
452    JsonWebKey2020,
453    X25519KeyAgreementKey2019,
454    X25519KeyAgreementKey2020,
455    Ed25519VerificationKey2018,
456    Ed25519VerificationKey2020,
457    EcdsaSecp256k1VerificationKey2019,
458    Multikey,
459    Other,
460}
461
462// KeyType is re-exported from affinidi_crypto
463
464/// Represents secret crypto material.
465#[derive(Debug, Clone, Deserialize, Serialize, Zeroize)]
466#[serde(rename_all = "camelCase")]
467pub enum SecretMaterial {
468    #[serde(rename = "privateKeyJwk")]
469    JWK(JWK),
470
471    PrivateKeyMultibase(String),
472
473    Base58 {
474        private_key_base58: String,
475    },
476
477    /// Not used - legacy reference
478    /// This can be removed in the future (affinidi-messaging-didcomm using this)
479    Multibase {
480        private_key_multibase: String,
481    },
482}
483
484#[cfg(test)]
485mod tests {
486    use super::Secret;
487    use base64::{Engine, prelude::BASE64_URL_SAFE_NO_PAD};
488    use serde_json::json;
489
490    #[test]
491    fn check_hash() {
492        let input = "z6MkgfFvvWA7sw8WkNWyK3y74kwNVvWc7Qrs5tWnsnqMfLD3";
493        let output = Secret::base58_hash_string(input).expect("Hash of input");
494        assert_eq!(&output, "QmY1kaguPMgjndEh1sdDZ8kdjX4Uc1SW4vziMfgWC6ndnJ")
495    }
496
497    #[test]
498    fn check_hash_bad() {
499        let input = "z6MkgfFvvWA7sw8WkNWyK3y74kwNVvWc7Qrs5tWnsnqMfLD4";
500        let output = Secret::base58_hash_string(input).expect("Hash of input");
501        assert_ne!(&output, "QmY1kaguPMgjndEh1sdDZ8kdjX4Uc1SW4vziMfgWC6ndnJ")
502    }
503
504    #[test]
505    fn check_x25519() {
506        // ED25519 Secret Key
507        // https://docs.rs/ed25519_to_curve25519/latest/ed25519_to_curve25519/fn.ed25519_sk_to_curve25519.html
508        /* let ed25519_sk_bytes: [u8; 32] = [
509            202, 104, 239, 81, 53, 110, 80, 252, 198, 23, 155, 162, 215, 98, 223, 173, 227, 188,
510            110, 54, 127, 45, 185, 206, 174, 29, 44, 147, 76, 66, 196, 195,
511        ]; */
512
513        let x25519_sk_bytes: [u8; 32] = [
514            200, 255, 64, 61, 17, 52, 112, 33, 205, 71, 186, 13, 131, 12, 241, 136, 223, 5, 152,
515            40, 95, 187, 83, 168, 142, 10, 234, 215, 70, 210, 148, 104,
516        ];
517
518        // The following JWK is created from the ed25519 secret key above
519        let jwk = json!({
520        "crv": "Ed25519",
521        "d": "ymjvUTVuUPzGF5ui12LfreO8bjZ_LbnOrh0sk0xCxMM",
522        "kty": "OKP",
523        "x": "d17TbZmkoYHZUQpzJTcuOtq0tjWYm8CKvKGYHDW6ZaE"
524        });
525
526        let ed25519 = Secret::from_str("test", &jwk).unwrap();
527
528        let x25519 = ed25519
529            .to_x25519()
530            .expect("Couldn't convert ed25519 to x25519");
531
532        assert_eq!(x25519.private_bytes.as_slice(), x25519_sk_bytes);
533    }
534
535    #[test]
536    fn check_secret_deserialize() {
537        let txt = r#"{
538        "id": "did:web:localhost%3A7037:mediator:v1:.well-known#key-2",
539        "type": "JsonWebKey2020",
540        "privateKeyJwk": {
541            "crv": "secp256k1",
542            "d": "Cs5xn7WCkUWEua5vGxjP9_wBzIzMtEwjQ4KWKHHQR14",
543            "kty": "EC",
544            "x": "Lk1FY8MmyLjBswU4KbLoBQ_1THZJBMx2n6aIBXt1uXo",
545            "y": "tEv7EQHj4g4njOfrsjjDJBPKOI9RGWWMS8NYClo2cqo"
546        }
547    }"#;
548
549        let secret = serde_json::from_str::<Secret>(txt);
550
551        assert!(secret.is_ok());
552    }
553
554    #[test]
555    fn from_multiencode_ed25519() {
556        let seed = BASE64_URL_SAFE_NO_PAD
557            .decode("oihAhqs-h9V9rq6KYEhiEWwdBDpTI7xL0EEiwC9heFg")
558            .expect("Couldn't decode ed25519 BASE64 encoding");
559
560        let public_bytes = BASE64_URL_SAFE_NO_PAD
561            .decode("eC1vNebw6IJ8SJ4Tg9g2Q9W-Zy8xIS80byxTZXlPaHk")
562            .expect("Couldn't BASE64 decode ed25519 public bytes");
563
564        assert_eq!(seed.len(), 32);
565        let mut private_bytes: [u8; 32] = [0; 32];
566        private_bytes.copy_from_slice(seed.as_slice());
567
568        let secret = Secret::generate_ed25519(None, Some(&private_bytes));
569
570        assert_eq!(
571            secret.get_private_keymultibase().unwrap(),
572            "z3u2c8oS2oKgATvakQzVF66EAcZWJqPUzGQzWMUTKnFkv5DR"
573        );
574        assert_eq!(secret.get_public_bytes(), public_bytes.as_slice());
575        assert_eq!(secret.get_private_bytes(), private_bytes.as_slice());
576
577        let secret2 =
578            Secret::from_multibase("z3u2c8oS2oKgATvakQzVF66EAcZWJqPUzGQzWMUTKnFkv5DR", None)
579                .expect("Failed to transform ed25519 to secret");
580
581        assert_eq!(
582            secret2.get_public_keymultibase().unwrap(),
583            secret.get_public_keymultibase().unwrap()
584        );
585        assert_eq!(secret2.get_public_bytes(), secret.get_public_bytes());
586        assert_eq!(secret2.get_private_bytes(), secret.get_private_bytes());
587
588        assert_eq!(
589            secret2.get_private_keymultibase().unwrap(),
590            secret.get_private_keymultibase().unwrap()
591        );
592    }
593
594    #[test]
595    fn from_multiencode_x25519() {
596        let seed = BASE64_URL_SAFE_NO_PAD
597            .decode("eYN37ZX0ij4TYdklZax2jiRiyHYMNOzwW2bvNauAzKk")
598            .expect("Couldn't decode x25519 BASE64 encoding");
599
600        let public_bytes = BASE64_URL_SAFE_NO_PAD
601            .decode("Ephwf5xVmhVnDj2KtIPDKcGYBG9CQR_mZKlRqETZ62U")
602            .expect("Couldn't BASE64 decode x25519 public bytes");
603
604        assert_eq!(seed.len(), 32);
605        let mut private_bytes: [u8; 32] = [0; 32];
606        private_bytes.copy_from_slice(seed.as_slice());
607
608        let secret = Secret::generate_x25519(None, Some(&private_bytes))
609            .expect("x25519 generate secret failed");
610
611        assert_eq!(
612            secret.get_private_keymultibase().unwrap(),
613            "z3weexK9erGUKF41d3tJoDu2Fetx1xnsC7WhFWnjuCJXJGxp"
614        );
615        assert_eq!(secret.get_public_bytes(), public_bytes.as_slice());
616        assert_eq!(secret.get_private_bytes(), private_bytes.as_slice());
617
618        let secret2 =
619            Secret::from_multibase("z3weexK9erGUKF41d3tJoDu2Fetx1xnsC7WhFWnjuCJXJGxp", None)
620                .expect("Failed to transform x25519 to secret");
621
622        assert_eq!(
623            secret2.get_public_keymultibase().unwrap(),
624            secret.get_public_keymultibase().unwrap()
625        );
626        assert_eq!(secret2.get_public_bytes(), secret.get_public_bytes());
627        assert_eq!(secret2.get_private_bytes(), secret.get_private_bytes());
628
629        assert_eq!(
630            secret2.get_private_keymultibase().unwrap(),
631            secret.get_private_keymultibase().unwrap()
632        );
633    }
634
635    #[test]
636    fn from_multiencode_p256() {
637        let seed = BASE64_URL_SAFE_NO_PAD
638            .decode("B5ZIiXYkpEPczVbyWP85H75wrBifiRcFgtqYvI5I9AI")
639            .expect("Couldn't decode P-256 BASE64 encoding");
640
641        let pub_x = BASE64_URL_SAFE_NO_PAD
642            .decode("Iy3cHBWCRhcjohhS-iSucYMUNjH77DIQRSdn-NylcCw")
643            .expect("Couldn't BASE64 decode P-256 X public bytes");
644
645        let pub_y = BASE64_URL_SAFE_NO_PAD
646            .decode("p9MikGh-O3nbLWA-6tP4Oanch5AF3ZhRD907tQojH3k")
647            .expect("Couldn't BASE64 decode P-256 Y public bytes");
648
649        let public_bytes = [vec![4], pub_x, pub_y].concat();
650
651        assert_eq!(seed.len(), 32);
652        let mut private_bytes: [u8; 32] = [0; 32];
653        private_bytes.copy_from_slice(seed.as_slice());
654
655        let secret =
656            Secret::generate_p256(None, Some(&private_bytes)).expect("P256 secret generate failed");
657
658        assert_eq!(
659            secret.get_private_keymultibase().unwrap(),
660            "z42tiPvqM1uFz2QxbF7wTsQkfAf3hCsq1Uf9JbUMRaRiV1yb"
661        );
662        assert_eq!(secret.get_public_bytes(), public_bytes.as_slice());
663        assert_eq!(secret.get_private_bytes(), private_bytes.as_slice());
664
665        let secret2 =
666            Secret::from_multibase("z42tiPvqM1uFz2QxbF7wTsQkfAf3hCsq1Uf9JbUMRaRiV1yb", None)
667                .expect("Failed to transform P256 to secret");
668
669        assert_eq!(
670            secret2.get_public_keymultibase().unwrap(),
671            secret.get_public_keymultibase().unwrap()
672        );
673        assert_eq!(secret2.get_public_bytes(), secret.get_public_bytes());
674        assert_eq!(secret2.get_private_bytes(), secret.get_private_bytes());
675
676        assert_eq!(
677            secret2.get_private_keymultibase().unwrap(),
678            secret.get_private_keymultibase().unwrap()
679        );
680    }
681
682    #[test]
683    fn from_multiencode_p384() {
684        let seed = BASE64_URL_SAFE_NO_PAD
685            .decode("nka5zKVpVpOdCKdZZgnZ-VaSXk6V_ovYibzr2nf-mKAgct6wBdvWCXWLaNr80zY0")
686            .expect("Couldn't decode P-384 BASE64 encoding");
687
688        let pub_x = BASE64_URL_SAFE_NO_PAD
689            .decode("uitQkpTA3Vw8t_qOGrdLlbIzdzF0K9NsScgsVgmpQdQJgshCifOCUehxeazzL-Ow")
690            .expect("Couldn't BASE64 decode P-384 X public bytes");
691
692        let pub_y = BASE64_URL_SAFE_NO_PAD
693            .decode("4BIcrueQfhxfnrqToZEOujOfJOmwEsWJAdFNZ9dksIBCnWiCLBEn2HnR7ikyyPMJ")
694            .expect("Couldn't BASE64 decode P-384 Y public bytes");
695
696        let public_bytes = [vec![4], pub_x, pub_y].concat();
697
698        assert_eq!(seed.len(), 48);
699        let mut private_bytes: [u8; 48] = [0; 48];
700        private_bytes.copy_from_slice(seed.as_slice());
701
702        let secret =
703            Secret::generate_p384(None, Some(&private_bytes)).expect("P384 secret generate failed");
704
705        assert_eq!(
706            secret.get_private_keymultibase().unwrap(),
707            "z2fapqKp6mPoQCwkQzvL9Ns35Y57R4LRRfVwbXoSTQjTHdjD4MqFZnw5PueieuTWG4pN5q"
708        );
709        assert_eq!(secret.get_public_bytes(), public_bytes.as_slice());
710        assert_eq!(secret.get_private_bytes(), private_bytes.as_slice());
711
712        let secret2 = Secret::from_multibase(
713            "z2fapqKp6mPoQCwkQzvL9Ns35Y57R4LRRfVwbXoSTQjTHdjD4MqFZnw5PueieuTWG4pN5q",
714            None,
715        )
716        .expect("Failed to transform P384 to secret");
717
718        assert_eq!(
719            secret2.get_public_keymultibase().unwrap(),
720            secret.get_public_keymultibase().unwrap()
721        );
722        assert_eq!(secret2.get_public_bytes(), secret.get_public_bytes());
723        assert_eq!(secret2.get_private_bytes(), secret.get_private_bytes());
724
725        assert_eq!(
726            secret2.get_private_keymultibase().unwrap(),
727            secret.get_private_keymultibase().unwrap()
728        );
729    }
730
731    #[cfg(feature = "ml-dsa")]
732    #[test]
733    fn from_multiencode_ml_dsa_44() {
734        let seed = [7u8; 32];
735        let secret = Secret::generate_ml_dsa_44(Some("k-44"), Some(&seed));
736
737        let mb = secret.get_private_keymultibase().expect("encode priv");
738        let pub_mb = secret.get_public_keymultibase().expect("encode pub");
739
740        let secret2 = Secret::from_multibase(&mb, Some("k-44")).expect("decode");
741        assert_eq!(secret2.get_public_bytes(), secret.get_public_bytes());
742        assert_eq!(secret2.get_private_bytes(), secret.get_private_bytes());
743        assert_eq!(
744            secret2.get_public_keymultibase().unwrap(),
745            pub_mb,
746            "public multikey roundtrip"
747        );
748    }
749
750    /// Varint-encoded multicodec prefix bytes for the given codec.
751    #[cfg(any(feature = "ml-dsa", feature = "slh-dsa"))]
752    fn varint_prefix(codec: u64) -> Vec<u8> {
753        let mut buf = [0u8; 10];
754        use unsigned_varint::encode;
755        let slice = encode::u64(codec, &mut buf);
756        slice.to_vec()
757    }
758
759    /// After multibase-decoding a multikey string, the first N bytes must
760    /// match the registered varint multicodec. This guards against the class
761    /// of bug where we invent our own codec value — tests that only sign and
762    /// verify cannot catch it because encode and decode use the same constant.
763    #[cfg(feature = "ml-dsa")]
764    #[test]
765    fn ml_dsa_multikey_uses_registered_codecs() {
766        use crate::multicodec::{
767            ML_DSA_44_PRIV_SEED, ML_DSA_44_PUB, ML_DSA_65_PRIV_SEED, ML_DSA_65_PUB,
768            ML_DSA_87_PRIV_SEED, ML_DSA_87_PUB,
769        };
770        use affinidi_crypto::KeyType;
771
772        let cases: &[(KeyType, u64, u64, usize, usize)] = &[
773            // (key_type, pub codec, priv-seed codec, pub_len, priv_len)
774            (
775                KeyType::MlDsa44,
776                ML_DSA_44_PUB,
777                ML_DSA_44_PRIV_SEED,
778                1312,
779                32,
780            ),
781            (
782                KeyType::MlDsa65,
783                ML_DSA_65_PUB,
784                ML_DSA_65_PRIV_SEED,
785                1952,
786                32,
787            ),
788            (
789                KeyType::MlDsa87,
790                ML_DSA_87_PUB,
791                ML_DSA_87_PRIV_SEED,
792                2592,
793                32,
794            ),
795        ];
796
797        for (kt, pub_code, priv_code, pub_len, priv_len) in cases {
798            let s = match kt {
799                KeyType::MlDsa44 => Secret::generate_ml_dsa_44(None, Some(&[1u8; 32])),
800                KeyType::MlDsa65 => Secret::generate_ml_dsa_65(None, Some(&[1u8; 32])),
801                KeyType::MlDsa87 => Secret::generate_ml_dsa_87(None, Some(&[1u8; 32])),
802                _ => unreachable!(),
803            };
804
805            let pub_mb = s.get_public_keymultibase().unwrap();
806            let (_, pub_raw) = multibase::decode(&pub_mb).unwrap();
807            let expected = varint_prefix(*pub_code);
808            assert_eq!(
809                &pub_raw[..expected.len()],
810                expected.as_slice(),
811                "{kt:?} pub codec prefix mismatch (expected {pub_code:#06x})"
812            );
813            assert_eq!(pub_raw.len() - expected.len(), *pub_len);
814
815            let priv_mb = s.get_private_keymultibase().unwrap();
816            let (_, priv_raw) = multibase::decode(&priv_mb).unwrap();
817            let expected = varint_prefix(*priv_code);
818            assert_eq!(
819                &priv_raw[..expected.len()],
820                expected.as_slice(),
821                "{kt:?} priv-seed codec prefix mismatch (expected {priv_code:#06x})"
822            );
823            assert_eq!(priv_raw.len() - expected.len(), *priv_len);
824        }
825    }
826
827    #[cfg(feature = "slh-dsa")]
828    #[test]
829    fn slh_dsa_multikey_uses_registered_public_codec() {
830        use crate::multicodec::SLH_DSA_SHA2_128S_PUB;
831        let s = Secret::generate_slh_dsa_sha2_128s(None);
832        let pub_mb = s.get_public_keymultibase().unwrap();
833        let (_, raw) = multibase::decode(&pub_mb).unwrap();
834        let expected = varint_prefix(SLH_DSA_SHA2_128S_PUB);
835        assert_eq!(&raw[..expected.len()], expected.as_slice());
836        assert_eq!(raw.len() - expected.len(), 32);
837    }
838
839    #[test]
840    fn get_public_keymultibase_bounds_check_p256() {
841        // Construct a Secret with deliberately too-short EC public_bytes.
842        // The compression path used to panic on index 64; now it must
843        // return a structured error.
844        let mut s = Secret::generate_p256(None, Some(&[1u8; 32])).unwrap();
845        s.public_bytes.clear(); // zero bytes — clearly insufficient
846        let err = s.get_public_keymultibase().unwrap_err();
847        let msg = format!("{err}");
848        assert!(
849            msg.contains("too short"),
850            "expected bounds-check error, got: {msg}"
851        );
852    }
853
854    #[test]
855    fn get_public_keymultibase_bounds_check_p384() {
856        let mut s = Secret::generate_p384(None, Some(&[1u8; 48])).unwrap();
857        s.public_bytes.truncate(10);
858        assert!(s.get_public_keymultibase().is_err());
859    }
860
861    #[cfg(feature = "slh-dsa")]
862    #[test]
863    fn slh_dsa_private_multibase_unsupported() {
864        // SLH-DSA has no registered private-key multicodec; we surface that
865        // as an error rather than inventing a code.
866        let secret = Secret::generate_slh_dsa_sha2_128s(Some("k-slh"));
867        assert!(secret.get_private_keymultibase().is_err());
868        // Public-key encoding still works (slhdsa-sha2-128s-pub = 0x1220).
869        assert!(secret.get_public_keymultibase().is_ok());
870    }
871
872    #[test]
873    fn from_multiencode_secp256k1() {
874        let seed = BASE64_URL_SAFE_NO_PAD
875            .decode("CzR8XKYmrxbeEeUKojSgXUskLmGjbLXFf4CoJd6he6A")
876            .expect("Couldn't decode secp256k1 BASE64 encoding");
877
878        let pub_x = BASE64_URL_SAFE_NO_PAD
879            .decode("jcGMDsxKBME8GmaN_-XTaAEKk2ET6ajWe_8-2RsU-is")
880            .expect("Couldn't BASE64 decode secp256k1 X public bytes");
881
882        let pub_y = BASE64_URL_SAFE_NO_PAD
883            .decode("9ECTinCwW9bA36fmUBg0_iu0oyLR-Tn54guX8exrUjM")
884            .expect("Couldn't BASE64 decode secp256k1 Y public bytes");
885
886        let public_bytes = [vec![4], pub_x, pub_y].concat();
887
888        assert_eq!(seed.len(), 32);
889        let mut private_bytes: [u8; 32] = [0; 32];
890        private_bytes.copy_from_slice(seed.as_slice());
891
892        let secret = Secret::generate_secp256k1(None, Some(&private_bytes))
893            .expect("secp256k1 secret generate failed");
894
895        assert_eq!(
896            secret.get_private_keymultibase().unwrap(),
897            "z3vLUkda21MTbdECEEyjUWEQmJ8r1CKekvRLqQbZXxfLieL7"
898        );
899        assert_eq!(secret.get_public_bytes(), public_bytes.as_slice());
900        assert_eq!(secret.get_private_bytes(), private_bytes.as_slice());
901
902        let secret2 =
903            Secret::from_multibase("z3vLUkda21MTbdECEEyjUWEQmJ8r1CKekvRLqQbZXxfLieL7", None)
904                .expect("Failed to transform secp256k1 to secret");
905
906        assert_eq!(
907            secret2.get_public_keymultibase().unwrap(),
908            secret.get_public_keymultibase().unwrap()
909        );
910        assert_eq!(secret2.get_public_bytes(), secret.get_public_bytes());
911        assert_eq!(secret2.get_private_bytes(), secret.get_private_bytes());
912
913        assert_eq!(
914            secret2.get_private_keymultibase().unwrap(),
915            secret.get_private_keymultibase().unwrap()
916        );
917    }
918}