use std::fmt::{self, Debug};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop};
use crate::encoding::Base64UrlBytes;
use crate::error::{IncompatibleKeyError, InvalidKeyError, Result};
fn validate_base64url_uint(value: &Base64UrlBytes, name: &str) -> Result<()> {
let bytes = value.as_bytes();
if bytes.is_empty() {
return Err(InvalidKeyError::InvalidParameter {
name: "Base64urlUInt",
reason: format!(
"RFC 7518: '{}' must be a non-empty Base64urlUInt value",
name
),
}
.into());
}
if bytes.len() > 1 && bytes[0] == 0 {
return Err(InvalidKeyError::InvalidParameter {
name: "Base64urlUInt",
reason: format!(
"RFC 7518: '{}' has non-canonical Base64urlUInt encoding (leading zero bytes)",
name
),
}
.into());
}
Ok(())
}
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
#[non_exhaustive]
pub struct RsaOtherPrime {
pub r: Base64UrlBytes,
pub d: Base64UrlBytes,
pub t: Base64UrlBytes,
}
impl RsaOtherPrime {
pub fn new(r: Base64UrlBytes, d: Base64UrlBytes, t: Base64UrlBytes) -> Self {
Self { r, d, t }
}
pub fn validate(&self) -> Result<()> {
if self.r.is_empty() {
return Err(InvalidKeyError::MissingParameter("oth.r").into());
}
if self.d.is_empty() {
return Err(InvalidKeyError::MissingParameter("oth.d").into());
}
if self.t.is_empty() {
return Err(InvalidKeyError::MissingParameter("oth.t").into());
}
validate_base64url_uint(&self.r, "oth.r")?;
validate_base64url_uint(&self.d, "oth.d")?;
validate_base64url_uint(&self.t, "oth.t")?;
Ok(())
}
}
impl Debug for RsaOtherPrime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RsaOtherPrime")
.field("r", &format!("[{} bytes]", self.r.len()))
.field("d", &"[REDACTED]")
.field("t", &"[REDACTED]")
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
#[non_exhaustive]
pub struct RsaParams {
pub n: Base64UrlBytes,
pub e: Base64UrlBytes,
#[serde(skip_serializing_if = "Option::is_none")]
pub d: Option<Base64UrlBytes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub p: Option<Base64UrlBytes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub q: Option<Base64UrlBytes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dp: Option<Base64UrlBytes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dq: Option<Base64UrlBytes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub qi: Option<Base64UrlBytes>,
#[serde(skip_serializing_if = "Option::is_none")]
pub oth: Option<Vec<RsaOtherPrime>>,
}
impl RsaParams {
#[must_use]
pub fn new_public(n: Base64UrlBytes, e: Base64UrlBytes) -> Self {
Self {
n,
e,
d: None,
p: None,
q: None,
dp: None,
dq: None,
qi: None,
oth: None,
}
}
#[must_use]
pub fn builder(n: Base64UrlBytes, e: Base64UrlBytes, d: Base64UrlBytes) -> RsaParamsBuilder {
RsaParamsBuilder::new(n, e, d)
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new_private(
n: Base64UrlBytes,
e: Base64UrlBytes,
d: Base64UrlBytes,
p: Option<Base64UrlBytes>,
q: Option<Base64UrlBytes>,
dp: Option<Base64UrlBytes>,
dq: Option<Base64UrlBytes>,
qi: Option<Base64UrlBytes>,
) -> Self {
Self {
n,
e,
d: Some(d),
p,
q,
dp,
dq,
qi,
oth: None,
}
}
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new_multi_prime(
n: Base64UrlBytes,
e: Base64UrlBytes,
d: Base64UrlBytes,
p: Base64UrlBytes,
q: Base64UrlBytes,
dp: Base64UrlBytes,
dq: Base64UrlBytes,
qi: Base64UrlBytes,
oth: Vec<RsaOtherPrime>,
) -> Self {
Self {
n,
e,
d: Some(d),
p: Some(p),
q: Some(q),
dp: Some(dp),
dq: Some(dq),
qi: Some(qi),
oth: Some(oth),
}
}
pub fn is_public_key_only(&self) -> bool {
self.d.is_none()
}
pub fn has_private_key(&self) -> bool {
self.d.is_some()
}
pub fn key_size_bits(&self) -> usize {
let bytes = self.n.as_bytes();
if bytes.is_empty() {
return 0;
}
let mut idx = 0;
while idx < bytes.len() && bytes[idx] == 0 {
idx += 1;
}
if idx == bytes.len() {
return 0;
}
let first = bytes[idx];
let leading_zeros = first.leading_zeros() as usize;
((bytes.len() - idx - 1) * 8) + (8 - leading_zeros)
}
pub fn validate(&self) -> Result<()> {
if self.n.is_empty() {
return Err(InvalidKeyError::MissingParameter("n").into());
}
if self.e.is_empty() {
return Err(InvalidKeyError::MissingParameter("e").into());
}
validate_base64url_uint(&self.n, "n")?;
validate_base64url_uint(&self.e, "e")?;
if self.n.as_bytes().iter().all(|&b| b == 0) {
return Err(InvalidKeyError::InvalidParameter {
name: "n",
reason: "RSA modulus must be non-zero".to_string(),
}
.into());
}
let e_bytes = self.e.as_bytes();
let is_odd = (e_bytes[e_bytes.len() - 1] & 1) == 1;
let ge_three = if e_bytes.len() > 1 {
true
} else {
e_bytes[0] >= 3
};
if !ge_three || !is_odd {
return Err(InvalidKeyError::InvalidParameter {
name: "e",
reason: "RSA public exponent must be odd and >= 3".to_string(),
}
.into());
}
if let Some(ref d) = self.d {
validate_base64url_uint(d, "d")?;
}
if let Some(ref p) = self.p {
validate_base64url_uint(p, "p")?;
}
if let Some(ref q) = self.q {
validate_base64url_uint(q, "q")?;
}
if let Some(ref dp) = self.dp {
validate_base64url_uint(dp, "dp")?;
}
if let Some(ref dq) = self.dq {
validate_base64url_uint(dq, "dq")?;
}
if let Some(ref qi) = self.qi {
validate_base64url_uint(qi, "qi")?;
}
if self.d.is_some() {
let has_crt = self.p.is_some()
|| self.q.is_some()
|| self.dp.is_some()
|| self.dq.is_some()
|| self.qi.is_some();
let has_all_crt = self.p.is_some()
&& self.q.is_some()
&& self.dp.is_some()
&& self.dq.is_some()
&& self.qi.is_some();
if has_crt && !has_all_crt {
return Err(InvalidKeyError::InconsistentParameters(
"RSA CRT parameters must all be present or all be absent".to_string(),
)
.into());
}
if self.oth.is_some() && !has_all_crt {
return Err(InvalidKeyError::InconsistentParameters(
"RSA 'oth' parameter requires all CRT parameters (p, q, dp, dq, qi)"
.to_string(),
)
.into());
}
if let Some(ref oth) = self.oth {
if oth.is_empty() {
return Err(InvalidKeyError::InvalidParameter {
name: "oth",
reason: "RFC 7518: 'oth' must contain one or more entries when present"
.to_string(),
}
.into());
}
for (i, prime) in oth.iter().enumerate() {
prime.validate().map_err(|e| match e {
crate::Error::InvalidKey(source) => InvalidKeyError::InvalidOtherPrime {
index: i,
source: Box::new(source),
},
_ => {
unreachable!("RsaOtherPrime::validate always yields Error::InvalidKey")
}
})?;
}
}
} else {
if self.p.is_some()
|| self.q.is_some()
|| self.dp.is_some()
|| self.dq.is_some()
|| self.qi.is_some()
|| self.oth.is_some()
{
return Err(InvalidKeyError::InconsistentParameters(
"CRT parameters present without private exponent d".to_string(),
)
.into());
}
}
Ok(())
}
pub fn validate_key_size(&self, min_bits: usize) -> Result<()> {
let actual_bits = self.key_size_bits();
if actual_bits < min_bits {
return Err(IncompatibleKeyError::InsufficientKeyStrength {
minimum_bits: min_bits,
actual_bits,
context: "RSA modulus",
}
.into());
}
Ok(())
}
pub fn is_multi_prime(&self) -> bool {
self.oth.is_some()
}
#[must_use]
pub fn to_public(&self) -> Self {
Self {
n: self.n.clone(),
e: self.e.clone(),
d: None,
p: None,
q: None,
dp: None,
dq: None,
qi: None,
oth: None,
}
}
}
#[derive(Clone, Debug)]
pub struct RsaParamsBuilder {
n: Base64UrlBytes,
e: Base64UrlBytes,
d: Base64UrlBytes,
p: Option<Base64UrlBytes>,
q: Option<Base64UrlBytes>,
dp: Option<Base64UrlBytes>,
dq: Option<Base64UrlBytes>,
qi: Option<Base64UrlBytes>,
oth: Option<Vec<RsaOtherPrime>>,
}
impl RsaParamsBuilder {
#[must_use]
fn new(n: Base64UrlBytes, e: Base64UrlBytes, d: Base64UrlBytes) -> Self {
Self {
n,
e,
d,
p: None,
q: None,
dp: None,
dq: None,
qi: None,
oth: None,
}
}
#[must_use]
pub fn p(mut self, p: Base64UrlBytes) -> Self {
self.p = Some(p);
self
}
#[must_use]
pub fn q(mut self, q: Base64UrlBytes) -> Self {
self.q = Some(q);
self
}
#[must_use]
pub fn dp(mut self, dp: Base64UrlBytes) -> Self {
self.dp = Some(dp);
self
}
#[must_use]
pub fn dq(mut self, dq: Base64UrlBytes) -> Self {
self.dq = Some(dq);
self
}
#[must_use]
pub fn qi(mut self, qi: Base64UrlBytes) -> Self {
self.qi = Some(qi);
self
}
#[must_use]
pub fn crt(
mut self,
p: Base64UrlBytes,
q: Base64UrlBytes,
dp: Base64UrlBytes,
dq: Base64UrlBytes,
qi: Base64UrlBytes,
) -> Self {
self.p = Some(p);
self.q = Some(q);
self.dp = Some(dp);
self.dq = Some(dq);
self.qi = Some(qi);
self
}
#[must_use]
pub fn oth(mut self, oth: Vec<RsaOtherPrime>) -> Self {
self.oth = Some(oth);
self
}
#[must_use]
pub fn build(self) -> RsaParams {
RsaParams {
n: self.n,
e: self.e,
d: Some(self.d),
p: self.p,
q: self.q,
dp: self.dp,
dq: self.dq,
qi: self.qi,
oth: self.oth,
}
}
}
impl Debug for RsaParams {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RsaParams")
.field("n", &format!("[{} bytes]", self.n.len()))
.field("e", &self.e)
.field("has_private_key", &self.has_private_key())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_public_key_only() {
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3]),
Base64UrlBytes::new(vec![1, 0, 1]),
);
assert!(params.is_public_key_only());
assert!(!params.has_private_key());
}
#[test]
fn test_validate_empty_modulus() {
let params =
RsaParams::new_public(Base64UrlBytes::new(vec![]), Base64UrlBytes::new(vec![1]));
assert!(params.validate().is_err());
}
#[test]
fn test_validate_zero_modulus_rejected() {
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![0]),
Base64UrlBytes::new(vec![1, 0, 1]),
);
assert!(params.validate().is_err());
}
#[test]
fn test_validate_exponent_constraints() {
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3]),
Base64UrlBytes::new(vec![1]),
);
assert!(params.validate().is_err(), "e=1 should be rejected");
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3]),
Base64UrlBytes::new(vec![2]),
);
assert!(
params.validate().is_err(),
"even exponent should be rejected"
);
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3]),
Base64UrlBytes::new(vec![3]),
);
assert!(params.validate().is_ok(), "e=3 should be accepted");
}
#[test]
fn test_validate_partial_crt() {
let params = RsaParams {
n: Base64UrlBytes::new(vec![1]),
e: Base64UrlBytes::new(vec![1]),
d: Some(Base64UrlBytes::new(vec![1])),
p: Some(Base64UrlBytes::new(vec![1])),
q: None, dp: None,
dq: None,
qi: None,
oth: None,
};
assert!(params.validate().is_err());
}
#[test]
fn test_validate_rejects_empty_private_integer() {
let params = RsaParams {
n: Base64UrlBytes::new(vec![1]),
e: Base64UrlBytes::new(vec![1]),
d: Some(Base64UrlBytes::new(vec![])),
p: None,
q: None,
dp: None,
dq: None,
qi: None,
oth: None,
};
assert!(params.validate().is_err());
}
#[test]
fn test_validate_rejects_empty_oth_array() {
let params = RsaParams {
n: Base64UrlBytes::new(vec![1]),
e: Base64UrlBytes::new(vec![1]),
d: Some(Base64UrlBytes::new(vec![1])),
p: Some(Base64UrlBytes::new(vec![1])),
q: Some(Base64UrlBytes::new(vec![1])),
dp: Some(Base64UrlBytes::new(vec![1])),
dq: Some(Base64UrlBytes::new(vec![1])),
qi: Some(Base64UrlBytes::new(vec![1])),
oth: Some(vec![]),
};
assert!(params.validate().is_err());
}
#[test]
fn test_validate_preserves_typed_oth_errors_with_index() {
let params = RsaParams {
n: Base64UrlBytes::new(vec![1]),
e: Base64UrlBytes::new(vec![3]),
d: Some(Base64UrlBytes::new(vec![1])),
p: Some(Base64UrlBytes::new(vec![1])),
q: Some(Base64UrlBytes::new(vec![1])),
dp: Some(Base64UrlBytes::new(vec![1])),
dq: Some(Base64UrlBytes::new(vec![1])),
qi: Some(Base64UrlBytes::new(vec![1])),
oth: Some(vec![RsaOtherPrime::new(
Base64UrlBytes::new(vec![]),
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
)]),
};
let err = params.validate().unwrap_err();
assert!(matches!(
err,
crate::Error::InvalidKey(InvalidKeyError::InvalidOtherPrime {
index: 0,
source
}) if matches!(*source, InvalidKeyError::MissingParameter("oth.r"))
));
}
#[test]
fn test_validate_canonical_base64url_uint() {
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![0, 1, 2, 3]), Base64UrlBytes::new(vec![1, 0, 1]),
);
let result = params.validate();
assert!(result.is_err(), "Leading zero byte in n should be rejected");
let err = result.unwrap_err().to_string();
assert!(
err.contains("Base64urlUInt") || err.contains("canonical"),
"Error should mention canonical encoding: {}",
err
);
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3]),
Base64UrlBytes::new(vec![0, 1, 0, 1]), );
assert!(
params.validate().is_err(),
"Leading zero byte in e should be rejected"
);
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![0]), Base64UrlBytes::new(vec![1, 0, 1]),
);
assert!(
params.validate().is_err(),
"RSA modulus 0 should be rejected as semantically invalid"
);
let params = RsaParams::new_public(
Base64UrlBytes::new(vec![1, 2, 3]),
Base64UrlBytes::new(vec![1, 0, 1]),
);
assert!(
params.validate().is_ok(),
"Canonical values should pass validation"
);
}
#[test]
fn test_validate_key_size() {
let mut n_2048 = vec![0; 256];
n_2048[0] = 0x80; let params_2048 = RsaParams::new_public(
Base64UrlBytes::new(n_2048),
Base64UrlBytes::new(vec![1, 0, 1]),
);
assert!(params_2048.validate_key_size(2048).is_ok());
assert!(params_2048.validate_key_size(4096).is_err());
let mut n_1024 = vec![0; 128];
n_1024[0] = 0x80; let params_1024 = RsaParams::new_public(
Base64UrlBytes::new(n_1024),
Base64UrlBytes::new(vec![1, 0, 1]),
);
assert!(params_1024.validate_key_size(2048).is_err());
}
#[test]
fn test_multi_prime_detection() {
let public_key =
RsaParams::new_public(Base64UrlBytes::new(vec![1]), Base64UrlBytes::new(vec![1]));
assert!(!public_key.is_multi_prime());
let standard_private = RsaParams::new_private(
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
Some(Base64UrlBytes::new(vec![1])),
Some(Base64UrlBytes::new(vec![1])),
Some(Base64UrlBytes::new(vec![1])),
Some(Base64UrlBytes::new(vec![1])),
Some(Base64UrlBytes::new(vec![1])),
);
assert!(!standard_private.is_multi_prime());
let multi_prime = RsaParams::new_multi_prime(
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
vec![RsaOtherPrime::new(
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
Base64UrlBytes::new(vec![1]),
)],
);
assert!(multi_prime.is_multi_prime());
}
}