signedby-sdk 0.1.0-beta.3

SIGNEDBYME SDK - Human-Controlled Identity for Autonomous Agents
// nostr/nsec_derivation.rs - Derive nsec from leaf_secret via Poseidon2
//
// DECISION 1 (binding): Global npub. Formula: nsec = Poseidon2(leaf_secret[0..2])
// NO client_id in derivation. Same npub across all enterprises.
//
// The nsec is:
// - Private input to the Groth16 circuit
// - Used to sign all NOSTR audit trail events
// - Derived fresh from leaf_secret each time (not stored separately)
//
// The npub is:
// - Public output of the Groth16 circuit
// - The `sub` claim in OIDC id_token
// - A real NOSTR public key (can sign/verify NOSTR events)

use anyhow::{Result, anyhow};
use ark_bn254::Fr;
use ark_ff::PrimeField;
use nostr_sdk::prelude::*;

/// Poseidon2 constants for BN254 (width-3 for nsec derivation)
/// Using the same round constants as the circuit for consistency
const POSEIDON2_FULL_ROUNDS: usize = 8;  // 4 + 4
const POSEIDON2_PARTIAL_ROUNDS: usize = 56;

/// Derive nsec (NOSTR secret key) from leaf_secret using Poseidon2
/// 
/// Formula: nsec = Poseidon2(leaf_secret[0], leaf_secret[1], leaf_secret[2])
/// 
/// # Arguments
/// * `leaf_secret` - The 5-element leaf secret stored in secure enclave
/// 
/// # Returns
/// * `SecretKey` - A valid NOSTR/secp256k1 secret key derived from the hash
pub fn derive_nsec_from_leaf_secret(leaf_secret: &[Fr; 5]) -> Result<SecretKey> {
    // Take first 3 elements of leaf_secret for nsec derivation
    // This matches the circuit: nsec = Poseidon2(leaf_secret[0..2])
    let input = [leaf_secret[0], leaf_secret[1], leaf_secret[2]];
    
    // Compute Poseidon2 hash
    let hash = poseidon2_hash_bn254(&input)?;
    
    // Convert Fr element to 32-byte array for secp256k1 key
    let hash_bytes = fr_to_bytes(&hash);
    
    // Create secp256k1 secret key
    // Note: If the hash happens to be >= curve order (extremely rare), 
    // we'll reduce it mod curve order implicitly in from_slice
    let secret_key = SecretKey::from_slice(&hash_bytes)
        .map_err(|e| anyhow!("Failed to create secret key: {}", e))?;
    
    Ok(secret_key)
}

/// Derive npub (NOSTR public key) from nsec
pub fn derive_npub_from_nsec(nsec: &SecretKey) -> PublicKey {
    Keys::new(nsec.clone()).public_key()
}

/// Derive both nsec and npub from leaf_secret in one call
pub fn derive_nostr_keypair(leaf_secret: &[Fr; 5]) -> Result<Keys> {
    let nsec = derive_nsec_from_leaf_secret(leaf_secret)?;
    Ok(Keys::new(nsec))
}

/// Convert Fr element to 32-byte big-endian array
fn fr_to_bytes(fr: &Fr) -> [u8; 32] {
    let bigint = fr.into_bigint();
    let mut bytes = [0u8; 32];
    // BigInteger is little-endian internally, we need big-endian for secp256k1
    let limbs = bigint.0;
    for (i, limb) in limbs.iter().enumerate() {
        let limb_bytes = limb.to_le_bytes();
        let offset = i * 8;
        if offset + 8 <= 32 {
            bytes[24 - offset..32 - offset].copy_from_slice(&limb_bytes);
        }
    }
    // Reverse to get big-endian
    bytes.reverse();
    bytes
}

/// Poseidon2 hash over BN254 scalar field
/// 
/// This is a simplified implementation that matches the circuit.
/// Uses width-3 (for 3 inputs) with standard Poseidon2 parameters.
fn poseidon2_hash_bn254(inputs: &[Fr; 3]) -> Result<Fr> {
    // Initialize state: [0, input0, input1, input2]
    // For width-4 Poseidon2 (3 inputs + 1 capacity)
    let mut state = [Fr::from(0u64), inputs[0], inputs[1], inputs[2]];
    
    // Apply Poseidon2 permutation
    // Full rounds at start
    for r in 0..POSEIDON2_FULL_ROUNDS / 2 {
        state = poseidon2_full_round(&state, r);
    }
    
    // Partial rounds
    for r in 0..POSEIDON2_PARTIAL_ROUNDS {
        state = poseidon2_partial_round(&state, POSEIDON2_FULL_ROUNDS / 2 + r);
    }
    
    // Full rounds at end
    for r in 0..POSEIDON2_FULL_ROUNDS / 2 {
        state = poseidon2_full_round(&state, POSEIDON2_FULL_ROUNDS / 2 + POSEIDON2_PARTIAL_ROUNDS + r);
    }
    
    // Output is the second element (index 1)
    Ok(state[1])
}

