Skip to main content

codec_multikey/
parse.rs

1// SPDX-FileCopyrightText: 2026 ReallyMe LLC
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use codec_multibase::multibase_to_bytes;
6use codec_multicodec::{lookup_codec_prefix, KeyLength, KeyMaterialKind};
7
8use crate::encode::MAX_RSA_PUBLIC_KEY_DER_LEN;
9use crate::error::{classify_multikey_codec, MultikeyError};
10use zeroize::Zeroizing;
11
12/// A multikey decoded into its codec metadata and raw public key bytes.
13///
14/// Its metadata cannot be replaced after parsing: binding validation relies
15/// on the codec and algorithm being the ones established by the parser.
16///
17/// ```compile_fail
18/// use codec_multikey::parse_multikey;
19///
20/// if let Ok(mut parsed) = parse_multikey("invalid") {
21///     parsed.alg = "Ed25519";
22/// }
23/// ```
24pub struct ParsedMultikey {
25    /// Canonical multicodec name of the key type (e.g. `ed25519-pub`).
26    codec_name: &'static str,
27    /// Human-readable algorithm name implied by the codec (e.g. `Ed25519`).
28    alg: &'static str,
29    /// Raw public key bytes with the multicodec prefix stripped.
30    public_key: Vec<u8>,
31    /// Expected public key length for the codec.
32    key_length: KeyLength,
33}
34
35impl ParsedMultikey {
36    /// Returns the canonical multicodec name established by parsing.
37    #[must_use]
38    pub const fn codec_name(&self) -> &'static str {
39        self.codec_name
40    }
41
42    /// Returns the algorithm name associated with the parsed codec.
43    #[must_use]
44    pub const fn algorithm_name(&self) -> &'static str {
45        self.alg
46    }
47
48    /// Borrows the validated public key bytes.
49    #[must_use]
50    pub fn public_key(&self) -> &[u8] {
51        &self.public_key
52    }
53
54    /// Returns the length policy associated with the parsed codec.
55    #[must_use]
56    pub const fn key_length(&self) -> KeyLength {
57        self.key_length
58    }
59
60    /// Transfers ownership of the validated public key bytes.
61    #[must_use]
62    pub fn into_public_key(self) -> Vec<u8> {
63        self.public_key
64    }
65}
66
67/// Parses a multibase-encoded multikey string into its codec and key bytes.
68///
69/// Fails closed: returns an error on non-canonical multibase, unknown codec
70/// prefix, or a key length that does not match the codec.
71pub fn parse_multikey(multibase_key: &str) -> Result<ParsedMultikey, MultikeyError> {
72    if multibase_key.len() < 2 {
73        return Err(MultikeyError::InvalidMultibase);
74    }
75    // Multikey has a single canonical string form: base58btc (`z`).
76    // Rejecting alternate multibase alphabets prevents duplicate encodings of
77    // the same key from bypassing string-based blocklists or deduplication.
78    if !multibase_key.starts_with('z') {
79        return Err(MultikeyError::InvalidMultibase);
80    }
81
82    // 1) multibase decode
83    let raw = Zeroizing::new(
84        multibase_to_bytes(multibase_key).map_err(|_| MultikeyError::InvalidMultibase)?,
85    );
86
87    if raw.len() < 2 {
88        return Err(MultikeyError::DecodedTooShort(raw.len()));
89    }
90
91    // 2) multicodec prefix lookup
92    let found = lookup_codec_prefix(&raw).ok_or(MultikeyError::UnknownCodecPrefix)?;
93
94    if found.key_material != KeyMaterialKind::PublicKey {
95        return Err(MultikeyError::NonPublicKeyMaterial);
96    }
97
98    let public_key = raw[found.codec.len()..].to_vec();
99
100    // 3) key length validation
101    match found.key_length {
102        KeyLength::Fixed(expected) if public_key.len() != expected => {
103            return Err(MultikeyError::KeyLengthMismatch {
104                codec: classify_multikey_codec(found.name),
105                expected,
106                actual: public_key.len(),
107            });
108        }
109        KeyLength::Variable if public_key.is_empty() => return Err(MultikeyError::EmptyKey),
110        KeyLength::NotApplicable => return Err(MultikeyError::NonPublicKeyMaterial),
111        KeyLength::Fixed(_) | KeyLength::Variable => {}
112    }
113
114    if found.name == "rsa-pub" && public_key.len() > MAX_RSA_PUBLIC_KEY_DER_LEN {
115        return Err(MultikeyError::KeyTooLarge {
116            codec: classify_multikey_codec(found.name),
117            max: MAX_RSA_PUBLIC_KEY_DER_LEN,
118            actual: public_key.len(),
119        });
120    }
121
122    if matches!(
123        found.name,
124        "p256-pub" | "p384-pub" | "p521-pub" | "secp256k1-pub"
125    ) && !matches!(public_key.first(), Some(0x02 | 0x03))
126    {
127        return Err(MultikeyError::InvalidCompressedPoint);
128    }
129
130    Ok(ParsedMultikey {
131        codec_name: found.name,
132        alg: found.alg,
133        public_key,
134        key_length: found.key_length,
135    })
136}