Skip to main content

codec_multikey/
encode.rs

1// SPDX-FileCopyrightText: 2026 ReallyMe LLC
2//
3// SPDX-License-Identifier: MIT OR Apache-2.0
4
5use codec_multibase::bytes_to_multibase58btc;
6use codec_multicodec::{KeyLength, KeyMaterialKind, MULTICODEC_TABLE};
7
8use crate::error::{classify_multikey_codec, CodecNameReason, MultikeyError};
9
10/// Maximum accepted RSA public-key DER payload in a multikey.
11///
12/// The supported RSA multicodec is variable-length because DER size depends on
13/// modulus size and encoding details. A 4 KiB cap leaves room for common public
14/// keys while preventing unbounded base58 and FFI amplification through RSA.
15pub const MAX_RSA_PUBLIC_KEY_DER_LEN: usize = 4 * 1024;
16
17/// Encodes a public key as a multibase (base58btc) multikey string.
18///
19/// Fails closed: returns an error if the codec name is unknown or the key
20/// length does not match the codec.
21pub fn encode_multikey(codec_name: &str, public_key: &[u8]) -> Result<String, MultikeyError> {
22    let (canonical_codec_name, spec) = MULTICODEC_TABLE
23        .iter()
24        .find(|(name, _)| *name == codec_name)
25        .ok_or(MultikeyError::UnknownCodecName {
26            reason: CodecNameReason::Unsupported,
27        })?;
28
29    if spec.key_material != KeyMaterialKind::PublicKey {
30        return Err(MultikeyError::UnknownCodecName {
31            reason: CodecNameReason::Unsupported,
32        });
33    }
34
35    match spec.key_length {
36        KeyLength::Fixed(expected) if public_key.len() != expected => {
37            return Err(MultikeyError::KeyLengthMismatch {
38                codec: classify_multikey_codec(canonical_codec_name),
39                expected,
40                actual: public_key.len(),
41            });
42        }
43        KeyLength::Variable if public_key.is_empty() => return Err(MultikeyError::EmptyKey),
44        KeyLength::NotApplicable => {
45            return Err(MultikeyError::UnknownCodecName {
46                reason: CodecNameReason::Unsupported,
47            });
48        }
49        KeyLength::Fixed(_) | KeyLength::Variable => {}
50    }
51
52    if *canonical_codec_name == "rsa-pub" && public_key.len() > MAX_RSA_PUBLIC_KEY_DER_LEN {
53        return Err(MultikeyError::KeyTooLarge {
54            codec: classify_multikey_codec(canonical_codec_name),
55            max: MAX_RSA_PUBLIC_KEY_DER_LEN,
56            actual: public_key.len(),
57        });
58    }
59
60    if matches!(
61        *canonical_codec_name,
62        "p256-pub" | "p384-pub" | "p521-pub" | "secp256k1-pub"
63    ) && !matches!(public_key.first(), Some(0x02 | 0x03))
64    {
65        return Err(MultikeyError::InvalidCompressedPoint);
66    }
67
68    let capacity = spec
69        .codec
70        .len()
71        .checked_add(public_key.len())
72        .ok_or(MultikeyError::EncodedPayloadTooLarge)?;
73    let mut payload = Vec::with_capacity(capacity);
74    payload.extend_from_slice(spec.codec);
75    payload.extend_from_slice(public_key);
76
77    bytes_to_multibase58btc(&payload).map_err(|_| MultikeyError::EncodedPayloadTooLarge)
78}