#[cfg(not(feature = "std"))]
use alloc::string::ToString;
use alloc::{format, string::String, vec::Vec};
use k256::schnorr::{Signature, SigningKey, VerifyingKey};
use zeroize::{ZeroizeOnDrop, Zeroizing};
use crate::{SignError, SignOutput};
pub struct SchnorrSigner {
key: SigningKey,
raw_bytes: Zeroizing<[u8; 32]>,
}
impl core::fmt::Debug for SchnorrSigner {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SchnorrSigner")
.field("key", &"[REDACTED]")
.finish()
}
}
impl ZeroizeOnDrop for SchnorrSigner {}
impl SchnorrSigner {
pub fn from_bytes(bytes: &[u8; 32]) -> Result<Self, SignError> {
let key = SigningKey::from_bytes(bytes.as_slice())
.map_err(|e| SignError::InvalidKey(e.to_string()))?;
Ok(Self {
key,
raw_bytes: Zeroizing::new(*bytes),
})
}
pub fn from_hex(hex_str: &str) -> Result<Self, SignError> {
let stripped = hex_str.strip_prefix("0x").unwrap_or(hex_str);
let decoded = hex::decode(stripped).map_err(|e| SignError::InvalidKey(e.to_string()))?;
let bytes: [u8; 32] = decoded.try_into().map_err(|v: Vec<u8>| {
SignError::InvalidKey(format!("expected 32 bytes, got {}", v.len()))
})?;
Self::from_bytes(&bytes)
}
#[cfg(feature = "getrandom")]
pub fn try_random() -> Result<Self, SignError> {
let mut bytes = Zeroizing::new([0u8; 32]);
getrandom::fill(&mut *bytes).map_err(|e| SignError::SigningFailed(e.to_string()))?;
let key = SigningKey::from_bytes(bytes.as_slice())
.map_err(|e| SignError::InvalidKey(e.to_string()))?;
Ok(Self {
key,
raw_bytes: bytes,
})
}
#[cfg(feature = "getrandom")]
#[must_use]
#[allow(
clippy::expect_used,
reason = "panicking wrapper — callers needing graceful handling use try_random"
)]
pub fn random() -> Self {
Self::try_random().expect("SchnorrSigner::random: entropy source failed")
}
#[must_use]
pub const fn signing_key(&self) -> &SigningKey {
&self.key
}
#[must_use]
pub fn verifying_key(&self) -> &VerifyingKey {
self.key.verifying_key()
}
#[must_use]
pub fn to_bytes(&self) -> [u8; 32] {
*self.raw_bytes
}
#[must_use]
pub fn xonly_public_key(&self) -> [u8; 32] {
let mut out = [0u8; 32];
out.copy_from_slice(&self.key.verifying_key().to_bytes());
out
}
#[must_use]
pub fn xonly_public_key_hex(&self) -> String {
hex::encode(self.xonly_public_key())
}
pub fn sign(&self, message: &[u8]) -> Result<SignOutput, SignError> {
let sig = self
.key
.sign_raw(message, &[0u8; 32])
.map_err(|e| SignError::SigningFailed(e.to_string()))?;
Ok(self.output(sig))
}
pub fn sign_prehash(&self, digest: &[u8]) -> Result<SignOutput, SignError> {
if digest.len() != 32 {
return Err(SignError::InvalidMessage(format!(
"expected 32-byte digest, got {}",
digest.len()
)));
}
self.sign(digest)
}
pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<(), SignError> {
let sig = Signature::try_from(signature)
.map_err(|e| SignError::InvalidSignature(e.to_string()))?;
self.key
.verifying_key()
.verify_raw(message, &sig)
.map_err(|e| SignError::InvalidSignature(e.to_string()))
}
fn output(&self, sig: Signature) -> SignOutput {
SignOutput::Schnorr {
signature: sig.to_bytes(),
xonly_public_key: self.xonly_public_key(),
}
}
}