newton-core 0.4.16

newton protocol core sdk
//! Ed25519 key derivation from ECDSA keys and signing utilities.
//!
//! Provides deterministic derivation of Ed25519 signing keys from existing
//! ECDSA operator keys via HKDF-SHA256, so operators need zero configuration
//! changes to participate in the privacy layer. Also provides Ed25519-to-X25519
//! key conversion for use with HPKE.

use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
use hkdf::Hkdf;
use sha2::{Digest, Sha256, Sha512};
use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret as X25519StaticSecret};

use super::error::CryptoError;

/// HKDF salt used when deriving Ed25519 keys from ECDSA material for the
/// privacy / HPKE-X25519-conversion path.
const DERIVATION_SALT: &[u8] = b"newton-privacy-ed25519";

/// HKDF salt used when deriving the Ed25519 key the operator uses to sign
/// `newt_signedRead` responses (PDS §9.2).
///
/// Distinct from [`DERIVATION_SALT`] so that compromise of one signing context
/// does not produce a key usable in the other. Bumping this constant rotates
/// every operator's signed-read identity in lock-step with redeploy.
const SIGNED_READ_DERIVATION_SALT: &[u8] = b"newton-pds-signed-read-v1";

/// Deterministically derive an Ed25519 signing key from an ECDSA private key.
///
/// Uses HKDF-SHA256 with a fixed salt to produce a 32-byte seed that is used
/// to construct the Ed25519 `SigningKey`. The derivation is deterministic:
/// the same ECDSA key always produces the same Ed25519 key.
pub fn derive_ed25519_from_ecdsa(ecdsa_key: &[u8; 32]) -> Result<SigningKey, CryptoError> {
    let hkdf = Hkdf::<Sha256>::new(Some(DERIVATION_SALT), ecdsa_key);
    let mut okm = [0u8; 32];
    hkdf.expand(b"", &mut okm)
        .map_err(|e| CryptoError::KeyDerivation(e.to_string()))?;
    Ok(SigningKey::from_bytes(&okm))
}

/// Deterministically derive the Ed25519 signing key the operator uses to sign
/// `newt_signedRead` responses, from the operator's ECDSA private key.
///
/// Mirrors [`derive_ed25519_from_ecdsa`] but uses [`SIGNED_READ_DERIVATION_SALT`]
/// so the resulting key is domain-separated from the privacy/HPKE Ed25519 key.
/// PDS §9.2 mandates this separation: compromise of the signed-read key MUST
/// NOT produce material usable to forge HPKE-bound signatures, and vice versa.
///
/// Rotation is 1:1 with the ECDSA key — there is no separate registry. Per
/// PDS §9.2, verifiers re-derive the verifying key from the operator's
/// on-chain ECDSA key in `OperatorRegistry` rather than trusting any echoed
/// bytes; the inline pubkey on responses is a convenience field only.
pub fn derive_ed25519_signed_read(ecdsa_key: &[u8; 32]) -> Result<SigningKey, CryptoError> {
    let hkdf = Hkdf::<Sha256>::new(Some(SIGNED_READ_DERIVATION_SALT), ecdsa_key);
    let mut okm = [0u8; 32];
    hkdf.expand(b"", &mut okm)
        .map_err(|e| CryptoError::KeyDerivation(e.to_string()))?;
    Ok(SigningKey::from_bytes(&okm))
}

/// Convert an Ed25519 signing key to a raw X25519 private key (32 bytes).
///
/// Follows the standard Ed25519-to-X25519 conversion: compute SHA-512 over the
/// Ed25519 seed, then take the first 32 bytes. The result can be passed to
/// [`super::hpke::HpkePrivateKey::from_bytes`].
pub fn ed25519_to_x25519_private(signing_key: &SigningKey) -> Result<Vec<u8>, CryptoError> {
    let hash = Sha512::digest(signing_key.to_bytes());
    let mut x25519_bytes = [0u8; 32];
    x25519_bytes.copy_from_slice(&hash[..32]);
    Ok(x25519_bytes.to_vec())
}

