Skip to main content

crypto_dispatch/
multikey.rs

1// SPDX-FileCopyrightText: 2026 ReallyMe LLC
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5#![allow(clippy::needless_return)]
6
7use crate::AlgorithmError;
8#[cfg(any(
9    feature = "ed25519",
10    feature = "x25519",
11    feature = "secp256k1",
12    feature = "p256",
13    feature = "p384",
14    feature = "p521",
15    feature = "ml-dsa-44",
16    feature = "ml-dsa-65",
17    feature = "ml-dsa-87",
18    feature = "ml-kem-512",
19    feature = "ml-kem-768",
20    feature = "ml-kem-1024"
21))]
22use codec_multikey::encode_multikey;
23use crypto_core::Algorithm;
24
25#[cfg(feature = "p256")]
26fn compress_p256_public_key(input: &[u8]) -> Result<Vec<u8>, AlgorithmError> {
27    #[cfg(all(
28        feature = "p256",
29        feature = "native",
30        not(all(feature = "wasm", target_arch = "wasm32"))
31    ))]
32    {
33        return crypto_p256::compress_p256(input)
34            .map_err(|_| AlgorithmError::InvalidKey(Algorithm::P256));
35    }
36
37    #[cfg(all(feature = "p256", feature = "wasm", target_arch = "wasm32"))]
38    {
39        return crypto_p256::compress_p256(input)
40            .map_err(|_| AlgorithmError::InvalidKey(Algorithm::P256));
41    }
42
43    #[cfg(not(any(
44        all(
45            feature = "p256",
46            feature = "native",
47            not(all(feature = "wasm", target_arch = "wasm32"))
48        ),
49        all(feature = "p256", feature = "wasm", target_arch = "wasm32")
50    )))]
51    {
52        let _ = input;
53        Err(AlgorithmError::UnsupportedAlgorithm(Algorithm::P256))
54    }
55}
56
57#[cfg(feature = "p384")]
58fn compress_p384_public_key(input: &[u8]) -> Result<Vec<u8>, AlgorithmError> {
59    #[cfg(all(
60        feature = "p384",
61        feature = "native",
62        not(all(feature = "wasm", target_arch = "wasm32"))
63    ))]
64    {
65        return crypto_p384::compress_p384(input)
66            .map_err(|_| AlgorithmError::InvalidKey(Algorithm::P384));
67    }
68
69    #[cfg(all(feature = "p384", feature = "wasm", target_arch = "wasm32"))]
70    {
71        return crypto_p384::compress_p384(input)
72            .map_err(|_| AlgorithmError::InvalidKey(Algorithm::P384));
73    }
74
75    #[cfg(not(any(
76        all(
77            feature = "p384",
78            feature = "native",
79            not(all(feature = "wasm", target_arch = "wasm32"))
80        ),
81        all(feature = "p384", feature = "wasm", target_arch = "wasm32")
82    )))]
83    {
84        let _ = input;
85        Err(AlgorithmError::UnsupportedAlgorithm(Algorithm::P384))
86    }
87}
88
89#[cfg(feature = "p521")]
90fn compress_p521_public_key(input: &[u8]) -> Result<Vec<u8>, AlgorithmError> {
91    #[cfg(all(
92        feature = "p521",
93        feature = "native",
94        not(all(feature = "wasm", target_arch = "wasm32"))
95    ))]
96    {
97        return crypto_p521::compress_p521(input)
98            .map_err(|_| AlgorithmError::InvalidKey(Algorithm::P521));
99    }
100
101    #[cfg(all(feature = "p521", feature = "wasm", target_arch = "wasm32"))]
102    {
103        return crypto_p521::compress_p521(input)
104            .map_err(|_| AlgorithmError::InvalidKey(Algorithm::P521));
105    }
106
107    #[cfg(not(any(
108        all(
109            feature = "p521",
110            feature = "native",
111            not(all(feature = "wasm", target_arch = "wasm32"))
112        ),
113        all(feature = "p521", feature = "wasm", target_arch = "wasm32")
114    )))]
115    {
116        let _ = input;
117        Err(AlgorithmError::UnsupportedAlgorithm(Algorithm::P521))
118    }
119}
120
121#[cfg(any(feature = "p256", feature = "p384", feature = "p521"))]
122fn canonicalize_sec1_public_key(
123    alg: Algorithm,
124    public_key: &[u8],
125    compressed_len: usize,
126    uncompressed_len: usize,
127    raw_len: usize,
128    compress: fn(&[u8]) -> Result<Vec<u8>, AlgorithmError>,
129) -> Result<Vec<u8>, AlgorithmError> {
130    match public_key.len() {
131        len if len == compressed_len => {
132            crate::sec1_identity::validate_compressed_public_key(alg, public_key)?;
133            Ok(public_key.to_vec())
134        }
135        len if len == uncompressed_len => compress(public_key),
136        len if len == raw_len => {
137            let capacity = raw_len
138                .checked_add(1)
139                .ok_or(AlgorithmError::InvalidKey(alg))?;
140            let mut sec1 = Vec::with_capacity(capacity);
141            sec1.push(0x04);
142            sec1.extend_from_slice(public_key);
143            compress(&sec1)
144        }
145        _ => Err(AlgorithmError::InvalidKey(alg)),
146    }
147}
148
149/// Multicodec/multikey-encode a public key, canonicalizing P-256 to its
150/// compressed SEC1 form. Returns the `z`-prefixed multikey string.
151#[cfg(not(any(
152    feature = "ed25519",
153    feature = "x25519",
154    feature = "secp256k1",
155    feature = "p256",
156    feature = "p384",
157    feature = "p521",
158    feature = "ml-dsa-44",
159    feature = "ml-dsa-65",
160    feature = "ml-dsa-87",
161    feature = "ml-kem-512",
162    feature = "ml-kem-768",
163    feature = "ml-kem-1024"
164)))]
165pub fn public_key_to_multikey(alg: Algorithm, public_key: &[u8]) -> Result<String, AlgorithmError> {
166    let _ = public_key;
167    Err(AlgorithmError::UnsupportedAlgorithm(alg))
168}
169
170/// Multicodec/multikey-encode a public key, canonicalizing P-256 to its
171/// compressed SEC1 form. Returns the `z`-prefixed multikey string.
172#[cfg(any(
173    feature = "ed25519",
174    feature = "x25519",
175    feature = "secp256k1",
176    feature = "p256",
177    feature = "p384",
178    feature = "p521",
179    feature = "ml-dsa-44",
180    feature = "ml-dsa-65",
181    feature = "ml-dsa-87",
182    feature = "ml-kem-512",
183    feature = "ml-kem-768",
184    feature = "ml-kem-1024"
185))]
186pub fn public_key_to_multikey(alg: Algorithm, public_key: &[u8]) -> Result<String, AlgorithmError> {
187    let (codec, key_bytes) = match alg {
188        Algorithm::Ed25519 => {
189            #[cfg(feature = "ed25519")]
190            {
191                crypto_ed25519::validate_public_key_identity(public_key)
192                    .map_err(|_| AlgorithmError::InvalidKey(alg))?;
193                ("ed25519-pub", public_key.to_vec())
194            }
195            #[cfg(not(feature = "ed25519"))]
196            {
197                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
198            }
199        }
200        Algorithm::X25519 => {
201            #[cfg(feature = "x25519")]
202            {
203                if !crate::x25519_identity::is_canonical(public_key) {
204                    return Err(AlgorithmError::InvalidKey(alg));
205                }
206                ("x25519-pub", public_key.to_vec())
207            }
208            #[cfg(not(feature = "x25519"))]
209            {
210                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
211            }
212        }
213        Algorithm::Secp256k1 => {
214            #[cfg(feature = "secp256k1")]
215            {
216                crate::sec1_identity::validate_compressed_public_key(alg, public_key)?;
217                ("secp256k1-pub", public_key.to_vec())
218            }
219            #[cfg(not(feature = "secp256k1"))]
220            {
221                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
222            }
223        }
224        Algorithm::P384 => {
225            #[cfg(feature = "p384")]
226            {
227                (
228                    "p384-pub",
229                    canonicalize_sec1_public_key(
230                        alg,
231                        public_key,
232                        crypto_p384::P384_PUBLIC_KEY_COMPRESSED_LEN,
233                        crypto_p384::P384_PUBLIC_KEY_UNCOMPRESSED_LEN,
234                        crypto_p384::P384_PUBLIC_KEY_RAW_LEN,
235                        compress_p384_public_key,
236                    )?,
237                )
238            }
239            #[cfg(not(feature = "p384"))]
240            {
241                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
242            }
243        }
244        Algorithm::P521 => {
245            #[cfg(feature = "p521")]
246            {
247                (
248                    "p521-pub",
249                    canonicalize_sec1_public_key(
250                        alg,
251                        public_key,
252                        crypto_p521::P521_PUBLIC_KEY_COMPRESSED_LEN,
253                        crypto_p521::P521_PUBLIC_KEY_UNCOMPRESSED_LEN,
254                        crypto_p521::P521_PUBLIC_KEY_RAW_LEN,
255                        compress_p521_public_key,
256                    )?,
257                )
258            }
259            #[cfg(not(feature = "p521"))]
260            {
261                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
262            }
263        }
264        Algorithm::MlDsa44 => {
265            #[cfg(feature = "ml-dsa-44")]
266            {
267                ("mldsa-44-pub", public_key.to_vec())
268            }
269            #[cfg(not(feature = "ml-dsa-44"))]
270            {
271                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
272            }
273        }
274        Algorithm::MlDsa65 => {
275            #[cfg(feature = "ml-dsa-65")]
276            {
277                ("mldsa-65-pub", public_key.to_vec())
278            }
279            #[cfg(not(feature = "ml-dsa-65"))]
280            {
281                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
282            }
283        }
284        Algorithm::MlDsa87 => {
285            #[cfg(feature = "ml-dsa-87")]
286            {
287                ("mldsa-87-pub", public_key.to_vec())
288            }
289            #[cfg(not(feature = "ml-dsa-87"))]
290            {
291                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
292            }
293        }
294        Algorithm::SlhDsaSha2_128s => {
295            return Err(AlgorithmError::UnsupportedAlgorithm(alg));
296        }
297        Algorithm::MlKem512 => {
298            #[cfg(feature = "ml-kem-512")]
299            {
300                ("mlkem-512-pub", public_key.to_vec())
301            }
302            #[cfg(not(feature = "ml-kem-512"))]
303            {
304                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
305            }
306        }
307        Algorithm::MlKem768 => {
308            #[cfg(feature = "ml-kem-768")]
309            {
310                ("mlkem-768-pub", public_key.to_vec())
311            }
312            #[cfg(not(feature = "ml-kem-768"))]
313            {
314                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
315            }
316        }
317        Algorithm::MlKem1024 => {
318            #[cfg(feature = "ml-kem-1024")]
319            {
320                ("mlkem-1024-pub", public_key.to_vec())
321            }
322            #[cfg(not(feature = "ml-kem-1024"))]
323            {
324                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
325            }
326        }
327        Algorithm::XWing768 => {
328            return Err(AlgorithmError::UnsupportedAlgorithm(alg));
329        }
330
331        // Canonicalization step: P-256 must be compressed
332        Algorithm::P256 => {
333            #[cfg(not(feature = "p256"))]
334            {
335                return Err(AlgorithmError::UnsupportedAlgorithm(alg));
336            }
337
338            #[cfg(feature = "p256")]
339            {
340                let compressed = canonicalize_sec1_public_key(
341                    alg,
342                    public_key,
343                    33,
344                    65,
345                    64,
346                    compress_p256_public_key,
347                )?;
348
349                ("p256-pub", compressed)
350            }
351        }
352    };
353
354    encode_multikey(codec, &key_bytes).map_err(|_| AlgorithmError::InvalidKey(alg))
355}