//! RSA key generation and signing for ActivityPub HTTP signatures.
//!
//! Keys are RSA-2048, the Mastodon-compatible size. Larger keys (4096, Ed25519)
//! would be more secure but slower to generate on first actor fetch; the real
//! prerequisite is a key rotation mechanism.
use pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding};
use rsa::pkcs1v15::{SigningKey, VerifyingKey};
use rsa::signature::{SignatureEncoding, Signer, Verifier};
use rsa::{RsaPrivateKey, RsaPublicKey};
use spki::{DecodePublicKey, EncodePublicKey};
/// Generate a new RSA-2048 keypair, returning (private_pem, public_pem).
pub fn generate() -> Result<(String, String), rsa::Error> {
let mut rng = rand::rng();
let private_key = RsaPrivateKey::new(&mut rng, 2048)?;
let public_key = private_key.to_public_key();
let private_pem = private_key
.to_pkcs8_pem(LineEnding::LF)
.map_err(|_| rsa::Error::Decryption)?
.to_string();
let public_pem = public_key
.to_public_key_pem(LineEnding::LF)
.map_err(|_| rsa::Error::Decryption)?;
Ok((private_pem, public_pem))
}
/// Sign `message` with the private key (PEM-encoded), returning the signature.
///
/// Uses PKCS#1v1.5 with SHA-256, the algorithm HTTP Signatures expect.
pub fn sign(private_pem: &str, message: &[u8]) -> Result<Vec<u8>, rsa::Error> {
let private_key =
RsaPrivateKey::from_pkcs8_pem(private_pem).map_err(|_| rsa::Error::Decryption)?;
let signing_key = SigningKey::<rsa::sha2::Sha256>::new(private_key);
let signature = signing_key.sign(message);
Ok(signature.to_vec())
}
/// Verify `signature` over `message` using the public key (PEM-encoded).
pub fn verify(public_pem: &str, message: &[u8], signature: &[u8]) -> bool {
let Ok(public_key) = RsaPublicKey::from_public_key_pem(public_pem) else {
return false;
};
let Ok(sig) = rsa::pkcs1v15::Signature::try_from(signature) else {
return false;
};
let verifying_key = VerifyingKey::<rsa::sha2::Sha256>::new(public_key);
verifying_key.verify(message, &sig).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sign_verify_round_trip() {
let (private_pem, public_pem) = generate().expect("key generation should succeed");
let message = b"hello, activitypub";
let signature = sign(&private_pem, message).expect("signing should succeed");
assert!(
verify(&public_pem, message, &signature),
"signature should verify"
);
}
#[test]
fn wrong_key_fails() {
let (private_pem, _public_pem) = generate().expect("key generation should succeed");
let (_other_private, other_public) = generate().expect("key generation should succeed");
let message = b"hello, activitypub";
let signature = sign(&private_pem, message).expect("signing should succeed");
assert!(
!verify(&other_public, message, &signature),
"signature should not verify with a different key"
);
}
}