/// Derive the X25519 public key that corresponds to an Ed25519 signing key.
///
/// Internally converts the Ed25519 private key to an X25519 static secret
/// and then computes the associated public key. The result can be passed to
/// [`super::hpke::HpkePublicKey::from_bytes`].
pub fn ed25519_to_x25519_public(signing_key: &SigningKey) -> Result<Vec<u8>, CryptoError> {
    let x25519_secret_bytes = ed25519_to_x25519_private(signing_key)?;
    let secret_array: [u8; 32] = x25519_secret_bytes
        .as_slice()
        .try_into()
        .map_err(|_| CryptoError::KeyDerivation("x25519 secret must be 32 bytes".into()))?;
    let secret = X25519StaticSecret::from(secret_array);
    let public = X25519PublicKey::from(&secret);
    Ok(public.as_bytes().to_vec())
}

/// Sign `data` with an Ed25519 signing key and return the 64-byte signature.
pub fn sign_data_ref(signing_key: &SigningKey, data: &[u8]) -> Vec<u8> {
    let sig: Signature = signing_key.sign(data);
    sig.to_bytes().to_vec()
}

/// Verify an Ed25519 signature over `data`.
pub fn verify_data_ref(verifying_key: &VerifyingKey, data: &[u8], signature: &[u8]) -> Result<(), CryptoError> {
    let sig = Signature::from_slice(signature)
        .map_err(|e| CryptoError::VerificationFailed(format!("invalid signature bytes: {e}")))?;
    verifying_key
        .verify(data, &sig)
        .map_err(|e| CryptoError::VerificationFailed(format!("{e}")))
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    fn test_ecdsa_key() -> [u8; 32] {
        // Deterministic test key
        let mut key = [0u8; 32];
        key[0] = 0x42;
        key[31] = 0x01;
        key
    }

    #[test]
    fn derivation_is_deterministic() {
        let ecdsa_key = test_ecdsa_key();
        let sk1 = derive_ed25519_from_ecdsa(&ecdsa_key).expect("derivation 1 failed");
        let sk2 = derive_ed25519_from_ecdsa(&ecdsa_key).expect("derivation 2 failed");
        assert_eq!(
            sk1.to_bytes(),
            sk2.to_bytes(),
            "same ECDSA key must produce same Ed25519 key"
        );
    }

    #[test]
    fn different_ecdsa_keys_produce_different_ed25519_keys() {
        let key_a = {
            let mut k = [0u8; 32];
            k[0] = 0x01;
            k
        };
        let key_b = {
            let mut k = [0u8; 32];
            k[0] = 0x02;
            k
        };
        let sk_a = derive_ed25519_from_ecdsa(&key_a).expect("derivation a failed");
        let sk_b = derive_ed25519_from_ecdsa(&key_b).expect("derivation b failed");
        assert_ne!(
            sk_a.to_bytes(),
            sk_b.to_bytes(),
            "different ECDSA keys must produce different Ed25519 keys"
        );
    }

    #[test]
    fn sign_verify_roundtrip() {
        let ecdsa_key = test_ecdsa_key();
        let sk = derive_ed25519_from_ecdsa(&ecdsa_key).expect("derivation failed");
        let vk = sk.verifying_key();
        let data = b"newton privacy test message";

        let signature = sign_data_ref(&sk, data);
        assert_eq!(signature.len(), 64, "Ed25519 signature must be 64 bytes");

        verify_data_ref(&vk, data, &signature).expect("verification should succeed");
    }

    #[test]
    fn verify_rejects_wrong_data() {
        let ecdsa_key = test_ecdsa_key();
        let sk = derive_ed25519_from_ecdsa(&ecdsa_key).expect("derivation failed");
        let vk = sk.verifying_key();

        let signature = sign_data_ref(&sk, b"correct data");
        let result = verify_data_ref(&vk, b"wrong data", &signature);
        assert!(result.is_err(), "verification with wrong data should fail");
    }

    #[test]
    fn verify_rejects_invalid_signature_bytes() {
        let ecdsa_key = test_ecdsa_key();
        let sk = derive_ed25519_from_ecdsa(&ecdsa_key).expect("derivation failed");
        let vk = sk.verifying_key();

        let result = verify_data_ref(&vk, b"data", &[0u8; 10]);
        assert!(result.is_err(), "verification with truncated signature should fail");
    }

    #[test]
    fn x25519_conversion_consistency() {
        let ecdsa_key = test_ecdsa_key();
        let sk = derive_ed25519_from_ecdsa(&ecdsa_key).expect("derivation failed");

        let x25519_priv = ed25519_to_x25519_private(&sk).expect("x25519 private conversion failed");
        assert_eq!(x25519_priv.len(), 32, "X25519 private key must be 32 bytes");

        let x25519_pub = ed25519_to_x25519_public(&sk).expect("x25519 public conversion failed");
        assert_eq!(x25519_pub.len(), 32, "X25519 public key must be 32 bytes");

        // Verify the public key matches the private key
        let secret_array: [u8; 32] = x25519_priv.as_slice().try_into().unwrap();
        let expected_pub = X25519PublicKey::from(&X25519StaticSecret::from(secret_array));
        assert_eq!(
            x25519_pub,
            expected_pub.as_bytes().to_vec(),
            "X25519 public key must correspond to the derived private key"
        );
    }

    #[test]
    fn x25519_keys_work_with_hpke() {
        let ecdsa_key = test_ecdsa_key();
        let sk = derive_ed25519_from_ecdsa(&ecdsa_key).expect("derivation failed");

        let x25519_priv = ed25519_to_x25519_private(&sk).expect("x25519 priv failed");
        let x25519_pub = ed25519_to_x25519_public(&sk).expect("x25519 pub failed");

        // Construct HPKE key wrappers
        let hpke_sk = super::super::hpke::HpkePrivateKey::from_bytes(&x25519_priv).expect("hpke sk failed");
        let hpke_pk = super::super::hpke::HpkePublicKey::from_bytes(&x25519_pub).expect("hpke pk failed");

        // Roundtrip encrypt/decrypt
        let plaintext = b"cross-module integration test";
        let aad = b"test-aad";
        let (enc, ct) = super::super::hpke::encrypt(&hpke_pk, plaintext, aad).expect("encrypt failed");
        let recovered = super::super::hpke::decrypt(&hpke_sk, &enc, &ct, aad).expect("decrypt failed");
        assert_eq!(&*recovered, plaintext);
    }

    #[test]
    fn signed_read_derivation_is_deterministic() {
        let ecdsa_key = test_ecdsa_key();
        let sk1 = derive_ed25519_signed_read(&ecdsa_key).expect("derivation 1 failed");
        let sk2 = derive_ed25519_signed_read(&ecdsa_key).expect("derivation 2 failed");
        assert_eq!(
            sk1.to_bytes(),
            sk2.to_bytes(),
            "same ECDSA key must produce same signed-read key"
        );
    }

    #[test]
    fn signed_read_different_from_privacy_derivation() {
        // Security invariant (PDS §9.2): HKDF with the same secret + same info but
        // different salts MUST produce different keys. Compromise of the privacy/HPKE
        // Ed25519 key MUST NOT produce material usable to forge `newt_signedRead`
        // responses, and vice versa.
        let ecdsa_key = test_ecdsa_key();
        let privacy_sk = derive_ed25519_from_ecdsa(&ecdsa_key).expect("privacy derivation failed");
        let signed_read_sk = derive_ed25519_signed_read(&ecdsa_key).expect("signed-read derivation failed");
        assert_ne!(
            privacy_sk.to_bytes(),
            signed_read_sk.to_bytes(),
            "domain separation violated: privacy and signed-read derivations produced same key"
        );
    }

    #[test]
    fn signed_read_different_ecdsa_keys_produce_different_keys() {
        let key_a = {
            let mut k = [0u8; 32];
            k[0] = 0x01;
            k
        };
        let key_b = {
            let mut k = [0u8; 32];
            k[0] = 0x02;
            k
        };
        let sk_a = derive_ed25519_signed_read(&key_a).expect("derivation a failed");
        let sk_b = derive_ed25519_signed_read(&key_b).expect("derivation b failed");
        assert_ne!(
            sk_a.to_bytes(),
            sk_b.to_bytes(),
            "different ECDSA keys must produce different signed-read keys"
        );
    }
}