🐸 Frogue(Fed)

~ololduck/Frogue

Raw

//! 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"
        );
    }
}