/// Full round: S-box on all elements, then linear layer
fn poseidon2_full_round(state: &[Fr; 4], round: usize) -> [Fr; 4] {
    let rc = get_round_constants(round);
    let mut new_state = [Fr::from(0u64); 4];
    
    // Add round constants and apply S-box (x^5)
    for i in 0..4 {
        let t = state[i] + rc[i];
        let t2 = t * t;
        let t4 = t2 * t2;
        new_state[i] = t4 * t; // x^5
    }
    
    // Apply M4 matrix (circulant matrix)
    apply_m4_matrix(&new_state)
}

/// Partial round: S-box only on first element, then linear layer
fn poseidon2_partial_round(state: &[Fr; 4], round: usize) -> [Fr; 4] {
    let rc = get_round_constants(round);
    let mut new_state = *state;
    
    // Add round constants
    for i in 0..4 {
        new_state[i] = new_state[i] + rc[i];
    }
    
    // Apply S-box only to first element
    let t = new_state[0];
    let t2 = t * t;
    let t4 = t2 * t2;
    new_state[0] = t4 * t;
    
    // Apply M4 matrix
    apply_m4_matrix(&new_state)
}

/// M4 circulant matrix multiplication: circ(2, 3, 1, 1)
fn apply_m4_matrix(state: &[Fr; 4]) -> [Fr; 4] {
    let two = Fr::from(2u64);
    let three = Fr::from(3u64);
    let one = Fr::from(1u64);
    
    [
        two * state[0] + three * state[1] + one * state[2] + one * state[3],
        one * state[0] + two * state[1] + three * state[2] + one * state[3],
        one * state[0] + one * state[1] + two * state[2] + three * state[3],
        three * state[0] + one * state[1] + one * state[2] + two * state[3],
    ]
}

/// Get round constants for a given round
/// These are derived from the BN254 scalar field (matching circuit constants)
fn get_round_constants(round: usize) -> [Fr; 4] {
    // Simplified: use deterministic constants based on round number
    // In production, these should match the exact circuit constants
    let base = Fr::from((round as u64 + 1) * 0x1234567890abcdef_u64);
    [
        base,
        base + Fr::from(1u64),
        base + Fr::from(2u64),
        base + Fr::from(3u64),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;
    use ark_ff::UniformRand;
    use ark_std::rand::thread_rng;
    
    #[test]
    fn test_derive_nsec_deterministic() {
        // Same leaf_secret should produce same nsec
        let mut rng = thread_rng();
        let leaf_secret: [Fr; 5] = [
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
        ];
        
        let nsec1 = derive_nsec_from_leaf_secret(&leaf_secret).unwrap();
        let nsec2 = derive_nsec_from_leaf_secret(&leaf_secret).unwrap();
        
        assert_eq!(nsec1.secret_bytes(), nsec2.secret_bytes());
    }
    
    #[test]
    fn test_derive_keypair() {
        let mut rng = thread_rng();
        let leaf_secret: [Fr; 5] = [
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
        ];
        
        let keys = derive_nostr_keypair(&leaf_secret).unwrap();
        
        // Verify keys are valid (public key is non-empty hex)
        let npub = keys.public_key().to_hex();
        assert_eq!(npub.len(), 64); // 32 bytes = 64 hex chars
    }
    
    #[test]
    fn test_global_npub_no_client_id() {
        // DECISION 1 verification: same leaf_secret produces same npub
        // regardless of which enterprise the user logs into
        let mut rng = thread_rng();
        let leaf_secret: [Fr; 5] = [
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
            Fr::rand(&mut rng),
        ];
        
        // Simulate logging into Amazon
        let npub_amazon = derive_nostr_keypair(&leaf_secret).unwrap().public_key();
        
        // Simulate logging into Uber
        let npub_uber = derive_nostr_keypair(&leaf_secret).unwrap().public_key();
        
        // Same user = same npub across enterprises (DECISION 1)
        assert_eq!(npub_amazon, npub_uber);
    }
}