From d572e884268172c4df74c2cce1f2b2bcddcd4a3e Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Tue, 16 Dec 2025 17:54:33 -0400 Subject: [PATCH 01/11] p-256 algo acheivement unlocked --- crates/multikey/.cargo/config.toml | 2 + crates/multikey/.gitignore | 1 + crates/multikey/Cargo.toml | 13 +- crates/multikey/justfile | 3 + crates/multikey/src/error.rs | 6 + crates/multikey/src/mk.rs | 12 +- crates/multikey/src/views.rs | 6 +- crates/multikey/src/views/p256.rs | 739 +++++++++++++++++++++ crates/multikey/tests/p256_verification.rs | 192 ++++++ crates/multikey/tests/web.rs | 35 + crates/multisig/src/ms.rs | 9 +- crates/multisig/src/views.rs | 2 + crates/multisig/src/views/p256.rs | 71 ++ justfile | 89 +++ 14 files changed, 1170 insertions(+), 10 deletions(-) create mode 100644 crates/multikey/.cargo/config.toml create mode 100644 crates/multikey/.gitignore create mode 100644 crates/multikey/justfile create mode 100644 crates/multikey/src/views/p256.rs create mode 100644 crates/multikey/tests/p256_verification.rs create mode 100644 crates/multikey/tests/web.rs create mode 100644 crates/multisig/src/views/p256.rs diff --git a/crates/multikey/.cargo/config.toml b/crates/multikey/.cargo/config.toml new file mode 100644 index 0000000..0e465b2 --- /dev/null +++ b/crates/multikey/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.wasm32-unknown-unknown] +rustflags = ["--cfg", "getrandom_backend=\"wasm_js\""] diff --git a/crates/multikey/.gitignore b/crates/multikey/.gitignore new file mode 100644 index 0000000..01d0a08 --- /dev/null +++ b/crates/multikey/.gitignore @@ -0,0 +1 @@ +pkg/ diff --git a/crates/multikey/Cargo.toml b/crates/multikey/Cargo.toml index 94eaaae..5dfd8f3 100644 --- a/crates/multikey/Cargo.toml +++ b/crates/multikey/Cargo.toml @@ -7,6 +7,9 @@ description = "Multikey self-describing cryptographic key data" readme = "README.md" license = "Apache-2.0" +[lib] +crate-type = ["cdylib", "rlib"] + [features] default = ["serde"] wasm = ["getrandom/wasm_js"] # needed for CI testing on wasm32-unknown-unknown @@ -19,6 +22,10 @@ ed25519-dalek = { version = "2.1.1", features = ["rand_core"] } elliptic-curve.workspace = true hex.workspace = true k256 = "0.13" +p256 = { version = "0.13", default-features = false, features = [ + "ecdsa", + "arithmetic", +] } ml-kem = { version = "0.2.1", features = ["deterministic"] } multibase.workspace = true multicodec.workspace = true @@ -46,8 +53,11 @@ ssh-key = { version = "0.6", default-features = false, features = [ "alloc", "ecdsa", "ed25519", + "p256", ] } -getrandom = { version = "0.3.2", features = ["wasm_js"], optional = true } +getrandom = { version = "0.3", features = ["wasm_js"] } +# We have netsed deps that use v0.2, so we need to ensure the feature is flagged here +getrandom_v02 = { package = "getrandom", version = "0.2.15", features = ["js"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] ssh-key = { version = "0.6", features = ["alloc", "crypto", "ed25519"] } @@ -56,6 +66,7 @@ ssh-key = { version = "0.6", features = ["alloc", "crypto", "ed25519"] } serde_cbor.workspace = true serde_json.workspace = true serde_test.workspace = true +wasm-bindgen-test = "0.3" [lints] workspace = true diff --git a/crates/multikey/justfile b/crates/multikey/justfile new file mode 100644 index 0000000..bf6391d --- /dev/null +++ b/crates/multikey/justfile @@ -0,0 +1,3 @@ +test-wasm: + wasm-pack build --target web + wasm-pack test --headless --chrome --all-features diff --git a/crates/multikey/src/error.rs b/crates/multikey/src/error.rs index 64456a5..48dff51 100644 --- a/crates/multikey/src/error.rs +++ b/crates/multikey/src/error.rs @@ -63,6 +63,12 @@ pub enum Error { UnsupportedAlgorithm(String), } +impl From for Error { + fn from(e: ssh_key::Error) -> Self { + Error::Conversions(ConversionsError::Ssh(e.into())) + } +} + /// Attributes errors created by this library #[derive(Clone, Debug, thiserror::Error)] #[non_exhaustive] diff --git a/crates/multikey/src/mk.rs b/crates/multikey/src/mk.rs index 5912a3a..9306d71 100644 --- a/crates/multikey/src/mk.rs +++ b/crates/multikey/src/mk.rs @@ -1,7 +1,7 @@ -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{ error::{AttributesError, CipherError, ConversionsError, KdfError}, - views::{bcrypt, bls12381, chacha20, ed25519, mlkem, secp256k1}, + views::{bcrypt, bls12381, chacha20, ed25519, mlkem, p256, secp256k1}, AttrId, AttrView, CipherAttrView, CipherView, ConvView, DataView, Error, FingerprintView, KdfAttrView, KdfView, SignView, ThresholdAttrView, ThresholdView, VerifyView, Views, }; @@ -13,6 +13,7 @@ use multiutil::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes, VarbytesIter, Va use ssh_key::{ private::{EcdsaKeypair, KeypairData}, public::{EcdsaPublicKey, KeyData}, + Algorithm::*, EcdsaCurve, PrivateKey, PublicKey, }; use std::{collections::BTreeMap, fmt, num::NonZeroUsize}; @@ -205,6 +206,7 @@ impl Views for Multikey { | Codec::Bls12381G2Pub | Codec::Bls12381G2PubShare => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(self)?)), + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(p256::View::try_from(self)?)), Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } @@ -250,6 +252,7 @@ impl Views for Multikey { | Codec::Bls12381G2Pub | Codec::Bls12381G2PubShare => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(self)?)), + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(p256::View::try_from(self)?)), Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } @@ -318,6 +321,7 @@ impl Views for Multikey { | Codec::Bls12381G2Pub | Codec::Bls12381G2PubShare => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(self)?)), + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(p256::View::try_from(self)?)), Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } @@ -343,6 +347,7 @@ impl Views for Multikey { | Codec::Bls12381G2Pub | Codec::Bls12381G2PubShare => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(self)?)), + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(p256::View::try_from(self)?)), Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } @@ -377,6 +382,7 @@ impl Views for Multikey { | Codec::Bls12381G2Pub | Codec::Bls12381G2PubShare => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(self)?)), + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(p256::View::try_from(self)?)), Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } @@ -406,6 +412,7 @@ impl Views for Multikey { | Codec::Bls12381G2Pub | Codec::Bls12381G2PubShare => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::Ed25519Pub | Codec::Ed25519Priv => Ok(Box::new(ed25519::View::try_from(self)?)), + Codec::P256Pub | Codec::P256Priv => Ok(Box::new(p256::View::try_from(self)?)), Codec::Secp256K1Pub | Codec::Secp256K1Priv => { Ok(Box::new(secp256k1::View::try_from(self)?)) } @@ -487,7 +494,6 @@ impl Builder { /// new builder from ssh_key::PublicKey source pub fn new_from_ssh_public_key(sshkey: &PublicKey) -> Result { - use ssh_key::Algorithm::*; match sshkey.algorithm() { Ecdsa { curve } => { use EcdsaCurve::*; diff --git a/crates/multikey/src/views.rs b/crates/multikey/src/views.rs index 80b0500..60b28f0 100644 --- a/crates/multikey/src/views.rs +++ b/crates/multikey/src/views.rs @@ -1,10 +1,9 @@ -use std::num::NonZeroUsize; - -// SPDX-License-Idnetifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 use crate::{Error, Multikey}; use multicodec::Codec; use multihash::Multihash; use multisig::Multisig; +use std::num::NonZeroUsize; use zeroize::Zeroizing; // algorithms implement different sets of view @@ -13,6 +12,7 @@ pub(crate) mod bls12381; pub(crate) mod chacha20; pub(crate) mod ed25519; pub(crate) mod mlkem; +pub(crate) mod p256; pub(crate) mod secp256k1; // Attributes views let you inquire about the Multikey and retrieve data // associated with the particular view. diff --git a/crates/multikey/src/views/p256.rs b/crates/multikey/src/views/p256.rs new file mode 100644 index 0000000..cb56df9 --- /dev/null +++ b/crates/multikey/src/views/p256.rs @@ -0,0 +1,739 @@ +// SPDX-License-Identifier: Apache-2.0 +//! P-256 (NIST P-256 / secp256r1 / prime256v1) key view implementations +//! +//! This module provides full support for P-256 ECDSA signatures (ES256). +//! Primary use case: WebAuthn/passkey signature verification, but signing is also supported. +//! +//! Note: For WebAuthn, passkeys handle signing externally via authenticator hardware/software. +//! This implementation can be used for general P-256 ECDSA operations when needed. + +use crate::{ + error::{AttributesError, CipherError, ConversionsError, KdfError, SignError, VerifyError}, + AttrId, AttrView, Builder, CipherAttrView, ConvView, DataView, Error, FingerprintView, + KdfAttrView, Multikey, SignView, VerifyView, Views, +}; + +use multicodec::Codec; +use multihash::{mh, Multihash}; +use multisig::{ms, Multisig, Views as SigViews}; +use multitrait::TryDecodeFrom; +use multiutil::Varuint; +use p256::ecdsa::signature::Signer; +use p256::ecdsa::{signature::Verifier, Signature, SigningKey, VerifyingKey}; +use zeroize::Zeroizing; + +/// The number of bytes in a P-256 secret key (scalar) +pub const SECRET_KEY_LENGTH: usize = 32; + +/// The number of bytes in a compressed P-256 public key (SEC1 format) +#[allow(dead_code)] +pub const PUBLIC_KEY_COMPRESSED_LENGTH: usize = 33; + +/// The number of bytes in an uncompressed P-256 public key (SEC1 format) +#[allow(dead_code)] +pub const PUBLIC_KEY_UNCOMPRESSED_LENGTH: usize = 65; + +/// The number of bytes in a P-256 ECDSA signature (raw r||s format) +#[allow(dead_code)] +pub const SIGNATURE_LENGTH: usize = 64; + +pub(crate) struct View<'a> { + mk: &'a Multikey, +} + +impl<'a> TryFrom<&'a Multikey> for View<'a> { + type Error = Error; + + fn try_from(mk: &'a Multikey) -> Result { + Ok(Self { mk }) + } +} + +impl AttrView for View<'_> { + fn is_encrypted(&self) -> bool { + if let Some(v) = self.mk.attributes.get(&AttrId::KeyIsEncrypted) { + if let Ok((b, _)) = Varuint::::try_decode_from(v.as_slice()) { + return b.to_inner(); + } + } + false + } + + fn is_secret_key(&self) -> bool { + self.mk.codec == Codec::P256Priv + } + + fn is_public_key(&self) -> bool { + self.mk.codec == Codec::P256Pub + } + + fn is_secret_key_share(&self) -> bool { + false // P-256 doesn't support threshold signatures natively + } +} + +impl DataView for View<'_> { + fn key_bytes(&self) -> Result>, Error> { + let key = self + .mk + .attributes + .get(&AttrId::KeyData) + .ok_or(AttributesError::MissingKey)?; + Ok(key.clone()) + } + + fn secret_bytes(&self) -> Result>, Error> { + if !self.is_secret_key() { + return Err(AttributesError::NotSecretKey(self.mk.codec).into()); + } + if self.is_encrypted() { + return Err(AttributesError::EncryptedKey.into()); + } + self.key_bytes() + } +} + +impl CipherAttrView for View<'_> { + fn cipher_codec(&self) -> Result { + let codec = self + .mk + .attributes + .get(&AttrId::CipherCodec) + .ok_or(CipherError::MissingCodec)?; + Ok(Codec::try_from(codec.as_slice())?) + } + + fn nonce_bytes(&self) -> Result>, Error> { + self.mk + .attributes + .get(&AttrId::CipherNonce) + .ok_or(CipherError::MissingNonce.into()) + .cloned() + } + + fn key_length(&self) -> Result { + let key_length = self + .mk + .attributes + .get(&AttrId::CipherKeyLen) + .ok_or(CipherError::MissingKeyLen)?; + Ok(Varuint::::try_from(key_length.as_slice())?.to_inner()) + } +} + +impl KdfAttrView for View<'_> { + fn kdf_codec(&self) -> Result { + let codec = self + .mk + .attributes + .get(&AttrId::KdfCodec) + .ok_or(KdfError::MissingCodec)?; + Ok(Codec::try_from(codec.as_slice())?) + } + + fn salt_bytes(&self) -> Result>, Error> { + self.mk + .attributes + .get(&AttrId::KdfSalt) + .ok_or(KdfError::MissingSalt.into()) + .cloned() + } + + fn rounds(&self) -> Result { + let rounds = self + .mk + .attributes + .get(&AttrId::KdfRounds) + .ok_or(KdfError::MissingRounds)?; + Ok(Varuint::::try_from(rounds.as_slice())?.to_inner()) + } +} + +impl FingerprintView for View<'_> { + fn fingerprint(&self, codec: Codec) -> Result { + let attr = self.mk.attr_view()?; + if attr.is_secret_key() { + // Convert to public key first, then fingerprint it + let pk = self.to_public_key()?; + let fp = pk.fingerprint_view()?; + fp.fingerprint(codec) + } else { + // Hash the public key bytes directly + let bytes = { + let kd = self.mk.data_view()?; + kd.key_bytes()? + }; + Ok(mh::Builder::new_from_bytes(codec, bytes)?.try_build()?) + } + } +} + +impl ConvView for View<'_> { + fn to_public_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + // Create P-256 signing key from 32-byte scalar + let bytes: [u8; SECRET_KEY_LENGTH] = secret_bytes.as_slice()[..SECRET_KEY_LENGTH] + .try_into() + .map_err(|_| { + ConversionsError::SecretKeyFailure("P-256 secret key must be 32 bytes".to_string()) + })?; + + let signing_key = SigningKey::from_bytes(&bytes.into()) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + + let verifying_key = signing_key.verifying_key(); + + // Encode as compressed SEC1 (33 bytes: 0x02/0x03 + x-coordinate) + let compressed = true; + let encoded_point = verifying_key.to_encoded_point(compressed); + let public_key_bytes = encoded_point.as_bytes(); + + Builder::new(Codec::P256Pub) + .with_comment(&self.mk.comment) + .with_key_bytes(&public_key_bytes) + .try_build() + } + + fn to_ssh_public_key(&self) -> Result { + use ssh_key::public::{EcdsaPublicKey, KeyData}; + + let mut pk = self.mk.clone(); + if self.is_secret_key() { + pk = self.to_public_key()?; + } + + let key_bytes = { + let kd = pk.data_view()?; + kd.key_bytes()? + }; + + // Parse SEC1 encoded point (handles both compressed and uncompressed) + let verifying_key = VerifyingKey::from_sec1_bytes(&key_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + + // Get uncompressed point (65 bytes: 0x04 || x || y) + let compressed = false; + let encoded_point = verifying_key.to_encoded_point(compressed); + + // SSH sec1::EncodedPoint expects the point WITHOUT the 0x04 prefix + // It should be exactly 64 bytes (32 bytes x + 32 bytes y) + let point_bytes = encoded_point.as_bytes(); + if point_bytes.len() != 65 || point_bytes[0] != 0x04 { + return Err(ConversionsError::PublicKeyFailure( + "Expected uncompressed point with 0x04 prefix".to_string(), + ) + .into()); + } + + // Create the 64-byte array (x || y coordinates) without the 0x04 prefix + let point_data: [u8; 64] = point_bytes[1..] + .try_into() + .map_err(|_| ConversionsError::PublicKeyFailure("Invalid point size".to_string()))?; + + // SSH expects a sec1::EncodedPoint, which we can create from the uncompressed bytes + // The sec1::EncodedPoint::from_untagged_bytes expects the bytes without the tag + let sec1_point = + sec1::EncodedPoint::::from_untagged_bytes(&point_data.into()); + + let ecdsa_key = EcdsaPublicKey::NistP256(sec1_point); + + Ok(ssh_key::PublicKey::new( + KeyData::Ecdsa(ecdsa_key), + pk.comment, + )) + } + + fn to_ssh_private_key(&self) -> Result { + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + let bytes: [u8; SECRET_KEY_LENGTH] = secret_bytes.as_slice()[..SECRET_KEY_LENGTH] + .try_into() + .map_err(|_| { + ConversionsError::SecretKeyFailure("P-256 secret key must be 32 bytes".to_string()) + })?; + + let secret_key = SigningKey::from_bytes(&bytes.into()) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + + let verifying_key = secret_key.verifying_key(); + + // Get the public key point in uncompressed format (without 0x04 prefix) + let encoded_point = verifying_key.to_encoded_point(false); + let point_bytes = encoded_point.as_bytes(); + if point_bytes.len() != 65 || point_bytes[0] != 0x04 { + return Err(ConversionsError::PublicKeyFailure( + "Expected uncompressed point".to_string(), + ) + .into()); + } + + // Create the public key bytes (64 bytes: x || y) + let public_bytes: [u8; 64] = point_bytes[1..] + .try_into() + .map_err(|_| ConversionsError::PublicKeyFailure("Invalid point size".to_string()))?; + + // Create the SSH keypair structure + // Convert to p256::SecretKey first, then to ssh_key types + let p256_secret = p256::SecretKey::from_bytes(&bytes.into()) + .map_err(|e| ConversionsError::SecretKeyFailure(e.to_string()))?; + + let private_key_bytes = + ssh_key::private::EcdsaPrivateKey::::from(p256_secret); + let public_key_point = + sec1::EncodedPoint::::from_untagged_bytes(&public_bytes.into()); + + let keypair = ssh_key::private::EcdsaKeypair::NistP256 { + private: private_key_bytes, + public: public_key_point, + }; + + Ok(ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::Ecdsa(keypair), + self.mk.comment.clone(), + )?) + } +} + +impl SignView for View<'_> { + /// Sign a message with P-256 ECDSA (ES256) + /// + /// Creates an ECDSA signature over the provided message. + /// For WebAuthn use cases, passkeys typically handle signing externally, + /// but this implementation supports general P-256 signing operations. + fn sign(&self, msg: &[u8], combined: bool, _scheme: Option) -> Result { + // Get the secret key bytes + let secret_bytes = { + let kd = self.mk.data_view()?; + kd.secret_bytes()? + }; + + // Create P-256 signing key from 32-byte scalar + let bytes: [u8; SECRET_KEY_LENGTH] = secret_bytes.as_slice()[..SECRET_KEY_LENGTH] + .try_into() + .map_err(|_| { + SignError::SigningFailed("P-256 secret key must be 32 bytes".to_string()) + })?; + + let signing_key = SigningKey::from_bytes(&bytes.into()) + .map_err(|e| SignError::SigningFailed(e.to_string()))?; + + let signature: Signature = signing_key.sign(msg); + + let mut builder = + ms::Builder::new(Codec::Es256Msig).with_signature_bytes(&signature.to_bytes()); + + if combined { + builder = builder.with_message_bytes(&msg); + } + + Ok(builder.try_build()?) + } +} + +impl VerifyView for View<'_> { + /// Verify a P-256 ECDSA signature (ES256) + /// + /// This is the primary function for passkey support, as it verifies signatures + /// created by external authenticators. + fn verify(&self, multisig: &Multisig, msg: Option<&[u8]>) -> Result<(), Error> { + let attr = self.mk.attr_view()?; + let pubmk = if attr.is_secret_key() { + let kc = self.mk.conv_view()?; + kc.to_public_key()? + } else { + self.mk.clone() + }; + + let key_bytes = { + let kd = pubmk.data_view()?; + kd.key_bytes()? + }; + + // Create verifying key from SEC1-encoded public key + let verifying_key = VerifyingKey::from_sec1_bytes(&key_bytes) + .map_err(|e| ConversionsError::PublicKeyFailure(e.to_string()))?; + + let sv = multisig.data_view()?; + let sig_bytes = sv.sig_bytes().map_err(|_| VerifyError::MissingSignature)?; + + // Create signature from bytes (handles both DER and raw r||s formats) + let signature = Signature::from_slice(&sig_bytes) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + + let msg = if let Some(msg) = msg { + msg + } else if !multisig.message.is_empty() { + multisig.message.as_slice() + } else { + return Err(VerifyError::MissingMessage.into()); + }; + + verifying_key + .verify(msg, &signature) + .map_err(|e| VerifyError::BadSignature(e.to_string()))?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Builder; + use multicodec::Codec; + use multiutil::CodecInfo; + use rand_core_6::OsRng; + use ssh_key::private::EcdsaKeypair; + use ssh_key::private::KeypairData; + use ssh_key::PrivateKey; + + fn create_test_keypair() -> Multikey { + let mut rng = OsRng; + Builder::new_from_random_bytes(Codec::P256Priv, &mut rng) + .unwrap() + .try_build() + .unwrap() + } + + fn create_test_keypair_from_ssh() -> Multikey { + let mut rng = OsRng; + let keypair = EcdsaKeypair::random(&mut rng, ssh_key::EcdsaCurve::NistP256).unwrap(); + let private_key_ssh = PrivateKey::new(KeypairData::Ecdsa(keypair), "test").unwrap(); + Builder::new_from_ssh_private_key(&private_key_ssh) + .unwrap() + .try_build() + .unwrap() + } + + #[test] + fn test_attr_view_secret_key() { + let mk = create_test_keypair(); + let view = View::try_from(&mk).unwrap(); + + assert!(!view.is_encrypted()); + assert!(view.is_secret_key()); + assert!(!view.is_public_key()); + assert!(!view.is_secret_key_share()); + } + + #[test] + fn test_attr_view_public_key() { + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let view = View::try_from(&pk).unwrap(); + + assert!(!view.is_encrypted()); + assert!(!view.is_secret_key()); + assert!(view.is_public_key()); + assert!(!view.is_secret_key_share()); + } + + #[test] + fn test_data_view_key_bytes() { + let mk = create_test_keypair(); + let view = View::try_from(&mk).unwrap(); + + let key_bytes = view.key_bytes().unwrap(); + assert_eq!(key_bytes.len(), SECRET_KEY_LENGTH); + } + + #[test] + fn test_data_view_secret_bytes() { + let mk = create_test_keypair(); + let view = View::try_from(&mk).unwrap(); + + let secret_bytes = view.secret_bytes().unwrap(); + assert_eq!(secret_bytes.len(), SECRET_KEY_LENGTH); + } + + #[test] + fn test_data_view_secret_bytes_fails_for_public_key() { + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let view = View::try_from(&pk).unwrap(); + + let result = view.secret_bytes(); + assert!(result.is_err()); + } + + #[test] + fn test_to_public_key() { + let mk = create_test_keypair(); + let view = View::try_from(&mk).unwrap(); + + let pk = view.to_public_key().unwrap(); + assert_eq!(pk.codec(), Codec::P256Pub); + + let pk_view = View::try_from(&pk).unwrap(); + assert!(pk_view.is_public_key()); + assert!(!pk_view.is_secret_key()); + + let key_bytes = pk_view.key_bytes().unwrap(); + assert_eq!(key_bytes.len(), PUBLIC_KEY_COMPRESSED_LENGTH); + } + + #[test] + fn test_to_public_key_is_deterministic() { + let mk = create_test_keypair(); + let view = View::try_from(&mk).unwrap(); + + let pk1 = view.to_public_key().unwrap(); + let pk2 = view.to_public_key().unwrap(); + + let pk1_bytes = pk1.data_view().unwrap().key_bytes().unwrap(); + let pk2_bytes = pk2.data_view().unwrap().key_bytes().unwrap(); + + assert_eq!(pk1_bytes.as_slice(), pk2_bytes.as_slice()); + } + + #[test] + fn test_fingerprint_view_for_secret_key() { + let mk = create_test_keypair(); + let view = View::try_from(&mk).unwrap(); + + let fingerprint = view.fingerprint(Codec::Sha2256).unwrap(); + let digest: Vec = fingerprint.into(); + // Multihash includes codec prefix (2 bytes) + digest (32 bytes for SHA-256) + assert!(digest.len() == 34, "Multihash should include digest bytes"); + } + + #[test] + fn test_fingerprint_view_for_public_key() { + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let view = View::try_from(&pk).unwrap(); + + let fingerprint = view.fingerprint(Codec::Sha2256).unwrap(); + let digest: Vec = fingerprint.into(); + // Multihash includes codec prefix (2 bytes) + digest (32 bytes for SHA-256) + assert!(digest.len() >= 32, "Multihash should include digest bytes"); + } + + #[test] + fn test_fingerprint_same_for_secret_and_public() { + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + + let sk_view = View::try_from(&mk).unwrap(); + let pk_view = View::try_from(&pk).unwrap(); + + let sk_fp = sk_view.fingerprint(Codec::Sha2256).unwrap(); + let pk_fp = pk_view.fingerprint(Codec::Sha2256).unwrap(); + + let sk_digest: Vec = sk_fp.into(); + let pk_digest: Vec = pk_fp.into(); + + assert_eq!(sk_digest, pk_digest); + } + + #[test] + fn test_to_ssh_public_key() { + let mk = create_test_keypair(); + let view = View::try_from(&mk).unwrap(); + + let ssh_pub = view.to_ssh_public_key().unwrap(); + assert_eq!( + ssh_pub.algorithm(), + ssh_key::Algorithm::Ecdsa { + curve: ssh_key::EcdsaCurve::NistP256 + } + ); + } + + #[test] + fn test_to_ssh_public_key_from_public_key() { + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let view = View::try_from(&pk).unwrap(); + + let ssh_pub = view.to_ssh_public_key().unwrap(); + assert_eq!( + ssh_pub.algorithm(), + ssh_key::Algorithm::Ecdsa { + curve: ssh_key::EcdsaCurve::NistP256 + } + ); + } + + #[test] + fn test_to_ssh_private_key() { + let mk = create_test_keypair(); + let view = View::try_from(&mk).unwrap(); + + let ssh_priv = view.to_ssh_private_key().unwrap(); + assert_eq!( + ssh_priv.algorithm(), + ssh_key::Algorithm::Ecdsa { + curve: ssh_key::EcdsaCurve::NistP256 + } + ); + } + + #[test] + fn test_to_ssh_private_key_roundtrip() { + // Create a key, convert to SSH, then back to Multikey + // The reconstructed key should be able to produce the same public key + let mk1 = create_test_keypair(); + let pk1 = mk1.conv_view().unwrap().to_public_key().unwrap(); + let pk1_bytes = pk1.data_view().unwrap().key_bytes().unwrap(); + + let view = View::try_from(&mk1).unwrap(); + let ssh_priv = view.to_ssh_private_key().unwrap(); + + // Convert back + let mk2 = Builder::new_from_ssh_private_key(&ssh_priv) + .unwrap() + .try_build() + .unwrap(); + let pk2 = mk2.conv_view().unwrap().to_public_key().unwrap(); + let pk2_bytes = pk2.data_view().unwrap().key_bytes().unwrap(); + + assert_eq!(pk1_bytes.as_slice(), pk2_bytes.as_slice()); + } + + #[test] + fn test_ssh_roundtrip_from_ssh_origin() { + // Start with SSH key, convert to Multikey, back to SSH, back to Multikey + let mk1 = create_test_keypair_from_ssh(); + let sk1_bytes = mk1.data_view().unwrap().secret_bytes().unwrap(); + + // Convert to SSH + let view = View::try_from(&mk1).unwrap(); + let ssh_priv = view.to_ssh_private_key().unwrap(); + + // Convert back to Multikey + let mk2 = Builder::new_from_ssh_private_key(&ssh_priv) + .unwrap() + .try_build() + .unwrap(); + let sk2_bytes = mk2.data_view().unwrap().secret_bytes().unwrap(); + + assert_eq!(sk1_bytes.as_slice(), sk2_bytes.as_slice()); + } + + #[test] + fn test_sign_and_verify() { + // Test that we can sign and verify using built-in signing + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + + let message = b"test message"; + + // Sign using built-in signing + let signature = mk.sign_view().unwrap().sign(message, false, None).unwrap(); + + // Verify + let result = pk.verify_view().unwrap().verify(&signature, Some(message)); + assert!(result.is_ok(), "Signature verification should succeed"); + } + + #[test] + fn test_verify_view_with_valid_signature() { + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + + let message = b"test message to sign"; + + // Sign using built-in signing + let signature = mk.sign_view().unwrap().sign(message, false, None).unwrap(); + + // Verify + let view = View::try_from(&pk).unwrap(); + let result = view.verify(&signature, Some(message)); + + assert!(result.is_ok(), "Signature verification should succeed"); + } + + #[test] + fn test_verify_view_with_wrong_message() { + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + + let original_message = b"original message"; + let wrong_message = b"wrong message"; + + // Sign the original message + let signature = mk + .sign_view() + .unwrap() + .sign(original_message, false, None) + .unwrap(); + + // Try to verify with wrong message + let view = View::try_from(&pk).unwrap(); + let result = view.verify(&signature, Some(wrong_message)); + + assert!( + result.is_err(), + "Verification should fail with wrong message" + ); + } + + #[test] + fn test_verify_view_with_combined_signature() { + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + + let message = b"combined signature test"; + + // Create a combined signature (message embedded) + let signature = mk.sign_view().unwrap().sign(message, true, None).unwrap(); + + assert!( + !signature.message.is_empty(), + "Combined signature should contain message" + ); + + // Verify without providing message (it's in the signature) + let view = View::try_from(&pk).unwrap(); + let result = view.verify(&signature, None); + + assert!( + result.is_ok(), + "Combined signature verification should succeed" + ); + } + + #[test] + fn test_verify_view_from_secret_key() { + // Should be able to verify using a secret key (auto-converts to public) + let mk = create_test_keypair(); + let message = b"test message"; + + // Sign + let signature = mk.sign_view().unwrap().sign(message, false, None).unwrap(); + + // Verify using secret key directly + let view = View::try_from(&mk).unwrap(); + let result = view.verify(&signature, Some(message)); + + assert!(result.is_ok(), "Should verify from secret key"); + } + + #[test] + fn test_verify_view_missing_message() { + let mk = create_test_keypair(); + let pk = mk.conv_view().unwrap().to_public_key().unwrap(); + let message = b"test"; + + // Create signature WITHOUT embedding message + let signature = mk.sign_view().unwrap().sign(message, false, None).unwrap(); + assert!( + signature.message.is_empty(), + "Non-combined signature should not contain message" + ); + + // Try to verify without providing message + let view = View::try_from(&pk).unwrap(); + let result = view.verify(&signature, None); + + assert!(result.is_err(), "Should fail when message is missing"); + } +} diff --git a/crates/multikey/tests/p256_verification.rs b/crates/multikey/tests/p256_verification.rs new file mode 100644 index 0000000..7ad7f98 --- /dev/null +++ b/crates/multikey/tests/p256_verification.rs @@ -0,0 +1,192 @@ +//! Integration tests for P-256 signature verification +//! +//! These tests verify that P-256 public keys can verify signatures, +//! which is the primary use case for WebAuthn/passkey support. +use multicodec::Codec; +use multikey::{Builder, Views}; +use multiutil::CodecInfo; + +#[test] +fn test_p256_public_key_from_ssh() { + // Test that we can import a P-256 public key from SSH format + // This is how we'd import a passkey public key from WebAuthn registration + // once the caller has extracted it from the attestation object and converted + // it from COSE to SSH format (0x04 || X || Y) + + let mut rng = rand_core_6::OsRng; + + // Generate a test P-256 keypair via SSH + let keypair = + ssh_key::private::EcdsaKeypair::random(&mut rng, ssh_key::EcdsaCurve::NistP256).unwrap(); + + let private_key = ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::Ecdsa(keypair), + "test p256 key", + ) + .unwrap(); + + let public_key_ssh = private_key.public_key(); + + // Import the public key into multikey (simulates WebAuthn registration) + let public_key = Builder::new_from_ssh_public_key(public_key_ssh) + .unwrap() + .try_build() + .unwrap(); + + assert_eq!(public_key.codec(), Codec::P256Pub); + assert!(public_key.attr_view().unwrap().is_public_key()); + assert!(!public_key.attr_view().unwrap().is_secret_key()); +} + +#[test] +fn test_p256_signature_verification() { + // Test end-to-end signature verification + // Simulates: passkey signs data (via SSH key infrastructure), we verify with stored public key + + let mut rng = rand_core_6::OsRng; + + // Create a P-256 keypair using SSH infrastructure (simulating passkey) + let keypair = + ssh_key::private::EcdsaKeypair::random(&mut rng, ssh_key::EcdsaCurve::NistP256).unwrap(); + + let private_key_ssh = ssh_key::PrivateKey::new( + ssh_key::private::KeypairData::Ecdsa(keypair), + "test passkey", + ) + .unwrap(); + + // Import private key to multikey (we have this from SSH) + let secret_key = Builder::new_from_ssh_private_key(&private_key_ssh) + .unwrap() + .try_build() + .unwrap(); + + // Derive public key (this is what we'd store from WebAuthn registration) + let public_key = secret_key.conv_view().unwrap().to_public_key().unwrap(); + + // Data to sign (e.g., authentication challenge) + let challenge = b"authenticate this challenge"; + + // Sign using built-in signing (simulates WebAuthn assertion creation) + let signature = secret_key + .sign_view() + .unwrap() + .sign(challenge, false, None) + .unwrap(); + + let result = public_key + .verify_view() + .unwrap() + .verify(&signature, Some(challenge)); + + assert!(result.is_ok(), "Signature verification should succeed"); +} + +#[test] +fn test_p256_verification_fails_wrong_message() { + // Verify that verification correctly fails with wrong message + + let mut rng = rand_core_6::OsRng; + + // Create keypair via SSH + let keypair = + ssh_key::private::EcdsaKeypair::random(&mut rng, ssh_key::EcdsaCurve::NistP256).unwrap(); + + let private_key_ssh = + ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "test").unwrap(); + + let secret_key = Builder::new_from_ssh_private_key(&private_key_ssh) + .unwrap() + .try_build() + .unwrap(); + + let public_key = secret_key.conv_view().unwrap().to_public_key().unwrap(); + + let original_message = b"original message"; + let wrong_message = b"wrong message"; + + // Create signature for original message + let signature = secret_key + .sign_view() + .unwrap() + .sign(original_message, false, None) + .unwrap(); + + // Verification should fail with wrong message + let result = public_key + .verify_view() + .unwrap() + .verify(&signature, Some(wrong_message)); + + assert!( + result.is_err(), + "Verification should fail with wrong message" + ); +} + +#[test] +fn test_p256_combined_signature() { + // Test combined signatures (message included in signature) + + let mut rng = rand_core_6::OsRng; + + let keypair = + ssh_key::private::EcdsaKeypair::random(&mut rng, ssh_key::EcdsaCurve::NistP256).unwrap(); + + let private_key_ssh = + ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "test").unwrap(); + + let secret_key = Builder::new_from_ssh_private_key(&private_key_ssh) + .unwrap() + .try_build() + .unwrap(); + + let public_key = secret_key.conv_view().unwrap().to_public_key().unwrap(); + + let message = b"combined signature test"; + + // Create combined signature (message embedded) + let signature = secret_key + .sign_view() + .unwrap() + .sign(message, true, None) + .unwrap(); + + assert!( + !signature.message.is_empty(), + "Combined signature should contain message" + ); + + // Verify without providing message (it's in the signature) + let result = public_key.verify_view().unwrap().verify(&signature, None); + + assert!( + result.is_ok(), + "Combined signature verification should succeed" + ); +} + +#[test] +fn test_p256_public_key_fingerprint() { + // Test that we can fingerprint P-256 public keys + // Useful for key identification + + let mut rng = rand_core_6::OsRng; + + let secret_key = Builder::new_from_random_bytes(Codec::P256Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let public_key = secret_key.conv_view().unwrap().to_public_key().unwrap(); + + // Get SHA-256 fingerprint + let fingerprint = public_key + .fingerprint_view() + .unwrap() + .fingerprint(Codec::Sha2256) + .unwrap(); + + let digest_bytes: Vec = fingerprint.into(); + assert!(!digest_bytes.is_empty()); +} diff --git a/crates/multikey/tests/web.rs b/crates/multikey/tests/web.rs new file mode 100644 index 0000000..4ebe521 --- /dev/null +++ b/crates/multikey/tests/web.rs @@ -0,0 +1,35 @@ +#![cfg(target_arch = "wasm32")] + +use multicodec::Codec; +use multikey::{Builder, Views}; +use multiutil::CodecInfo; + +use wasm_bindgen_test::*; + +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + +#[wasm_bindgen_test] +fn test_p256_wasm_compatibility() { + let mut rng = rand_core_6::OsRng; + + let secret_key = Builder::new_from_random_bytes(Codec::P256Priv, &mut rng) + .unwrap() + .try_build() + .unwrap(); + + let public_key = secret_key.conv_view().unwrap().to_public_key().unwrap(); + + let message = b"wasm test"; + let signature = secret_key + .sign_view() + .unwrap() + .sign(message, false, None) + .unwrap(); + + let result = public_key + .verify_view() + .unwrap() + .verify(&signature, Some(message)); + + assert!(result.is_ok()); +} diff --git a/crates/multisig/src/ms.rs b/crates/multisig/src/ms.rs index dd99d81..1701651 100644 --- a/crates/multisig/src/ms.rs +++ b/crates/multisig/src/ms.rs @@ -3,7 +3,7 @@ use crate::{ error::AttributesError, views::{ bls12381::{self, SchemeTypeId}, - ed25519, secp256k1, + ed25519, p256, secp256k1, }, AttrId, AttrView, ConvView, DataView, Error, ThresholdAttrView, ThresholdView, Views, }; @@ -15,11 +15,11 @@ use multiutil::{BaseEncoded, CodecInfo, EncodingInfo, Varbytes, VarbytesIter, Va use std::{collections::BTreeMap, fmt, num::NonZeroUsize}; /// the list of signature codecs currently supported -pub const SIG_CODECS: [Codec; 4] = [ +pub const SIG_CODECS: [Codec; 5] = [ Codec::Bls12381G1Msig, Codec::Bls12381G2Msig, Codec::EddsaMsig, - // Codec::Es256Msig, + Codec::Es256Msig, // P-256 (WebAuthn/passkey signatures) // Codec::Es384Msig, // Codec::Es521Msig, // Codec::Rs256Msig, @@ -185,6 +185,7 @@ impl Views for Multisig { | Codec::Bls12381G1ShareMsig | Codec::Bls12381G2ShareMsig => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::EddsaMsig => Ok(Box::new(ed25519::View::try_from(self)?)), + Codec::Es256Msig => Ok(Box::new(p256::View::try_from(self)?)), Codec::Es256KMsig => Ok(Box::new(secp256k1::View::try_from(self)?)), _ => Err(AttributesError::UnsupportedCodec(self.codec).into()), } @@ -197,6 +198,7 @@ impl Views for Multisig { | Codec::Bls12381G1ShareMsig | Codec::Bls12381G2ShareMsig => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::EddsaMsig => Ok(Box::new(ed25519::View::try_from(self)?)), + Codec::Es256Msig => Ok(Box::new(p256::View::try_from(self)?)), Codec::Es256KMsig => Ok(Box::new(secp256k1::View::try_from(self)?)), _ => Err(AttributesError::UnsupportedCodec(self.codec).into()), } @@ -209,6 +211,7 @@ impl Views for Multisig { | Codec::Bls12381G1ShareMsig | Codec::Bls12381G2ShareMsig => Ok(Box::new(bls12381::View::try_from(self)?)), Codec::EddsaMsig => Ok(Box::new(ed25519::View::try_from(self)?)), + Codec::Es256Msig => Ok(Box::new(p256::View::try_from(self)?)), Codec::Es256KMsig => Ok(Box::new(secp256k1::View::try_from(self)?)), _ => Err(AttributesError::UnsupportedCodec(self.codec).into()), } diff --git a/crates/multisig/src/views.rs b/crates/multisig/src/views.rs index 4679c6a..c8f3771 100644 --- a/crates/multisig/src/views.rs +++ b/crates/multisig/src/views.rs @@ -6,6 +6,8 @@ use multicodec::Codec; pub mod bls12381; /// Edwards curve 25519 signature implementation pub mod ed25519; +/// NIST P-256 curve signature implementation (ES256, used by WebAuthn/passkeys) +pub mod p256; /// Koblitz 256k1 curve implmentation (a.k.a. the Bitcoin curve) pub mod secp256k1; diff --git a/crates/multisig/src/views/p256.rs b/crates/multisig/src/views/p256.rs new file mode 100644 index 0000000..04e4b07 --- /dev/null +++ b/crates/multisig/src/views/p256.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +//! P-256 (ES256) signature view implementations for Multisig +//! +//! This module provides support for verifying ES256 signatures, +//! which are used by WebAuthn/passkeys. +use crate::{ + error::{AttributesError, ConversionsError}, + AttrId, AttrView, ConvView, DataView, Error, Multisig, Views, +}; +use multicodec::Codec; + +pub(crate) struct View<'a> { + ms: &'a Multisig, +} + +impl<'a> TryFrom<&'a Multisig> for View<'a> { + type Error = Error; + + fn try_from(ms: &'a Multisig) -> Result { + Ok(Self { ms }) + } +} + +impl AttrView for View<'_> { + /// For ES256 Multisigs, the payload encoding is stored using the + /// AttrId::PayloadEncoding attribute id. + fn payload_encoding(&self) -> Result { + let v = self + .ms + .attributes + .get(&AttrId::PayloadEncoding) + .ok_or(AttributesError::MissingPayloadEncoding)?; + let encoding = Codec::try_from(v.as_slice())?; + Ok(encoding) + } + + /// ES256 only has one scheme so this is meaningless + fn scheme(&self) -> Result { + Ok(0) + } +} + +impl DataView for View<'_> { + /// For P256Pub Multisig values, the sig data is stored using the + /// AttrId::SigData attribute id. + fn sig_bytes(&self) -> Result, Error> { + let sig = self + .ms + .attributes + .get(&AttrId::SigData) + .ok_or(AttributesError::MissingSignature)?; + Ok(sig.clone()) + } +} + +impl ConvView for View<'_> { + /// Convert to SSH signature format + fn to_ssh_signature(&self) -> Result { + // Get the signature data + let dv = self.ms.data_view()?; + let sig_bytes = dv.sig_bytes()?; + + Ok(ssh_key::Signature::new( + ssh_key::Algorithm::Ecdsa { + curve: ssh_key::EcdsaCurve::NistP256, + }, + sig_bytes, + ) + .map_err(|e| ConversionsError::Ssh(e.into()))?) + } +} diff --git a/justfile b/justfile index 2b7e93a..8316379 100644 --- a/justfile +++ b/justfile @@ -6,6 +6,95 @@ build: test: cargo test --all --workspace just crates/bs-peer/test-web + just crates/multikey/test-wasm check32: just crates/bs-peer/check32 + +# Update ChromeDriver to match the installed Chrome/Chromium version +update-chromedriver: + #!/usr/bin/env bash + set -euo pipefail + + # Function to get the major version + get_major_version() { + echo "$1" | cut -d '.' -f1 + } + + # Find the installed Chrome/Chromium version + if command -v google-chrome &> /dev/null; then + CHROME_VERSION=$(google-chrome --version | awk '{print $3}') + echo "Using Google Chrome" + elif command -v google-chrome-stable &> /dev/null; then + CHROME_VERSION=$(google-chrome-stable --version | awk '{print $3}') + echo "Using Google Chrome" + elif command -v chromium &> /dev/null; then + CHROME_VERSION=$(chromium --version | awk '{print $2}') + echo "Using Chromium" + elif command -v chromium-browser &> /dev/null; then + CHROME_VERSION=$(chromium-browser --version | awk '{print $2}') + echo "Using Chromium" + else + echo "Neither Chrome nor Chromium found. Please install one and try again." + exit 1 + fi + + echo "Installed Chrome/Chromium version: $CHROME_VERSION" + + # Get the major version + MAJOR_VERSION=$(get_major_version "$CHROME_VERSION") + + # Determine the operating system + OS=$(uname -s) + case $OS in + Linux) + OS_PATH="linux64" + CHROMEDRIVER_FILE="chromedriver-linux64.zip" + ;; + Darwin) + OS_PATH="mac-x64" + CHROMEDRIVER_FILE="chromedriver-mac-x64.zip" + ;; + *) + echo "Unsupported operating system: $OS" + exit 1 + ;; + esac + + # Construct the download URL + DOWNLOAD_URL="https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/${OS_PATH}/${CHROMEDRIVER_FILE}" + + echo "Attempting to download ChromeDriver from: $DOWNLOAD_URL" + + # Try to download ChromeDriver + if ! wget -O chromedriver.zip "$DOWNLOAD_URL"; then + echo "Failed to download ChromeDriver for version $CHROME_VERSION" + echo "Trying with major version only..." + + # Construct the download URL with major version only + DOWNLOAD_URL="https://storage.googleapis.com/chrome-for-testing-public/${MAJOR_VERSION}.0.0.0/${OS_PATH}/${CHROMEDRIVER_FILE}" + + echo "Attempting to download ChromeDriver from: $DOWNLOAD_URL" + + if ! wget -O chromedriver.zip "$DOWNLOAD_URL"; then + echo "Failed to download ChromeDriver. Please check your Chrome/Chromium version and try again." + exit 1 + fi + fi + + # Extract ChromeDriver + unzip -o chromedriver.zip + + # Make ChromeDriver executable + chmod +x chromedriver-*/chromedriver + + # Move ChromeDriver to /usr/local/bin (may require sudo) + sudo mv chromedriver-*/chromedriver /usr/local/bin/ + + # Clean up + rm -rf chromedriver.zip chromedriver-*/ + + echo "ChromeDriver for Chrome/Chromium version $CHROME_VERSION has been installed to /usr/local/bin/chromedriver" + + # Verify installation + chromedriver --version From 3a234464189ed5a9026db9bd82d50072f57371ee Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Tue, 16 Dec 2025 17:54:47 -0400 Subject: [PATCH 02/11] rm erroneous rustdoc --- crates/bs-peer/src/platform/browser/opfs.rs | 46 --------------------- 1 file changed, 46 deletions(-) diff --git a/crates/bs-peer/src/platform/browser/opfs.rs b/crates/bs-peer/src/platform/browser/opfs.rs index c3decbc..3e51382 100644 --- a/crates/bs-peer/src/platform/browser/opfs.rs +++ b/crates/bs-peer/src/platform/browser/opfs.rs @@ -107,52 +107,6 @@ impl Blockstore for OPFSBlockstore { } /// A Wrapper sturct around OPFSBlockstore so that we can make it [Send] -/// -/// # Example -/// ```no_run -/// use wasm_bindgen_futures::spawn_local; -/// use peerpiper_browser::opfs::OPFSWrapped; -/// use peerpiper_core::Blockstore; -/// -/// spawn_local(async move { -/// let Ok(blockstore) = OPFSWrapped::new().await else { -/// panic!("Failed to create OPFSWrapped"); -/// }; -/// -/// // Use blockstore when starting peerpiper -/// -/// // 16 is arbitrary, but should be enough for now -/// let (tx_evts, mut rx_evts) = mpsc::channel(16); -/// -/// // client sync oneshot -/// let (tx_client, rx_client) = oneshot::channel(); -/// -/// // command_sender will be used by other wasm_bindgen functions to send commands to the network -/// // so we will need to wrap it in a Mutex or something to make it thread safe. -/// let (network_command_sender, network_command_receiver) = tokio::sync::mpsc::channel(8); -/// -/// let bstore = blockstore.clone(); -/// -/// spawn_local(async move { -/// peerpiper::start( -/// tx_evts, -/// network_command_receiver, -/// tx_client, -/// libp2p_endpoints, -/// bstore, -/// ) -/// .await -/// .expect("never end") -/// }); -/// -/// // wait on rx_client to get the client handle -/// let client_handle = rx_client.await?; -/// -/// commander -/// .with_network(network_command_sender) -/// .with_client(client_handle); -/// }); -/// ``` #[derive(Debug, Clone)] pub struct OPFSWrapped { inner: SendWrapper, From 4e92e59a2c685b64e01d6bc6e62226da48b54ed1 Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Tue, 16 Dec 2025 19:50:43 -0400 Subject: [PATCH 03/11] add P256 to KEY_CODECS, cover edge cases of 0x04 --- crates/multikey/src/mk.rs | 46 +++++++++++++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/crates/multikey/src/mk.rs b/crates/multikey/src/mk.rs index 9306d71..0ad0fc2 100644 --- a/crates/multikey/src/mk.rs +++ b/crates/multikey/src/mk.rs @@ -20,15 +20,16 @@ use std::{collections::BTreeMap, fmt, num::NonZeroUsize}; use zeroize::Zeroizing; /// the list of key codecs supported for key generation -pub const KEY_CODECS: [Codec; 7] = [ +pub const KEY_CODECS: [Codec; 8] = [ Codec::Bls12381G1Priv, Codec::Bls12381G2Priv, Codec::Ed25519Priv, + Codec::P256Priv, /* Codec::LamportSha3256Priv, Codec::LamportSha3384Priv, Codec::LamportSha3512Priv, - Codec::P256Priv, + Codec::P384Priv, Codec::P521Priv, */ @@ -492,7 +493,7 @@ impl Builder { }) } - /// new builder from ssh_key::PublicKey source + /// Build from [ssh_key::PublicKey] source pub fn new_from_ssh_public_key(sshkey: &PublicKey) -> Result { match sshkey.algorithm() { Ecdsa { curve } => { @@ -500,7 +501,44 @@ impl Builder { let (key_bytes, codec) = match curve { NistP256 => { if let KeyData::Ecdsa(EcdsaPublicKey::NistP256(point)) = sshkey.key_data() { - (point.as_bytes().to_vec(), Codec::P256Pub) + // SSH stores points in uncompressed format + // Convert to compressed SEC1 format to match to_public_key() + let point_bytes = point.as_bytes(); + + // Parse the point - it may have the 0x04 tag (65 bytes) or not (64 bytes) + let verifying_key = if point_bytes.len() == 65 && point_bytes[0] == 0x04 + { + // Already has the tag, use as-is + ::p256::ecdsa::VerifyingKey::from_sec1_bytes(point_bytes).map_err( + |e| { + ConversionsError::PublicKeyFailure(format!( + "Invalid P-256 point: {}", + e + )) + }, + )? + } else if point_bytes.len() == 64 { + // Need to add the 0x04 tag + let mut uncompressed_bytes = vec![0x04]; + uncompressed_bytes.extend_from_slice(point_bytes); + ::p256::ecdsa::VerifyingKey::from_sec1_bytes(&uncompressed_bytes) + .map_err(|e| { + ConversionsError::PublicKeyFailure(format!( + "Invalid P-256 point: {}", + e + )) + })? + } else { + return Err(ConversionsError::PublicKeyFailure(format!( + "Invalid P-256 point length: {}", + point_bytes.len() + )) + .into()); + }; + + // Convert to compressed format + let compressed_point = verifying_key.to_encoded_point(true); + (compressed_point.as_bytes().to_vec(), Codec::P256Pub) } else { return Err(ConversionsError::UnsupportedAlgorithm( sshkey.algorithm().to_string(), From bf0b8a537c2d96a0252998c3a4c1bff77805722f Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Tue, 16 Dec 2025 19:58:17 -0400 Subject: [PATCH 04/11] clippy fix --- crates/multikey/src/views/bls12381.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/multikey/src/views/bls12381.rs b/crates/multikey/src/views/bls12381.rs index ac3dae3..e390234 100644 --- a/crates/multikey/src/views/bls12381.rs +++ b/crates/multikey/src/views/bls12381.rs @@ -476,8 +476,8 @@ impl ConvView for View<'_> { let tav = pk.threshold_attr_view()?; let key_share: Vec = KeyShare( tav.identifier()?, - tav.threshold()?.into(), - tav.limit()?.into(), + tav.threshold()?, + tav.limit()?, key_bytes.to_vec(), ) .into(); @@ -496,7 +496,7 @@ impl ConvView for View<'_> { let tav = pk.threshold_attr_view()?; let key_share: Vec = KeyShare( tav.identifier()?, - tav.threshold()?.into(), + tav.threshold()?, tav.limit()?, key_bytes.to_vec(), ) @@ -556,7 +556,7 @@ impl ConvView for View<'_> { let sav = self.mk.threshold_attr_view()?; let secret_key_share: Vec = KeyShare( sav.identifier()?, - sav.threshold()?.into(), + sav.threshold()?, sav.limit()?, secret_bytes.to_vec(), ) @@ -564,7 +564,7 @@ impl ConvView for View<'_> { let pav = pk.threshold_attr_view()?; let public_key_share: Vec = KeyShare( pav.identifier()?, - pav.threshold()?.into(), + pav.threshold()?, pav.limit()?, key_bytes.to_vec(), ) @@ -590,7 +590,7 @@ impl ConvView for View<'_> { let sav = self.mk.threshold_attr_view()?; let secret_key_share: Vec = KeyShare( sav.identifier()?, - sav.threshold()?.into(), + sav.threshold()?, sav.limit()?, secret_bytes.to_vec(), ) @@ -598,7 +598,7 @@ impl ConvView for View<'_> { let pav = pk.threshold_attr_view()?; let public_key_share: Vec = KeyShare( pav.identifier()?, - pav.threshold()?.into(), + pav.threshold()?, pav.limit()?, key_bytes.to_vec(), ) From 9ac6e9becd9e4ade5739d4ca91c72b223d1f6380 Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Wed, 17 Dec 2025 10:33:19 -0400 Subject: [PATCH 05/11] re-exports --- crates/bs/src/config/asynchronous.rs | 2 +- crates/bs/src/config/sync.rs | 2 +- crates/bs/src/lib.rs | 2 ++ crates/bs/src/ops/open/config.rs | 5 +++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/bs/src/config/asynchronous.rs b/crates/bs/src/config/asynchronous.rs index f2f074e..c49e219 100644 --- a/crates/bs/src/config/asynchronous.rs +++ b/crates/bs/src/config/asynchronous.rs @@ -1,5 +1,5 @@ use super::*; -use bs_traits::asyncro::{AsyncGetKey, AsyncSigner}; +pub use bs_traits::asyncro::{AsyncGetKey, AsyncSigner}; /// Supertrait for key management operations pub trait KeyManager: diff --git a/crates/bs/src/config/sync.rs b/crates/bs/src/config/sync.rs index b68ef03..6c9ad8e 100644 --- a/crates/bs/src/config/sync.rs +++ b/crates/bs/src/config/sync.rs @@ -1,5 +1,5 @@ //! Sync alterntives to the asynchronous traits. -use bs_traits::sync::{SyncGetKey, SyncPrepareEphemeralSigning, SyncSigner}; +pub use bs_traits::sync::{SyncGetKey, SyncPrepareEphemeralSigning, SyncSigner}; use bs_traits::EphemeralKey; use super::*; diff --git a/crates/bs/src/lib.rs b/crates/bs/src/lib.rs index 818d3d2..9a760c2 100644 --- a/crates/bs/src/lib.rs +++ b/crates/bs/src/lib.rs @@ -21,6 +21,8 @@ pub use ops::prelude::*; /// convenient export pub mod prelude { pub use super::*; + pub use multihash; + pub use multikey; } /// Opinionated configuation for the BetterSign library diff --git a/crates/bs/src/ops/open/config.rs b/crates/bs/src/ops/open/config.rs index 31ac78b..07e6b45 100644 --- a/crates/bs/src/ops/open/config.rs +++ b/crates/bs/src/ops/open/config.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: FSL-1.1 - -use provenance_log::{key::key_paths::ValidatedKeyParams, Script}; +pub use provenance_log::{ + entry::Field, format_with_fields, key::key_paths::ValidatedKeyParams, Script, +}; use crate::{ params::vlad::{FirstEntryKeyParams, VladParams}, From 206745c59279955ca553673ca2a0d860494f495c Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Wed, 17 Dec 2025 17:29:24 -0400 Subject: [PATCH 06/11] add async paths --- Cargo.toml | 1 + cli/Cargo.toml | 2 +- cli/src/subcmds/plog.rs | 8 +- crates/bs-peer/.gitignore | 1 + crates/bs-peer/src/peer.rs | 13 +- crates/bs-traits/src/asyncro.rs | 27 ++ crates/bs-traits/src/sync.rs | 2 +- crates/bs-wallets/Cargo.toml | 4 + crates/bs-wallets/src/async_memory.rs | 388 ++++++++++++++++++++++++++ crates/bs-wallets/src/lib.rs | 3 + crates/bs-wallets/src/memory.rs | 6 +- crates/bs/Cargo.toml | 7 +- crates/bs/src/config.rs | 5 +- crates/bs/src/config/adapters.rs | 158 +++++++++++ crates/bs/src/lib.rs | 3 + crates/bs/src/ops/open.rs | 98 ++++--- crates/bs/src/ops/update.rs | 147 +++++++--- crates/bs/src/resolver_ext.rs | 2 +- 18 files changed, 780 insertions(+), 95 deletions(-) create mode 100644 crates/bs-peer/.gitignore create mode 100644 crates/bs-wallets/src/async_memory.rs create mode 100644 crates/bs/src/config/adapters.rs diff --git a/Cargo.toml b/Cargo.toml index 31f901c..b5c47c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ bs-traits = { path = "crates/bs-traits" } bs-wallets = { path = "crates/bs-wallets" } comrade = { path = "crates/comrade" } comrade-reference = { path = "crates/comrade-reference" } +futures = "0.3.30" multibase = { path = "crates/multibase" } multicid = { path = "crates/multicid" } multicodec = { path = "crates/multicodec" } diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 5122d9f..41eb8f6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -19,7 +19,7 @@ default = ["serde"] anyhow = "1.0" async-trait = "0.1" best-practices.workspace = true -bs.workspace = true +bs = { workspace = true, features = ["sync"] } bs-traits.workspace = true clap = { version = "4.5.36", features = ["cargo"] } colored = "3.0.0" diff --git a/cli/src/subcmds/plog.rs b/cli/src/subcmds/plog.rs index 95744bf..b903d91 100644 --- a/cli/src/subcmds/plog.rs +++ b/cli/src/subcmds/plog.rs @@ -81,7 +81,7 @@ impl SyncPrepareEphemeralSigning for KeyManager { ) -> Result< ( ::PubKey, - Box Result<::Signature, ::Error>>, + Box Result<::Signature, ::Error> + Send>, ), ::Error, > { @@ -101,7 +101,7 @@ impl SyncPrepareEphemeralSigning for KeyManager { let public_key = secret_key.conv_view()?.to_public_key()?; // Create the signing closure that owns the secret key - let sign_once = Box::new( + let sign_once: Box Result<::Signature, ::Error> + Send> = Box::new( move |data: &[u8]| -> Result<::Signature, ::Error> { debug!("Signing data with ephemeral key"); let signature = secret_key.sign_view()?.sign(data, false, None)?; @@ -196,7 +196,7 @@ pub async fn go(cmd: Command, _config: &Config) -> Result<(), Error> { let key_manager = KeyManager::default(); // open the p.log - let plog = open::open_plog(&cfg, &key_manager, &key_manager)?; + let plog = open::open_plog_sync(&cfg, &key_manager, &key_manager)?; println!("Created p.log {}", writer_name(&output)?.to_string_lossy()); print_plog(&plog)?; @@ -266,7 +266,7 @@ pub async fn go(cmd: Command, _config: &Config) -> Result<(), Error> { let key_manager = KeyManager::default(); // update the p.log - update::update_plog::(&mut plog, &cfg, &key_manager, &key_manager)?; + update::update_plog_sync::(&mut plog, &cfg, &key_manager, &key_manager)?; println!("Writing p.log {}", writer_name(&output)?.to_string_lossy()); print_plog(&plog)?; diff --git a/crates/bs-peer/.gitignore b/crates/bs-peer/.gitignore new file mode 100644 index 0000000..01d0a08 --- /dev/null +++ b/crates/bs-peer/.gitignore @@ -0,0 +1 @@ +pkg/ diff --git a/crates/bs-peer/src/peer.rs b/crates/bs-peer/src/peer.rs index c43f18c..2c20a3d 100644 --- a/crates/bs-peer/src/peer.rs +++ b/crates/bs-peer/src/peer.rs @@ -16,6 +16,8 @@ use bs::{ }; pub use bs_p2p::events::api::{Client, Libp2pEvent}; pub use bs_p2p::events::PublicEvent; +use bs_traits::asyncro::AsyncKeyManager; +use bs_traits::asyncro::AsyncMultiSigner; use bs_traits::CondSync; use futures::channel::mpsc::{self}; pub use libp2p::PeerId; @@ -129,7 +131,11 @@ where impl BsPeer where - KP: KeyManager + MultiSigner + CondSync, + KP: KeyManager + + MultiSigner + + CondSync + + AsyncKeyManager + + AsyncMultiSigner, BS: BlockstoreTrait + CondSync, { /// Returns a clone of the p[p::Log] of the peer, if it exists. @@ -278,7 +284,7 @@ where } // Pass the key_provider directly as both key_manager and signer - let plog = bs::ops::open_plog(&config, &self.key_provider, &self.key_provider)?; + let plog = bs::ops::open_plog(&config, &self.key_provider, &self.key_provider).await?; { let verify_iter = &mut plog.verify(); @@ -337,12 +343,13 @@ where /// Update the BsPeer's Plog with new data. pub async fn update(&mut self, config: UpdateConfig) -> Result<(), Error> { { + // TODO: Fix async held across await issue let mut plog = self.plog.lock().map_err(|_| Error::LockPosioned)?; let Some(ref mut plog) = *plog else { return Err(Error::PlogNotInitialized); }; // Apply the update to the plog - bs::ops::update_plog(plog, &config, &self.key_provider, &self.key_provider)?; + bs::ops::update_plog(plog, &config, &self.key_provider, &self.key_provider).await?; // Verify the updated plog let verify_iter = &mut plog.verify(); diff --git a/crates/bs-traits/src/asyncro.rs b/crates/bs-traits/src/asyncro.rs index e28dbae..1bff9da 100644 --- a/crates/bs-traits/src/asyncro.rs +++ b/crates/bs-traits/src/asyncro.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: FSL-1.1 //! This module provides traits for asynchronous operations use crate::cond_send::CondSend; +use crate::sync::EphemeralSigningTuple; use crate::*; use std::future::Future; use std::num::NonZeroUsize; @@ -162,3 +163,29 @@ pub trait AsyncGetKey: GetKey { limit: usize, ) -> Result, Self::Error>; } + +/// An async version of KeyManager +pub trait AsyncKeyManager: GetKey + Send + Sync { + fn get_key<'a>( + &'a self, + key_path: &'a Self::KeyPath, + codec: &'a Self::Codec, + threshold: NonZeroUsize, + limit: NonZeroUsize, + ) -> BoxFuture<'a, Result>; +} + +/// An async version of MultiSigner, including ephemeral signing +pub trait AsyncMultiSigner: + AsyncSigner + EphemeralKey + GetKey +where + S: Send, + E: Send, +{ + fn prepare_ephemeral_signing<'a>( + &'a self, + codec: &'a Self::Codec, + threshold: NonZeroUsize, + limit: NonZeroUsize, + ) -> BoxFuture<'a, EphemeralSigningTuple>; +} diff --git a/crates/bs-traits/src/sync.rs b/crates/bs-traits/src/sync.rs index dde4572..e816255 100644 --- a/crates/bs-traits/src/sync.rs +++ b/crates/bs-traits/src/sync.rs @@ -18,7 +18,7 @@ pub trait SyncSigner: Signer { } } -pub type OneTimeSignFn = Box Result>; +pub type OneTimeSignFn = Box Result + Send>; pub type EphemeralSigningTuple = Result<(PK, OneTimeSignFn), E>; diff --git a/crates/bs-wallets/Cargo.toml b/crates/bs-wallets/Cargo.toml index ea6b653..8616bf7 100644 --- a/crates/bs-wallets/Cargo.toml +++ b/crates/bs-wallets/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true [dependencies] bs-traits.workspace = true +futures = { workspace = true, optional = true } multibase.workspace = true multicodec.workspace = true multikey.workspace = true @@ -28,5 +29,8 @@ bs.workspace = true bs-peer.workspace = true tracing-subscriber.workspace = true +[features] +sync = ["dep:futures"] + [lints] workspace = true diff --git a/crates/bs-wallets/src/async_memory.rs b/crates/bs-wallets/src/async_memory.rs new file mode 100644 index 0000000..c51e27c --- /dev/null +++ b/crates/bs-wallets/src/async_memory.rs @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: FSL-1.1 +//! Async implementation for the in-memory wallet. + +use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner, AsyncSigner, BoxFuture, SignerFuture}; +use bs_traits::sync::{EphemeralSigningTuple, SyncGetKey, SyncPrepareEphemeralSigning, SyncSigner}; +use bs_traits::{CondSync, EphemeralKey, GetKey, Signer}; +use multicodec::Codec; +use multikey::Multikey; +use multisig::Multisig; +use provenance_log::Key; +use std::num::NonZeroUsize; + +// Reuse the existing struct definition from memory.rs +pub use crate::memory::InMemoryKeyManager; + +impl AsyncSigner for InMemoryKeyManager +where + E: From + + From + + From + + From + + std::fmt::Debug + + Send + + Sync + + 'static, + Self: SyncSigner, + ::KeyPath: CondSync, +{ + fn try_sign<'a>( + &'a self, + key_path: &'a Self::KeyPath, + data: &'a [u8], + ) -> SignerFuture<'a, Self::Signature, Self::Error> { + Box::pin(async move { SyncSigner::try_sign(self, key_path, data) }) + } +} + +impl AsyncKeyManager for InMemoryKeyManager +where + E: From + From + std::fmt::Debug + Send + Sync + 'static, + Self: SyncGetKey, + ::KeyPath: CondSync, + ::Codec: CondSync, +{ + fn get_key<'a>( + &'a self, + key_path: &'a Self::KeyPath, + codec: &'a Self::Codec, + threshold: NonZeroUsize, + limit: NonZeroUsize, + ) -> BoxFuture<'a, Result> { + Box::pin(async move { SyncGetKey::get_key(self, key_path, codec, threshold, limit) }) + } +} + +impl AsyncMultiSigner for InMemoryKeyManager +where + E: From + + From + + From + + From + + std::fmt::Debug + + Send + + Sync + + 'static, + Self: SyncPrepareEphemeralSigning< + Codec = Codec, + PubKey = Multikey, + Signature = Multisig, + Error = E, + > + EphemeralKey + + GetKey + + Signer, + ::Codec: CondSync, + ::KeyPath: CondSync, +{ + fn prepare_ephemeral_signing<'a>( + &'a self, + codec: &'a ::Codec, + threshold: NonZeroUsize, + limit: NonZeroUsize, + ) -> BoxFuture<'a, EphemeralSigningTuple> { + Box::pin(async move { + SyncPrepareEphemeralSigning::prepare_ephemeral_signing(self, codec, threshold, limit) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner, AsyncSigner}; + use multicodec::Codec; + use multikey::Views; + use std::num::NonZero; + use tracing_subscriber::fmt; + + fn init_logger() { + let subscriber = fmt().with_env_filter("trace").finish(); + if let Err(e) = tracing::subscriber::set_global_default(subscriber) { + tracing::warn!("failed to set subscriber: {}", e); + } + } + + // Test fixture that ensures async traits are properly implemented + async fn test_async_traits(_km: &KM, _ms: &MS) + where + KM: AsyncKeyManager, + MS: AsyncMultiSigner, + { + } + + #[tokio::test] + async fn test_async_default_key_manager() { + // Create key manager with default error type + let key_manager = InMemoryKeyManager::default(); + test_async_traits(&key_manager, &key_manager).await; + } + + #[tokio::test] + async fn test_async_key_manager() { + // Create key manager + let key_manager = InMemoryKeyManager::::new(); + + // Test a regular non-ephemeral key + let key_path = Key::try_from("/async/test/key/path").unwrap(); + let test_mk = + multikey::Builder::new_from_random_bytes(Codec::Ed25519Priv, &mut rand_core_6::OsRng) + .unwrap() + .try_build() + .unwrap(); + + // Add to wallet + key_manager + .store_secret_key(key_path.clone(), test_mk.clone()) + .unwrap(); + + // Can sign with stored key using async API + let data = b"test async data"; + let signature = AsyncSigner::try_sign(&key_manager, &key_path, data) + .await + .unwrap(); + let verify_result = test_mk + .verify_view() + .unwrap() + .verify(&signature, Some(data)); + assert!(verify_result.is_ok()); + } + + #[tokio::test] + async fn test_async_dynamic_key_generation() { + // Create key manager + let key_manager = InMemoryKeyManager::::new(); + + // Request a key with a custom path using async API + let custom_path = Key::try_from("/async/custom/key/path").unwrap(); + + // First call to get_key generates a key pair and stores the secret key, + // but returns the public key + let public_key = AsyncKeyManager::get_key( + &key_manager, + &custom_path, + &Codec::Ed25519Priv, + NonZero::new(1).unwrap(), + NonZero::new(1).unwrap(), + ) + .await + .unwrap(); + + // Verify we got a public key + assert!(public_key.attr_view().unwrap().is_public_key()); + + // Get the key again - should be the same public key + let public_key2 = AsyncKeyManager::get_key( + &key_manager, + &custom_path, + &Codec::Ed25519Priv, + NonZero::new(1).unwrap(), + NonZero::new(1).unwrap(), + ) + .await + .unwrap(); + + assert!( + public_key.eq(&public_key2), + "The two retrieved public keys should be equal" + ); + + // Try signing with the key at the custom path using async API + // This works because the secret key is stored internally in the key manager + let data = b"test async custom key"; + let signature = AsyncSigner::try_sign(&key_manager, &custom_path, data) + .await + .unwrap(); + + // Verify signature with the public key we have + let verify_result = public_key + .verify_view() + .unwrap() + .verify(&signature, Some(data)); + assert!( + verify_result.is_ok(), + "Signature verification should succeed" + ); + } + + #[tokio::test] + async fn test_async_prepare_ephemeral_signing() { + init_logger(); + + tracing::info!("Starting test_async_prepare_ephemeral_signing"); + + let key_manager = InMemoryKeyManager::::new(); + let data = b"test async ephemeral signing"; + + // Use async API to get an ephemeral public key and a one-time signing function + let (public_key, sign_once) = AsyncMultiSigner::prepare_ephemeral_signing( + &key_manager, + &Codec::Ed25519Priv, + NonZero::new(1).unwrap(), + NonZero::new(1).unwrap(), + ) + .await + .expect("Failed to prepare ephemeral signing"); + + // Verify that we got a public key + assert!(public_key.attr_view().unwrap().is_public_key()); + + // Sign the data with the one-time function + let signature = sign_once(data).expect("Failed to sign with ephemeral key"); + + // Create a new multikey for verification since we only have the public key + let verify_key = public_key.clone(); + + // Verify the signature + let verify_result = verify_key + .verify_view() + .unwrap() + .verify(&signature, Some(data)); + + assert!( + verify_result.is_ok(), + "Signature verification should succeed" + ); + + tracing::info!("Async ephemeral signing test completed successfully"); + } + + #[tokio::test] + async fn test_async_concurrent_key_generation() { + // Test that multiple concurrent async operations work correctly + let key_manager = InMemoryKeyManager::::new(); + + // Create multiple tasks that generate keys concurrently + let mut handles = vec![]; + for i in 0..5 { + let km = key_manager.clone(); + let handle = tokio::spawn(async move { + let path = Key::try_from(format!("/concurrent/key/{}", i).as_str()).unwrap(); + AsyncKeyManager::get_key( + &km, + &path, + &Codec::Ed25519Priv, + NonZero::new(1).unwrap(), + NonZero::new(1).unwrap(), + ) + .await + .unwrap() + }); + handles.push(handle); + } + + // Wait for all tasks to complete + let mut public_keys = vec![]; + for handle in handles { + let pk = handle.await.unwrap(); + assert!(pk.attr_view().unwrap().is_public_key()); + public_keys.push(pk); + } + + // All keys should be unique + for i in 0..public_keys.len() { + for j in (i + 1)..public_keys.len() { + assert!( + !public_keys[i].eq(&public_keys[j]), + "Keys {} and {} should be different", + i, + j + ); + } + } + } + + #[tokio::test] + async fn test_async_concurrent_signing() { + // Test that multiple concurrent async signing operations work correctly + let key_manager = InMemoryKeyManager::::new(); + + // Generate a shared key + let key_path = Key::try_from("/shared/signing/key").unwrap(); + let _ = AsyncKeyManager::get_key( + &key_manager, + &key_path, + &Codec::Ed25519Priv, + NonZero::new(1).unwrap(), + NonZero::new(1).unwrap(), + ) + .await + .unwrap(); + + // Create multiple tasks that sign data concurrently + let mut handles = vec![]; + for i in 0..5 { + let km = key_manager.clone(); + let kp = key_path.clone(); + let handle = tokio::spawn(async move { + let data = format!("concurrent data {}", i); + AsyncSigner::try_sign(&km, &kp, data.as_bytes()) + .await + .unwrap() + }); + handles.push(handle); + } + + // Wait for all tasks to complete + let mut signatures = vec![]; + for handle in handles { + let sig = handle.await.unwrap(); + signatures.push(sig); + } + + // All signatures should be valid (we don't check uniqueness as they sign different data) + assert_eq!(signatures.len(), 5); + } + + #[tokio::test] + async fn test_async_multiple_ephemeral_keys() { + // Test creating multiple ephemeral keys concurrently + let key_manager = InMemoryKeyManager::::new(); + + let mut handles = vec![]; + for i in 0..3 { + let km = key_manager.clone(); + let handle = tokio::spawn(async move { + let (public_key, sign_once) = AsyncMultiSigner::prepare_ephemeral_signing( + &km, + &Codec::Ed25519Priv, + NonZero::new(1).unwrap(), + NonZero::new(1).unwrap(), + ) + .await + .unwrap(); + + let data = format!("ephemeral data {}", i); + let signature = sign_once(data.as_bytes()).unwrap(); + + // Verify the signature + public_key + .verify_view() + .unwrap() + .verify(&signature, Some(data.as_bytes())) + .unwrap(); + + public_key + }); + handles.push(handle); + } + + // Wait for all tasks to complete + let mut public_keys = vec![]; + for handle in handles { + let pk = handle.await.unwrap(); + public_keys.push(pk); + } + + // All ephemeral keys should be unique + for i in 0..public_keys.len() { + for j in (i + 1)..public_keys.len() { + assert!( + !public_keys[i].eq(&public_keys[j]), + "Ephemeral keys {} and {} should be different", + i, + j + ); + } + } + } +} diff --git a/crates/bs-wallets/src/lib.rs b/crates/bs-wallets/src/lib.rs index 347577c..4761420 100644 --- a/crates/bs-wallets/src/lib.rs +++ b/crates/bs-wallets/src/lib.rs @@ -3,3 +3,6 @@ pub use error::Error; /// In memory Key manager and signer pub mod memory; + +/// In memory async Key manager and signer +pub mod async_memory; diff --git a/crates/bs-wallets/src/memory.rs b/crates/bs-wallets/src/memory.rs index 76f4cca..e35c930 100644 --- a/crates/bs-wallets/src/memory.rs +++ b/crates/bs-wallets/src/memory.rs @@ -200,11 +200,7 @@ where // Generate a new key since we don't have it yet let secret_key = Self::generate_key(codec)?; let fingerprint = secret_key.fingerprint_view()?.fingerprint(Codec::Sha2256)?; - tracing::debug!( - "Generated new key for path {}: {:?}", - key_path, - fingerprint - ); + tracing::debug!("Generated new key for path {key_path}: {fingerprint:?}"); let public_key = secret_key.conv_view()?.to_public_key()?; // Store the secret key for future use diff --git a/crates/bs/Cargo.toml b/crates/bs/Cargo.toml index cb0cdf9..f0ee4b4 100644 --- a/crates/bs/Cargo.toml +++ b/crates/bs/Cargo.toml @@ -9,9 +9,10 @@ license = "Functional Source License 1.1" [dependencies] bs-traits.workspace = true -bs-wallets.workspace = true +bs-wallets = { workspace = true, features = ["sync"] } best-practices.workspace = true comrade.workspace = true +futures = { workspace = true, optional = true } multicid.workspace = true multicodec.workspace = true multihash.workspace = true @@ -27,11 +28,13 @@ bon = "3.6.3" [dev-dependencies] tracing-subscriber.workspace = true -bs-wallets.workspace = true +bs-wallets = { workspace = true, features = ["sync"] } multibase.workspace = true [lints] workspace = true [features] +default = ["sync"] serde = ["dep:serde"] +sync = ["dep:futures", "bs-wallets/sync"] diff --git a/crates/bs/src/config.rs b/crates/bs/src/config.rs index 9eedbb2..aec935e 100644 --- a/crates/bs/src/config.rs +++ b/crates/bs/src/config.rs @@ -3,12 +3,11 @@ //! Users can pick any concrete types that implement the traits, but this module provides //! default implementations that can be used directly. -/// Opinionated configuration for the async traits types -pub mod asynchronous; +/// Sync to async adapters +pub mod adapters; /// Opinionated configuration for the sync traits types pub mod sync; -use crate::Error; use bs_traits::{GetKey, Signer}; /// Re-export the types used in the traits diff --git a/crates/bs/src/config/adapters.rs b/crates/bs/src/config/adapters.rs new file mode 100644 index 0000000..b7ccfa8 --- /dev/null +++ b/crates/bs/src/config/adapters.rs @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: FSL-1.1 +//! Adapters to bridge sync traits to async traits. + +use crate::config::sync::{KeyManager, MultiSigner}; +use crate::Error; +use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner}; +use bs_traits::asyncro::{AsyncSigner, BoxFuture, SignerFuture}; +use bs_traits::sync::EphemeralSigningTuple; +use bs_traits::{self, EphemeralKey, GetKey, Signer}; +use multicodec::Codec; +use multikey::Multikey; +use multisig::Multisig; +use provenance_log::Key; +use std::marker::PhantomData; +use std::num::NonZeroUsize; + +/// An adapter that wraps a sync `KeyManager` to expose an `AsyncKeyManager` interface. +pub struct SyncToAsyncManager<'a, E: 'a> { + sync_manager: &'a (dyn KeyManager + Send + Sync), + _phantom: PhantomData, +} + +impl<'a, E> SyncToAsyncManager<'a, E> { + /// Create a new [`SyncToAsyncManager`] wrapping the given sync [`KeyManager`]. + pub fn new(sync_manager: &'a (dyn KeyManager + Send + Sync)) -> Self { + Self { + sync_manager, + _phantom: PhantomData, + } + } +} + +impl<'a, E> GetKey for SyncToAsyncManager<'a, E> +where + E: From + From + std::fmt::Debug + Send + Sync + 'static, +{ + type Key = Multikey; + type KeyPath = Key; + type Codec = Codec; + type Error = E; +} + +impl<'a, E> AsyncKeyManager for SyncToAsyncManager<'a, E> +where + E: From + From + std::fmt::Debug + Send + Sync + 'static, +{ + fn get_key( + &self, + key_path: &::KeyPath, + codec: &::Codec, + threshold: NonZeroUsize, + limit: NonZeroUsize, + ) -> BoxFuture<'_, Result<::Key, E>> { + let res = self.sync_manager.get_key(key_path, codec, threshold, limit); + Box::pin(async { res }) + } +} + +/// An adapter that wraps a sync `MultiSigner` to expose an `AsyncMultiSigner` interface. +pub struct SyncToAsyncSigner<'a, E: 'a> { + sync_signer: &'a (dyn MultiSigner + Send + Sync), + _phantom: PhantomData, +} + +impl<'a, E> SyncToAsyncSigner<'a, E> { + /// Create a new [`SyncToAsyncSigner`] wrapping the given sync [`MultiSigner`]. + pub fn new(sync_signer: &'a (dyn MultiSigner + Send + Sync)) -> Self { + Self { + sync_signer, + _phantom: PhantomData, + } + } +} + +impl<'a, E> Signer for SyncToAsyncSigner<'a, E> +where + E: From + + From + + From + + From + + std::fmt::Debug + + Send + + Sync + + 'static, +{ + type KeyPath = Key; + type Signature = Multisig; + type Error = E; +} + +impl<'a, E> EphemeralKey for SyncToAsyncSigner<'a, E> +where + E: From + + From + + From + + From + + std::fmt::Debug + + Send + + Sync + + 'static, +{ + type PubKey = Multikey; +} + +impl<'a, E> AsyncSigner for SyncToAsyncSigner<'a, E> +where + E: From + + From + + From + + From + + std::fmt::Debug + + Send + + Sync + + 'static, +{ + fn try_sign<'b>( + &'b self, + key_path: &'b ::KeyPath, + data: &'b [u8], + ) -> SignerFuture<'b, ::Signature, ::Error> { + let res = self.sync_signer.try_sign(key_path, data); + Box::pin(async { res }) + } +} + +impl<'a, E> AsyncMultiSigner for SyncToAsyncSigner<'a, E> +where + E: From + + From + + From + + From + + std::fmt::Debug + + Send + + Sync + + 'static, +{ + fn prepare_ephemeral_signing<'b>( + &'b self, + codec: &'b ::Codec, + threshold: NonZeroUsize, + limit: NonZeroUsize, + ) -> BoxFuture<'b, EphemeralSigningTuple<::PubKey, Multisig, E>> { + let res = self + .sync_signer + .prepare_ephemeral_signing(codec, threshold, limit); + Box::pin(async { res }) + } +} + +impl<'a, E> GetKey for SyncToAsyncSigner<'a, E> +where + E: 'a, +{ + type Key = Multikey; + type KeyPath = Key; + type Codec = Codec; + type Error = E; +} diff --git a/crates/bs/src/lib.rs b/crates/bs/src/lib.rs index 9a760c2..497b410 100644 --- a/crates/bs/src/lib.rs +++ b/crates/bs/src/lib.rs @@ -14,6 +14,9 @@ pub mod error; pub use error::Error; +/// The concrete type used for signatures in this crate. +pub type Signature = multisig::Multisig; + /// bettersign operations pub mod ops; pub use ops::prelude::*; diff --git a/crates/bs/src/ops/open.rs b/crates/bs/src/ops/open.rs index ac6e904..9e9ec99 100644 --- a/crates/bs/src/ops/open.rs +++ b/crates/bs/src/ops/open.rs @@ -8,7 +8,9 @@ use crate::{ error::{BsCompatibleError, OpenError}, params::vlad::{FirstEntryKeyParams, VladParams}, update::{op, OpParams}, + Signature, }; +use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner}; pub use config::Config; use multicid::{cid, Cid, Vlad}; use multicodec::Codec; @@ -17,41 +19,59 @@ use multikey::{Multikey, Views}; use provenance_log::{entry, error::EntryError, Error as PlogError, Key, Log, OpId}; use tracing::debug; -/// Open a new provenance log based on the [Config] provided. -// -// To Open a Plog, the critical steps are: -// - First get the public key of the ephemeral first entry key -// - Add the public key of the ephemeral first entry key operation to `op_params` -// - Add ALL operations to the entry builder -// - Sign that operated entry using the ephemeral first entry key's one-time signing function -// - Finalize the Entry with the signature -// -// When the script runtime checks the first entry data (the Entry without the proof), against the -// first lock script, it will use the first entry key's public key to verify the signature. -pub fn open_plog( +/// Open a new provenance log based on the [Config] provided. (async) +pub async fn open_plog(config: &Config, key_manager: &KM, signer: &S) -> Result +where + E: BsCompatibleError + Send, + KM: AsyncKeyManager + ?Sized, + S: AsyncMultiSigner + ?Sized, +{ + open_plog_core(config, key_manager, signer).await +} + +/// Synchronous version of [open_plog] +#[cfg(feature = "sync")] +pub fn open_plog_sync( config: &Config, - key_manager: &dyn crate::config::sync::KeyManager, - signer: &dyn crate::config::sync::MultiSigner, -) -> Result { + key_manager: &(dyn crate::config::sync::KeyManager + Send + Sync), + signer: &(dyn crate::config::sync::MultiSigner + Send + Sync), +) -> Result +where + E: BsCompatibleError + Send + Sync + 'static, +{ + use crate::config::adapters::{SyncToAsyncManager, SyncToAsyncSigner}; + + let key_manager_adapter = SyncToAsyncManager::new(key_manager); + let signer_adapter = SyncToAsyncSigner::new(signer); + + futures::executor::block_on(open_plog_core( + config, + &key_manager_adapter, + &signer_adapter, + )) +} + +async fn open_plog_core(config: &Config, key_manager: &KM, signer: &S) -> Result +where + E: BsCompatibleError + Send, + KM: AsyncKeyManager + ?Sized, + S: AsyncMultiSigner + ?Sized, +{ // 0. Set up the list of ops let mut op_params = Vec::default(); // Process initial operations - config - .additional_ops() - .iter() - .try_for_each(|params| -> Result<(), E> { - match params { - p @ OpParams::KeyGen { .. } => { - let _ = load_key::(&mut op_params, p, key_manager)?; - } - p @ OpParams::CidGen { .. } => { - let _ = load_cid::(&mut op_params, p)?; - } - p => op_params.push(p.clone()), + for params in config.additional_ops().iter() { + match params { + p @ OpParams::KeyGen { .. } => { + let _ = load_key::(&mut op_params, p, key_manager).await?; } - Ok(()) - })?; + p @ OpParams::CidGen { .. } => { + let _ = load_cid::(&mut op_params, p)?; + } + p => op_params.push(p.clone()), + } + } // 1. Extract VLAD parameters and prepare signing let (vlad_key_params, vlad_cid_params): (OpParams, OpParams) = @@ -59,7 +79,9 @@ pub fn open_plog( let (codec, threshold, limit) = extract_key_params::(&vlad_key_params)?; // get ephemeral public key and one time signing function - let (vlad_pubkey, sign_vlad) = signer.prepare_ephemeral_signing(&codec, threshold, limit)?; + let (vlad_pubkey, sign_vlad) = signer + .prepare_ephemeral_signing(&codec, threshold, limit) + .await?; let cid = load_cid::(&mut op_params, &vlad_cid_params)?; @@ -86,7 +108,9 @@ pub fn open_plog( let (codec, threshold, limit) = extract_key_params::(entrykey_params)?; // Get the public key and signing function - let (entry_pubkey, sign_entry) = signer.prepare_ephemeral_signing(&codec, threshold, limit)?; + let (entry_pubkey, sign_entry) = signer + .prepare_ephemeral_signing(&codec, threshold, limit) + .await?; // 3. Add the entry public key operation to op_params if let OpParams::KeyGen { key, .. } = entrykey_params { @@ -97,7 +121,7 @@ pub fn open_plog( } // 4. Continue with other preparations - let _ = load_key::(&mut op_params, config.pubkey(), key_manager)?; + let _ = load_key::(&mut op_params, config.pubkey(), key_manager).await?; let lock_script = config.lock_script().clone(); let unlock_script = config.unlock().clone(); @@ -182,13 +206,14 @@ fn extract_key_params( } } -fn load_key( +async fn load_key( ops: &mut Vec, params: &OpParams, - key_manager: &dyn crate::config::sync::KeyManager, + key_manager: &KM, ) -> Result where E: From + From + From, + KM: AsyncKeyManager + ?Sized, { debug!("load_key: {:?}", params); match params { @@ -200,7 +225,7 @@ where revoke, } => { // call back to generate the key - let mk = key_manager.get_key(key, codec, *threshold, *limit)?; + let mk = key_manager.get_key(key, codec, *threshold, *limit).await?; // get the public key let pk = if mk.attr_view()?.is_secret_key() { @@ -386,7 +411,8 @@ mod tests { let key_manager = InMemoryKeyManager::::default(); - let plog = open_plog(&config, &key_manager, &key_manager).expect("Failed to open plog"); + let plog = + open_plog_sync(&config, &key_manager, &key_manager).expect("Failed to open plog"); // log.first_lock should match assert_eq!(&plog.first_lock, config.first_lock()); diff --git a/crates/bs/src/ops/update.rs b/crates/bs/src/ops/update.rs index 8281108..4ccf926 100644 --- a/crates/bs/src/ops/update.rs +++ b/crates/bs/src/ops/update.rs @@ -13,27 +13,33 @@ pub use config::Config; pub mod op_params; pub use op_params::OpParams; -use crate::error::UpdateError; +use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner, AsyncSigner}; +use crate::{ + error::{BsCompatibleError, UpdateError}, + Signature, +}; use multicid::{cid, Cid}; +use multicodec::Codec; use multihash::mh; use multikey::{Multikey, Views}; use provenance_log::{ entry::{self, Entry}, error::EntryError, - Error as PlogError, Lipmaa as _, Log, OpId, + Error as PlogError, Key, Lipmaa as _, Log, OpId, }; use std::{fs::read, path::Path}; use tracing::debug; -/// Updates a provenance log given the update config -pub fn update_plog( +/// Updates a provenance log given the update config (async) +pub async fn update_plog( plog: &mut Log, config: &Config, - key_manager: &dyn crate::config::sync::KeyManager, - signer: &dyn crate::config::sync::MultiSigner, + key_manager: &KM, + signer: &S, ) -> Result where - E: From + E: BsCompatibleError + + From + From + From + From @@ -41,30 +47,91 @@ where + From + From + ToString - + std::fmt::Debug, + + std::fmt::Debug + + Send, + KM: AsyncKeyManager + ?Sized, + S: AsyncMultiSigner + ?Sized, + S: AsyncSigner, +{ + update_plog_core(plog, config, key_manager, signer).await +} + +/// Updates a provenance log given the update config (sync) +#[cfg(feature = "sync")] +pub fn update_plog_sync( + plog: &mut Log, + config: &Config, + key_manager: &(dyn crate::config::sync::KeyManager + Send + Sync), + signer: &(dyn crate::config::sync::MultiSigner + Send + Sync), +) -> Result +where + E: BsCompatibleError + + From + + From + + From + + From + + From + + From + + From + + ToString + + std::fmt::Debug + + Send + + Sync + + 'static, +{ + use crate::config::adapters::{SyncToAsyncManager, SyncToAsyncSigner}; + + let key_manager_adapter = SyncToAsyncManager::new(key_manager); + let signer_adapter = SyncToAsyncSigner::new(signer); + + futures::executor::block_on(update_plog_core( + plog, + config, + &key_manager_adapter, + &signer_adapter, + )) +} + +async fn update_plog_core( + plog: &mut Log, + config: &Config, + key_manager: &KM, + signer: &S, +) -> Result +where + E: BsCompatibleError + + From + + From + + From + + From + + From + + From + + From + + ToString + + std::fmt::Debug + + Send, + KM: AsyncKeyManager + ?Sized, + S: AsyncMultiSigner + ?Sized, + S: AsyncSigner, { // 0. Set up the list of ops we're going to add let mut op_params = Vec::default(); // go through the additional ops and generate CIDs and keys and adding the resulting op params // to the vec of op params - config - .additional_ops() - .iter() - .try_for_each(|params| -> Result<(), E> { - match params { - p @ OpParams::KeyGen { .. } => { - let _ = load_key::(&mut op_params, p, key_manager)?; - } - p @ OpParams::CidGen { .. } => { - let _ = load_cid(&mut op_params, p, |path| -> Result, E> { - read(path).map_err(E::from) - })?; - } - p => op_params.push(p.clone()), + for params in config.additional_ops().iter() { + match params { + p @ OpParams::KeyGen { .. } => { + let _ = load_key::(&mut op_params, p, key_manager).await?; } - Ok(()) - })?; + p @ OpParams::CidGen { .. } => { + let _ = load_cid(&mut op_params, p, |path| -> Result, E> { + read(path).map_err(E::from) + })?; + } + p => op_params.push(p.clone()), + } + } // 1. validate the p.log and get the last entry and state let (_, last_entry, _kvp) = plog.verify().last().ok_or(UpdateError::NoLastEntry)??; @@ -136,6 +203,7 @@ where // Sign the entry let signature = signer .try_sign(entry_key_path, &entry_bytes) + .await .map_err(|e| PlogError::from(EntryError::SignFailed(e.to_string())))?; // Finalize the entry with the signature as proof @@ -147,13 +215,14 @@ where Ok(entry) } -fn load_key( +async fn load_key( ops: &mut Vec, params: &OpParams, - key_manager: &dyn crate::config::sync::KeyManager, + key_manager: &KM, ) -> Result where E: From + From + From, + KM: AsyncKeyManager + ?Sized, { debug!("load_key: {:?}", params); match params { @@ -165,7 +234,7 @@ where revoke, } => { // call back to generate the key - let mk = key_manager.get_key(key, codec, *threshold, *limit)?; + let mk = key_manager.get_key(key, codec, *threshold, *limit).await?; // get the public key let pk = if mk.attr_view()?.is_secret_key() { @@ -247,7 +316,7 @@ mod tests { anykey::PubkeyParams, vlad::{FirstEntryKeyParams, VladParams}, }; - use crate::{open, open_plog}; + use crate::open::{self, open_plog_sync}; use bs_traits::sync::SyncGetKey; use bs_wallets::memory::InMemoryKeyManager; @@ -319,17 +388,17 @@ mod tests { let key_manager = InMemoryKeyManager::::default(); let mut plog = - open_plog(&open_config, &key_manager, &key_manager).expect("Failed to open plog"); + open_plog_sync(&open_config, &key_manager, &key_manager).expect("Failed to open plog"); // We need to generate PubkeyParams key in our wallet: - let old_pk = key_manager - .get_key( - &PubkeyParams::KEY_PATH.into(), - &pubkey_params.codec(), - pubkey_params.threshold(), - pubkey_params.limit(), - ) - .expect("Failed to create and store pubkey"); + let old_pk = SyncGetKey::get_key( + &key_manager, + &PubkeyParams::KEY_PATH.into(), + &pubkey_params.codec(), + pubkey_params.threshold(), + pubkey_params.limit(), + ) + .expect("Failed to create and store pubkey"); // 2. Update the p.log with a new entry // - add a lock Script @@ -361,7 +430,7 @@ mod tests { let prev = plog.head.clone(); // take config and use update method with TestKeyManager to update the log - update_plog(&mut plog, &update_cfg, &key_manager, &key_manager) + update_plog_sync(&mut plog, &update_cfg, &key_manager, &key_manager) .expect("Failed to update plog"); // plog head prev should match prev @@ -413,7 +482,7 @@ mod tests { ]) .build(); - update_plog(&mut plog, &key_rotation_config, &key_manager, &key_manager) + update_plog_sync(&mut plog, &key_rotation_config, &key_manager, &key_manager) .expect("Failed to rotate key"); // Update the key manager's path mapping diff --git a/crates/bs/src/resolver_ext.rs b/crates/bs/src/resolver_ext.rs index 2ecdcea..23ea075 100644 --- a/crates/bs/src/resolver_ext.rs +++ b/crates/bs/src/resolver_ext.rs @@ -106,7 +106,7 @@ pub trait ResolverExt: Resolver { tracing::debug!("First lock script built Rebuilt plog"); let rebuilt_plog = provenance_log::log::Builder::new() - .with_vlad(&vlad) + .with_vlad(vlad) .with_first_lock(&first_lock_script) .with_entries(&entry_chain.entries) .with_head(head_cid) From a5c52f731e8b6f8a85e58d95e8eefdb111fb06dc Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Wed, 17 Dec 2025 19:56:00 -0400 Subject: [PATCH 07/11] Betterign struct --- crates/bs-peer/src/peer.rs | 163 ++++++----- crates/bs-peer/src/utils.rs | 34 +-- crates/bs/Cargo.toml | 1 + crates/bs/src/better_sign.rs | 274 ++++++++++++++++++ crates/bs/src/lib.rs | 5 +- crates/bs/src/ops/open.rs | 7 +- crates/bs/src/ops/update.rs | 6 +- .../comrade-reference/src/context/parser.rs | 2 +- crates/multikey/tests/web.rs | 2 - 9 files changed, 395 insertions(+), 99 deletions(-) create mode 100644 crates/bs/src/better_sign.rs diff --git a/crates/bs-peer/src/peer.rs b/crates/bs-peer/src/peer.rs index 2c20a3d..a0bb70b 100644 --- a/crates/bs-peer/src/peer.rs +++ b/crates/bs-peer/src/peer.rs @@ -1,11 +1,10 @@ //! BetterSign Peer: BetterSign core + libp2p networking + Blockstore -use std::sync::{Arc, Mutex}; - use crate::{platform, Error}; use ::cid::Cid; use blockstore::Blockstore as BlockstoreTrait; pub use bs::resolver_ext::ResolverExt; pub use bs::update::Config as UpdateConfig; +use bs::BetterSign; use bs::{ config::sync::{KeyManager, MultiSigner}, params::{ @@ -27,6 +26,8 @@ use multihash::mh; use provenance_log::key::key_paths::ValidatedKeyParams; pub use provenance_log::resolver::{ResolvedPlog, Resolver}; pub use provenance_log::{self as p, Key, Script}; +use std::sync::Arc; +use tokio::sync::Mutex; /// A peer that is generic over the blockstore type. /// @@ -37,8 +38,8 @@ where KP: KeyManager + MultiSigner, BS: BlockstoreTrait + CondSync, { - /// The Provenance Log of the peer, which contains the history of operations - plog: Arc>>, + /// The BetterSign instance containing the plog and crypto operations + better_sign: Arc>>>, /// Key provider for the peer, used for signing and key management key_provider: KP, /// [Blockstore] to save data @@ -58,15 +59,15 @@ where { fn eq(&self, other: &Self) -> bool { // Compare peer IDs and blockstore references - // Equal is the plogs match - self.peer_id == other.peer_id && Arc::ptr_eq(&self.plog, &other.plog) + // Equal if the better_sign Arc pointers match + self.peer_id == other.peer_id && Arc::ptr_eq(&self.better_sign, &other.better_sign) } } /// Impl Clone for BsPeer - You get everything except the events because you can't clone a /// Receiver. /// -/// Plog is wraped in Arc to allow shared access of a single [provenance_log::Log] across threads, +/// BetterSign is wrapped in Arc to allow shared access across threads. impl Clone for BsPeer where KP: KeyManager + MultiSigner + CondSync + Clone, @@ -74,7 +75,7 @@ where { fn clone(&self) -> Self { Self { - plog: self.plog.clone(), + better_sign: self.better_sign.clone(), key_provider: self.key_provider.clone(), blockstore: self.blockstore.clone(), network_client: self.network_client.clone(), @@ -120,7 +121,7 @@ where Ok(Self { network_client: Some(network_client), - plog: Default::default(), + better_sign: Default::default(), key_provider, blockstore, events: Some(rx_evts), @@ -135,25 +136,27 @@ where + MultiSigner + CondSync + AsyncKeyManager - + AsyncMultiSigner, + + AsyncMultiSigner + + Clone, BS: BlockstoreTrait + CondSync, { - /// Returns a clone of the p[p::Log] of the peer, if it exists. - pub fn plog(&self) -> Option { - self.plog.lock().unwrap().as_ref().cloned() - // { - // Ok(plog) => plog.clone(), - // Err(_) => { - // tracing::error!("Failed to acquire lock on Plog"); - // None - // } - // } + /// Returns a clone of the provenance log of the peer, if it exists. + pub async fn plog(&self) -> Option { + self.better_sign + .lock() + .await + .as_ref() + .map(|bs| bs.plog().clone()) } - /// use lock to replace current plog with given plog - fn set_plog(&mut self, plog: p::Log) -> Result<(), Error> { - let mut plog_lock = self.plog.lock().map_err(|_| Error::LockPosioned)?; - *plog_lock = Some(plog); + /// Set the BetterSign instance with the given plog and key provider + async fn set_better_sign(&mut self, plog: p::Log, key_provider: KP) -> Result<(), Error> { + let mut bs_lock = self.better_sign.lock().await; + *bs_lock = Some(BetterSign::from_parts( + plog, + key_provider.clone(), + key_provider, + )); Ok(()) } @@ -161,7 +164,7 @@ where pub fn with_blockstore(key_provider: KP, blockstore: BS) -> Self { Self { key_provider, - plog: Default::default(), + better_sign: Default::default(), blockstore, network_client: Default::default(), events: None, @@ -220,15 +223,16 @@ where /// Store all the plog [provenance_log::Entry]s in the [blockstore::Blockstore] async fn store_entries(&self) -> Result<(), Error> { let (first_lock_cid, first_lock_bytes, entries) = { - let plog = self.plog.lock().map_err(|_| Error::LockPosioned)?; + let bs = self.better_sign.lock().await; - plog.as_ref() - .map(|p| { - let first_lock_cid_bytes: Vec = p.vlad.cid().clone().into(); + bs.as_ref() + .map(|bs| { + let plog = bs.plog(); + let first_lock_cid_bytes: Vec = plog.vlad.cid().clone().into(); let first_lock_cid = Cid::try_from(first_lock_cid_bytes).unwrap(); - let first_lock_bytes: Vec = p.first_lock.clone().into(); + let first_lock_bytes: Vec = plog.first_lock.clone().into(); - (first_lock_cid, first_lock_bytes, p.entries.clone()) + (first_lock_cid, first_lock_bytes, plog.entries.clone()) }) .ok_or(Error::PlogNotInitialized)? }; @@ -265,29 +269,32 @@ where } /// Generate a new Plog with the given configuration. - pub async fn generate_with_config(&mut self, config: bs::open::Config) -> Result<(), Error> { + pub async fn generate_with_config(&mut self, config: bs::open::Config) -> Result<(), Error> + where + KP: AsyncKeyManager + AsyncMultiSigner + Clone, + { { - match self.plog.lock() { - Ok(plog) => { - if plog.is_some() { - tracing::error!("[generate_with_config]: Plog already exists, cannot generate a new one"); - return Err(Error::PlogAlreadyExists); - } else { - tracing::debug!("[generate_with_config]: Acquired lock on Plog"); - } - } - Err(_) => { - tracing::error!("[generate_with_config]: Failed to acquire lock on Plog"); - return Err(Error::LockPosioned); - } + let bs = self.better_sign.lock().await; + if bs.is_some() { + tracing::error!( + "[generate_with_config]: BetterSign already exists, cannot generate a new one" + ); + return Err(Error::PlogAlreadyExists); } + tracing::debug!("[generate_with_config]: Acquired lock on BetterSign"); } - // Pass the key_provider directly as both key_manager and signer - let plog = bs::ops::open_plog(&config, &self.key_provider, &self.key_provider).await?; - { - let verify_iter = &mut plog.verify(); + // Create BetterSign instance (no lock held) + let bs = BetterSign::new( + config.clone(), + self.key_provider.clone(), + self.key_provider.clone(), + ) + .await?; + // Verify the plog + { + let verify_iter = &mut bs.plog().verify(); for result in verify_iter { if let Err(e) = result { tracing::error!("Plog verification failed: {}", e); @@ -296,8 +303,12 @@ where } } + // Store ops and set the better_sign self.store_ops(config.into()).await?; - self.set_plog(plog)?; + { + let mut bs_lock = self.better_sign.lock().await; + *bs_lock = Some(bs); + } self.store_entries().await?; self.record_plog_to_dht().await?; self.publish_to_pubsub().await?; @@ -309,11 +320,14 @@ where &mut self, lock: impl AsRef, unlock: impl AsRef, - ) -> Result<(), Error> { + ) -> Result<(), Error> + where + KP: AsyncKeyManager + AsyncMultiSigner + Clone, + { { - let plog = self.plog.lock().map_err(|_| Error::LockPosioned)?; - if plog.is_some() { - tracing::error!("[generate]: Plog already exists, cannot generate a new one"); + let bs = self.better_sign.lock().await; + if bs.is_some() { + tracing::error!("[generate]: BetterSign already exists, cannot generate a new one"); return Err(Error::PlogAlreadyExists); } } @@ -342,26 +356,26 @@ where /// Update the BsPeer's Plog with new data. pub async fn update(&mut self, config: UpdateConfig) -> Result<(), Error> { + // Update with lock held briefly { - // TODO: Fix async held across await issue - let mut plog = self.plog.lock().map_err(|_| Error::LockPosioned)?; - let Some(ref mut plog) = *plog else { - return Err(Error::PlogNotInitialized); - }; - // Apply the update to the plog - bs::ops::update_plog(plog, &config, &self.key_provider, &self.key_provider).await?; + let mut bs_guard = self.better_sign.lock().await; + let bs = bs_guard.as_mut().ok_or(Error::PlogNotInitialized)?; + + // Update happens - this is async but the lock is held + // This is acceptable because BetterSign encapsulates both the plog and crypto ops + bs.update(config.clone()).await?; // Verify the updated plog - let verify_iter = &mut plog.verify(); + let verify_iter = &mut bs.plog().verify(); for result in verify_iter { if let Err(e) = result { - tracing::error!("Plog verification failed after update: {}", e); + tracing::error!("Plog verification failed after update: {e}"); return Err(Error::PlogVerificationFailed(e)); } } } - // After successful update, store CIDs and publish DHT record + // Now do async operations without holding any lock self.store_ops(config.into()).await?; self.store_entries().await?; self.record_plog_to_dht().await?; @@ -369,11 +383,11 @@ where Ok(()) } - /// Load a Plog into ths BsPeer. + /// Load a Plog into this BsPeer. pub async fn load(&mut self, plog: p::Log) -> Result<(), Error> { { - let plog = self.plog.lock().map_err(|_| Error::LockPosioned)?; - if plog.is_some() { + let bs = self.better_sign.lock().await; + if bs.is_some() { return Err(Error::PlogAlreadyExists); } } @@ -389,8 +403,9 @@ where } } - // Store the plog, entries, and record to DHT - self.set_plog(plog)?; + // Store the plog and create BetterSign instance + self.set_better_sign(plog, self.key_provider.clone()) + .await?; self.store_entries().await?; self.record_plog_to_dht().await?; @@ -401,11 +416,12 @@ where pub async fn publish_to_pubsub(&self) -> Result<(), Error> { // publish Vlad as topic with head cid bytes to pubsub let (vlad_bytes, head) = { - let plog = self.plog.lock().map_err(|_| Error::LockPosioned)?; - let Some(ref plog) = *plog else { + let bs = self.better_sign.lock().await; + let Some(ref bs) = *bs else { return Err(Error::PlogNotInitialized); }; + let plog = bs.plog(); let vlad_bytes: Vec = plog.vlad.clone().into(); (vlad_bytes, plog.head.clone()) @@ -436,11 +452,12 @@ where /// - There is an error putting the record into the DHT. pub async fn record_plog_to_dht(&mut self) -> Result<(), Error> { let (vlad_bytes, head_bytes) = { - let plog = self.plog.lock().map_err(|_| Error::LockPosioned)?; - let Some(ref plog) = *plog else { + let bs = self.better_sign.lock().await; + let Some(ref bs) = *bs else { return Err(Error::PlogNotInitialized); }; + let plog = bs.plog(); let vlad_bytes: Vec = plog.vlad.clone().into(); let head_bytes: Vec = plog.head.clone().into(); (vlad_bytes, head_bytes) diff --git a/crates/bs-peer/src/utils.rs b/crates/bs-peer/src/utils.rs index 9d26c8f..46df08b 100644 --- a/crates/bs-peer/src/utils.rs +++ b/crates/bs-peer/src/utils.rs @@ -188,12 +188,12 @@ pub async fn run_basic_test() { // Check if the plog is initialized assert!( - fixture.peer.plog().is_some(), + fixture.peer.plog().await.is_some(), "Expected plog to be initialized" ); // Check if the plog can be verified - let plog = fixture.peer.plog().unwrap(); + let plog = fixture.peer.plog().await.unwrap(); let verify_iter = &mut plog.verify(); for result in verify_iter { if let Err(e) = result { @@ -237,12 +237,12 @@ pub async fn run_basic_network_test() { // Check if the plog is initialized assert!( - fixture.peer.plog().is_some(), + fixture.peer.plog().await.is_some(), "Expected plog to be initialized in network peer" ); // Check if the plog can be verified - let plog = fixture.peer.plog().unwrap(); + let plog = fixture.peer.plog().await.unwrap(); let verify_iter = &mut plog.verify(); for result in verify_iter { if let Err(e) = result { @@ -306,7 +306,7 @@ pub async fn run_in_memory_blockstore_test() { assert!(res.is_ok(), "Expected successful creation of peer"); // verify the plog - let plog = fixture.peer.plog(); + let plog = fixture.peer.plog().await; // Verify the CID was stored let cid = { @@ -448,7 +448,7 @@ pub async fn run_store_entries_test() { // Let's verify the stored entries // Get the first lock CID from the plog for verification - let plog = fixture.peer.plog(); + let plog = fixture.peer.plog().await; let cid = { let binding = plog.as_ref().unwrap(); let first_lock_cid = binding.vlad.cid(); @@ -497,7 +497,7 @@ pub async fn run_network_store_entries_test() { .await .expect("Should create initialized network peer"); - let plog = fixture.peer.plog(); + let plog = fixture.peer.plog().await; // Get the first lock CID from the plog for verification let cid = { @@ -586,7 +586,7 @@ pub async fn run_update_test() { assert!(stored, "Updated CID should be stored in blockstore"); // Verify plog is still valid after update - let plog = fixture.peer.plog(); + let plog = fixture.peer.plog().await; let binding = plog.as_ref().unwrap(); let verify_iter = &mut binding.verify(); for result in verify_iter { @@ -643,7 +643,7 @@ pub async fn run_network_update_test() { ); // Verify plog is still valid - let plog = fixture.peer.plog(); + let plog = fixture.peer.plog().await; let binding = plog.as_ref().unwrap(); let verify_iter = &mut binding.verify(); for result in verify_iter { @@ -660,14 +660,14 @@ pub async fn run_load_test() { let fixture = setup_initialized_peer().await; // Get the plog from the initialized peer - let original_plog = fixture.peer.plog().as_ref().unwrap().clone(); + let original_plog = fixture.peer.plog().await.as_ref().unwrap().clone(); // Create a new peer with empty state let mut new_fixture = setup_test_peer().await; // Ensure the new peer has no plog yet assert!( - new_fixture.peer.plog().is_none(), + new_fixture.peer.plog().await.is_none(), "New peer should have no plog initially" ); @@ -677,12 +677,12 @@ pub async fn run_load_test() { // Verify the plog was loaded assert!( - new_fixture.peer.plog().is_some(), + new_fixture.peer.plog().await.is_some(), "Plog should now be loaded" ); // Verify the loaded plog has the correct data - let loaded_plog = new_fixture.peer.plog().unwrap(); + let loaded_plog = new_fixture.peer.plog().await.unwrap(); assert_eq!( loaded_plog.vlad.cid(), original_plog.vlad.cid(), @@ -730,7 +730,7 @@ pub async fn run_network_load_test() { let fixture = setup_initialized_peer().await; // Get the plog from the initialized peer - let original_plog = fixture.peer.plog().clone().unwrap().clone(); + let original_plog = fixture.peer.plog().await.clone().unwrap().clone(); // Create a new network peer with empty state let mut new_fixture = setup_network_test_peer() @@ -739,7 +739,7 @@ pub async fn run_network_load_test() { // Ensure the new network peer has no plog yet assert!( - new_fixture.peer.plog().is_none(), + new_fixture.peer.plog().await.is_none(), "New network peer should have no plog initially" ); @@ -752,12 +752,12 @@ pub async fn run_network_load_test() { // Verify the plog was loaded assert!( - new_fixture.peer.plog().is_some(), + new_fixture.peer.plog().await.is_some(), "Plog should now be loaded in network peer" ); // Verify the loaded plog has the correct data - let loaded_plog = new_fixture.peer.plog().as_ref().unwrap().clone(); + let loaded_plog = new_fixture.peer.plog().await.as_ref().unwrap().clone(); assert_eq!( loaded_plog.vlad.cid(), original_plog.vlad.cid(), diff --git a/crates/bs/Cargo.toml b/crates/bs/Cargo.toml index f0ee4b4..1750c6f 100644 --- a/crates/bs/Cargo.toml +++ b/crates/bs/Cargo.toml @@ -30,6 +30,7 @@ bon = "3.6.3" tracing-subscriber.workspace = true bs-wallets = { workspace = true, features = ["sync"] } multibase.workspace = true +tokio = { workspace = true, features = ["macros", "rt"] } [lints] workspace = true diff --git a/crates/bs/src/better_sign.rs b/crates/bs/src/better_sign.rs new file mode 100644 index 0000000..62b364a --- /dev/null +++ b/crates/bs/src/better_sign.rs @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: FSL-1.1 +//! The [`BetterSign`] struct provides an encapsulated interface to provenance log operations. +//! +//! This module offers a clean, preferred API compared to directly using the functional +//! `open_plog` and `update_plog` functions. +use crate::{ + error::{BsCompatibleError, Error}, + ops::{open, update}, +}; +use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner, AsyncSigner}; +use multicodec::Codec; +use multikey::Multikey; +use provenance_log::{entry::Entry, Key, Log}; + +/// The concrete type used for signatures in this crate. +pub type Signature = multisig::Multisig; + +/// A BetterSign instance that encapsulates a provenance log with its key manager and signer. +/// +/// This struct provides an ergonomic API for working with provenance logs by keeping +/// the log, key manager, and signer together as a single unit. +#[derive(Debug)] +pub struct BetterSign { + plog: Log, + key_manager: KM, + signer: S, + _phantom: std::marker::PhantomData, +} + +impl BetterSign { + /// Create a BetterSign instance from existing parts. + /// + /// This is useful when you already have a plog and want to wrap it with key management. + pub fn from_parts(plog: Log, key_manager: KM, signer: S) -> Self { + Self { + plog, + key_manager, + signer, + _phantom: std::marker::PhantomData, + } + } + + /// Get a reference to the provenance [Log]. + pub fn plog(&self) -> &Log { + &self.plog + } + + /// Get a mutable reference to the provenance [Log]. + pub fn plog_mut(&mut self) -> &mut Log { + &mut self.plog + } + + /// Get a reference to the key manager. + pub fn key_manager(&self) -> &KM { + &self.key_manager + } + + /// Get a reference to the signer. + pub fn signer(&self) -> &S { + &self.signer + } + + /// Consume self and return the provenance log. + pub fn into_plog(self) -> Log { + self.plog + } + + /// Consume self and return all components. + pub fn into_parts(self) -> (Log, KM, S) { + (self.plog, self.key_manager, self.signer) + } +} + +impl BetterSign +where + E: BsCompatibleError + Send, + KM: AsyncKeyManager, + S: AsyncMultiSigner, + S: AsyncSigner, +{ + /// Create a new BetterSign instance with the given configuration. + pub async fn new(config: open::Config, key_manager: KM, signer: S) -> Result { + let plog = open::open_plog_core(&config, &key_manager, &signer).await?; + Ok(Self { + plog, + key_manager, + signer, + _phantom: std::marker::PhantomData, + }) + } + + /// Update the provenance log with new operations. + pub async fn update(&mut self, config: update::Config) -> Result { + update::update_plog_core(&mut self.plog, &config, &self.key_manager, &self.signer).await?; + // Return a clone of the last entry (the one we just added) + Ok(self + .plog + .entries + .get(&self.plog.head) + .expect("Head entry should exist") + .clone()) + } +} + +#[cfg(feature = "sync")] +impl BetterSign +where + E: BsCompatibleError + Send, + KM: AsyncKeyManager, + S: AsyncMultiSigner, + S: AsyncSigner, +{ + /// Synchronously create a new BetterSign instance with the given configuration. + /// + /// This blocks on the async `new` method using `futures::executor::block_on`. + /// + /// # Errors + /// + /// Returns an error if the provenance log creation fails. + pub fn new_sync(config: open::Config, key_manager: KM, signer: S) -> Result { + futures::executor::block_on(Self::new(config, key_manager, signer)) + } + + /// Synchronously update the provenance log with new operations. + /// + /// This blocks on the async `update` method using `futures::executor::block_on`. + /// + /// # Errors + /// + /// Returns an error if the update operation fails. + pub fn update_sync(&mut self, config: update::Config) -> Result { + futures::executor::block_on(self.update(config)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::params::vlad::FirstEntryKeyParams; + use crate::params::{anykey::PubkeyParams, vlad::VladParams}; + use bs_wallets::memory::InMemoryKeyManager; + use multicodec::Codec; + use provenance_log::key::key_paths::ValidatedKeyParams; + use provenance_log::Script; + + #[tokio::test] + async fn test_better_sign_new() { + let config = open::Config::builder() + .vlad(VladParams::default()) + .pubkey( + PubkeyParams::builder() + .codec(Codec::Ed25519Priv) + .build() + .into(), + ) + .entrykey( + FirstEntryKeyParams::builder() + .codec(Codec::Ed25519Priv) + .build() + .into(), + ) + .lock(Script::Code( + Key::default(), + "check_signature(\"/pubkey\", \"/entry/\")".to_string(), + )) + .unlock(Script::Code( + Key::default(), + "push(\"/entry/\"); push(\"/entry/proof\")".to_string(), + )) + .build(); + + let key_manager = InMemoryKeyManager::::default(); + let signer = key_manager.clone(); + + let bs = BetterSign::new(config, key_manager, signer) + .await + .expect("Failed to create BetterSign"); + + // Verify the plog was created + assert!(!bs.plog().entries.is_empty()); + assert!(bs.plog().verify().count() > 0); + } + + #[tokio::test] + async fn test_better_sign_update() { + let open_config = open::Config::builder() + .vlad(VladParams::default()) + .pubkey( + PubkeyParams::builder() + .codec(Codec::Ed25519Priv) + .build() + .into(), + ) + .entrykey( + FirstEntryKeyParams::builder() + .codec(Codec::Ed25519Priv) + .build() + .into(), + ) + .lock(Script::Code( + Key::default(), + "check_signature(\"/pubkey\", \"/entry/\")".to_string(), + )) + .unlock(Script::Code( + Key::default(), + "push(\"/entry/\"); push(\"/entry/proof\")".to_string(), + )) + .build(); + + let key_manager = InMemoryKeyManager::::default(); + let signer = key_manager.clone(); + + let mut bs = BetterSign::new(open_config, key_manager, signer) + .await + .expect("Failed to create BetterSign"); + + let initial_entry_count = bs.plog().entries.len(); + + // Update the plog + let update_config = update::Config::builder() + .unlock(Script::Code( + Key::default(), + "push(\"/entry/\"); push(\"/entry/proof\")".to_string(), + )) + .entry_signing_key(PubkeyParams::KEY_PATH.into()) + .build(); + + let entry = bs + .update(update_config) + .await + .expect("Failed to update plog"); + + // Verify a new entry was added + assert_eq!(bs.plog().entries.len(), initial_entry_count + 1); + assert_eq!(bs.plog().head, entry.cid()); + } + + #[cfg(feature = "sync")] + #[test] + fn test_better_sign_sync() { + let config = open::Config::builder() + .vlad(VladParams::default()) + .pubkey( + PubkeyParams::builder() + .codec(Codec::Ed25519Priv) + .build() + .into(), + ) + .entrykey( + FirstEntryKeyParams::builder() + .codec(Codec::Ed25519Priv) + .build() + .into(), + ) + .lock(Script::Code( + Key::default(), + "check_signature(\"/pubkey\", \"/entry/\")".to_string(), + )) + .unlock(Script::Code( + Key::default(), + "push(\"/entry/\"); push(\"/entry/proof\")".to_string(), + )) + .build(); + + let key_manager = InMemoryKeyManager::::default(); + let signer = key_manager.clone(); + + let bs = + BetterSign::new_sync(config, key_manager, signer).expect("Failed to create BetterSign"); + + // Verify the plog was created + assert!(!bs.plog().entries.is_empty()); + } +} diff --git a/crates/bs/src/lib.rs b/crates/bs/src/lib.rs index 497b410..fa4dfa5 100644 --- a/crates/bs/src/lib.rs +++ b/crates/bs/src/lib.rs @@ -14,8 +14,9 @@ pub mod error; pub use error::Error; -/// The concrete type used for signatures in this crate. -pub type Signature = multisig::Multisig; +/// BetterSign module for managing provenance logs +pub mod better_sign; +pub use better_sign::{BetterSign, Signature}; /// bettersign operations pub mod ops; diff --git a/crates/bs/src/ops/open.rs b/crates/bs/src/ops/open.rs index 9e9ec99..bcf5c36 100644 --- a/crates/bs/src/ops/open.rs +++ b/crates/bs/src/ops/open.rs @@ -51,7 +51,12 @@ where )) } -async fn open_plog_core(config: &Config, key_manager: &KM, signer: &S) -> Result +/// Core function to open a provenance log based on the [Config] provided. (async) +pub(crate) async fn open_plog_core( + config: &Config, + key_manager: &KM, + signer: &S, +) -> Result where E: BsCompatibleError + Send, KM: AsyncKeyManager + ?Sized, diff --git a/crates/bs/src/ops/update.rs b/crates/bs/src/ops/update.rs index 4ccf926..56ec1e0 100644 --- a/crates/bs/src/ops/update.rs +++ b/crates/bs/src/ops/update.rs @@ -13,11 +13,11 @@ pub use config::Config; pub mod op_params; pub use op_params::OpParams; -use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner, AsyncSigner}; use crate::{ error::{BsCompatibleError, UpdateError}, Signature, }; +use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner, AsyncSigner}; use multicid::{cid, Cid}; use multicodec::Codec; use multihash::mh; @@ -92,7 +92,7 @@ where )) } -async fn update_plog_core( +pub(crate) async fn update_plog_core( plog: &mut Log, config: &Config, key_manager: &KM, @@ -312,11 +312,11 @@ where #[cfg(test)] mod tests { use super::*; + use crate::open::{self, open_plog_sync}; use crate::params::{ anykey::PubkeyParams, vlad::{FirstEntryKeyParams, VladParams}, }; - use crate::open::{self, open_plog_sync}; use bs_traits::sync::SyncGetKey; use bs_wallets::memory::InMemoryKeyManager; diff --git a/crates/comrade-reference/src/context/parser.rs b/crates/comrade-reference/src/context/parser.rs index 2d4082f..002938f 100644 --- a/crates/comrade-reference/src/context/parser.rs +++ b/crates/comrade-reference/src/context/parser.rs @@ -41,7 +41,7 @@ pub enum Expression<'a> { } /// Parse a script from a string into expressions -pub fn parse(script_str: &str) -> Result, ApiError> { +pub fn parse(script_str: &str) -> Result>, ApiError> { let pairs = ScriptParser::parse(Rule::script, script_str) .map_err(|e| ApiError::PestParse(Box::new(e)))?; diff --git a/crates/multikey/tests/web.rs b/crates/multikey/tests/web.rs index 4ebe521..bf510f0 100644 --- a/crates/multikey/tests/web.rs +++ b/crates/multikey/tests/web.rs @@ -2,8 +2,6 @@ use multicodec::Codec; use multikey::{Builder, Views}; -use multiutil::CodecInfo; - use wasm_bindgen_test::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); From 2a18570f5b615d165245ae381b68fc882834c664 Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Wed, 17 Dec 2025 23:49:01 -0400 Subject: [PATCH 08/11] add better async --- crates/bs-peer/src/peer.rs | 13 +-- crates/bs-traits/src/asyncro.rs | 4 +- crates/bs/src/better_sign.rs | 21 ++--- crates/bs/src/config.rs | 99 ++++++++++++++++++++++- crates/bs/src/config/asynchronous.rs | 114 +++++++++++++++++++++++---- crates/bs/src/config/sync.rs | 55 ++++++++++++- crates/bs/src/lib.rs | 4 +- crates/bs/src/ops/open.rs | 40 +++++++--- crates/bs/src/ops/update.rs | 51 ++++++++---- 9 files changed, 331 insertions(+), 70 deletions(-) diff --git a/crates/bs-peer/src/peer.rs b/crates/bs-peer/src/peer.rs index a0bb70b..b4461fc 100644 --- a/crates/bs-peer/src/peer.rs +++ b/crates/bs-peer/src/peer.rs @@ -6,7 +6,10 @@ pub use bs::resolver_ext::ResolverExt; pub use bs::update::Config as UpdateConfig; use bs::BetterSign; use bs::{ - config::sync::{KeyManager, MultiSigner}, + config::{ + asynchronous::{KeyManager as AsyncKeyManager, MultiSigner as AsyncMultiSigner}, + sync::{KeyManager, MultiSigner}, + }, params::{ anykey::PubkeyParams, vlad::{FirstEntryKeyParams, VladParams}, @@ -15,8 +18,6 @@ use bs::{ }; pub use bs_p2p::events::api::{Client, Libp2pEvent}; pub use bs_p2p::events::PublicEvent; -use bs_traits::asyncro::AsyncKeyManager; -use bs_traits::asyncro::AsyncMultiSigner; use bs_traits::CondSync; use futures::channel::mpsc::{self}; pub use libp2p::PeerId; @@ -136,7 +137,7 @@ where + MultiSigner + CondSync + AsyncKeyManager - + AsyncMultiSigner + + AsyncMultiSigner + Clone, BS: BlockstoreTrait + CondSync, { @@ -271,7 +272,7 @@ where /// Generate a new Plog with the given configuration. pub async fn generate_with_config(&mut self, config: bs::open::Config) -> Result<(), Error> where - KP: AsyncKeyManager + AsyncMultiSigner + Clone, + KP: AsyncKeyManager + AsyncMultiSigner + Clone, { { let bs = self.better_sign.lock().await; @@ -322,7 +323,7 @@ where unlock: impl AsRef, ) -> Result<(), Error> where - KP: AsyncKeyManager + AsyncMultiSigner + Clone, + KP: AsyncKeyManager + AsyncMultiSigner + Clone, { { let bs = self.better_sign.lock().await; diff --git a/crates/bs-traits/src/asyncro.rs b/crates/bs-traits/src/asyncro.rs index 1bff9da..515ffa0 100644 --- a/crates/bs-traits/src/asyncro.rs +++ b/crates/bs-traits/src/asyncro.rs @@ -159,8 +159,8 @@ pub trait AsyncGetKey: GetKey { &'a self, key_path: &'a Self::KeyPath, codec: &'a Self::Codec, - threshold: usize, - limit: usize, + threshold: NonZeroUsize, + limit: NonZeroUsize, ) -> Result, Self::Error>; } diff --git a/crates/bs/src/better_sign.rs b/crates/bs/src/better_sign.rs index 62b364a..ac17f7b 100644 --- a/crates/bs/src/better_sign.rs +++ b/crates/bs/src/better_sign.rs @@ -4,16 +4,11 @@ //! This module offers a clean, preferred API compared to directly using the functional //! `open_plog` and `update_plog` functions. use crate::{ + config::asynchronous::{KeyManager, MultiSigner}, error::{BsCompatibleError, Error}, ops::{open, update}, }; -use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner, AsyncSigner}; -use multicodec::Codec; -use multikey::Multikey; -use provenance_log::{entry::Entry, Key, Log}; - -/// The concrete type used for signatures in this crate. -pub type Signature = multisig::Multisig; +use provenance_log::{entry::Entry, Log}; /// A BetterSign instance that encapsulates a provenance log with its key manager and signer. /// @@ -74,9 +69,8 @@ impl BetterSign { impl BetterSign where E: BsCompatibleError + Send, - KM: AsyncKeyManager, - S: AsyncMultiSigner, - S: AsyncSigner, + KM: KeyManager, + S: MultiSigner, { /// Create a new BetterSign instance with the given configuration. pub async fn new(config: open::Config, key_manager: KM, signer: S) -> Result { @@ -106,9 +100,8 @@ where impl BetterSign where E: BsCompatibleError + Send, - KM: AsyncKeyManager, - S: AsyncMultiSigner, - S: AsyncSigner, + KM: KeyManager, + S: MultiSigner, { /// Synchronously create a new BetterSign instance with the given configuration. /// @@ -141,7 +134,7 @@ mod tests { use bs_wallets::memory::InMemoryKeyManager; use multicodec::Codec; use provenance_log::key::key_paths::ValidatedKeyParams; - use provenance_log::Script; + use provenance_log::{Key, Script}; #[tokio::test] async fn test_better_sign_new() { diff --git a/crates/bs/src/config.rs b/crates/bs/src/config.rs index aec935e..101ba18 100644 --- a/crates/bs/src/config.rs +++ b/crates/bs/src/config.rs @@ -1,10 +1,87 @@ -//! Holds opinionated configuration about what concrete types should be used for the traits. +//! Opinionated configuration layer for BetterSign trait bounds with concrete types. //! -//! Users can pick any concrete types that implement the traits, but this module provides -//! default implementations that can be used directly. +//! # Architecture Overview +//! +//! This module provides a convenience layer on top of the generic traits from `bs-traits`, +//! eliminating repetitive type parameter boilerplate throughout the BetterSign codebase. +//! +//! ## Layered Design +//! +//! ```text +//! ┌─────────────────────────────────────────────────────┐ +//! │ Application Layer (bs-peer, bs-wallets, etc.) │ +//! │ Uses: config::sync::KeyManager │ +//! │ config::asynchronous::MultiSigner │ +//! └─────────────────────────────────────────────────────┘ +//! ↓ +//! ┌─────────────────────────────────────────────────────┐ +//! │ Configuration Layer (this module) │ +//! │ Provides: Opinionated supertraits with concrete │ +//! │ types (Key, Codec, Multikey, Multisig) │ +//! └─────────────────────────────────────────────────────┘ +//! ↓ +//! ┌─────────────────────────────────────────────────────┐ +//! │ Traits Layer (bs-traits) │ +//! │ Provides: Generic sync/async traits │ +//! │ (SyncSigner, AsyncSigner, etc.) │ +//! └─────────────────────────────────────────────────────┘ +//! ``` +//! +//! ## Modules +//! +//! - **`sync`**: Synchronous supertraits combining `bs_traits::sync` traits with concrete types +//! - **`asynchronous`**: Asynchronous supertraits combining `bs_traits::asyncro` traits with concrete types +//! - **`adapters`**: Bridges that convert sync trait implementations to async interfaces +//! +//! ## Concrete Types +//! +//! This configuration layer standardizes on these concrete types: +//! +//! - **Key paths**: [`provenance_log::Key`] +//! - **Codec**: [`multicodec::Codec`] +//! - **Public keys**: [`multikey::Multikey`] +//! - **Signatures**: [`multisig::Multisig`] (re-exported as [`crate::Signature`]) +//! +//! ## Benefits +//! +//! 1. **Reduced boilerplate**: Write `KeyManager` instead of repeating all associated types +//! 2. **Type safety**: Ensures consistent concrete types across the application +//! 3. **Flexibility**: Generic over error type `E` for different error handling strategies +//! 4. **Maintainability**: Change concrete types in one place if needed +//! +//! ## When to Use +//! +//! - **Use this module** when writing BetterSign-specific code that uses the standard concrete types +//! - **Use `bs-traits` directly** when writing generic code that should work with any types +//! +//! ## Example +//! +//! ```ignore +//! // Without config module (verbose) +//! use bs_traits::asyncro::AsyncKeyManager; +//! use multicodec::Codec; +//! use multikey::Multikey; +//! use provenance_log::Key; +//! +//! async fn verbose(km: &KM) -> Result +//! where +//! KM: AsyncKeyManager, +//! { +//! // ... +//! } +//! +//! // With config module (clean) +//! use bs::config::asynchronous::KeyManager; +//! +//! async fn clean(km: &(dyn KeyManager + Send + Sync)) -> Result { +//! // ... +//! } +//! ``` /// Sync to async adapters pub mod adapters; +/// Opinionated configuration for the async traits types +pub mod asynchronous; /// Opinionated configuration for the sync traits types pub mod sync; @@ -15,3 +92,19 @@ pub use multicodec::Codec; pub use multikey::Multikey; pub use multisig::Multisig; pub use provenance_log::Key; + +/// The concrete signature type used throughout BetterSign. +/// +/// This is an alias for [`multisig::Multisig`] and is the standard signature type +/// used in both [`sync::MultiSigner`] and [`asynchronous::MultiSigner`] traits. +/// +/// # Example +/// +/// ```ignore +/// use bs::config::Signature; +/// +/// fn verify_signature(sig: &Signature) { +/// // Work with the concrete signature type +/// } +/// ``` +pub type Signature = Multisig; diff --git a/crates/bs/src/config/asynchronous.rs b/crates/bs/src/config/asynchronous.rs index c49e219..7021263 100644 --- a/crates/bs/src/config/asynchronous.rs +++ b/crates/bs/src/config/asynchronous.rs @@ -1,36 +1,118 @@ +//! Asynchronous trait supertraits with opinionated concrete types for BetterSign. +//! +//! This module provides convenience supertraits that combine the generic asynchronous traits +//! from `bs_traits::asyncro` with concrete types specific to the BetterSign application. +//! +//! # Purpose +//! +//! Rather than repeating verbose trait bounds throughout the codebase: +//! ```ignore +//! async fn example( +//! key_manager: &KM, +//! signer: &S, +//! ) -> Result<(), E> +//! where +//! KM: bs_traits::asyncro::AsyncKeyManager< +//! E, +//! KeyPath = provenance_log::Key, +//! Codec = multicodec::Codec, +//! Key = multikey::Multikey, +//! >, +//! S: bs_traits::asyncro::AsyncMultiSigner< +//! multisig::Multisig, +//! E, +//! PubKey = multikey::Multikey, +//! Codec = multicodec::Codec, +//! >, +//! ``` +//! +//! You can use the opinionated supertraits from this module: +//! ```ignore +//! use bs::config::asynchronous::{KeyManager, MultiSigner}; +//! +//! async fn example( +//! key_manager: &(dyn KeyManager + Send + Sync), +//! signer: &(dyn MultiSigner + Send + Sync), +//! ) -> Result<(), E> +//! ``` +//! +//! # Relationship to Other Modules +//! +//! - **`bs_traits::asyncro`**: Provides generic asynchronous traits that work with any types +//! - **`bs::config::asynchronous`** (this module): Provides opinionated supertraits with concrete types for BetterSign +//! - **`bs::config::sync`**: Parallel module providing synchronous supertraits +//! - **`bs::config::adapters`**: Bridges between sync and async implementations +//! +//! # Concrete Types Used +//! +//! - **KeyPath**: `provenance_log::Key` +//! - **Codec**: `multicodec::Codec` +//! - **Key**: `multikey::Multikey` +//! - **Signature**: `multisig::Multisig` (aliased as `bs::Signature`) +//! +//! # Example +//! +//! ```ignore +//! use bs::config::asynchronous::{KeyManager, MultiSigner}; +//! +//! async fn process_async( +//! km: &(dyn KeyManager + Send + Sync), +//! signer: &(dyn MultiSigner + Send + Sync), +//! ) -> Result<(), E> { +//! // All concrete types are already specified in the trait bounds +//! // ... +//! } +//! ``` use super::*; -pub use bs_traits::asyncro::{AsyncGetKey, AsyncSigner}; +pub use bs_traits::asyncro::{ + AsyncKeyManager as AsyncGetKeyTrait, AsyncMultiSigner as AsyncMultiSignerTrait, AsyncSigner, +}; +use bs_traits::EphemeralKey; /// Supertrait for key management operations -pub trait KeyManager: - GetKey - + AsyncGetKey +pub trait KeyManager: + GetKey + + AsyncGetKeyTrait + Send + Sync + 'static { } -/// Supertrait for signing operations -pub trait MultiSigner: - Signer + AsyncSigner + Send + Sync + 'static -{ -} - -impl KeyManager for T where - T: GetKey - + AsyncGetKey +impl KeyManager for T where + T: GetKey + + AsyncGetKeyTrait + Send + Sync + 'static { } -impl MultiSigner for T where - T: Signer +/// Supertrait for signing operations +pub trait MultiSigner: + Signer + + AsyncSigner + + EphemeralKey + + GetKey + + AsyncMultiSignerTrait + + Send + + Sync + + 'static +where + E: Send, +{ +} + +impl MultiSigner for T +where + T: Signer + AsyncSigner + + EphemeralKey + + GetKey + + AsyncMultiSignerTrait + Send + Sync - + 'static + + 'static, + E: Send, { } diff --git a/crates/bs/src/config/sync.rs b/crates/bs/src/config/sync.rs index 6c9ad8e..e8875d0 100644 --- a/crates/bs/src/config/sync.rs +++ b/crates/bs/src/config/sync.rs @@ -1,4 +1,57 @@ -//! Sync alterntives to the asynchronous traits. +//! Synchronous trait supertraits with opinionated concrete types for BetterSign. +//! +//! This module provides convenience supertraits that combine the generic synchronous traits +//! from `bs_traits::sync` with concrete types specific to the BetterSign application. +//! +//! # Purpose +//! +//! Rather than repeating verbose trait bounds throughout the codebase: +//! ```ignore +//! fn example( +//! key_manager: &KM +//! ) where +//! KM: bs_traits::sync::SyncGetKey< +//! KeyPath = provenance_log::Key, +//! Codec = multicodec::Codec, +//! Key = multikey::Multikey, +//! Error = E +//! > +//! ``` +//! +//! You can use the opinionated supertraits from this module: +//! ```ignore +//! use bs::config::sync::KeyManager; +//! +//! fn example(key_manager: &(dyn KeyManager + Send + Sync)) +//! ``` +//! +//! # Relationship to Other Modules +//! +//! - **`bs_traits::sync`**: Provides generic synchronous traits that work with any types +//! - **`bs::config::sync`** (this module): Provides opinionated supertraits with concrete types for BetterSign +//! - **`bs::config::asynchronous`**: Parallel module providing async supertraits +//! - **`bs::config::adapters`**: Bridges between sync and async implementations +//! +//! # Concrete Types Used +//! +//! - **KeyPath**: `provenance_log::Key` +//! - **Codec**: `multicodec::Codec` +//! - **Key**: `multikey::Multikey` +//! - **Signature**: `multisig::Multisig` (aliased as `bs::Signature`) +//! +//! # Example +//! +//! ```ignore +//! use bs::config::sync::{KeyManager, MultiSigner}; +//! +//! fn process_sync( +//! km: &(dyn KeyManager + Send + Sync), +//! signer: &(dyn MultiSigner + Send + Sync), +//! ) -> Result<(), E> { +//! // All concrete types are already specified in the trait bounds +//! // ... +//! } +//! ``` pub use bs_traits::sync::{SyncGetKey, SyncPrepareEphemeralSigning, SyncSigner}; use bs_traits::EphemeralKey; diff --git a/crates/bs/src/lib.rs b/crates/bs/src/lib.rs index fa4dfa5..50b4e06 100644 --- a/crates/bs/src/lib.rs +++ b/crates/bs/src/lib.rs @@ -16,7 +16,7 @@ pub use error::Error; /// BetterSign module for managing provenance logs pub mod better_sign; -pub use better_sign::{BetterSign, Signature}; +pub use better_sign::BetterSign; /// bettersign operations pub mod ops; @@ -31,6 +31,8 @@ pub mod prelude { /// Opinionated configuation for the BetterSign library pub mod config; +// Re-export the concrete Signature type from config for convenience +pub use config::Signature; /// Resolver extension for bettersign pub mod resolver_ext; diff --git a/crates/bs/src/ops/open.rs b/crates/bs/src/ops/open.rs index bcf5c36..220a5a9 100644 --- a/crates/bs/src/ops/open.rs +++ b/crates/bs/src/ops/open.rs @@ -5,12 +5,12 @@ pub mod config; use std::num::NonZeroUsize; use crate::{ + config::asynchronous::{KeyManager, MultiSigner}, error::{BsCompatibleError, OpenError}, params::vlad::{FirstEntryKeyParams, VladParams}, update::{op, OpParams}, Signature, }; -use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner}; pub use config::Config; use multicid::{cid, Cid, Vlad}; use multicodec::Codec; @@ -20,11 +20,13 @@ use provenance_log::{entry, error::EntryError, Error as PlogError, Key, Log, OpI use tracing::debug; /// Open a new provenance log based on the [Config] provided. (async) -pub async fn open_plog(config: &Config, key_manager: &KM, signer: &S) -> Result +pub async fn open_plog( + config: &Config, + key_manager: &(dyn KeyManager + Send + Sync), + signer: &(dyn MultiSigner + Send + Sync), +) -> Result where E: BsCompatibleError + Send, - KM: AsyncKeyManager + ?Sized, - S: AsyncMultiSigner + ?Sized, { open_plog_core(config, key_manager, signer).await } @@ -44,7 +46,7 @@ where let key_manager_adapter = SyncToAsyncManager::new(key_manager); let signer_adapter = SyncToAsyncSigner::new(signer); - futures::executor::block_on(open_plog_core( + futures::executor::block_on(open_plog_impl( config, &key_manager_adapter, &signer_adapter, @@ -52,15 +54,26 @@ where } /// Core function to open a provenance log based on the [Config] provided. (async) -pub(crate) async fn open_plog_core( +pub(crate) async fn open_plog_core( config: &Config, - key_manager: &KM, - signer: &S, + key_manager: &(dyn KeyManager + Send + Sync), + signer: &(dyn MultiSigner + Send + Sync), ) -> Result where E: BsCompatibleError + Send, - KM: AsyncKeyManager + ?Sized, - S: AsyncMultiSigner + ?Sized, +{ + open_plog_impl(config, key_manager, signer).await +} + +/// Internal implementation that works with base async traits (for adapters) +async fn open_plog_impl(config: &Config, key_manager: &KM, signer: &S) -> Result +where + E: BsCompatibleError + Send, + KM: bs_traits::asyncro::AsyncKeyManager + + ?Sized, + S: bs_traits::asyncro::AsyncMultiSigner + + bs_traits::asyncro::AsyncSigner + + ?Sized, { // 0. Set up the list of ops let mut op_params = Vec::default(); @@ -69,7 +82,7 @@ where for params in config.additional_ops().iter() { match params { p @ OpParams::KeyGen { .. } => { - let _ = load_key::(&mut op_params, p, key_manager).await?; + let _ = load_key(&mut op_params, p, key_manager).await?; } p @ OpParams::CidGen { .. } => { let _ = load_cid::(&mut op_params, p)?; @@ -126,7 +139,7 @@ where } // 4. Continue with other preparations - let _ = load_key::(&mut op_params, config.pubkey(), key_manager).await?; + let _ = load_key(&mut op_params, config.pubkey(), key_manager).await?; let lock_script = config.lock_script().clone(); let unlock_script = config.unlock().clone(); @@ -218,7 +231,8 @@ async fn load_key( ) -> Result where E: From + From + From, - KM: AsyncKeyManager + ?Sized, + KM: bs_traits::asyncro::AsyncKeyManager + + ?Sized, { debug!("load_key: {:?}", params); match params { diff --git a/crates/bs/src/ops/update.rs b/crates/bs/src/ops/update.rs index 56ec1e0..7f1eb01 100644 --- a/crates/bs/src/ops/update.rs +++ b/crates/bs/src/ops/update.rs @@ -14,10 +14,10 @@ pub mod op_params; pub use op_params::OpParams; use crate::{ + config::asynchronous::{KeyManager, MultiSigner}, error::{BsCompatibleError, UpdateError}, Signature, }; -use bs_traits::asyncro::{AsyncKeyManager, AsyncMultiSigner, AsyncSigner}; use multicid::{cid, Cid}; use multicodec::Codec; use multihash::mh; @@ -31,11 +31,11 @@ use std::{fs::read, path::Path}; use tracing::debug; /// Updates a provenance log given the update config (async) -pub async fn update_plog( +pub async fn update_plog( plog: &mut Log, config: &Config, - key_manager: &KM, - signer: &S, + key_manager: &(dyn KeyManager + Send + Sync), + signer: &(dyn MultiSigner + Send + Sync), ) -> Result where E: BsCompatibleError @@ -49,9 +49,6 @@ where + ToString + std::fmt::Debug + Send, - KM: AsyncKeyManager + ?Sized, - S: AsyncMultiSigner + ?Sized, - S: AsyncSigner, { update_plog_core(plog, config, key_manager, signer).await } @@ -84,7 +81,7 @@ where let key_manager_adapter = SyncToAsyncManager::new(key_manager); let signer_adapter = SyncToAsyncSigner::new(signer); - futures::executor::block_on(update_plog_core( + futures::executor::block_on(update_plog_impl( plog, config, &key_manager_adapter, @@ -92,7 +89,30 @@ where )) } -pub(crate) async fn update_plog_core( +pub(crate) async fn update_plog_core( + plog: &mut Log, + config: &Config, + key_manager: &(dyn KeyManager + Send + Sync), + signer: &(dyn MultiSigner + Send + Sync), +) -> Result +where + E: BsCompatibleError + + From + + From + + From + + From + + From + + From + + From + + ToString + + std::fmt::Debug + + Send, +{ + update_plog_impl(plog, config, key_manager, signer).await +} + +/// Internal implementation that works with base async traits (for adapters) +async fn update_plog_impl( plog: &mut Log, config: &Config, key_manager: &KM, @@ -110,9 +130,11 @@ where + ToString + std::fmt::Debug + Send, - KM: AsyncKeyManager + ?Sized, - S: AsyncMultiSigner + ?Sized, - S: AsyncSigner, + KM: bs_traits::asyncro::AsyncKeyManager + + ?Sized, + S: bs_traits::asyncro::AsyncMultiSigner + + bs_traits::asyncro::AsyncSigner + + ?Sized, { // 0. Set up the list of ops we're going to add let mut op_params = Vec::default(); @@ -122,7 +144,7 @@ where for params in config.additional_ops().iter() { match params { p @ OpParams::KeyGen { .. } => { - let _ = load_key::(&mut op_params, p, key_manager).await?; + let _ = load_key(&mut op_params, p, key_manager).await?; } p @ OpParams::CidGen { .. } => { let _ = load_cid(&mut op_params, p, |path| -> Result, E> { @@ -222,7 +244,8 @@ async fn load_key( ) -> Result where E: From + From + From, - KM: AsyncKeyManager + ?Sized, + KM: bs_traits::asyncro::AsyncKeyManager + + ?Sized, { debug!("load_key: {:?}", params); match params { From 0b1c820d74b471155c7221031fa60856127a91a7 Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Thu, 18 Dec 2025 15:31:00 -0400 Subject: [PATCH 09/11] use CondSync --- crates/bs-traits/src/asyncro.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bs-traits/src/asyncro.rs b/crates/bs-traits/src/asyncro.rs index 515ffa0..afbfb7d 100644 --- a/crates/bs-traits/src/asyncro.rs +++ b/crates/bs-traits/src/asyncro.rs @@ -165,7 +165,7 @@ pub trait AsyncGetKey: GetKey { } /// An async version of KeyManager -pub trait AsyncKeyManager: GetKey + Send + Sync { +pub trait AsyncKeyManager: GetKey + CondSync { fn get_key<'a>( &'a self, key_path: &'a Self::KeyPath, From a4e484071610e99e195686563ed97fe078ed8aaa Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Thu, 18 Dec 2025 15:33:37 -0400 Subject: [PATCH 10/11] add docs for traits --- crates/bs-traits/README.md | 106 ++++++++++++++- docs/ARCHITECTURE.md | 268 +++++++++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 docs/ARCHITECTURE.md diff --git a/crates/bs-traits/README.md b/crates/bs-traits/README.md index 9241901..d00ea68 100644 --- a/crates/bs-traits/README.md +++ b/crates/bs-traits/README.md @@ -3,4 +3,108 @@ # BS-Traits -The traits used by Better Sign \ No newline at end of file +**Generic trait definitions for BetterSign - the foundation for maximum flexibility.** + +## Purpose + +This crate provides pure, generic trait definitions that enable custom implementations with **any concrete types you choose**. It is intentionally free of opinions about specific cryptographic implementations. + +## When to Use + +**Use this crate directly when:** +- You need custom key types (e.g., HSM integration, hardware wallets) +- You want custom signature formats (e.g., Ethereum, Bitcoin-specific formats) +- You're integrating with existing cryptographic infrastructure +- You need maximum flexibility and control + +**Use `bs::config` instead when:** +- You're building a standard wallet with Multikey/Multisig +- You want rapid development with minimal boilerplate +- You want to use the reference `open_plog` and `update_plog` functions + +## Architecture + +BS-Traits is **Layer 1** in BetterSign's three-layer architecture: + +``` +Layer 3: Application Code (your implementations) + ↓ +Layer 2: Configuration Layer (bs::config - optional convenience) + ↓ +Layer 1: Generic Traits (bs-traits - maximum flexibility) ← YOU ARE HERE +``` + +See [ARCHITECTURE.md](../../docs/ARCHITECTURE.md) for complete documentation. + +## Trait Categories + +### Core Traits +- `Signer` / `Verifier` - Cryptographic signing and verification +- `Encryptor` / `Decryptor` - Encryption operations +- `GetKey` / `KeyDetails` - Key management +- `SecretSplitter` / `SecretCombiner` - Secret sharing schemes + +### Async Traits (`bs_traits::asyncro`) +- `AsyncSigner`, `AsyncVerifier` +- `AsyncEncryptor`, `AsyncDecryptor` +- `AsyncKeyManager`, `AsyncMultiSigner` +- All async operations return `BoxFuture` for dyn compatibility + +### Sync Traits (`bs_traits::sync`) +- `SyncSigner`, `SyncVerifier` +- `SyncEncryptor`, `SyncDecryptor` +- `SyncGetKey`, `SyncPrepareEphemeralSigning` + +## Example: Custom Implementation + +```rust +use bs_traits::{GetKey, Signer}; +use bs_traits::sync::{SyncGetKey, SyncSigner}; + +struct MyHsmWallet { + // Your custom HSM integration +} + +// Use YOUR types +impl GetKey for MyHsmWallet { + type Key = YourPublicKeyType; + type KeyPath = YourPathType; + type Codec = YourCodecType; + type Error = YourError; +} + +impl Signer for MyHsmWallet { + type KeyPath = YourPathType; + type Signature = YourSignatureType; // Not limited to Multisig! + type Error = YourError; +} + +impl SyncSigner for MyHsmWallet { + fn try_sign(&self, key: &Self::KeyPath, data: &[u8]) + -> Result + { + // Your custom signing logic + } +} +``` + +## Comparison with bs::config + +| Aspect | bs-traits (this crate) | bs::config | +|--------|----------------------|------------| +| Flexibility | Maximum - any types | Fixed types | +| Boilerplate | More verbose | Minimal | +| Types | Your choice | Multikey, Multisig, etc. | +| Use with open_plog | Need custom implementation | Direct use | +| Best for | Custom integrations | Standard wallets | + +## Features + +- `dyn-compatible` (default): Ensures traits can be used as `dyn Trait` objects + +## License + +Functional Source License 1.1 + +[CRYPTID]: https://cryptid.tech +[PROVENANCE]: https://github.com/cryptidtech/provenance-specifications \ No newline at end of file diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..f661e5d --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,268 @@ +# BetterSign Architecture: Traits and Configuration Layers + +## Overview + +BetterSign implements a **layered trait architecture** that provides both maximum flexibility for custom implementations and convenience for the common case. This document explains the design philosophy and how to use each layer. + +## Design Philosophy + +**Core Principle**: Enable users to provide custom implementations with their own types, while providing a batteries-included reference implementation for rapid development. + +## Three-Layer Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Layer 3: Application Code │ +│ • Your wallet implementations │ +│ • bs-wallets reference implementations │ +│ • Integration code │ +│ Choice: Use config layer OR implement traits directly │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Layer 2: Configuration Layer (bs::config) │ +│ • OPTIONAL convenience supertraits │ +│ • Opinionated concrete types for reference impl │ +│ • Reduces boilerplate by ~80% for common cases │ +│ Types: Key, Codec, Multikey, Multisig │ +└─────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────┐ +│ Layer 1: Generic Traits (bs-traits) │ +│ • Pure generic traits with zero opinions │ +│ • Maximum flexibility - use ANY types you want │ +│ • No dependencies on concrete implementations │ +└─────────────────────────────────────────────────────────┘ +``` + +## Layer 1: Generic Traits (bs-traits) + +**Purpose**: Provide the most flexible trait definitions that work with ANY concrete types. + +**When to use**: +- You want complete control over types +- You're building a custom wallet with non-standard key formats +- You're integrating with existing cryptographic infrastructure +- You need types other than Multikey/Multisig + +**Example - Custom Implementation**: +```rust +use bs_traits::{GetKey, Signer}; +use bs_traits::sync::{SyncGetKey, SyncSigner}; + +struct MyHsmWallet { + // Your custom HSM integration +} + +// Implement with YOUR types +impl GetKey for MyHsmWallet { + type Key = YourCustomPublicKey; // Your type + type KeyPath = YourCustomPath; // Your type + type Codec = YourCustomCodec; // Your type + type Error = YourCustomError; // Your error +} + +impl Signer for MyHsmWallet { + type KeyPath = YourCustomPath; + type Signature = YourCustomSignature; // Your signature format + type Error = YourCustomError; +} + +impl SyncSigner for MyHsmWallet { + fn try_sign(&self, key: &Self::KeyPath, data: &[u8]) + -> Result + { + // Your custom HSM signing logic + todo!() + } +} +``` + +**Trade-offs**: +- ✅ Maximum flexibility - any types you want +- ✅ Zero coupling to BetterSign's concrete types +- ⚠️ More verbose trait bounds in your code +- ⚠️ You'll need to implement your own `open_plog`/`update_plog` equivalents + +## Layer 2: Configuration Layer (bs::config) + +**Purpose**: Provide pre-configured supertraits with concrete types for the reference implementation. + +**When to use**: +- You're happy with the reference types (Multikey, Multisig, etc.) +- You want rapid development with minimal boilerplate +- You're building a standard wallet implementation +- You want to use the provided `open_plog` and `update_plog` functions + +**Concrete Types Used**: +- **Key paths**: `provenance_log::Key` +- **Codec**: `multicodec::Codec` +- **Public keys**: `multikey::Multikey` +- **Signatures**: `multisig::Multisig` + +**Example - Using Config Layer**: + +```rust +use bs::config::sync::{KeyManager, MultiSigner}; + +struct MyWallet { + // Your wallet state using reference types +} + +// Implement with concrete types already specified +impl KeyManager for MyWallet { + // Only need to specify Error type, everything else is fixed + // Key = Multikey, KeyPath = Key, Codec = Codec +} + +impl MultiSigner for MyWallet { + // Signature = Multisig, KeyPath = Key +} + +// Much shorter trait bounds in your code: +fn use_wallet(wallet: &impl KeyManager) { + // vs the verbose bs_traits version +} +``` + +**Benefits**: +- ✅ Dramatically reduced boilerplate +- ✅ Works seamlessly with `open_plog`, `update_plog`, `BetterSign` struct +- ✅ Type safety enforced at compile time +- ✅ Clear, consistent types across the ecosystem + +**Trade-offs**: +- ⚠️ Locked to reference types (Multikey, Multisig, Key, Codec) +- ⚠️ Not suitable if you need custom signature formats or key types + +### Config Submodules + +#### `bs::config::sync` +Synchronous trait supertraits: +- `KeyManager`: Key management operations +- `MultiSigner`: Signing and ephemeral key operations + +#### `bs::config::asynchronous` +Asynchronous trait supertraits: +- `KeyManager`: Async key management +- `MultiSigner`: Async signing and ephemeral key operations + +#### `bs::config::adapters` +Bridges between sync and async: +- `SyncToAsyncManager`: Adapts sync KeyManager to async +- `SyncToAsyncSigner`: Adapts sync MultiSigner to async + +Used internally by `open_plog_sync` and `update_plog_sync`. + +## Layer 3: Reference Operations (open_plog, update_plog) + +**Design Decision**: The core operations `open_plog` and `update_plog` are **opinionated** and work with the config layer types. + +**Why?** +- Pragmatic: 95% of users will use the reference types +- Cleaner: Avoids excessive generic complexity +- Maintainable: Single code path to maintain + +**For Users with Custom Types**: +You have two options: + +### Option 1: Use the patterns, write your own operations +```rust +// Study the implementation of open_plog in crates/bs/src/ops/open.rs +// Adapt it to work with your custom types + +pub async fn my_open_plog( + config: &Config, + my_key_manager: &impl MyKeyManagerTrait, + my_signer: &impl MySignerTrait, +) -> Result { + // Your adapted implementation +} +``` + +### Option 2: Create adapters to the reference types +```rust +// Create adapters that convert your types to/from reference types +struct MyKeyAdapter { + inner: MyCustomKeyManager, +} + +impl bs::config::sync::KeyManager for MyKeyAdapter { + // Adapt your types to Multikey, Key, Codec +} +``` + +## Reference Implementation: bs-wallets + +The `bs-wallets` crate provides reference implementations: + +### `InMemoryKeyManager` + +```rust +use bs_wallets::memory::InMemoryKeyManager; +use bs::config::sync::{KeyManager, MultiSigner}; + +// Generic over error type +let wallet = InMemoryKeyManager::::new(); + +// Can be used with any error that meets the trait bounds +let wallet2 = InMemoryKeyManager::::new(); + +// Implements both config layer traits +fn test_wallet(w: &impl KeyManager + MultiSigner) { + // Works! +} +test_wallet(&wallet); +``` + +Key features: +- ✅ Implements `bs::config::sync::{KeyManager, MultiSigner}` +- ✅ Generic over error type +- ✅ Can be wrapped for async use via adapters +- ✅ Stores keys in memory with secure ephemeral key support + +## Decision Matrix: Which Layer Should I Use? + +| Scenario | Recommended Approach | +|----------|---------------------| +| Building a standard wallet | **Config Layer** (bs::config) | +| Using Multikey/Multisig | **Config Layer** (bs::config) | +| Want to use open_plog/update_plog directly | **Config Layer** (bs::config) | +| Integrating HSM or hardware wallet | **Generic Traits** (bs-traits) | +| Custom signature format (e.g., Ethereum) | **Generic Traits** (bs-traits) | +| Custom key derivation scheme | **Generic Traits** (bs-traits) | +| Need maximum flexibility | **Generic Traits** (bs-traits) | +| Prototyping quickly | **Config Layer** (bs::config) | + +## FAQ + +### Q: Can I mix and match layers? +**A**: Yes! You can use the generic traits for some components and the config layer for others. The adapters in `bs::config::adapters` help bridge between them. + +### Q: Will using the config layer lock me in? +**A**: No. The config layer is just convenient type aliases and supertraits. You can always drop down to implementing `bs-traits` directly if your needs change. + +### Q: Why not make open_plog/update_plog fully generic? +**A**: Pragmatism. It would add significant generic complexity for minimal benefit. 95% of users will use the reference types. Users with custom types can adapt the implementation (it's open source). + +### Q: Can I contribute a new wallet implementation? +**A**: Yes! Add it to `bs-wallets` if it uses the reference types, or publish your own crate if it uses custom types. Both are supported patterns. + +### Q: Is the config layer less flexible than using traits directly? +**A**: Yes, by design. It trades some flexibility for massive boilerplate reduction. Choose the right tool for your use case. + +## Examples + +See: +- `crates/bs-wallets/src/memory.rs` - Reference implementation using config layer +- `crates/interop-tests/src/bin/native.rs` - Integration example +- `crates/bs/src/better_sign.rs` - High-level API using config layer + +## Summary + +**BetterSign's trait architecture gives you choice**: +- **Rapid development**: Use the config layer with reference types +- **Maximum control**: Use bs-traits directly with your own types +- **Both**: Mix and match as needed + +The key insight: **bs-traits remains generic and flexible** while **bs::config provides pragmatic defaults**. You're never locked in - pick the right tool for your use case. From 9dd9c7f6bc49e6bc0ca644bdb23aef674fc0058b Mon Sep 17 00:00:00 2001 From: Doug Anderson444 Date: Fri, 19 Dec 2025 07:50:17 -0400 Subject: [PATCH 11/11] remove abiguity and make KeyManger mut --- cli/src/subcmds/plog.rs | 12 +++++++++--- crates/bs-traits/Cargo.toml | 1 + crates/bs-traits/src/asyncro.rs | 8 ++++++++ crates/bs/src/better_sign.rs | 4 ++-- crates/bs/src/ops/open.rs | 17 ++++++++++++----- crates/bs/src/ops/update.rs | 4 ++-- crates/bs/src/ops/update/config.rs | 7 ++++--- 7 files changed, 38 insertions(+), 15 deletions(-) diff --git a/cli/src/subcmds/plog.rs b/cli/src/subcmds/plog.rs index b903d91..5fc66ab 100644 --- a/cli/src/subcmds/plog.rs +++ b/cli/src/subcmds/plog.rs @@ -81,7 +81,10 @@ impl SyncPrepareEphemeralSigning for KeyManager { ) -> Result< ( ::PubKey, - Box Result<::Signature, ::Error> + Send>, + Box< + dyn FnOnce(&[u8]) -> Result<::Signature, ::Error> + + Send, + >, ), ::Error, > { @@ -101,7 +104,10 @@ impl SyncPrepareEphemeralSigning for KeyManager { let public_key = secret_key.conv_view()?.to_public_key()?; // Create the signing closure that owns the secret key - let sign_once: Box Result<::Signature, ::Error> + Send> = Box::new( + let sign_once: Box< + dyn FnOnce(&[u8]) -> Result<::Signature, ::Error> + + Send, + > = Box::new( move |data: &[u8]| -> Result<::Signature, ::Error> { debug!("Signing data with ephemeral key"); let signature = secret_key.sign_view()?.sign(data, false, None)?; @@ -257,7 +263,7 @@ pub async fn go(cmd: Command, _config: &Config) -> Result<(), Error> { }; let cfg = update::Config::builder() - .add_entry_lock_scripts(vec![lock_script.clone()]) + .with_entry_lock_scripts(vec![lock_script.clone()]) .unlock(unlock_script) .entry_signing_key(entry_signing_key) .additional_ops(entry_ops) diff --git a/crates/bs-traits/Cargo.toml b/crates/bs-traits/Cargo.toml index 69a254e..4617913 100644 --- a/crates/bs-traits/Cargo.toml +++ b/crates/bs-traits/Cargo.toml @@ -9,6 +9,7 @@ license = "Functional Source License 1.1" [dependencies] thiserror.workspace = true +multicid.workspace = true [dev-dependencies] futures = "0.3" diff --git a/crates/bs-traits/src/asyncro.rs b/crates/bs-traits/src/asyncro.rs index afbfb7d..a9b5391 100644 --- a/crates/bs-traits/src/asyncro.rs +++ b/crates/bs-traits/src/asyncro.rs @@ -166,6 +166,7 @@ pub trait AsyncGetKey: GetKey { /// An async version of KeyManager pub trait AsyncKeyManager: GetKey + CondSync { + /// Get the key asynchronously fn get_key<'a>( &'a self, key_path: &'a Self::KeyPath, @@ -173,6 +174,13 @@ pub trait AsyncKeyManager: GetKey + CondSync { threshold: NonZeroUsize, limit: NonZeroUsize, ) -> BoxFuture<'a, Result>; + + /// Emables you to pre-process the Vlad during the creation process + /// For example, you can provide a function that will be called shortly after the Vlad is created + #[allow(unused_variables)] + fn preprocess_vlad<'a>(&'a mut self, vlad: &'a multicid::Vlad) -> BoxFuture<'a, Result<(), E>> { + Box::pin(async move { Ok(()) }) + } } /// An async version of MultiSigner, including ephemeral signing diff --git a/crates/bs/src/better_sign.rs b/crates/bs/src/better_sign.rs index ac17f7b..2358c04 100644 --- a/crates/bs/src/better_sign.rs +++ b/crates/bs/src/better_sign.rs @@ -73,8 +73,8 @@ where S: MultiSigner, { /// Create a new BetterSign instance with the given configuration. - pub async fn new(config: open::Config, key_manager: KM, signer: S) -> Result { - let plog = open::open_plog_core(&config, &key_manager, &signer).await?; + pub async fn new(config: open::Config, mut key_manager: KM, signer: S) -> Result { + let plog = open::open_plog_core(&config, &mut key_manager, &signer).await?; Ok(Self { plog, key_manager, diff --git a/crates/bs/src/ops/open.rs b/crates/bs/src/ops/open.rs index 220a5a9..89438fb 100644 --- a/crates/bs/src/ops/open.rs +++ b/crates/bs/src/ops/open.rs @@ -22,7 +22,7 @@ use tracing::debug; /// Open a new provenance log based on the [Config] provided. (async) pub async fn open_plog( config: &Config, - key_manager: &(dyn KeyManager + Send + Sync), + key_manager: &mut (dyn KeyManager + Send + Sync), signer: &(dyn MultiSigner + Send + Sync), ) -> Result where @@ -43,12 +43,12 @@ where { use crate::config::adapters::{SyncToAsyncManager, SyncToAsyncSigner}; - let key_manager_adapter = SyncToAsyncManager::new(key_manager); + let mut key_manager_adapter = SyncToAsyncManager::new(key_manager); let signer_adapter = SyncToAsyncSigner::new(signer); futures::executor::block_on(open_plog_impl( config, - &key_manager_adapter, + &mut key_manager_adapter, &signer_adapter, )) } @@ -56,7 +56,7 @@ where /// Core function to open a provenance log based on the [Config] provided. (async) pub(crate) async fn open_plog_core( config: &Config, - key_manager: &(dyn KeyManager + Send + Sync), + key_manager: &mut (dyn KeyManager + Send + Sync), signer: &(dyn MultiSigner + Send + Sync), ) -> Result where @@ -66,7 +66,11 @@ where } /// Internal implementation that works with base async traits (for adapters) -async fn open_plog_impl(config: &Config, key_manager: &KM, signer: &S) -> Result +async fn open_plog_impl( + config: &Config, + key_manager: &mut KM, + signer: &S, +) -> Result where E: BsCompatibleError + Send, KM: bs_traits::asyncro::AsyncKeyManager @@ -121,6 +125,9 @@ where Ok(multisig.into()) })?; + // Pass vlad to the caller for any pre-processing + key_manager.preprocess_vlad(&vlad).await?; + // 2. Extract entry key parameters and prepare signing let entrykey_params = &config.entrykey(); let (codec, threshold, limit) = extract_key_params::(entrykey_params)?; diff --git a/crates/bs/src/ops/update.rs b/crates/bs/src/ops/update.rs index 7f1eb01..0f12f88 100644 --- a/crates/bs/src/ops/update.rs +++ b/crates/bs/src/ops/update.rs @@ -167,7 +167,7 @@ where let entry_builder = entry::EntryBuilder::from(&last_entry); let mut mutable_entry = entry_builder.unlock(config.unlock().clone()).build(); - for lock in config.add_entry_lock_scripts() { + for lock in config.entry_lock_scripts() { mutable_entry.add_lock(lock); } @@ -444,7 +444,7 @@ mod tests { key: VladParams::::FIRST_ENTRY_KEY_PATH.into(), }]) // Entry lock scripts define conditions which must be met by the next entry in the plog for it to be valid. - .add_entry_lock_scripts(vec![Script::Code( + .with_entry_lock_scripts(vec![Script::Code( Key::try_from("/delegated/").unwrap(), delegated_lock, )]) diff --git a/crates/bs/src/ops/update/config.rs b/crates/bs/src/ops/update/config.rs index 6138e52..1a0cfea 100644 --- a/crates/bs/src/ops/update/config.rs +++ b/crates/bs/src/ops/update/config.rs @@ -32,7 +32,7 @@ pub struct Config { /// ); /// ``` #[builder(default = Vec::new())] - add_entry_lock_scripts: Vec