Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 43 additions & 26 deletions src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use core::fmt;
use core::hash::{Hash, Hasher};

use crypto_bigint::modular::{BoxedMontyForm, BoxedMontyParams};
use crypto_bigint::{BoxedUint, Integer, NonZero, Odd, Resize};
use crypto_bigint::{
BoxedUint, Integer, Monty, NonZero, Odd, Resize, Unsigned, U2048, U3072, U4096,
};
use rand_core::CryptoRng;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[cfg(feature = "serde")]
Expand All @@ -27,18 +29,28 @@ use crate::traits::{PaddingScheme, SignatureScheme};

/// Represents the public part of an RSA key.
#[derive(Debug, Clone)]
pub struct RsaPublicKey {
pub struct GenericRsaPublicKey<U: Unsigned> {
/// Modulus: product of prime numbers `p` and `q`
n: NonZero<BoxedUint>,
n: NonZero<U>,
/// Public exponent: power to which a plaintext message is raised in
/// order to encrypt it.
///
/// Typically `0x10001` (`65537`)
e: BoxedUint,
e: U,
Copy link
Contributor

@pinkforest pinkforest Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If everything is U which could be optimized smaller size we end up taking n+e+d+p+q U * 3 + (U * primes) size in stack

In case of Rsa4096 it would be 512 * 5 (~4 kB) assuming just p and q kept plus Precomputed copies..

Having it as enum would allow different sizes and then requiring dispatch through that though like there is already the litany of U-type alias and you will have another litany of enum wrapping again all of them :)

Copy link
Member Author

@tarcieri tarcieri Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An enum would monomorphize the code for all sizes up front. A generic lets you pick a specific size and it just monomorphizes for that.

An enum would also be as large in memory as the largest supported size.

Copy link
Contributor

@pinkforest pinkforest Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yes. Associated type/s wouldn't monomorphize between them though and allows T bounds.

Another is to have two/three different generics - would get the lowest hanging fruit at least 🤷‍♀️

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe HeaplessUint<N> ? RustCrypto/crypto-bigint#1148

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of using U, an options i something based on LIMBS:

impl<
    const LIMBS: usize,
    const LIMBS_HALF: usize,
> ... where
    // LIMBS = LIMBS_HALF * LIMBS_HALF
    Uint<LIMBS>: Split<Output = Uint<LIMBS_HALF>>,
    Uint<LIMBS>: SplitMixed<Uint<LIMBS_HALF>, Uint<LIMBS_HALF>>,
    Uint<LIMBS_HALF>: ConcatMixed<Uint<LIMBS_HALF>, MixedOutput = Uint<LIMBS>>,
{

where smaller primes can be expressed, in standard 2 prime RSA keys for example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have one that's generic around U, you can easily make one that's generic around LIMBS with a type alias:

pub type FixedRsaPublicKey<const LIMBS: usize> = GenericRsaPublicKey<Uint<LIMBS>>;

Copy link
Contributor

@pinkforest pinkforest Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw Why do we need a holder struct? Can't it be just provided as trait impl over hazmat over fixed size well known "safe" variants e.g. RSA2048, 3072, 4096 etc. that implements the validated component trait and which impls can be gated around what RSA sizes people need/want ? Associated types allows multiple types and even the provided methods could be used to check between them

Bonus is only one generic needs to be dragged around that needs it instead of multiple.


n_params: BoxedMontyParams,
n_params: <U::Monty as Monty>::Params,
}

/// RSA private key using dynamically sized heap-allocated integers for backing storage.
pub type RsaPublicKey = GenericRsaPublicKey<BoxedUint>;

/// RSA-2048 public key (stack-allocated).
pub type Rsa2048PublicKey = GenericRsaPublicKey<U2048>;
/// RSA-3072 public key (stack-allocated).
pub type Rsa3072PublicKey = GenericRsaPublicKey<U3072>;
/// RSA-4096 public key (stack-allocated).
pub type Rsa4096PublicKey = GenericRsaPublicKey<U4096>;

impl Eq for RsaPublicKey {}

impl PartialEq for RsaPublicKey {
Expand All @@ -60,17 +72,27 @@ impl Hash for RsaPublicKey {

/// Represents a whole RSA key, public and private parts.
#[derive(Clone)]
pub struct RsaPrivateKey {
pub struct GenericRsaPrivateKey<U: Unsigned + Zeroize> {
/// Public components of the private key.
pubkey_components: RsaPublicKey,
pubkey_components: GenericRsaPublicKey<U>,
/// Private exponent
pub(crate) d: BoxedUint,
pub(crate) d: U,
/// Prime factors of N, contains >= 2 elements.
pub(crate) primes: Vec<BoxedUint>,
pub(crate) primes: Vec<U>,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to think if we represent MP-RSA in here given Vec now

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oof yeah, that needs a solution.

Maybe a helper trait? I can't think of a particularly good one here. You can probably reasonably cap the number of primes at a small number but if they're stack allocated it would still waste a lot of space.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally some type of borrowing injection mechanism that could f.ex. borrow already parsed PKCS#8 data that can be extended to other containers given this whole thing is very itsy bits?

/// Precomputed values to speed up private operations
pub(crate) precomputed: Option<PrecomputedValues>,
pub(crate) precomputed: Option<GenericPrecomputedValues<U>>,
Copy link
Contributor

@pinkforest pinkforest Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option is problematic given it takes it's largest layout in memory regardless of it being None leading to using memory if the precomputed is not used in case that type of variant is to be ever supported in no-alloc.

Keeping up the precomputed stuff is essentially copies of stuff

}

/// RSA private key using dynamically sized heap-allocated integers for backing storage.
pub type RsaPrivateKey = GenericRsaPrivateKey<BoxedUint>;

/// RSA-2048 private key (stack-allocated).
pub type Rsa2048PrivateKey = GenericRsaPrivateKey<U2048>;
/// RSA-3072 private key (stack-allocated).
pub type Rsa3072PrivateKey = GenericRsaPrivateKey<U3072>;
/// RSA-4096 private key (stack-allocated).
pub type Rsa4096PrivateKey = GenericRsaPrivateKey<U4096>;

impl fmt::Debug for RsaPrivateKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let precomputed = if self.precomputed.is_some() {
Expand Down Expand Up @@ -111,35 +133,36 @@ impl Hash for RsaPrivateKey {
}
}

impl Drop for RsaPrivateKey {
impl<U: Unsigned + Zeroize> Drop for GenericRsaPrivateKey<U> {
fn drop(&mut self) {
self.d.zeroize();
self.primes.zeroize();
self.precomputed.zeroize();
}
}

impl ZeroizeOnDrop for RsaPrivateKey {}

#[derive(Clone)]
pub(crate) struct PrecomputedValues {
pub(crate) struct GenericPrecomputedValues<U: Unsigned + Zeroize> {
/// D mod (P-1)
pub(crate) dp: BoxedUint,
pub(crate) dp: U,
/// D mod (Q-1)
pub(crate) dq: BoxedUint,
pub(crate) dq: U,
/// Q^-1 mod P
pub(crate) qinv: BoxedMontyForm,
pub(crate) qinv: U::Monty,

/// Montgomery params for `p`
pub(crate) p_params: BoxedMontyParams,
pub(crate) p_params: <U::Monty as Monty>::Params,
/// Montgomery params for `q`
pub(crate) q_params: BoxedMontyParams,
pub(crate) q_params: <U::Monty as Monty>::Params,
}

pub(crate) type PrecomputedValues = GenericPrecomputedValues<BoxedUint>;

impl ZeroizeOnDrop for PrecomputedValues {}

impl Zeroize for PrecomputedValues {
fn zeroize(&mut self) {
impl<U: Unsigned + Zeroize> Drop for GenericPrecomputedValues<U> {
fn drop(&mut self) {
self.dp.zeroize();
self.dq.zeroize();
// TODO: once these have landed in crypto-bigint
Expand All @@ -148,12 +171,6 @@ impl Zeroize for PrecomputedValues {
}
}

impl Drop for PrecomputedValues {
fn drop(&mut self) {
self.zeroize();
}
}

impl From<RsaPrivateKey> for RsaPublicKey {
fn from(private_key: RsaPrivateKey) -> Self {
(&private_key).into()
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,10 @@ pub use sha2;

pub use crate::{
errors::{Error, Result},
key::{RsaPrivateKey, RsaPublicKey},
key::{
Rsa2048PrivateKey, Rsa2048PublicKey, Rsa3072PrivateKey, Rsa3072PublicKey,
Rsa4096PrivateKey, Rsa4096PublicKey, RsaPrivateKey, RsaPublicKey,
},
oaep::Oaep,
pkcs1v15::{Pkcs1v15Encrypt, Pkcs1v15Sign},
pss::Pss,
Expand Down