pub mod narrowing;
pub mod receipt;
use ed25519_dalek::{Signature, Signer as DalekSigner, SigningKey, VerifyingKey};
use rand::rngs::OsRng;
use zeroize::ZeroizeOnDrop;
pub trait Signer: Send + Sync {
fn verifying_key(&self) -> VerifyingKey;
fn sign_message(&self, msg: &[u8]) -> Signature;
}
#[cfg(feature = "async")]
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
#[async_trait::async_trait]
pub trait AsyncSigner: Send + Sync {
fn verifying_key(&self) -> VerifyingKey;
async fn sign_message(&self, msg: &[u8]) -> Signature;
}
#[derive(ZeroizeOnDrop)]
pub struct DyoloIdentity {
signing_key: SigningKey,
}
impl DyoloIdentity {
pub fn generate() -> Self {
Self {
signing_key: SigningKey::generate(&mut OsRng),
}
}
pub fn from_signing_bytes(bytes: &[u8; 32]) -> Self {
Self {
signing_key: SigningKey::from_bytes(bytes),
}
}
pub fn to_signing_bytes(&self) -> [u8; 32] {
self.signing_key.to_bytes()
}
pub fn verifying_key(&self) -> VerifyingKey {
self.signing_key.verifying_key()
}
#[allow(dead_code)]
pub(crate) fn sign(&self, message: &[u8]) -> Signature {
self.signing_key.sign(message)
}
}
impl Signer for DyoloIdentity {
fn verifying_key(&self) -> VerifyingKey {
self.signing_key.verifying_key()
}
fn sign_message(&self, msg: &[u8]) -> Signature {
self.signing_key.sign(msg)
}
}
impl std::fmt::Debug for DyoloIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let vk = self.verifying_key();
let b = vk.as_bytes();
write!(
f,
"DyoloIdentity(vk:{:02x}{:02x}{:02x}{:02x}…)",
b[0], b[1], b[2], b[3]
)
}
}
#[derive(Clone, Debug)]
pub struct SharedIdentity(pub std::sync::Arc<DyoloIdentity>);
impl Signer for SharedIdentity {
fn verifying_key(&self) -> VerifyingKey {
self.0.verifying_key()
}
fn sign_message(&self, msg: &[u8]) -> Signature {
Signer::sign_message(&*self.0, msg)
}
}
#[cfg(feature = "async")]
#[async_trait::async_trait]
impl AsyncSigner for SharedIdentity {
fn verifying_key(&self) -> VerifyingKey {
self.0.verifying_key()
}
async fn sign_message(&self, msg: &[u8]) -> Signature {
Signer::sign_message(&*self.0, msg)
}
}
#[cfg(feature = "async")]
#[async_trait::async_trait]
impl AsyncSigner for DyoloIdentity {
fn verifying_key(&self) -> VerifyingKey {
self.verifying_key()
}
async fn sign_message(&self, msg: &[u8]) -> Signature {
Signer::sign_message(self, msg)
}
}
#[cfg(feature = "async")]
#[async_trait::async_trait]
pub trait KmsHttpClient: Send + Sync {
async fn post(
&self,
url: &str,
headers: &[(&str, &str)],
body: &[u8],
) -> Result<Vec<u8>, crate::error::A1Error>;
}
#[cfg(feature = "async")]
pub struct VaultSigner {
vault_addr: String,
token: String,
key_name: String,
public_key: VerifyingKey,
http_client: Box<dyn KmsHttpClient>,
}
#[cfg(feature = "async")]
impl std::fmt::Debug for VaultSigner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VaultSigner")
.field("vault_addr", &self.vault_addr)
.field("key_name", &self.key_name)
.field("public_key", &hex::encode(self.public_key.as_bytes()))
.finish()
}
}
#[cfg(feature = "async")]
impl VaultSigner {
pub fn new(
vault_addr: String,
token: String,
key_name: String,
public_key_hex: &str,
http_client: Box<dyn KmsHttpClient>,
) -> Result<Self, crate::error::A1Error> {
let pk_bytes = hex::decode(public_key_hex)
.map_err(|_| crate::error::A1Error::WireFormatError("invalid hex".into()))?;
let public_key = VerifyingKey::from_bytes(
&pk_bytes
.try_into()
.map_err(|_| crate::error::A1Error::WireFormatError("must be 32 bytes".into()))?,
)
.map_err(|_| crate::error::A1Error::WireFormatError("invalid ed25519 key".into()))?;
Ok(Self {
vault_addr,
token,
key_name,
public_key,
http_client,
})
}
}
#[cfg(feature = "async")]
#[async_trait::async_trait]
impl AsyncSigner for VaultSigner {
fn verifying_key(&self) -> VerifyingKey {
self.public_key
}
async fn sign_message(&self, msg: &[u8]) -> Signature {
use base64::{engine::general_purpose, Engine as _};
let encoded_input = general_purpose::STANDARD.encode(msg);
let payload = format!(
"{{\"input\": \"{}\", \"context\": \"ZHlvbG9fdjIuOC4w\"}}",
encoded_input
);
let url = format!("{}/v1/transit/sign/{}", self.vault_addr, self.key_name);
let headers = [
("X-Vault-Token", self.token.as_str()),
("Content-Type", "application/json"),
];
let resp_bytes = self
.http_client
.post(&url, &headers, payload.as_bytes())
.await
.expect("vault KMS post failed");
let resp_json: serde_json::Value =
serde_json::from_slice(&resp_bytes).expect("vault returned invalid JSON");
let sig_b64 = resp_json["data"]["signature"]
.as_str()
.expect("vault response missing data.signature")
.trim_start_matches("vault:v1:");
let sig_bytes = general_purpose::STANDARD
.decode(sig_b64)
.expect("vault signature base64 decode failed");
Signature::from_bytes(
&sig_bytes
.try_into()
.expect("vault signature must be 64 bytes"),
)
}
}