///|
/// RSA signer & verifier for JWT (PKCS#1 v1.5).
///
/// Implements the RSASSA-PKCS1-v1_5 signature scheme with SHA-256 hash,
/// as specified in RFC 8017 §8.2 and RFC 7518 §3.3.
///
/// # Supported algorithms
///
/// | Algorithm | Hash | Status |
/// |-----------|---------|--------|
/// | RS256 | SHA-256 | ✅ |
/// | RS384 | SHA-384 | ⏳ |
/// | RS512 | SHA-512 | ⏳ |
///
/// # Usage
///
/// ```moonbit
/// let signer = RsaSigner::new(n_bytes, d_bytes, "RS256")?
/// let token = @mjwt.encode_with(signer, claims)?
///
/// let verifier = RsaVerifier::new(n_bytes, e_bytes, "RS256")?
/// let decoded = @mjwt.decode_with(verifier, token)?
/// ```
///
/// # Key format
///
/// All parameters are big-endian byte arrays:
/// - `n_bytes` — RSA modulus (k bytes, where k is the octet length)
/// - `d_bytes` — RSA private exponent (k bytes)
/// - `e_bytes` — RSA public exponent (commonly 3 bytes: `0x010001` = 65537)
///
/// # Notes
///
/// - `BigInt::from_octets` treats input bytes as **signed** (two's complement).
/// If the first byte has MSB=1, the value becomes negative. The constructors
/// handle this transparently by prepending a `\x00` byte.
/// - The modulus **must** be large enough so that `k - hash_len - 19 ≥ 8`
/// (i.e. at least 8 padding bytes). A 2048-bit key (k=256) satisfies this
/// for SHA-256.
// =============================================================================
// DER prefix for DigestInfo
// =============================================================================
///|
fn der_prefix(hash_len : Int) -> FixedArray[Byte] {
match hash_len {
32 =>
FixedArray::from_array([
0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
0x02, 0x01, 0x05, 0x00, 0x04, 0x20,
])
48 =>
FixedArray::from_array([
0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
0x02, 0x02, 0x05, 0x00, 0x04, 0x30,
])
64 =>
FixedArray::from_array([
0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04,
0x02, 0x03, 0x05, 0x00, 0x04, 0x40,
])
_ => abort("unsupported hash length")
}
}
// =============================================================================
// RsaSigner
// =============================================================================
///|
/// RSA signer using PKCS#1 v1.5 signature scheme.
///
/// - `n` — RSA modulus
/// - `d` — private exponent
/// - `hash_len` — output length of the hash (32=SHA256, 48=SHA384, 64=SHA512)
pub struct RsaSigner {
n : BigInt
d : BigInt
hash_len : Int
alg : String
}
///|
/// Create an `RsaSigner`.
///
/// - `n_bytes` — modulus in big-endian bytes
/// - `d_bytes` — private exponent in big-endian bytes
/// - `alg` — `"RS256"`, `"RS384"`, or `"RS512"`
///|
/// Convert big-endian bytes to an unsigned `BigInt`.
///
/// MoonBit's `BigInt::from_octets` interprets bytes as signed (two's complement),
/// so when the most significant byte has MSB=1 (e.g. `0xAB…`), the resulting
/// `BigInt` would be **negative** — which is incorrect for RSA modulus or
/// exponent values that are inherently unsigned.
///
/// This function prepends a `\x00` byte to force an unsigned interpretation,
/// yielding the correct positive `BigInt`.
fn unsigned_bigint_from_bytes(data : BytesView) -> BigInt {
let padded = FixedArray::make(data.length() + 1, b'\x00')
for i = 0; i < data.length(); i = i + 1 {
padded[i + 1] = data[i]
}
BigInt::from_octets(padded.unsafe_reinterpret_as_bytes().view())
}
///|
pub fn RsaSigner::new(
n_bytes : BytesView,
d_bytes : BytesView,
alg : String,
) -> RsaSigner raise JwtError {
let (hash_len, alg_name) = match alg {
"RS256" => (32, "RS256")
"RS384" => (48, "RS384")
"RS512" => (64, "RS512")
_ => raise UnsupportedAlgorithm(alg)
}
let n = unsigned_bigint_from_bytes(n_bytes)
let d = unsigned_bigint_from_bytes(d_bytes)
{ n, d, hash_len, alg: alg_name }
}
///|
pub impl JwtSigner for RsaSigner with fn alg_name(self : RsaSigner) -> String {
self.alg
}
///|
pub impl JwtSigner for RsaSigner with fn sign(
self : RsaSigner,
message : BytesView,
) -> FixedArray[Byte] raise JwtError {
// 1. Hash
let hash = match self.hash_len {
32 => @crypto.sha256(message)
48 => sha384(message)
64 => sha512(message)
_ => raise UnsupportedAlgorithm(self.alg)
}
let prefix = der_prefix(self.hash_len)
// k = octet length of the RSA modulus
let k = (self.n.bit_length() + 7) / 8
// 2. Build EMSA-PKCS1-v1_5-ENCODE (RFC 8017 §9.2)
// EM = 0x00 || 0x01 || PS || 0x00 || T
// T = DER-encoded DigestInfo
// PS = k - len(T) - 3 (must be >= 8)
let t_len = prefix.length() + hash.length()
let ps_len = k - t_len - 3
if ps_len < 8 {
raise CryptoError("RSA key too short for this hash")
}
let em = FixedArray::make(k, b'\x00')
em[1] = b'\x01' // block type
for i = 0; i < ps_len; i = i + 1 {
em[2 + i] = b'\xff'
}
em[2 + ps_len] = b'\x00'
let di_off = 2 + ps_len + 1
for i = 0; i < prefix.length(); i = i + 1 {
em[di_off + i] = prefix[i]
}
for i = 0; i < hash.length(); i = i + 1 {
em[di_off + prefix.length() + i] = hash[i]
}
// 3. m = bytes_to_int(EM)
let m = unsigned_bigint_from_bytes(em.unsafe_reinterpret_as_bytes().view())
// 4. s = m^d mod n
let s = m.pow(self.d, modulus=self.n)
// 5. Output s as k bytes
s.to_octets(length=k).to_fixedarray()
}
///|
pub extend RsaSigner with JwtSigner::{alg_name, sign}
// =============================================================================
// RsaVerifier
// =============================================================================
///|
/// RSA verifier using PKCS#1 v1.5 signature scheme.
pub struct RsaVerifier {
n : BigInt
e : BigInt
hash_len : Int
alg : String
}
///|
/// Create an `RsaVerifier`.
///
/// - `n_bytes` — modulus in big-endian bytes
/// - `e_bytes` — public exponent in big-endian bytes (usually `0x010001` = 65537)
/// - `alg` — `"RS256"`, `"RS384"`, or `"RS512"`
pub fn RsaVerifier::new(
n_bytes : BytesView,
e_bytes : BytesView,
alg : String,
) -> RsaVerifier raise JwtError {
let (hash_len, alg_name) = match alg {
"RS256" => (32, "RS256")
"RS384" => (48, "RS384")
"RS512" => (64, "RS512")
_ => raise UnsupportedAlgorithm(alg)
}
{
n: unsigned_bigint_from_bytes(n_bytes),
e: unsigned_bigint_from_bytes(e_bytes),
hash_len,
alg: alg_name,
}
}
///|
pub impl JwtVerifier for RsaVerifier with fn alg_name(self : RsaVerifier) -> String {
self.alg
}
///|
pub impl JwtVerifier for RsaVerifier with fn verify(
self : RsaVerifier,
message : BytesView,
signature : BytesView,
) -> Bool {
// 1. s^e mod n
let sig_int = unsigned_bigint_from_bytes(signature)
let m = sig_int.pow(self.e, modulus=self.n)
let k = (self.n.bit_length() + 7) / 8
let em_decoded = m.to_octets(length=k).to_fixedarray()
// 2. Hash the message
let hash = match self.hash_len {
32 => @crypto.sha256(message)
48 => sha384(message)
64 => sha512(message)
_ => return false
}
let prefix = der_prefix(self.hash_len)
let t_len = prefix.length() + hash.length()
let ps_len = k - t_len - 3
if ps_len < 8 {
return false
}
// 3. Verify EM = 0x00 || 0x01 || PS || 0x00 || T
if em_decoded.length() != k {
return false
}
if em_decoded[0] != b'\x00' || em_decoded[1] != b'\x01' {
return false
}
for i = 0; i < ps_len; i = i + 1 {
if em_decoded[2 + i] != b'\xff' {
return false
}
}
if em_decoded[2 + ps_len] != b'\x00' {
return false
}
let di_off = 2 + ps_len + 1
for i = 0; i < prefix.length(); i = i + 1 {
if em_decoded[di_off + i] != prefix[i] {
return false
}
}
for i = 0; i < hash.length(); i = i + 1 {
if em_decoded[di_off + prefix.length() + i] != hash[i] {
return false
}
}
true
}
///|
pub extend RsaVerifier with JwtVerifier::{alg_name, verify}