//! RSA-PSS signing for Web Bot Auth RFC 9421 `rsa-pss-sha512`.
//!
//! Why this is its own module: the Ed25519 path in
//! [`super::web_bot_auth_sign`] already carries the signature-base assembly
//! and header shaping, and folding the RSA key-loading, blinded signer, and
//! public-key fingerprint in next to it would push that module past the size
//! guard. This module owns only the RSA-PSS specifics: load the PKCS#8 PEM
//! private key, sign a prepared signature base with `rsa-pss-sha512`, and
//! derive a stable non-secret key id from the public key.
//!
//! Secret discipline: the private key is loaded, signed with, and dropped.
//! It is never returned, logged, or surfaced. Only the signature bytes and
//! the derived public-key id leave this module.

use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use rsa::pkcs8::{DecodePrivateKey, EncodePublicKey};
use rsa::pss::BlindedSigningKey;
use rsa::sha2::{Digest, Sha512};
use rsa::signature::{RandomizedSigner, SignatureEncoding};
use rsa::RsaPrivateKey;

use super::AuthError;

/// RFC 9421 algorithm identifier emitted in the `alg` signature parameter
/// for the RSA-PSS path. Kept here so the signer and any future verifier
/// agree on the exact spelling.
pub(crate) const RSA_PSS_ALG: &str = "rsa-pss-sha512";

/// Sign a prepared RFC 9421 signature base with `rsa-pss-sha512` and return
/// the raw signature bytes (the caller base64-encodes them into the
/// structured-field byte sequence).
///
/// `pem` is the PKCS#8 PEM private key. The blinded signer adds RSA blinding
/// so the signature does not leak timing information about the key.
pub(crate) fn sign_pss(base: &[u8], pem: &str) -> Result<Vec<u8>, AuthError> {
    let key = load_private_key(pem)?;
    let signing_key = BlindedSigningKey::<Sha512>::new(key);
    let mut rng = rand::rngs::OsRng;
    let signature = signing_key
        .try_sign_with_rng(&mut rng, base)
        .map_err(|e| crypto_error(&format!("rsa-pss signing failed: {e}")))?;
    Ok(signature.to_bytes().into_vec())
}

/// Derive a stable, non-secret key id from the RSA public key: base64 of the
/// SHA-256 over the DER-encoded SubjectPublicKeyInfo.
///
/// Why SPKI: it is the canonical public-key encoding, so the id is
/// reproducible by any verifier that holds the same public key without
/// agreeing on a bespoke format. The private key never enters the id.
pub(crate) fn key_id_from_pem(pem: &str) -> Result<String, AuthError> {
    let key = load_private_key(pem)?;
    let spki = key
        .to_public_key()
        .to_public_key_der()
        .map_err(|e| crypto_error(&format!("could not encode rsa public key: {e}")))?;
    // SHA-512 over the SPKI DER. The fingerprint just needs to be stable and
    // reproducible from the public key, not match a specific external
    // convention.
    let fingerprint = Sha512::digest(spki.as_bytes());
    Ok(BASE64.encode(fingerprint))
}

/// Decode the PKCS#8 PEM private key. A parse failure is a typed crypto
/// error whose message never includes the key bytes.
fn load_private_key(pem: &str) -> Result<RsaPrivateKey, AuthError> {
    RsaPrivateKey::from_pkcs8_pem(pem)
        .map_err(|e| crypto_error(&format!("failed to parse RSA private key: {e}")))
}

fn crypto_error(reason: &str) -> AuthError {
    AuthError::Crypto {
        scheme: "web_bot_auth",
        reason: reason.to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use rsa::pkcs8::EncodePrivateKey;
    use rsa::pss::VerifyingKey;
    use rsa::signature::Verifier;
    use rsa::RsaPublicKey;

    /// Generate a small RSA test key in-process so no key fixture is
    /// committed. 2048 bits keeps the test fast while exercising the real
    /// signing path.
    fn test_key_pem() -> String {
        let mut rng = rand::rngs::OsRng;
        let key = RsaPrivateKey::new(&mut rng, 2048).expect("generate rsa key");
        key.to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
            .expect("encode pkcs8 pem")
            .to_string()
    }

    #[test]
    fn rsa_pss_signature_verifies_with_the_public_key() {
        let pem = test_key_pem();
        let base = b"\"@authority\": mcptest.sh\n\"@signature-params\": (\"@authority\")";
        let sig_bytes = sign_pss(base, &pem).expect("sign");

        // Reconstruct the verifying key from the same private key's public
        // half and confirm the produced signature checks out.
        let priv_key = RsaPrivateKey::from_pkcs8_pem(&pem).unwrap();
        let pub_key: RsaPublicKey = priv_key.to_public_key();
        let verifying_key = VerifyingKey::<Sha512>::new(pub_key);
        let signature = rsa::pss::Signature::try_from(sig_bytes.as_slice()).expect("decode sig");
        verifying_key
            .verify(base, &signature)
            .expect("public key verifies the rsa-pss signature");
    }

    #[test]
    fn key_id_is_stable_for_the_same_key() {
        let pem = test_key_pem();
        let first = key_id_from_pem(&pem).expect("key id");
        let second = key_id_from_pem(&pem).expect("key id");
        assert_eq!(first, second, "same key must yield the same id");
        assert!(!first.is_empty());
    }

    #[test]
    fn key_id_differs_between_keys() {
        let a = key_id_from_pem(&test_key_pem()).unwrap();
        let b = key_id_from_pem(&test_key_pem()).unwrap();
        assert_ne!(a, b, "distinct keys must yield distinct ids");
    }

    #[test]
    fn malformed_pem_is_a_typed_crypto_error() {
        let err = sign_pss(
            b"base",
            "-----BEGIN PRIVATE KEY-----\nnope\n-----END PRIVATE KEY-----",
        )
        .unwrap_err();
        assert!(matches!(err, AuthError::Crypto { .. }));
        // The error message never echoes key bytes.
        if let AuthError::Crypto { reason, .. } = err {
            assert!(!reason.contains("nope"));
        }
    }
}
