cli/key/external_key/
rsa.rs

1// SPDX-License-Identifier: GPL-3-0-or-later
2// Copyright (c) 2025 Opinsys Oy
3
4#![allow(clippy::no_effect_underscore_binding)]
5
6use crate::key::{external_key::ExternalKey, KeyError};
7use num_bigint::{BigUint, ToBigInt};
8use num_traits::ToPrimitive;
9use rasn::{
10    types::{Integer, SequenceOf},
11    AsnType, Decode, Decoder, Encode,
12};
13use rsa::{traits::PublicKeyParts, RsaPrivateKey};
14use tpm2_protocol::data::{
15    Tpm2bDigest, Tpm2bPublicKeyRsa, TpmAlgId, TpmaObject, TpmsRsaParms, TpmsSchemeHash,
16    TpmtRsaScheme, TpmtSymDefObject, TpmuAsymScheme, TpmuPublicId, TpmuPublicParms,
17};
18
19#[derive(AsnType, Decode, Encode, Debug)]
20pub struct OtherPrimeInfo {
21    pub prime: Integer,
22    pub exponent: Integer,
23    pub coefficient: Integer,
24}
25
26/// A struct representing the full 9-field PKCS#1 `RSAPrivateKey` structure.
27#[allow(clippy::no_effect_underscore_binding)]
28#[derive(AsnType, Decode, Encode, Debug)]
29pub struct RsaPrivateKeyAsn1 {
30    pub version: Integer,
31    pub modulus: Integer,
32    pub public_exponent: Integer,
33    pub private_exponent: Integer,
34    pub prime1: Integer,
35    pub prime2: Integer,
36    pub exponent1: Integer,
37    pub exponent2: Integer,
38    pub coefficient: Integer,
39    pub other_prime_infos: Option<SequenceOf<OtherPrimeInfo>>,
40}
41
42/// A struct representing an 8-field PKCS#1 `RSAPrivateKey` structure,
43/// for compatibility with encoders that omit the `version` field when it is 0.
44#[allow(clippy::no_effect_underscore_binding)]
45#[derive(AsnType, Decode, Encode, Debug)]
46pub struct RsaPrivateKeyPkcs1V0 {
47    pub modulus: Integer,
48    pub public_exponent: Integer,
49    pub private_exponent: Integer,
50    pub prime1: Integer,
51    pub prime2: Integer,
52    pub exponent1: Integer,
53    pub exponent2: Integer,
54    pub coefficient: Integer,
55    pub other_prime_infos: Option<SequenceOf<OtherPrimeInfo>>,
56}
57
58/// Builds an `ExternalKey` by deriving it from the public exponent and prime factors.
59fn build_external_key_from_primes(
60    public_exponent: &Integer,
61    prime1: &Integer,
62    prime2: &Integer,
63) -> Result<ExternalKey, KeyError> {
64    let e_num = public_exponent
65        .to_bigint()
66        .ok_or(KeyError::InvalidFormat)?
67        .to_biguint()
68        .ok_or(KeyError::InvalidFormat)?;
69    let p_num = prime1
70        .to_bigint()
71        .ok_or(KeyError::InvalidFormat)?
72        .to_biguint()
73        .ok_or(KeyError::InvalidFormat)?;
74    let q_num = prime2
75        .to_bigint()
76        .ok_or(KeyError::InvalidFormat)?
77        .to_biguint()
78        .ok_or(KeyError::InvalidFormat)?;
79
80    if e_num != BigUint::from(65537u32) {
81        return Err(KeyError::ValueConversionFailed(
82            "unsupported RSA exponent: must be 65537".to_string(),
83        ));
84    }
85
86    let e = rsa::BigUint::from_bytes_be(&e_num.to_bytes_be());
87    let p = rsa::BigUint::from_bytes_be(&p_num.to_bytes_be());
88    let q = rsa::BigUint::from_bytes_be(&q_num.to_bytes_be());
89
90    let key = RsaPrivateKey::from_p_q(p, q, e).map_err(|_| KeyError::InvalidFormat)?;
91
92    match key.size() * 8 {
93        2048 => Ok(ExternalKey::Rsa2048(Box::new(key))),
94        3072 => Ok(ExternalKey::Rsa3072(Box::new(key))),
95        4096 => Ok(ExternalKey::Rsa4096(Box::new(key))),
96        bits => Err(KeyError::ValueConversionFailed(format!(
97            "invalid RSA key size: {bits}"
98        ))),
99    }
100}
101
102/// Parses a PKCS#1 DER-encoded RSA private key with fallback logic.
103fn parse_pkcs1_rsa_from_der(der_bytes: &[u8]) -> Result<ExternalKey, KeyError> {
104    if let Ok(pkcs1_key) = rasn::der::decode::<RsaPrivateKeyAsn1>(der_bytes) {
105        let version = pkcs1_key.version.to_u8().ok_or(KeyError::InvalidFormat)?;
106        if version != 0 || pkcs1_key.other_prime_infos.is_some() {
107            return Err(KeyError::UnsupportedFileFormat);
108        }
109        return build_external_key_from_primes(
110            &pkcs1_key.public_exponent,
111            &pkcs1_key.prime1,
112            &pkcs1_key.prime2,
113        );
114    }
115
116    if let Ok(pkcs1_v0_key) = rasn::der::decode::<RsaPrivateKeyPkcs1V0>(der_bytes) {
117        if pkcs1_v0_key.other_prime_infos.is_some() {
118            return Err(KeyError::UnsupportedFileFormat);
119        }
120        return build_external_key_from_primes(
121            &pkcs1_v0_key.public_exponent,
122            &pkcs1_v0_key.prime1,
123            &pkcs1_v0_key.prime2,
124        );
125    }
126
127    Err(KeyError::InvalidFormat)
128}
129
130/// Parses a DER-encoded RSA private key, supporting only the PKCS#1 format.
131///
132/// # Errors
133///
134/// Returns a `KeyError` if the DER data is malformed or the key parameters are unsupported.
135pub fn parse_rsa_from_der(der_bytes: &[u8]) -> Result<ExternalKey, KeyError> {
136    parse_pkcs1_rsa_from_der(der_bytes)
137}
138
139/// Converts an `RsaPrivateKey` to a `TpmtPublic` structure.
140///
141/// # Errors
142///
143/// Returns a `KeyError` if the key's public modulus cannot be converted to the
144/// `Tpm2bPublicKeyRsa` type.
145pub fn rsa_to_public(
146    key: &RsaPrivateKey,
147    key_bits: u16,
148    hash_alg: TpmAlgId,
149    symmetric: TpmtSymDefObject,
150) -> Result<tpm2_protocol::data::TpmtPublic, KeyError> {
151    Ok(tpm2_protocol::data::TpmtPublic {
152        object_type: TpmAlgId::Rsa,
153        name_alg: hash_alg,
154        object_attributes: TpmaObject::USER_WITH_AUTH | TpmaObject::DECRYPT,
155        auth_policy: Tpm2bDigest::default(),
156        parameters: TpmuPublicParms::Rsa(TpmsRsaParms {
157            symmetric,
158            scheme: TpmtRsaScheme {
159                scheme: TpmAlgId::Oaep,
160                details: TpmuAsymScheme::Any(TpmsSchemeHash { hash_alg }),
161            },
162            key_bits,
163            exponent: 0,
164        }),
165        unique: TpmuPublicId::Rsa(
166            Tpm2bPublicKeyRsa::try_from(key.n().to_bytes_be().as_slice())
167                .map_err(|e| KeyError::ValueConversionFailed(e.to_string()))?,
168        ),
169    })
170}