diff --git a/Cargo.lock b/Cargo.lock index e08e44ca..3e71bcab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1813,6 +1813,7 @@ version = "0.1.0" dependencies = [ "casper-types 4.0.1", "hex", + "kairos-tx", "thiserror", ] @@ -1869,6 +1870,7 @@ dependencies = [ "hex", "num-traits", "rasn", + "sha2 0.10.8", ] [[package]] diff --git a/kairos-crypto/Cargo.toml b/kairos-crypto/Cargo.toml index f0c0213f..584d0282 100644 --- a/kairos-crypto/Cargo.toml +++ b/kairos-crypto/Cargo.toml @@ -5,8 +5,9 @@ edition.workspace = true license.workspace = true [features] -default = ["crypto-casper"] +default = ["crypto-casper", "tx"] crypto-casper = ["casper-types"] +tx = ["kairos-tx"] fs = ["casper-types/std"] # TODO: Change `std` -> `std-fs-io` in the future version. [lib] @@ -14,6 +15,7 @@ fs = ["casper-types/std"] # TODO: Change `std` -> `std-fs-io` in the future vers [dependencies] hex = "0.4" thiserror = "1" +kairos-tx = { path = "../kairos-tx", optional = true } # Casper signer implementation. casper-types = { version = "4", optional = true } diff --git a/kairos-crypto/src/error.rs b/kairos-crypto/src/error.rs index 885720af..d911b899 100644 --- a/kairos-crypto/src/error.rs +++ b/kairos-crypto/src/error.rs @@ -17,4 +17,13 @@ pub enum CryptoError { /// Private key is not provided. #[error("private key is not provided")] MissingPrivateKey, + + /// Unable to compute transaction hash - invalid data given. + #[cfg(feature = "tx")] + #[error("unable to hash transaction data: {error}")] + TxHashingError { error: String }, + /// Signing algorithm is not available in `kairos-tx`. + #[cfg(feature = "tx")] + #[error("algorithm not available in tx format")] + InvalidSigningAlgorithm, } diff --git a/kairos-crypto/src/implementations/casper.rs b/kairos-crypto/src/implementations/casper.rs index 645a73da..ebcfa0ca 100644 --- a/kairos-crypto/src/implementations/casper.rs +++ b/kairos-crypto/src/implementations/casper.rs @@ -88,6 +88,50 @@ impl CryptoSigner for Signer { Ok(public_key) } + + #[cfg(feature = "tx")] + fn verify_tx(&self, tx: kairos_tx::asn::Transaction) -> Result<(), CryptoError> { + let tx_hash = tx.payload.hash().map_err(|e| CryptoError::TxHashingError { + error: e.to_string(), + })?; + let signature: Vec = tx.signature.into(); + self.verify(tx_hash, signature)?; + + Ok(()) + } + + #[cfg(feature = "tx")] + fn sign_tx_payload( + &self, + payload: kairos_tx::asn::SigningPayload, + ) -> Result { + // Compute payload signature. + let tx_hash = payload.hash().map_err(|e| CryptoError::TxHashingError { + error: e.to_string(), + })?; + let signature = self.sign(tx_hash)?; + + // Prepare public key. + let public_key = self.to_public_key()?; + + // Prepare algorithm. + let algorithm = match self.public_key { + PublicKey::Ed25519(_) => Ok(kairos_tx::asn::SigningAlgorithm::CasperEd25519), + PublicKey::Secp256k1(_) => Ok(kairos_tx::asn::SigningAlgorithm::CasperSecp256k1), + _ => Err(CryptoError::InvalidSigningAlgorithm), + }?; + + // Build full transaction. + let tx = kairos_tx::asn::Transaction::new( + public_key, + payload, + &tx_hash, + algorithm, + signature.into(), + ); + + Ok(tx) + } } #[cfg(test)] diff --git a/kairos-crypto/src/lib.rs b/kairos-crypto/src/lib.rs index e45477cf..f6fc99cb 100644 --- a/kairos-crypto/src/lib.rs +++ b/kairos-crypto/src/lib.rs @@ -22,5 +22,13 @@ pub trait CryptoSigner { signature_bytes: U, ) -> Result<(), CryptoError>; + #[cfg(feature = "tx")] + fn verify_tx(&self, tx: kairos_tx::asn::Transaction) -> Result<(), CryptoError>; + #[cfg(feature = "tx")] + fn sign_tx_payload( + &self, + payload: kairos_tx::asn::SigningPayload, + ) -> Result; + fn to_public_key(&self) -> Result, CryptoError>; } diff --git a/kairos-tx/Cargo.toml b/kairos-tx/Cargo.toml index 0b4aacc3..a6faaad9 100644 --- a/kairos-tx/Cargo.toml +++ b/kairos-tx/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true [dependencies] num-traits = "0.2" rasn = { version = "0.12", default-features = false, features = ["macros"] } +sha2 = "0.10" [dev-dependencies] hex = "0.4" diff --git a/kairos-tx/schema.asn b/kairos-tx/schema.asn index 049315ea..0f712f6f 100644 --- a/kairos-tx/schema.asn +++ b/kairos-tx/schema.asn @@ -4,9 +4,28 @@ TxSchema DEFINITIONS AUTOMATIC TAGS ::= BEGIN -- Basic types. PublicKey ::= OCTET STRING + Signature ::= OCTET STRING + PayloadHash ::= OCTET STRING Amount ::= INTEGER (0..18446744073709551615) Nonce ::= INTEGER (0..18446744073709551615) + -- Full, top-level transaction type. + Transaction ::= SEQUENCE { + publicKey PublicKey, + payload SigningPayload, + hash PayloadHash, + algorithm SigningAlgorithm, + signature Signature, + ... + } + + -- Support for multiple signing algorithms. + SigningAlgorithm ::= ENUMERATED { + casperSecp256k1 (0), + casperEd25519 (1), + ... + } + -- Transaction payload for signing. SigningPayload ::= SEQUENCE { nonce Nonce, diff --git a/kairos-tx/src/asn.rs b/kairos-tx/src/asn.rs index 3765ffcd..0e0d32bf 100644 --- a/kairos-tx/src/asn.rs +++ b/kairos-tx/src/asn.rs @@ -9,6 +9,7 @@ pub use rasn::types::{Integer, OctetString}; use num_traits::cast::ToPrimitive; use rasn::types::AsnType; use rasn::{Decode, Encode}; +use sha2::Digest; #[derive(AsnType, Encode, Decode, Debug, Clone)] #[rasn(delegate)] @@ -21,6 +22,12 @@ impl From for Vec { } } +impl From> for PublicKey { + fn from(value: Vec) -> Self { + PublicKey(OctetString::copy_from_slice(&value)) + } +} + impl From<&[u8]> for PublicKey { fn from(value: &[u8]) -> Self { PublicKey(OctetString::copy_from_slice(value)) @@ -33,6 +40,40 @@ impl From<&[u8; N]> for PublicKey { } } +#[derive(AsnType, Encode, Decode, Debug, Clone)] +#[rasn(delegate)] +pub struct Signature(pub(crate) OctetString); + +// Converts an ASN.1 decoded signature into raw byte representation. +impl From for Vec { + fn from(value: Signature) -> Self { + value.0.into() + } +} + +impl From> for Signature { + fn from(value: Vec) -> Self { + Signature(OctetString::copy_from_slice(&value)) + } +} + +#[derive(AsnType, Encode, Decode, Debug, Clone)] +#[rasn(delegate)] +pub struct PayloadHash(pub(crate) OctetString); + +// Converts an ASN.1 decoded payload hash into raw byte representation. +impl From for Vec { + fn from(value: PayloadHash) -> Self { + value.0.into() + } +} + +impl From<&[u8; 32]> for PayloadHash { + fn from(value: &[u8; 32]) -> Self { + PayloadHash(OctetString::copy_from_slice(value)) + } +} + #[derive(AsnType, Encode, Decode, Debug, Clone)] #[rasn(delegate)] pub struct Amount(pub(crate) Integer); @@ -82,6 +123,54 @@ impl From for Nonce { } } +#[derive(AsnType, Encode, Decode, Debug)] +#[non_exhaustive] +pub struct Transaction { + pub public_key: PublicKey, // NOTE: Field name can be different than defined in schema, as only **order** is crucial + pub payload: SigningPayload, + pub hash: PayloadHash, + pub algorithm: SigningAlgorithm, + pub signature: Signature, +} + +impl Transaction { + /// Wraps full transaction data for storing. + /// + /// CAUTION: This method does NOT perform validity checks - please use + /// `kairos-crypto::sign_tx_payload()` to construct it safely. + pub fn new( + public_key: impl Into, + payload: SigningPayload, + hash: impl Into, + algorithm: SigningAlgorithm, + signature: Signature, + ) -> Self { + Self { + public_key: public_key.into(), + payload, + hash: hash.into(), + algorithm, + signature, + } + } + + pub fn der_encode(&self) -> Result, TxError> { + rasn::der::encode(self).map_err(TxError::EncodeError) + } + + pub fn der_decode(value: impl AsRef<[u8]>) -> Result { + rasn::der::decode(value.as_ref()).map_err(TxError::DecodeError) + } +} + +#[derive(AsnType, Encode, Decode, Debug, PartialEq, Copy, Clone)] +#[rasn(enumerated)] +#[non_exhaustive] +pub enum SigningAlgorithm { + CasperSecp256k1 = 0, + CasperEd25519 = 1, +} + #[derive(AsnType, Encode, Decode, Debug)] #[non_exhaustive] pub struct SigningPayload { @@ -130,6 +219,15 @@ impl SigningPayload { pub fn der_decode(value: impl AsRef<[u8]>) -> Result { rasn::der::decode(value.as_ref()).map_err(TxError::DecodeError) } + + // Computes the hash for a transaction. + // Hash is obtained from payload by computing sha256 of DER encoded ASN.1 data. + pub fn hash(&self) -> Result<[u8; 32], TxError> { + let data = self.der_encode()?; + let tx_hash: [u8; 32] = sha2::Sha256::digest(data).into(); + + Ok(tx_hash) + } } #[derive(AsnType, Encode, Decode, Debug)] @@ -206,6 +304,22 @@ impl Withdrawal { } } +impl TryFrom<&[u8]> for Transaction { + type Error = TxError; + + fn try_from(value: &[u8]) -> Result { + Transaction::der_decode(value) + } +} + +impl TryFrom for Vec { + type Error = TxError; + + fn try_from(value: Transaction) -> Result { + value.der_encode() + } +} + impl TryFrom<&[u8]> for SigningPayload { type Error = TxError;