diff --git a/.gitignore b/.gitignore index 3995a772..3067195e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ .idea .vscode -peer* sync_test.sh openapi-generator-cli.jar diff --git a/Cargo.lock b/Cargo.lock index aac79b96..066bb8ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2074,7 +2074,6 @@ dependencies = [ "hex", "multihash-codetable", "reqwest 0.11.27", - "ring 0.17.8", "serde", "serde_ipld_dagcbor", "serde_json", @@ -2205,6 +2204,7 @@ dependencies = [ "int-enum", "libp2p-identity", "minicbor", + "multiaddr", "multibase 0.9.1", "multihash-codetable", "multihash-derive 0.9.0", diff --git a/anchor-remote/Cargo.toml b/anchor-remote/Cargo.toml index 4bde36de..8a93db87 100644 --- a/anchor-remote/Cargo.toml +++ b/anchor-remote/Cargo.toml @@ -21,7 +21,6 @@ expect-test.workspace = true hex.workspace = true multihash-codetable.workspace = true reqwest.workspace = true -ring.workspace = true serde.workspace = true serde_ipld_dagcbor.workspace = true serde_json.workspace = true diff --git a/anchor-remote/src/cas_remote.rs b/anchor-remote/src/cas_remote.rs index 1470a2eb..37dbcf86 100644 --- a/anchor-remote/src/cas_remote.rs +++ b/anchor-remote/src/cas_remote.rs @@ -7,7 +7,6 @@ use base64::{ Engine as _, }; use multihash_codetable::{Code, MultihashDigest}; -use ring::signature::Ed25519KeyPair; use serde::{Deserialize, Serialize}; use tokio::time::interval; use tracing::{debug, info, warn}; @@ -17,7 +16,7 @@ use ceramic_anchor_service::{ DetachedTimeEvent, MerkleNode, MerkleNodes, RootTimeEvent, TransactionManager, }; use ceramic_car::CarReader; -use ceramic_core::{Cid, NodeId, StreamId}; +use ceramic_core::{Cid, NodeKey, StreamId}; use ceramic_event::unvalidated::AnchorProof; pub const AGENT_VERSION: &str = concat!("ceramic-one/", env!("CARGO_PKG_VERSION")); @@ -63,8 +62,7 @@ struct CasAnchorResponse { /// Remote CAS transaction manager pub struct RemoteCas { - node_id: NodeId, - signing_key: Ed25519KeyPair, + node_key: NodeKey, url: String, poll_interval: Duration, poll_retry_count: u64, @@ -112,13 +110,12 @@ impl TransactionManager for RemoteCas { impl RemoteCas { /// Create a new RemoteCas instance pub fn new( - node_id: NodeId, - keypair: Ed25519KeyPair, + node_key: NodeKey, remote_anchor_service_url: String, anchor_poll_interval: Duration, anchor_poll_retry_count: u64, ) -> Self { - let controller = node_id.did_key(); + let controller = node_key.did_key(); let jws_header = Header { kid: format!( "{}#{}", @@ -133,8 +130,7 @@ impl RemoteCas { let jws_header_b64 = b64.encode(serde_json::to_vec(&jws_header).expect("invalid jws header")); Self { - node_id, - signing_key: keypair, + node_key, url: format!("{}/api/v0/requests", remote_anchor_service_url), poll_interval: anchor_poll_interval, poll_retry_count: anchor_poll_retry_count, @@ -146,7 +142,7 @@ impl RemoteCas { /// Create an anchor request on the remote CAS pub async fn create_anchor_request(&self, root_cid: Cid) -> Result { let cas_request_body = serde_json::to_string(&CasAnchorRequest { - stream_id: self.node_id.stream_id(), + stream_id: self.node_key.stream_id(), cid: root_cid.to_string(), timestamp: chrono::Utc::now().to_rfc3339(), ceramic_one_version: AGENT_VERSION.to_owned(), @@ -175,7 +171,7 @@ impl RemoteCas { }; let body_b64 = b64.encode(serde_json::to_vec(&body)?); let message = [self.jws_header_b64.clone(), body_b64].join("."); - let sig_b64 = b64.encode(self.signing_key.sign(message.as_bytes())); + let sig_b64 = b64.encode(self.node_key.sign(message.as_bytes())); Ok([message.clone(), sig_b64].join(".")) } } @@ -237,16 +233,15 @@ mod tests { use expect_test::expect_file; use multihash_codetable::{Code, MultihashDigest}; - use ring::signature::Ed25519KeyPair; use ceramic_anchor_service::{ AnchorService, MockAnchorEventService, Store, TransactionManager, }; - use ceramic_core::Cid; + use ceramic_core::{Cid, NodeKey}; use ceramic_sql::sqlite::SqlitePool; - fn node_id_and_private_key() -> (NodeId, Ed25519KeyPair) { - NodeId::try_from_secret( + fn node_key() -> NodeKey { + NodeKey::try_from_secret( std::env::var("NODE_PRIVATE_KEY") // The following secret is NOT authenticated with CAS, it is only used for testing. .unwrap_or( @@ -263,13 +258,11 @@ mod tests { async fn test_anchor_batch_with_cas() { let anchor_client = Arc::new(MockAnchorEventService::new(10)); let anchor_requests = anchor_client - .events_since_high_water_mark(NodeId::random().0, 0, 1_000_000) + .events_since_high_water_mark(NodeKey::random().id(), 0, 1_000_000) .await .unwrap(); - let (node_id, keypair) = node_id_and_private_key(); let remote_cas = Arc::new(RemoteCas::new( - node_id, - keypair, + node_key(), "https://cas-dev.3boxlabs.com".to_owned(), Duration::from_secs(1), 1, @@ -278,7 +271,7 @@ mod tests { remote_cas, anchor_client, SqlitePool::connect_in_memory().await.unwrap(), - NodeId::random().0, + NodeKey::random().id(), Duration::from_secs(1), 10, ); @@ -295,11 +288,9 @@ mod tests { async fn test_create_anchor_request_with_cas() { let mock_root_cid = Cid::from_str("bafyreia776z4jdg5zgycivcpr3q6lcu6llfowkrljkmq3bex2k5hkzat54").unwrap(); - let (node_id, keypair) = node_id_and_private_key(); let remote_cas = RemoteCas::new( - node_id, - keypair, + node_key(), "https://cas-dev.3boxlabs.com".to_owned(), Duration::from_secs(1), 1, @@ -323,10 +314,8 @@ mod tests { async fn test_jwt() { let mock_data = serde_ipld_dagcbor::to_vec(b"mock root").unwrap(); let mock_hash = MultihashDigest::digest(&Code::Sha2_256, &mock_data); - let (node_id, keypair) = node_id_and_private_key(); let remote_cas = Arc::new(RemoteCas::new( - node_id, - keypair, + node_key(), "https://cas-dev.3boxlabs.com".to_owned(), Duration::from_secs(1), 1, diff --git a/anchor-service/src/anchor_batch.rs b/anchor-service/src/anchor_batch.rs index ee49d572..d15eadf2 100644 --- a/anchor-service/src/anchor_batch.rs +++ b/anchor-service/src/anchor_batch.rs @@ -232,7 +232,7 @@ impl AnchorService { mod tests { use std::{sync::Arc, time::Duration}; - use ceramic_core::NodeId; + use ceramic_core::NodeKey; use ceramic_sql::sqlite::SqlitePool; use expect_test::expect_file; use tokio::{sync::broadcast, time::sleep}; @@ -245,7 +245,7 @@ mod tests { let tx_manager = Arc::new(MockCas); let event_service = Arc::new(MockAnchorEventService::new(10)); let pool = SqlitePool::connect_in_memory().await.unwrap(); - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let anchor_interval = Duration::from_millis(5); let anchor_batch_size = 1000000; let mut anchor_service = AnchorService::new( @@ -277,7 +277,7 @@ mod tests { let tx_manager = Arc::new(MockCas); let event_service = Arc::new(MockAnchorEventService::new(1)); let pool = SqlitePool::connect_in_memory().await.unwrap(); - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let anchor_interval = Duration::from_millis(5); let anchor_batch_size = 1000000; let mut anchor_service = AnchorService::new( diff --git a/anchor-service/src/cas_mock.rs b/anchor-service/src/cas_mock.rs index f9bd7252..9604f9c8 100644 --- a/anchor-service/src/cas_mock.rs +++ b/anchor-service/src/cas_mock.rs @@ -93,7 +93,7 @@ impl Store for MockAnchorEventService { #[cfg(test)] pub mod tests { use super::*; - use ceramic_core::NodeId; + use ceramic_core::NodeKey; use ceramic_sql::sqlite::SqlitePool; use expect_test::expect_file; use std::sync::Arc; @@ -105,14 +105,14 @@ pub mod tests { async fn test_anchor_batch_with_10_requests() { let anchor_client = Arc::new(MockAnchorEventService::new(10)); let anchor_requests = anchor_client - .events_since_high_water_mark(NodeId::random().0, 0, 1_000_000) + .events_since_high_water_mark(NodeKey::random().id(), 0, 1_000_000) .await .unwrap(); let anchor_service = AnchorService::new( Arc::new(MockCas), anchor_client, SqlitePool::connect_in_memory().await.unwrap(), - NodeId::random().0, + NodeKey::random().id(), Duration::from_secs(1), 10, ); @@ -128,14 +128,14 @@ pub mod tests { async fn test_anchor_batch_with_pow2_requests() { let anchor_client = Arc::new(MockAnchorEventService::new(16)); let anchor_requests = anchor_client - .events_since_high_water_mark(NodeId::random().0, 0, 1_000_000) + .events_since_high_water_mark(NodeKey::random().id(), 0, 1_000_000) .await .unwrap(); let anchor_service = AnchorService::new( Arc::new(MockCas), anchor_client, SqlitePool::connect_in_memory().await.unwrap(), - NodeId::random().0, + NodeKey::random().id(), Duration::from_secs(1), 16, ); @@ -151,14 +151,14 @@ pub mod tests { async fn test_anchor_batch_with_more_than_pow2_requests() { let anchor_client = Arc::new(MockAnchorEventService::new(18)); let anchor_requests = anchor_client - .events_since_high_water_mark(NodeId::random().0, 0, 1_000_000) + .events_since_high_water_mark(NodeKey::random().id(), 0, 1_000_000) .await .unwrap(); let anchor_service = AnchorService::new( Arc::new(MockCas), anchor_client, SqlitePool::connect_in_memory().await.unwrap(), - NodeId::random().0, + NodeKey::random().id(), Duration::from_secs(1), 18, ); @@ -174,14 +174,14 @@ pub mod tests { async fn test_anchor_batch_with_less_than_pow2_requests() { let anchor_client = Arc::new(MockAnchorEventService::new(15)); let anchor_requests = anchor_client - .events_since_high_water_mark(NodeId::random().0, 0, 1_000_000) + .events_since_high_water_mark(NodeKey::random().id(), 0, 1_000_000) .await .unwrap(); let anchor_service = AnchorService::new( Arc::new(MockCas), anchor_client, SqlitePool::connect_in_memory().await.unwrap(), - NodeId::random().0, + NodeKey::random().id(), Duration::from_secs(1), 15, ); @@ -197,14 +197,14 @@ pub mod tests { async fn test_anchor_batch_with_0_requests() { let anchor_client = Arc::new(MockAnchorEventService::new(0)); let anchor_requests = anchor_client - .events_since_high_water_mark(NodeId::random().0, 0, 1_000_000) + .events_since_high_water_mark(NodeKey::random().id(), 0, 1_000_000) .await .unwrap(); let anchor_service = AnchorService::new( Arc::new(MockCas), anchor_client, SqlitePool::connect_in_memory().await.unwrap(), - NodeId::random().0, + NodeKey::random().id(), Duration::from_secs(1), 1, ); @@ -219,14 +219,14 @@ pub mod tests { async fn test_anchor_batch_with_1_request() { let anchor_client = Arc::new(MockAnchorEventService::new(1)); let anchor_requests = anchor_client - .events_since_high_water_mark(NodeId::random().0, 0, 1_000_000) + .events_since_high_water_mark(NodeKey::random().id(), 0, 1_000_000) .await .unwrap(); let anchor_service = AnchorService::new( Arc::new(MockCas), anchor_client, SqlitePool::connect_in_memory().await.unwrap(), - NodeId::random().0, + NodeKey::random().id(), Duration::from_secs(1), 1, ); diff --git a/api/src/tests.rs b/api/src/tests.rs index df73b990..460f8ca7 100644 --- a/api/src/tests.rs +++ b/api/src/tests.rs @@ -15,7 +15,7 @@ use ceramic_api_server::{ InterestsPostResponse, InterestsSortKeySortValuePostResponse, }; use ceramic_api_server::{Api, StreamsStreamIdGetResponse}; -use ceramic_core::{Cid, Interest}; +use ceramic_core::{Cid, Interest, NodeKey}; use ceramic_core::{EventId, Network, NodeId, PeerId, StreamId}; use ceramic_pipeline::EVENT_STATES_TABLE; use datafusion::arrow::array::{ @@ -197,7 +197,7 @@ where #[test(tokio::test)] async fn create_event() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::Mainnet; let expected_event_id = EventId::try_from(hex::decode(DATA_EVENT_ID).unwrap()).unwrap(); @@ -245,7 +245,7 @@ async fn create_event() { #[test(tokio::test)] async fn create_event_twice() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::Mainnet; let expected_event_id = EventId::try_from(hex::decode(UNSIGNED_INIT_EVENT_ID).unwrap()).unwrap(); @@ -299,7 +299,7 @@ async fn create_event_twice() { } #[test(tokio::test)] async fn create_event_fails() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::Mainnet; let expected_event_id = EventId::try_from(hex::decode(DATA_EVENT_ID).unwrap()).unwrap(); @@ -352,7 +352,7 @@ async fn create_event_fails() { #[test(tokio::test)] async fn register_interest_sort_value() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::InMemory; let model = "k2t6wz4ylx0qr6v7dvbczbxqy7pqjb0879qx930c1e27gacg3r8sllonqt4xx9"; // cspell:disable-line @@ -407,7 +407,7 @@ async fn register_interest_sort_value() { #[test(tokio::test)] async fn register_interest_sort_value_bad_request() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::InMemory; let model = "2t6wz4ylx0qr6v7dvbczbxqy7pqjb0879qx930c1e27gacg3r8sllonqt4xx9"; //missing 'k' cspell:disable-line @@ -435,7 +435,7 @@ async fn register_interest_sort_value_bad_request() { #[test(tokio::test)] async fn register_interest_sort_value_controller() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::InMemory; let model = "z3KWHw5Efh2qLou2FEdz3wB8ZvLgURJP94HeijLVurxtF1Ntv6fkg2G"; // base58 encoded should work cspell:disable-line // we convert to base36 before storing @@ -493,7 +493,7 @@ async fn register_interest_sort_value_controller() { #[test(tokio::test)] async fn register_interest_value_controller_stream() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::InMemory; let model = "k2t6wz4ylx0qr6v7dvbczbxqy7pqjb0879qx930c1e27gacg3r8sllonqt4xx9"; // cspell:disable-line let controller = "did:key:zGs1Det7LHNeu7DXT4nvoYrPfj3n6g7d6bj2K4AMXEvg1"; @@ -725,7 +725,7 @@ async fn get_interests_for_peer() { #[test(tokio::test)] async fn get_events_for_interest_range() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::InMemory; let model = "k2t6wz4ylx0qr6v7dvbczbxqy7pqjb0879qx930c1e27gacg3r8sllonqt4xx9"; // cspell:disable-line let controller = "did:key:zGs1Det7LHNeu7DXT4nvoYrPfj3n6g7d6bj2K4AMXEvg1"; @@ -794,7 +794,7 @@ async fn get_events_for_interest_range() { #[test(tokio::test)] async fn events_event_id_get_by_event_id_success() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::InMemory; let event_cid = Cid::from_str("baejbeicqtpe5si4qvbffs2s7vtbk5ccbsfg6owmpidfj3zeluqz4hlnz6m").unwrap(); // cspell:disable-line @@ -838,7 +838,7 @@ async fn events_event_id_get_by_event_id_success() { #[test(tokio::test)] async fn events_event_id_get_by_cid_success() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::InMemory; let event_cid = Cid::from_str("baejbeihyr3kf77etqdccjfoc33dmko2ijyugn6qk6yucfkioasjssz3bbu").unwrap(); // cspell:disable-line @@ -873,7 +873,7 @@ async fn events_event_id_get_by_cid_success() { #[test(tokio::test)] async fn stream_state() { - let node_id = NodeId::random().0; + let node_id = NodeKey::random().id(); let network = Network::InMemory; let mock_event_store = MockEventStoreTest::new(); let mock_interest = MockAccessInterestStoreTest::new(); diff --git a/core/Cargo.toml b/core/Cargo.toml index bff10877..80440fe8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,6 +17,7 @@ hex.workspace = true int-enum.workspace = true libp2p-identity.workspace = true minicbor.workspace = true +multiaddr.workspace = true multibase.workspace = true multihash-codetable.workspace = true multihash-derive.workspace = true diff --git a/core/src/deserialize_ext.rs b/core/src/deserialize_ext.rs new file mode 100644 index 00000000..a7d2050d --- /dev/null +++ b/core/src/deserialize_ext.rs @@ -0,0 +1,18 @@ +use std::convert::Infallible; + +use serde::Deserialize; + +/// DeserializeExt extends [`Deserialize`] with methods specific to dag-json and dag-cbor +/// deserialization. +pub trait DeserializeExt<'de>: Deserialize<'de> { + /// Deserialize dag-json encoded data into self. + fn from_json(data: &'de [u8]) -> Result { + serde_ipld_dagjson::from_slice(data) + } + /// Deserialize dag-cbor encoded data into self. + fn from_cbor(data: &'de [u8]) -> Result> { + serde_ipld_dagcbor::from_slice(data) + } +} + +impl<'de, T: Deserialize<'de>> DeserializeExt<'de> for T {} diff --git a/core/src/lib.rs b/core/src/lib.rs index c829c3f9..adfe5372 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -2,21 +2,26 @@ //! Core functionality for ceramic, including the StreamId, Cid, and Jws types. #![warn(missing_docs)] mod bytes; +mod deserialize_ext; pub mod event_id; pub mod interest; mod jwk; mod network; mod node_id; +mod peer; mod range; mod serialize_ext; +pub mod signer; mod stream_id; pub use bytes::Bytes; +pub use deserialize_ext::DeserializeExt; pub use event_id::EventId; pub use interest::{Interest, PeerId}; pub use jwk::Jwk; pub use network::Network; -pub use node_id::NodeId; +pub use node_id::{NodeId, NodeKey}; +pub use peer::{PeerEntry, PeerKey}; pub use range::RangeOpen; pub use serialize_ext::SerializeExt; pub use stream_id::{StreamId, StreamIdType, METAMODEL_STREAM_ID}; diff --git a/core/src/node_id.rs b/core/src/node_id.rs index 3837254f..f205858d 100644 --- a/core/src/node_id.rs +++ b/core/src/node_id.rs @@ -1,21 +1,25 @@ use std::fmt::Display; use std::{fs, path::PathBuf, str::FromStr}; -use crate::{StreamId, StreamIdType}; use anyhow::{anyhow, Context, Ok, Result}; -use cid::multihash::Multihash; -use cid::Cid; +use cid::{multihash::Multihash, Cid}; use libp2p_identity::PeerId; use rand::Rng; -use ring::signature::{Ed25519KeyPair, KeyPair}; +use ring::signature::{Ed25519KeyPair, KeyPair, Signature}; +use serde::{Deserialize, Serialize}; +use ssi::jwk::{Algorithm, Base64urlUInt, OctetParams, Params, JWK}; + +use crate::{signer::Signer, DidDocument, StreamId, StreamIdType}; const ED25519_MULTICODEC: u64 = 0xed; +const ED25519_CURVE_NAME: &str = "Ed25519"; const ED25519_PUBLIC_KEY_MULTICODEC_PREFIX: &[u8; 2] = b"\xed\x01"; const ED25519_PRIVATE_KEY_MULTICODEC_PREFIX: &[u8; 2] = b"\x80\x26"; const ED25519_LIBP2P_PEER_ID_PREFIX: &[u8; 4] = b"\x08\x01\x12\x20"; /// NodeId is the public_ed25519_key_bytes of the node -#[derive(Clone, Eq, PartialEq, Copy)] +/// See [`NodeKey`] for a structure that also contains the private key. +#[derive(Clone, Eq, PartialEq, Copy, Serialize, Deserialize)] pub struct NodeId { public_ed25519_key_bytes: [u8; 32], } @@ -27,6 +31,14 @@ impl std::fmt::Debug for NodeId { } impl NodeId { + fn public_multibase(&self) -> String { + let public_with_prefix = [ + ED25519_PUBLIC_KEY_MULTICODEC_PREFIX, + self.public_ed25519_key_bytes.as_ref(), + ] + .concat(); + multibase::encode(multibase::Base::Base58Btc, public_with_prefix) + } /// public_ed25519_key_bytes as a CID pub fn cid(&self) -> Cid { let hash = Multihash::<64>::wrap(0, self.public_ed25519_key_bytes.as_slice()) @@ -42,13 +54,28 @@ impl NodeId { } /// public_ed25519_key_bytes as a did:key pub fn did_key(&self) -> String { - let public_with_prefix = [ - ED25519_PUBLIC_KEY_MULTICODEC_PREFIX, - self.public_ed25519_key_bytes.as_ref(), - ] - .concat(); - let public_multibase = multibase::encode(multibase::Base::Base58Btc, public_with_prefix); - format!("did:key:{}", public_multibase) + format!("did:key:{}", self.public_multibase()) + } + /// public_ed25519_key_bytes as a did:key document + pub fn did(&self) -> DidDocument { + DidDocument { + context: ssi::did::Contexts::One(ssi::did::Context::URI( + ssi::did::DEFAULT_CONTEXT.to_owned().into(), + )), + id: self.did_key(), + also_known_as: None, + controller: None, + verification_method: None, + authentication: None, + assertion_method: None, + key_agreement: None, + capability_invocation: None, + capability_delegation: None, + service: None, + proof: None, + property_set: None, + public_key: None, + } } /// public_ed25519_key_bytes as a PeerID pub fn peer_id(&self) -> PeerId { @@ -63,6 +90,16 @@ impl NodeId { PeerId::from_multihash(libp2p_key_multihash) .expect("self.public_ed25519_key_bytes to be well formed") } + /// json web key from this id. + pub fn jwk(&self) -> JWK { + let mut jwk = JWK::from(Params::OKP(OctetParams { + curve: ED25519_CURVE_NAME.to_string(), + public_key: Base64urlUInt(self.public_ed25519_key_bytes.to_vec()), + private_key: None, + })); + jwk.key_id = Some(self.public_multibase()); + jwk + } /// public_ed25519_key_bytes from a Cid pub fn try_from_cid(cid: Cid) -> Result { let mh = cid.hash(); @@ -93,8 +130,82 @@ impl NodeId { public_ed25519_key_bytes, }) } - /// Read an Ed25519 key from a directory and return a NodeID with a key pair - pub fn try_from_dir(key_dir: PathBuf) -> Result<(Self, Ed25519KeyPair)> { + /// public_ed25519_key_bytes from a PeerId + pub fn try_from_peer_id(peer_id: &PeerId) -> Result { + let peer_id_mh = peer_id.as_ref(); + if peer_id_mh.code() != 0x00 { + return Err(anyhow!("peer ID multihash is not identity")); + } + if peer_id_mh.size() != 36 { + return Err(anyhow!("peer ID multihash is not 36 bytes")); + } + let libp2p_key = peer_id_mh.digest(); + let ed25519_public_key = libp2p_key + .strip_prefix(ED25519_LIBP2P_PEER_ID_PREFIX) + .context( + "libp2p peer ID must be 0x08011220 followed by 32 bytes of ed25519 public key", + )?; + let public_ed25519_key_bytes: [u8; 32] = ed25519_public_key.try_into()?; + Ok(Self { + public_ed25519_key_bytes, + }) + } +} + +impl Display for NodeId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.did_key()) + } +} + +/// A [`NodeId`] with its private key. +#[derive(Debug)] +pub struct NodeKey { + id: NodeId, + // It would be preferable to not store the private_key_bytes directly and instead use only the + // key_pair. However to use JWK we need to keep the private_key_bytes around. + // Maybe in future versions of ssi_jwk we can change this. + private_key_bytes: [u8; 32], + key_pair: Ed25519KeyPair, + did: DidDocument, +} + +impl PartialEq for NodeKey { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.private_key_bytes == other.private_key_bytes + } +} +impl Eq for NodeKey {} + +impl NodeKey { + /// Construct a new key with both private and public keys. + fn new(id: NodeId, private_key_bytes: [u8; 32], key_pair: Ed25519KeyPair) -> Self { + Self { + id, + private_key_bytes, + key_pair, + did: id.did(), + } + } + + /// Construct a [`JWK`] with both the private and public keys. + pub fn jwk(&self) -> JWK { + let mut jwk = JWK::from(Params::OKP(OctetParams { + curve: ED25519_CURVE_NAME.to_string(), + public_key: Base64urlUInt(self.id.public_ed25519_key_bytes.to_vec()), + private_key: Some(Base64urlUInt(self.private_key_bytes.to_vec())), + })); + jwk.key_id = Some(self.public_multibase()); + jwk + } + + /// Report the [`NodeId`] of this key. + pub fn id(&self) -> NodeId { + self.id + } + + /// Read an Ed25519 key from a directory + pub fn try_from_dir(key_dir: PathBuf) -> Result { let key_path = key_dir.join("id_ed25519_0"); let content = fs::read_to_string(key_path)?; let seed = ssh_key::private::PrivateKey::from_str(&content) @@ -107,10 +218,11 @@ impl NodeId { let key_pair = Ed25519KeyPair::from_seed_unchecked(seed.as_ref()) .map_err(|e| anyhow::anyhow!("failed to create key pair: {}", e))?; let public_ed25519_key_bytes = key_pair.public_key().as_ref().try_into()?; - Ok(( - Self { + Ok(Self::new( + NodeId { public_ed25519_key_bytes, }, + seed, key_pair, )) } @@ -121,7 +233,7 @@ impl NodeId { /// /// - Multibase of unchecked Secret (i.e. not matched against public key) /// (e.g. z3u2WLX8jeyN6sfbDowLGudoZHudxgVkNJfrw2TDTVx4tijd) - pub fn try_from_secret(secret: &str) -> Result<(Self, Ed25519KeyPair)> { + pub fn try_from_secret(secret: &str) -> Result { let mut parts = secret.split(':'); let secret = parts.next().expect("split should never give zero parts"); let secret_with_prefix: [u8; 34] = multibase::decode(secret) @@ -164,41 +276,15 @@ impl NodeId { } }; let public_ed25519_key_bytes = key_pair.public_key().as_ref().try_into()?; - Ok(( - Self { - public_ed25519_key_bytes, - }, - key_pair, - )) - } - /// public_ed25519_key_bytes from a PeerId - pub fn try_from_peer_id(peer_id: &PeerId) -> Result { - let peer_id_mh = peer_id.as_ref(); - if peer_id_mh.code() != 0x00 { - return Err(anyhow!("peer ID multihash is not identity")); - } - if peer_id_mh.size() != 36 { - return Err(anyhow!("peer ID multihash is not 36 bytes")); - } - let libp2p_key = peer_id_mh.digest(); - let ed25519_public_key = libp2p_key - .strip_prefix(ED25519_LIBP2P_PEER_ID_PREFIX) - .context( - "libp2p peer ID must be 0x08011220 followed by 32 bytes of ed25519 public key", - )?; - let public_ed25519_key_bytes: [u8; 32] = ed25519_public_key.try_into()?; - Ok(Self { + let id = NodeId { public_ed25519_key_bytes, - }) + }; + Ok(NodeKey::new(id, secret, key_pair)) } /// Create a NodeId using a random Ed25519 key pair /// - /// Returns (NodeId, secret:public_key) - /// e.g. secret:public_key - /// z3u2WLX8jeyN6sfbDowLGudoZHudxgVkNJfrw2TDTVx4tijd:z6MkueF19qChpGQJBJXcXjfoM1MYCwC167RMwUiNWXXvEm1M - /// In this example, the DID will be did:key:z6MkueF19qChpGQJBJXcXjfoM1MYCwC167RMwUiNWXXvEm1M. - pub fn random() -> (Self, String) { - // Generate random secret key and corresponding keypair + pub fn random() -> NodeKey { + // Generate random secret key and corresponding key_pair let random_secret = rand::thread_rng().gen::<[u8; 32]>(); let key_pair = Ed25519KeyPair::from_seed_unchecked(random_secret.as_ref()) .expect("expect 32 bytes to be well-formed"); @@ -208,32 +294,45 @@ impl NodeId { .as_ref() .try_into() .expect("expect public key to be 32 bytes"); - let public_key_with_prefix = [ - ED25519_PUBLIC_KEY_MULTICODEC_PREFIX, - public_ed25519_key_bytes.as_ref(), - ] - .concat(); - let public_key_multibase = - multibase::encode(multibase::Base::Base58Btc, public_key_with_prefix); - let private_key_with_prefix = [ - ED25519_PRIVATE_KEY_MULTICODEC_PREFIX, - random_secret.as_ref(), - ] - .concat(); - let private_key_multibase = - multibase::encode(multibase::Base::Base58Btc, private_key_with_prefix); - ( - Self { + NodeKey::new( + NodeId { public_ed25519_key_bytes, }, - format!("{}:{}", private_key_multibase, public_key_multibase), + random_secret, + key_pair, ) } + /// Sign data with this key + pub fn sign(&self, data: &[u8]) -> Signature { + self.key_pair.sign(data) + } } -impl Display for NodeId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.did_key()) +impl std::ops::Deref for NodeKey { + type Target = NodeId; + + fn deref(&self) -> &Self::Target { + &self.id + } +} + +impl Signer for NodeKey { + fn algorithm(&self) -> ssi::jwk::Algorithm { + Algorithm::EdDSA + } + + fn id(&self) -> &ssi::did::Document { + &self.did + } + + fn sign_bytes(&self, bytes: &[u8]) -> anyhow::Result> { + let jwk = self.jwk(); + Ok(ssi::jws::sign_bytes(self.algorithm(), bytes, &jwk)?) + } + + fn sign_jws(&self, payload: &str) -> anyhow::Result { + let jwk = self.jwk(); + Ok(ssi::jws::encode_sign(self.algorithm(), payload, &jwk)?) } } @@ -245,12 +344,12 @@ mod tests { #[test] fn test_ed25519_key_pair_from_secret() { let secret = "z3u2WLX8jeyN6sfbDowLGudoZHudxgVkNJfrw2TDTVx4tijd"; - let (node_id_1, _) = NodeId::try_from_secret(secret).unwrap(); + let node_key_1 = NodeKey::try_from_secret(secret).unwrap(); let secret_and_public = "z3u2WLX8jeyN6sfbDowLGudoZHudxgVkNJfrw2TDTVx4tijd:z6MkueF19qChpGQJBJXcXjfoM1MYCwC167RMwUiNWXXvEm1M"; - let (node_id_2, _) = NodeId::try_from_secret(secret_and_public).unwrap(); - assert_eq!(node_id_1, node_id_2); + let node_key_2 = NodeKey::try_from_secret(secret_and_public).unwrap(); + assert_eq!(node_key_1, node_key_2); expect![["did:key:z6MkueF19qChpGQJBJXcXjfoM1MYCwC167RMwUiNWXXvEm1M"]] - .assert_eq(&node_id_1.did_key()); + .assert_eq(&node_key_1.did_key()); } #[test] diff --git a/core/src/peer.rs b/core/src/peer.rs new file mode 100644 index 00000000..6cf63fd9 --- /dev/null +++ b/core/src/peer.rs @@ -0,0 +1,207 @@ +use anyhow::{anyhow, bail}; +use multiaddr::{Multiaddr, PeerId}; +use serde::{Deserialize, Serialize}; +use ssi::jws::DecodedJWS; + +use crate::{node_id::NodeKey, signer::Signer, DeserializeExt as _, NodeId, SerializeExt as _}; + +/// Peer entry that is signed and can be shared. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct PeerEntry { + id: NodeId, + // Number of seconds after UNIX epoch when this entry is no longer valid. + expiration: u32, + addresses: Vec, +} + +impl PeerEntry { + /// Construct an entry about a peer with address that is no longer valid after expiration seconds after the + /// UNIX epoch. + pub fn new(local_id: NodeId, expiration: u32, addresses: Vec) -> Self { + let peer_id = local_id.peer_id(); + Self { + id: local_id, + expiration, + addresses: addresses + .into_iter() + .map(|addr| ensure_multiaddr_has_p2p(addr, peer_id)) + .collect(), + } + } + + fn from_jws(jws: &str) -> anyhow::Result { + let (header_b64, payload_enc, signature_b64) = ssi::jws::split_jws(jws)?; + let DecodedJWS { + header, + signing_input, + payload, + signature, + } = ssi::jws::decode_jws_parts(header_b64, payload_enc.as_bytes(), signature_b64)?; + let mut entry = PeerEntry::from_json(&payload)?; + let peer_id = entry.id.peer_id(); + entry.addresses = entry + .addresses + .into_iter() + .map(|addr| ensure_multiaddr_has_p2p(addr, peer_id)) + .collect(); + let key = entry.id.jwk(); + ssi::jws::verify_bytes(header.algorithm, &signing_input, &key, &signature)?; + Ok(entry) + } + fn to_jws(&self, signer: impl Signer) -> anyhow::Result { + let entry = self.to_json()?; + signer.sign_jws(&entry) + } + + /// Report the id of this peer. + pub fn id(&self) -> NodeId { + self.id + } + + /// Report the number of seconds after the UNIX epoch when this entry is no longer valid. + pub fn expiration(&self) -> u32 { + self.expiration + } + + /// Report the addresses where this peer can be dialed. These are guaranteed to contain the + /// peer id within the address. + pub fn addresses(&self) -> &[Multiaddr] { + &self.addresses + } +} + +fn ensure_multiaddr_has_p2p(addr: Multiaddr, peer_id: PeerId) -> Multiaddr { + if !addr.iter().any(|protocol| match protocol { + multiaddr::Protocol::P2p(id) => id == peer_id, + _ => false, + }) { + addr.with(multiaddr::Protocol::P2p(peer_id)) + } else { + addr + } +} + +/// Encoded [`PeerEntry`] prefixed with its expiration. +/// The sort order matters as its used in a Recon ring. +/// The key is valid utf-8 of the form `.`; +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct PeerKey(String); + +impl PeerKey { + /// Construct a signed key from a [`PeerEntry`]. + pub fn from_entry(entry: &PeerEntry, node_key: &NodeKey) -> anyhow::Result { + if entry.id() != node_key.id() { + bail!("peer key must be signed by its own ID") + } + Ok(Self(format!( + // 11 digits of a timestamp gets us 1000+ years of padding for a consistent sort order. + "{:0>11}.{}", + entry.expiration, + entry.to_jws(node_key)? + ))) + } + /// Decode and verify key as a [`PeerEntry`]. + pub fn to_entry(&self) -> anyhow::Result { + let (expiration, jws) = self + .0 + .split_once('.') + .ok_or_else(|| anyhow!("peer key must contain a '.'"))?; + let expiration: u32 = expiration.parse()?; + let peer = PeerEntry::from_jws(jws)?; + if expiration != peer.expiration { + Err(anyhow!( + "peer key expiration must match peer entry: {expiration} != {}", + peer.expiration + )) + } else { + Ok(peer) + } + } +} + +#[cfg(test)] +mod tests { + + use super::{PeerEntry, PeerKey}; + + use anyhow::Result; + use expect_test::expect; + use test_log::test; + use tracing::debug; + + use crate::node_id::NodeKey; + + #[test] + fn peer_roundtrip() -> Result<()> { + let node_key = NodeKey::random(); + let entry = PeerEntry::new( + node_key.id(), + 1732211100, + ["/ip4/127.0.0.1/tcp/5100", "/ip4/127.0.0.2/udp/5100/quic-v1"] + .into_iter() + .map(std::str::FromStr::from_str) + .collect::>()?, + ); + debug!(?entry, "peer entry"); + let key = PeerKey::from_entry(&entry, &node_key)?; + debug!(?key, "peer key"); + assert_eq!(entry, key.to_entry()?); + Ok(()) + } + #[test] + fn peer_entry_p2p_multiaddrs() -> Result<()> { + let node_key = + NodeKey::try_from_secret("z3u2WLX8jeyN6sfbDowLGudoZHudxgVkNJfrw2TDTVx4tijd")?; + let entry = PeerEntry::new( + node_key.id(), + 1732211100, + ["/ip4/127.0.0.1/tcp/5100", "/ip4/127.0.0.2/udp/5100/quic-v1"] + .into_iter() + .map(std::str::FromStr::from_str) + .collect::>()?, + ); + expect![[r#" + PeerEntry { + id: did:key:z6MkueF19qChpGQJBJXcXjfoM1MYCwC167RMwUiNWXXvEm1M, + expiration: 1732211100, + addresses: [ + /ip4/127.0.0.1/tcp/5100/p2p/12D3KooWR1M8JiXyfdBKUhCLUmTJGhtNsgxnhvFVD4AU4EioDUwu, + /ip4/127.0.0.2/udp/5100/quic-v1/p2p/12D3KooWR1M8JiXyfdBKUhCLUmTJGhtNsgxnhvFVD4AU4EioDUwu, + ], + } + "#]] + .assert_debug_eq(&entry); + let key = PeerKey::from_entry(&entry, &node_key)?; + expect![[r#" + PeerKey( + "01732211100.eyJhbGciOiJFZERTQSIsImtpZCI6Ino2TWt1ZUYxOXFDaHBHUUpCSlhjWGpmb00xTVlDd0MxNjdSTXdVaU5XWFh2RW0xTSJ9.eyJpZCI6eyJwdWJsaWNfZWQyNTUxOV9rZXlfYnl0ZXMiOlsyMjUsMTc1LDEzMSwxODYsNDIsNTIsMTg2LDEyMiw0OCwxMzEsOTIsNTIsMTI3LDE4MywyMjYsMTcsMiw2MCwxMDgsMTY2LDEwMCw0NCwyMTksMzIsMTgsMjMwLDI0Miw2NywxNTQsMTg0LDE1NCw5Ml19LCJleHBpcmF0aW9uIjoxNzMyMjExMTAwLCJhZGRyZXNzZXMiOlsiL2lwNC8xMjcuMC4wLjEvdGNwLzUxMDAvcDJwLzEyRDNLb29XUjFNOEppWHlmZEJLVWhDTFVtVEpHaHROc2d4bmh2RlZENEFVNEVpb0RVd3UiLCIvaXA0LzEyNy4wLjAuMi91ZHAvNTEwMC9xdWljLXYxL3AycC8xMkQzS29vV1IxTThKaVh5ZmRCS1VoQ0xVbVRKR2h0TnNneG5odkZWRDRBVTRFaW9EVXd1Il19.X1LOJlSQSMAMyYhO8OhpjKJ-Q2SqoTuw6Ak-O6ZZN6oEl1XNLsuf2smq5CotYZPTKhRqPazwBEZzm5K3SEz1Cw", + ) + "#]].assert_debug_eq(&key); + Ok(()) + } + #[test] + fn peer_jws_verify() -> Result<()> { + let n1 = NodeKey::random(); + let n2 = NodeKey::random(); + let entry = PeerEntry::new( + n1.id(), + 1732211100, + ["/ip4/127.0.0.1/tcp/5100", "/ip4/127.0.0.2/udp/5100/quic-v1"] + .into_iter() + .map(std::str::FromStr::from_str) + .collect::>()?, + ); + let jws = entry.to_jws(&n2)?; + expect![[r#" + Err( + JWK( + CryptoErr( + signature::Error { source: Some(Verification equation was not satisfied) }, + ), + ), + ) + "#]] + .assert_debug_eq(&PeerEntry::from_jws(&jws)); + Ok(()) + } +} diff --git a/core/src/serialize_ext.rs b/core/src/serialize_ext.rs index 1191c3d9..5cd28285 100644 --- a/core/src/serialize_ext.rs +++ b/core/src/serialize_ext.rs @@ -2,7 +2,8 @@ use cid::Cid; use serde::Serialize; use std::collections::TryReserveError; -/// SerializeExt is a trait for serialization, deserialization, CID calculation assuming dag-cbor. +/// SerializeExt extends [`Serialize`] with methods specific to dag-json and dag-cbor +/// serialization. pub trait SerializeExt: Serialize { /// Serialize to dag-json string fn to_json(&self) -> Result { diff --git a/core/src/signer.rs b/core/src/signer.rs new file mode 100644 index 00000000..c37a3a74 --- /dev/null +++ b/core/src/signer.rs @@ -0,0 +1,31 @@ +//! Define a trait [`Signer`] that provides a synchronous API for signing data. +use ssi::jwk::Algorithm; + +use crate::DidDocument; + +/// Sign bytes for an id and algorithm +pub trait Signer { + /// Algorithm used by signer + fn algorithm(&self) -> Algorithm; + /// Id of signer + fn id(&self) -> &DidDocument; + /// Sign bytes + fn sign_bytes(&self, bytes: &[u8]) -> anyhow::Result>; + /// Sign payload returning compact JWS string + fn sign_jws(&self, payload: &str) -> anyhow::Result; +} + +impl<'a, S: Signer + Sync> Signer for &'a S { + fn algorithm(&self) -> Algorithm { + (*self).algorithm() + } + fn id(&self) -> &DidDocument { + (*self).id() + } + fn sign_bytes(&self, bytes: &[u8]) -> anyhow::Result> { + (*self).sign_bytes(bytes) + } + fn sign_jws(&self, payload: &str) -> anyhow::Result { + (*self).sign_jws(payload) + } +} diff --git a/event-svc/src/tests/event.rs b/event-svc/src/tests/event.rs index 25e43ad5..889e5faa 100644 --- a/event-svc/src/tests/event.rs +++ b/event-svc/src/tests/event.rs @@ -4,7 +4,7 @@ use crate::EventService; use anyhow::Error; use bytes::Bytes; use ceramic_api::{ApiItem, EventService as ApiEventService}; -use ceramic_core::NodeId; +use ceramic_core::NodeKey; use ceramic_pipeline::{ConclusionEvent, ConclusionFeed as _}; use ceramic_sql::sqlite::SqlitePool; use cid::{Cid, CidGeneric}; @@ -66,10 +66,10 @@ where let init_cid = one.key.cid().unwrap(); let min_id = event_id_min(&init_cid, &model); let max_id = event_id_max(&init_cid, &model); - recon::Store::insert_many(&store, &[one.clone()], NodeId::random().0) + recon::Store::insert_many(&store, &[one.clone()], NodeKey::random().id()) .await .unwrap(); - recon::Store::insert_many(&store, &[two.clone()], NodeId::random().0) + recon::Store::insert_many(&store, &[two.clone()], NodeKey::random().id()) .await .unwrap(); let values: Vec<(EventId, Vec)> = @@ -105,16 +105,20 @@ where let item = &[ReconItem::new(id, car)]; // first insert reports its a new key - assert!(recon::Store::insert_many(&store, item, NodeId::random().0) - .await - .unwrap() - .included_new_key()); + assert!( + recon::Store::insert_many(&store, item, NodeKey::random().id()) + .await + .unwrap() + .included_new_key() + ); // second insert of same key reports it already existed - assert!(!recon::Store::insert_many(&store, item, NodeId::random().0) - .await - .unwrap() - .included_new_key()); + assert!( + !recon::Store::insert_many(&store, item, NodeKey::random().id()) + .await + .unwrap() + .included_new_key() + ); } test_with_dbs!( @@ -141,7 +145,7 @@ where let actual = recon::Store::insert_many( &store, &[ReconItem::new(id.clone(), car1)], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); @@ -150,7 +154,7 @@ where let res = recon::Store::insert_many( &store, &[ReconItem::new(id.clone(), car2)], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); @@ -196,7 +200,7 @@ where recon::Store::insert_many( &store, &[ReconItem::new(key.clone(), store_value)], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); @@ -230,7 +234,7 @@ where recon::Store::insert_many( &store, &[ReconItem::new(key.clone(), store_value)], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); @@ -265,7 +269,10 @@ async fn prep_highwater_tests(store: &dyn ApiEventService) -> (Cid, Cid, Cid) { keys[1].key.cid().unwrap(), keys[2].key.cid().unwrap(), ); - store.insert_many(keys, NodeId::random().0).await.unwrap(); + store + .insert_many(keys, NodeKey::random().id()) + .await + .unwrap(); res } @@ -463,7 +470,7 @@ where } = build_event().await; let item = ApiItem::new(key, store_value); store - .insert_many(vec![item.clone()], NodeId::random().0) + .insert_many(vec![item.clone()], NodeKey::random().id()) .await .unwrap(); @@ -493,7 +500,7 @@ where let item = ApiItem::new(key, store_value); store - .insert_many(vec![item.clone()], NodeId::random().0) + .insert_many(vec![item.clone()], NodeKey::random().id()) .await .unwrap(); @@ -621,7 +628,7 @@ async fn test_conclusion_events_since() -> Result<(), Box ) }) .collect(), - NodeId::random().0, + NodeKey::random().id(), ) .await?; diff --git a/event-svc/src/tests/ordering.rs b/event-svc/src/tests/ordering.rs index 6581dbec..43921ddb 100644 --- a/event-svc/src/tests/ordering.rs +++ b/event-svc/src/tests/ordering.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, sync::Arc}; use ceramic_api::{ApiItem, EventDataResult, EventService as ApiEventService, IncludeEventData}; -use ceramic_core::{EventId, NodeId}; +use ceramic_core::{EventId, NodeKey}; use rand::seq::SliceRandom; use rand::thread_rng; use recon::ReconItem; @@ -20,7 +20,7 @@ async fn setup_service() -> EventService { async fn add_and_assert_new_recon_event(store: &EventService, item: ReconItem) { tracing::trace!("inserted event: {}", item.key.cid().unwrap()); - let new = recon::Store::insert_many(store, &[item], NodeId::random().0) + let new = recon::Store::insert_many(store, &[item], NodeKey::random().id()) .await .unwrap(); assert!(new.included_new_key()); @@ -31,7 +31,7 @@ async fn add_and_assert_new_recon_event_not_inserted_yet( item: ReconItem, ) { tracing::trace!("inserted event: {}", item.key.cid().unwrap()); - let new = recon::Store::insert_many(store, &[item], NodeId::random().0) + let new = recon::Store::insert_many(store, &[item], NodeKey::random().id()) .await .unwrap(); assert!(!new.included_new_key()); @@ -40,7 +40,7 @@ async fn add_and_assert_new_recon_event_not_inserted_yet( // insert a recon event without checking whether its persisted (could be pending or stored) async fn add_new_recon_event(store: &EventService, item: ReconItem) { tracing::trace!("inserted event: {}", item.key.cid().unwrap()); - let new = recon::Store::insert_many(store, &[item], NodeId::random().0) + let new = recon::Store::insert_many(store, &[item], NodeKey::random().id()) .await .unwrap(); // TODO @@ -50,7 +50,7 @@ async fn add_new_recon_event(store: &EventService, item: ReconItem) { } async fn add_and_assert_new_local_event(store: &EventService, item: ApiItem) { - let new = ceramic_api::EventService::insert_many(store, vec![item], NodeId::random().0) + let new = ceramic_api::EventService::insert_many(store, vec![item], NodeKey::random().id()) .await .unwrap(); let new = new.iter().filter(|e| e.success()).count(); @@ -85,7 +85,7 @@ async fn test_missing_prev_history_required_not_inserted() { let data = &events[1]; let data = ApiItem::new_arced(data.key.clone(), data.value.clone()); - let new = ceramic_api::EventService::insert_many(&store, vec![data], NodeId::random().0) + let new = ceramic_api::EventService::insert_many(&store, vec![data], NodeKey::random().id()) .await .unwrap(); assert_eq!(0, new.iter().filter(|e| e.success()).count()); @@ -142,7 +142,7 @@ async fn test_prev_in_same_write_history_required() { ApiItem::new_arced(init.key.to_owned(), init.value.clone()), ApiItem::new_arced(data.key.to_owned(), data.value.clone()), ], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); diff --git a/event/src/unvalidated/signed/mod.rs b/event/src/unvalidated/signed/mod.rs index 556c3fd9..58df0dbb 100644 --- a/event/src/unvalidated/signed/mod.rs +++ b/event/src/unvalidated/signed/mod.rs @@ -9,10 +9,10 @@ use cid::Cid; use ipld_core::ipld::Ipld; use multihash_codetable::{Code, MultihashDigest}; use serde::{Deserialize, Serialize}; -use ssi::jwk::Algorithm; use ceramic_car::sync::{CarHeader, CarWriter}; -use ceramic_core::{DidDocument, Jwk, SerializeExt}; +use ceramic_core::{signer::Signer, DidDocument, Jwk, SerializeExt}; +use ssi::jwk::Algorithm; use crate::{bytes::Bytes, unvalidated::Payload}; @@ -94,7 +94,7 @@ impl Event { let header_bytes = serde_json::to_vec(&header)?; let header_str = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&header_bytes); let signing_input = format!("{}.{}", header_str, payload_cid_str); - let signed = signer.sign(signing_input.as_bytes())?; + let signed = signer.sign_bytes(signing_input.as_bytes())?; let envelope = Envelope { payload: payload_cid.to_bytes().into(), @@ -261,28 +261,6 @@ struct Protected { cap: Option, } -/// Sign bytes for an id and algorithm -pub trait Signer { - /// Algorithm used by signer - fn algorithm(&self) -> Algorithm; - /// Id of signer - fn id(&self) -> &DidDocument; - /// Sign bytes - fn sign(&self, bytes: &[u8]) -> anyhow::Result>; -} - -impl<'a, S: Signer + Sync> Signer for &'a S { - fn algorithm(&self) -> Algorithm { - (*self).algorithm() - } - fn id(&self) -> &DidDocument { - (*self).id() - } - fn sign(&self, bytes: &[u8]) -> anyhow::Result> { - (*self).sign(bytes) - } -} - /// Did and jwk based signer #[derive(Clone, Debug)] pub struct JwkSigner { @@ -311,7 +289,10 @@ impl Signer for JwkSigner { &self.did } - fn sign(&self, bytes: &[u8]) -> anyhow::Result> { + fn sign_bytes(&self, bytes: &[u8]) -> anyhow::Result> { Ok(ssi::jws::sign_bytes(self.algorithm(), bytes, &self.jwk)?) } + fn sign_jws(&self, payload: &str) -> anyhow::Result { + Ok(ssi::jws::encode_sign(self.algorithm(), payload, &self.jwk)?) + } } diff --git a/interest-svc/src/tests/interest.rs b/interest-svc/src/tests/interest.rs index f0ba73b9..ecad7952 100644 --- a/interest-svc/src/tests/interest.rs +++ b/interest-svc/src/tests/interest.rs @@ -3,7 +3,7 @@ use std::{collections::BTreeSet, str::FromStr}; use ceramic_api::InterestService; use ceramic_core::{ interest::{Builder, WithPeerId}, - Interest, NodeId, PeerId, + Interest, NodeKey, PeerId, }; use expect_test::expect; use rand::{thread_rng, Rng}; @@ -116,7 +116,7 @@ where random_interest(Some((&[0], &[1])), Some(42)), vec![], )], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); @@ -127,7 +127,7 @@ where random_interest(Some((&[0], &[1])), Some(24)), vec![], )], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); @@ -155,14 +155,14 @@ where recon::Store::insert_many( &store, &[ReconItem::new(interest_0.clone(), Vec::new())], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); recon::Store::insert_many( &store, &[ReconItem::new(interest_1.clone(), Vec::new())], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); @@ -194,14 +194,14 @@ where store .insert_many( &[ReconItem::new(interest_0.clone(), Vec::new())], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); store .insert_many( &[ReconItem::new(interest_1.clone(), Vec::new())], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); @@ -235,7 +235,7 @@ where assert!(&recon::Store::insert_many( &store, &[ReconItem::new(interest.clone(), Vec::new())], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap() @@ -245,7 +245,7 @@ where assert!(!recon::Store::insert_many( &store, &[ReconItem::new(interest.clone(), Vec::new())], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap() @@ -266,7 +266,7 @@ where recon::Store::insert_many( &store, &[ReconItem::new(key.clone(), Vec::new())], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); diff --git a/one/src/daemon.rs b/one/src/daemon.rs index 311ef2bc..b4de4c8c 100644 --- a/one/src/daemon.rs +++ b/one/src/daemon.rs @@ -7,7 +7,7 @@ use crate::{ use anyhow::{anyhow, bail, Context, Result}; use ceramic_anchor_remote::RemoteCas; use ceramic_anchor_service::AnchorService; -use ceramic_core::NodeId; +use ceramic_core::NodeKey; use ceramic_event_svc::eth_rpc::HttpEthRpc; use ceramic_event_svc::{ChainInclusionProvider, EventService}; use ceramic_interest_svc::InterestService; @@ -431,8 +431,9 @@ pub async fn run(opts: DaemonOpts) -> Result<()> { // Load node ID from key directory. Libp2p has their own wrapper around ed25519 keys (╯°□°)╯︵ ┻━┻ // So, we need to load the key from the key directory for libp2p to use, and then again for evaluating the Node ID // using a generic ed25519 processing library (ring). We'll assert that the keys are the same. - let (node_id, keypair) = NodeId::try_from_dir(opts.p2p_key_dir.clone())?; - assert_eq!(node_id.peer_id(), peer_id); + let node_key = NodeKey::try_from_dir(opts.p2p_key_dir.clone())?; + assert_eq!(node_key.peer_id(), peer_id); + let node_id = node_key.id(); // Register metrics for all components let recon_metrics = MetricsHandle::register(recon::Metrics::register); @@ -576,14 +577,13 @@ pub async fn run(opts: DaemonOpts) -> Result<()> { let anchor_service_handle = if let Some(remote_anchor_service_url) = opts.remote_anchor_service_url { info!( - node_did = node_id.did_key(), + node_did = node_key.did_key(), url = remote_anchor_service_url, poll_interval = opts.anchor_poll_interval, "starting remote cas anchor service" ); let remote_cas = RemoteCas::new( - node_id, - keypair, + node_key, remote_anchor_service_url, Duration::from_secs(opts.anchor_poll_interval), opts.anchor_poll_retry_count, @@ -610,7 +610,6 @@ pub async fn run(opts: DaemonOpts) -> Result<()> { }; // Build HTTP server - let (node_id, _) = NodeId::try_from_dir(opts.p2p_key_dir.clone())?; let mut ceramic_server = ceramic_api::Server::new( node_id, network, diff --git a/recon/src/recon/tests.rs b/recon/src/recon/tests.rs index 0cdb43a0..998af723 100644 --- a/recon/src/recon/tests.rs +++ b/recon/src/recon/tests.rs @@ -19,7 +19,7 @@ use std::{collections::BTreeSet, sync::Arc}; use anyhow::Result; use async_trait::async_trait; -use ceramic_core::{NodeId, RangeOpen}; +use ceramic_core::{NodeKey, RangeOpen}; use futures::{ready, Future, Sink, Stream}; use pin_project::pin_project; use prometheus_client::registry::Registry; @@ -515,7 +515,7 @@ async fn word_lists() { key.as_bytes().into(), key.to_uppercase().as_bytes().to_vec(), )], - NodeId::random().0, + NodeKey::random().id(), ) .await .unwrap(); @@ -570,12 +570,12 @@ async fn word_lists() { let local_handle = tokio::spawn(protocol::initiate_synchronize( local, local_channel, - ProtocolConfig::new(100, NodeId::random().0), + ProtocolConfig::new(100, NodeKey::random().id()), )); let remote_handle = tokio::spawn(protocol::respond_synchronize( remote, remote_channel, - ProtocolConfig::new(100, NodeId::random().0), + ProtocolConfig::new(100, NodeKey::random().id()), )); // Error if either synchronize method errors let (local, remote) = tokio::join!(local_handle, remote_handle); @@ -1141,12 +1141,12 @@ async fn recon_do_batch_size( let cat_fut = protocol::initiate_synchronize( cat.clone(), cat_channel, - ProtocolConfig::new(batch_size, NodeId::random().0), + ProtocolConfig::new(batch_size, NodeKey::random().id()), ); let dog_fut = protocol::respond_synchronize( dog.clone(), dog_channel, - ProtocolConfig::new(batch_size, NodeId::random().0), + ProtocolConfig::new(batch_size, NodeKey::random().id()), ); // Drive both synchronize futures on the same thread // This is to ensure a deterministic behavior.