use std::collections::{BTreeMap, VecDeque};
use async_trait::async_trait;
use zeroize::Zeroizing;
use crate::audit::{SignAudit, SignAuditRecord};
use crate::crypto;
use crate::guardrails::{Guardrails, PolicyDecision, SignContext};
use crate::keys::{KeyInfo, KeyRef, KeyRole, Scheme, Signature, Subject};
use crate::payload::Payload;
use crate::port::KeySigner;
use crate::port::SignerError;
pub struct FakeSigner {
fixed_seed: Option<[u8; 32]>,
#[allow(clippy::disallowed_types)]
keys: std::sync::Mutex<BTreeMap<String, Key>>,
#[allow(clippy::disallowed_types)]
counter: std::sync::Mutex<u64>,
}
impl Default for FakeSigner {
#[allow(clippy::disallowed_types)]
fn default() -> Self {
Self {
fixed_seed: None,
keys: std::sync::Mutex::new(BTreeMap::new()),
counter: std::sync::Mutex::new(0),
}
}
}
struct Key {
info: KeyInfo,
seed: Zeroizing<[u8; 32]>,
}
impl FakeSigner {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_fixed_seed(seed: [u8; 32]) -> Self {
Self {
fixed_seed: Some(seed),
..Self::default()
}
}
fn sign_seed(
seed: &[u8; 32],
scheme: Scheme,
payload: &Payload,
) -> Result<Signature, SignerError> {
match scheme {
Scheme::Secp256k1 => {
if payload.scheme() != Scheme::Secp256k1 {
return Err(SignerError::Unsupported {
reason: format!(
"a secp256k1 key cannot sign {} payloads",
payload.scheme()
),
});
}
let (signature, recovery_id) =
crypto::sign_secp256k1(seed, &payload.payload_hash()?)?;
let (r, s) = crypto::split(signature);
Ok(Signature::Secp256k1 {
r,
s,
v: crypto::v_of(recovery_id),
})
}
Scheme::Ed25519 => {
let Payload::SolanaMessage(message) = payload else {
return Err(SignerError::Unsupported {
reason: "an ed25519 key signs Solana messages here".to_owned(),
});
};
Ok(Signature::Ed25519 {
bytes: crypto::sign_ed25519(seed, &message.0),
})
}
}
}
}
#[async_trait]
impl KeySigner for FakeSigner {
async fn create_key(
&self,
subject: &Subject,
scheme: Scheme,
label: &str,
) -> Result<KeyInfo, SignerError> {
let mut counter = self.counter.lock().expect("uncontended");
*counter += 1;
let seed = self
.fixed_seed
.unwrap_or_else(|| derive_seed(subject, scheme, label, *counter));
let identity = match scheme {
Scheme::Secp256k1 => crypto::secp256k1_address(&seed)?,
Scheme::Ed25519 => crypto::ed25519_pubkey(&seed),
};
let info = KeyInfo {
key_ref: KeyRef::new(format!("fake-{scheme}-{}", *counter))?,
scheme,
role: KeyRole::Session,
label: label.to_owned(),
subject: subject.clone(),
identity,
};
self.keys.lock().expect("uncontended").insert(
info.key_ref.as_str().to_owned(),
Key {
info: info.clone(),
seed: Zeroizing::new(seed),
},
);
Ok(info)
}
async fn key(&self, key_ref: &KeyRef) -> Result<KeyInfo, SignerError> {
self.keys
.lock()
.expect("uncontended")
.get(key_ref.as_str())
.map(|key| key.info.clone())
.ok_or_else(|| SignerError::UnknownKey {
key_ref: key_ref.to_string(),
})
}
async fn sign(&self, key_ref: &KeyRef, payload: &Payload) -> Result<Signature, SignerError> {
let key = self
.keys
.lock()
.expect("uncontended")
.get(key_ref.as_str())
.map(|key| (key.info.scheme, key.seed.clone()))
.ok_or_else(|| SignerError::UnknownKey {
key_ref: key_ref.to_string(),
})?;
let (scheme, seed) = key;
if payload.scheme() != scheme {
return Err(SignerError::Unsupported {
reason: format!(
"the key `{key_ref}` is a {scheme} key, and cannot sign {} payloads",
payload.scheme()
),
});
}
Self::sign_seed(&seed, scheme, payload)
}
}
fn derive_seed(subject: &Subject, scheme: Scheme, label: &str, counter: u64) -> [u8; 32] {
use sha2::Digest as _;
let mut hasher = sha2::Sha256::new();
hasher.update(b"cratefield-signer/fake-key/v1");
hasher.update(subject.venture.as_bytes());
if let Some(user) = &subject.user {
hasher.update([0_u8]);
hasher.update(user.as_bytes());
} else {
hasher.update([1_u8]);
}
hasher.update(scheme.to_string().as_bytes());
hasher.update(label.as_bytes());
hasher.update(counter.to_le_bytes());
hasher.finalize().into()
}
pub struct FakeGuardrails {
#[allow(clippy::disallowed_types)]
outcomes: std::sync::Mutex<VecDeque<Result<PolicyDecision, SignerError>>>,
}
impl FakeGuardrails {
#[allow(clippy::disallowed_types)]
#[must_use]
pub fn new(outcomes: impl IntoIterator<Item = Result<PolicyDecision, SignerError>>) -> Self {
Self {
outcomes: std::sync::Mutex::new(outcomes.into_iter().collect()),
}
}
}
#[async_trait]
impl Guardrails for FakeGuardrails {
async fn check(&self, _context: &SignContext) -> Result<PolicyDecision, SignerError> {
match self.outcomes.lock().expect("uncontended").pop_front() {
Some(outcome) => outcome,
None => Ok(PolicyDecision::Deny {
reason: "the scripted guardrails have no outcome left".to_owned(),
}),
}
}
}
pub struct RefusingAudit {
reason: String,
}
impl RefusingAudit {
#[must_use]
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
#[async_trait]
impl SignAudit for RefusingAudit {
async fn record(&self, _record: &SignAuditRecord) -> Result<(), SignerError> {
Err(SignerError::Audit(self.reason.clone()))
}
}