From 07582fa3286950c15f7a3f015cbc1a7f11dba94b Mon Sep 17 00:00:00 2001 From: sugyan Date: Wed, 1 May 2024 14:08:39 +0900 Subject: [PATCH 01/17] Add common_web/did_doc --- Cargo.lock | 16 +++-- Cargo.toml | 3 +- atrium-libs/Cargo.toml | 23 ++++++++ atrium-libs/src/common_web.rs | 1 + atrium-libs/src/common_web/did_doc.rs | 84 +++++++++++++++++++++++++++ atrium-libs/src/lib.rs | 2 + 6 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 atrium-libs/Cargo.toml create mode 100644 atrium-libs/src/common_web.rs create mode 100644 atrium-libs/src/common_web/did_doc.rs create mode 100644 atrium-libs/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f69395b3..05bad66f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,14 @@ dependencies = [ "tokio", ] +[[package]] +name = "atrium-libs" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "atrium-xrpc" version = "0.10.5" @@ -1596,9 +1604,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] @@ -1614,9 +1622,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 18b9c0d9..938a2e03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "atrium-api", "atrium-cli", + "atrium-libs", "atrium-xrpc", "atrium-xrpc-client", ] @@ -37,7 +38,7 @@ serde_ipld_dagcbor = { version = "0.6.0", default-features = false, features = [ chrono = "0.4" langtag = "0.3" regex = "1" -serde = "1.0.160" +serde = "1.0.199" serde_bytes = "0.11.9" serde_json = "1.0.96" serde_html_form = "0.2.6" diff --git a/atrium-libs/Cargo.toml b/atrium-libs/Cargo.toml new file mode 100644 index 00000000..d8a92883 --- /dev/null +++ b/atrium-libs/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "atrium-libs" +version = "0.1.0" +authors = ["sugyan "] +edition.workspace = true +rust-version.workspace = true +description = "A collection of libraries for AT Protocol" +readme = "README.md" +repository.workspace = true +license.workspace = true +keywords.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { workspace = true, optional = true } + +[dev-dependencies] +serde_json = { workspace = true } + +[features] +default = ["common-web"] +common-web = ["serde/derive"] diff --git a/atrium-libs/src/common_web.rs b/atrium-libs/src/common_web.rs new file mode 100644 index 00000000..5c8b5586 --- /dev/null +++ b/atrium-libs/src/common_web.rs @@ -0,0 +1 @@ +pub mod did_doc; diff --git a/atrium-libs/src/common_web/did_doc.rs b/atrium-libs/src/common_web/did_doc.rs new file mode 100644 index 00000000..f0bf0687 --- /dev/null +++ b/atrium-libs/src/common_web/did_doc.rs @@ -0,0 +1,84 @@ +//! Definitions for DID document types. +//! https://atproto.com/specs/did#did-documents + +/// A DID document, containing information associated with the DID +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct DidDocument { + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "@context")] + pub context: Option>, + pub id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub also_known_as: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub verification_method: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub service: Option>, +} + +/// The public signing key for the account +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct VerificationMethod { + pub id: String, + pub r#type: String, + pub controller: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub public_key_multibase: Option, +} + +/// The PDS service network location for the account +#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Service { + pub id: String, + pub r#type: String, + pub service_endpoint: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + const DID_DOC_JSON: &str = r##"{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1","https://w3id.org/security/suites/secp256k1-2019/v1"],"id":"did:plc:4ee6oesrsbtmuln4gqsqf6fp","alsoKnownAs":["at://sugyan.com"],"verificationMethod":[{"id":"did:plc:4ee6oesrsbtmuln4gqsqf6fp#atproto","type":"Multikey","controller":"did:plc:4ee6oesrsbtmuln4gqsqf6fp","publicKeyMultibase":"zQ3shnw8ChQwGUE6gMghuvn5g7Q9YVej1MUJENqMsLmxZwRSz"}],"service":[{"id":"#atproto_pds","type":"AtprotoPersonalDataServer","serviceEndpoint":"https://puffball.us-east.host.bsky.network"}]}"##; + + fn did_doc_example() -> DidDocument { + DidDocument { + context: Some(vec![ + String::from("https://www.w3.org/ns/did/v1"), + String::from("https://w3id.org/security/multikey/v1"), + String::from("https://w3id.org/security/suites/secp256k1-2019/v1"), + ]), + id: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp"), + also_known_as: Some(vec![String::from("at://sugyan.com")]), + verification_method: Some(vec![VerificationMethod { + id: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp#atproto"), + r#type: String::from("Multikey"), + controller: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp"), + public_key_multibase: Some(String::from( + "zQ3shnw8ChQwGUE6gMghuvn5g7Q9YVej1MUJENqMsLmxZwRSz", + )), + }]), + service: Some(vec![Service { + id: String::from("#atproto_pds"), + r#type: String::from("AtprotoPersonalDataServer"), + service_endpoint: String::from("https://puffball.us-east.host.bsky.network"), + }]), + } + } + + #[test] + fn serialize_did_doc() { + let result = + serde_json::to_string(&did_doc_example()).expect("serialization should succeed"); + assert_eq!(result, DID_DOC_JSON); + } + + #[test] + fn deserialize_did_doc() { + let result = serde_json::from_str::(DID_DOC_JSON) + .expect("deserialization should succeed"); + assert_eq!(result, did_doc_example()); + } +} diff --git a/atrium-libs/src/lib.rs b/atrium-libs/src/lib.rs new file mode 100644 index 00000000..4cee5812 --- /dev/null +++ b/atrium-libs/src/lib.rs @@ -0,0 +1,2 @@ +#[cfg(feature = "common-web")] +mod common_web; From d1d114d677a44f0ee92584f507883b9bd1a753b1 Mon Sep 17 00:00:00 2001 From: sugyan Date: Thu, 2 May 2024 15:41:55 +0900 Subject: [PATCH 02/17] Add identity, implement web-resolver --- .github/workflows/libs.yml | 26 ++++ Cargo.lock | 17 ++- Cargo.toml | 4 +- atrium-libs/Cargo.toml | 11 +- atrium-libs/src/identity.rs | 2 + atrium-libs/src/identity/did.rs | 43 ++++++ atrium-libs/src/identity/did/did_resolver.rs | 131 +++++++++++++++++++ atrium-libs/src/identity/did/error.rs | 24 ++++ atrium-libs/src/identity/did/plc_resolver.rs | 30 +++++ atrium-libs/src/identity/did/web_resolver.rs | 52 ++++++++ atrium-libs/src/lib.rs | 2 + 11 files changed, 338 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/libs.yml create mode 100644 atrium-libs/src/identity.rs create mode 100644 atrium-libs/src/identity/did.rs create mode 100644 atrium-libs/src/identity/did/did_resolver.rs create mode 100644 atrium-libs/src/identity/did/error.rs create mode 100644 atrium-libs/src/identity/did/plc_resolver.rs create mode 100644 atrium-libs/src/identity/did/web_resolver.rs diff --git a/.github/workflows/libs.yml b/.github/workflows/libs.yml new file mode 100644 index 00000000..63c4630c --- /dev/null +++ b/.github/workflows/libs.yml @@ -0,0 +1,26 @@ +name: Libs + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: | + cargo build -p atrium-libs --verbose + cargo build -p atrium-libs --verbose --features common-web + cargo build -p atrium-libs --verbose --features identity + cargo build -p atrium-libs --verbose --all-features + - name: Run tests + run: | + cargo test -p atrium-libs --lib + cargo test -p atrium-libs --lib --all-features diff --git a/Cargo.lock b/Cargo.lock index 05bad66f..30f2c9e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,9 +118,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", @@ -168,8 +168,15 @@ dependencies = [ name = "atrium-libs" version = "0.1.0" dependencies = [ + "async-trait", + "mockito", + "reqwest", "serde", "serde_json", + "thiserror", + "tokio", + "url", + "urlencoding", ] [[package]] @@ -2014,6 +2021,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8parse" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 938a2e03..295c2f5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ atrium-xrpc-client = { version = "0.5.2", path = "atrium-xrpc-client" } # async in traits # Can be removed once MSRV is at least 1.75.0. -async-trait = "0.1.68" +async-trait = "0.1.80" # DAG-CBOR codec ipld-core = { version = "0.4.0", default-features = false, features = ["std"] } @@ -42,11 +42,13 @@ serde = "1.0.199" serde_bytes = "0.11.9" serde_json = "1.0.96" serde_html_form = "0.2.6" +urlencoding = "2.1.3" # Networking futures = { version = "0.3.30", default-features = false, features = ["alloc"] } http = "1.1.0" tokio = { version = "1.36", default-features = false } +url = "2.5.0" # HTTP client integrations isahc = "1.7.2" diff --git a/atrium-libs/Cargo.toml b/atrium-libs/Cargo.toml index d8a92883..80d3fabb 100644 --- a/atrium-libs/Cargo.toml +++ b/atrium-libs/Cargo.toml @@ -13,11 +13,20 @@ keywords.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = { workspace = true, optional = true } serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } +thiserror = { workspace = true, optional = true } +url = { workspace = true, optional = true } +urlencoding = { workspace = true, optional = true } [dev-dependencies] +mockito = { workspace = true } +reqwest = { workspace = true } serde_json = { workspace = true } +tokio = { workspace = true, features = ["macros"] } [features] -default = ["common-web"] +default = [] common-web = ["serde/derive"] +identity = ["common-web", "async-trait", "serde_json", "thiserror", "url", "urlencoding"] diff --git a/atrium-libs/src/identity.rs b/atrium-libs/src/identity.rs new file mode 100644 index 00000000..4f82e58a --- /dev/null +++ b/atrium-libs/src/identity.rs @@ -0,0 +1,2 @@ +//! A library for decentralized identities in [atproto](https://atproto.com) using DIDs and handles +pub mod did; diff --git a/atrium-libs/src/identity/did.rs b/atrium-libs/src/identity/did.rs new file mode 100644 index 00000000..ff5ca7d2 --- /dev/null +++ b/atrium-libs/src/identity/did.rs @@ -0,0 +1,43 @@ +use crate::common_web::did_doc::DidDocument; +pub mod did_resolver; +mod error; +mod plc_resolver; +mod web_resolver; + +use self::error::{Error, Result}; +use async_trait::async_trait; + +#[async_trait] +pub trait Fetcher { + async fn fetch( + url: &str, + timeout: Option, + ) -> std::result::Result>, Box>; +} + +#[async_trait] +pub trait Resolver { + async fn resolve_no_check(&self, did: &str) -> Result>>; + async fn resolve_no_cache(&self, did: &str) -> Result> { + if let Some(got) = self.resolve_no_check(did).await? { + Ok(serde_json::from_slice(&got)?) + } else { + Ok(None) + } + } + async fn resolve(&self, did: &str, force_refresh: bool) -> Result> { + // TODO: from cache + if let Some(got) = self.resolve_no_cache(did).await? { + // TODO: store in cache + Ok(Some(got)) + } else { + // TODO: clear cache + Ok(None) + } + } + async fn ensure_resolve(&self, did: &str, force_refresh: bool) -> Result { + self.resolve(did, force_refresh) + .await? + .ok_or_else(|| Error::DidNotFound(did.to_string())) + } +} diff --git a/atrium-libs/src/identity/did/did_resolver.rs b/atrium-libs/src/identity/did/did_resolver.rs new file mode 100644 index 00000000..c2ab7ab1 --- /dev/null +++ b/atrium-libs/src/identity/did/did_resolver.rs @@ -0,0 +1,131 @@ +use super::error::{Error, Result}; +use super::{plc_resolver::DidPlcResolver, web_resolver::DidWebResolver}; +use super::{Fetcher, Resolver}; +use async_trait::async_trait; + +#[derive(Debug)] +pub struct DidResolver { + pub plc: DidPlcResolver, + pub web: DidWebResolver, +} + +#[async_trait] +impl Resolver for DidResolver +where + T: Fetcher + Send + Sync, +{ + async fn resolve_no_check(&self, did: &str) -> Result>> { + let parts = did.split(':').collect::>(); + if parts.len() < 3 || parts[0] != "did" { + return Err(Error::PoorlyFormattedDid(did.to_string())); + } + match parts[1] { + "web" => self.web.resolve_no_check(did).await, + "plc" => self.plc.resolve_no_check(did).await, + _ => Err(Error::UnsupportedDidMethod(did.to_string())), + } + } +} + +impl Default for DidResolver { + fn default() -> Self { + let timeout = Some(3000); + let plc_url = String::from("https://plc.directory"); + Self { + plc: DidPlcResolver::new(plc_url, timeout), + web: DidWebResolver::new(timeout), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common_web::did_doc::{DidDocument, Service, VerificationMethod}; + use mockito::{Server, ServerGuard}; + use reqwest::{header::CONTENT_TYPE, Client}; + use std::time::Duration; + + struct ReqwestFetcher; + + #[async_trait] + impl Fetcher for ReqwestFetcher { + async fn fetch( + url: &str, + timeout: Option, + ) -> std::result::Result>, Box> + { + let mut builder = Client::builder(); + if let Some(timeout) = timeout { + builder = builder.timeout(Duration::from_millis(timeout)); + } + match builder.build()?.get(url).send().await?.error_for_status() { + Ok(response) => Ok(Some(response.bytes().await?.to_vec())), + Err(err) => { + if err + .status() + .map_or(false, |status| status.is_client_error()) + { + Ok(None) + } else { + Err(Box::new(err)) + } + } + } + } + } + + fn did_doc_example() -> DidDocument { + DidDocument { + context: Some(vec![ + String::from("https://www.w3.org/ns/did/v1"), + String::from("https://w3id.org/security/multikey/v1"), + String::from("https://w3id.org/security/suites/secp256k1-2019/v1"), + ]), + id: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp"), + also_known_as: Some(vec![String::from("at://sugyan.com")]), + verification_method: Some(vec![VerificationMethod { + id: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp#atproto"), + r#type: String::from("Multikey"), + controller: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp"), + public_key_multibase: Some(String::from( + "zQ3shnw8ChQwGUE6gMghuvn5g7Q9YVej1MUJENqMsLmxZwRSz", + )), + }]), + service: Some(vec![Service { + id: String::from("#atproto_pds"), + r#type: String::from("AtprotoPersonalDataServer"), + service_endpoint: String::from("https://puffball.us-east.host.bsky.network"), + }]), + } + } + + async fn server() -> ServerGuard { + let mut server = Server::new_async().await; + server + .mock("GET", "/.well-known/did.json") + .with_status(200) + .with_header(CONTENT_TYPE.as_str(), "application/did+ld+json") + .with_body(serde_json::to_vec(&did_doc_example()).expect("failed to serialize did_doc")) + .create(); + server + } + + #[tokio::test] + async fn resolve_valid_did_web() { + let server = server().await; + let resolver = DidResolver:: { + plc: DidPlcResolver::new("https://plc.directory".to_string(), Some(3000)), + web: DidWebResolver::new(Some(3000)), + }; + let web_did = format!( + "did:web:{}", + urlencoding::encode(&server.host_with_port()).into_owned() + ); + let result = resolver + .ensure_resolve(&web_did, false) + .await + .expect("ensure_resolve shoud succeed with a valid did:web"); + assert_eq!(result, did_doc_example()); + } +} diff --git a/atrium-libs/src/identity/did/error.rs b/atrium-libs/src/identity/did/error.rs new file mode 100644 index 00000000..ad87c6b7 --- /dev/null +++ b/atrium-libs/src/identity/did/error.rs @@ -0,0 +1,24 @@ +use std::string::FromUtf8Error; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + FromUtf8(#[from] FromUtf8Error), + #[error(transparent)] + UrlParse(#[from] url::ParseError), + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + #[error("fetch error: {0}")] + Fetch(Box), + #[error("Could not resolve DID: {0}")] + DidNotFound(String), + #[error("Poorly formatted DID: {0}")] + PoorlyFormattedDid(String), + #[error("Unsupported DID method: {0}")] + UnsupportedDidMethod(String), + #[error("Unsupported did:web paths: {0}")] + UnsupportedDidWebPath(String), +} + +pub type Result = std::result::Result; diff --git a/atrium-libs/src/identity/did/plc_resolver.rs b/atrium-libs/src/identity/did/plc_resolver.rs new file mode 100644 index 00000000..78193494 --- /dev/null +++ b/atrium-libs/src/identity/did/plc_resolver.rs @@ -0,0 +1,30 @@ +use super::error::{Error, Result}; +use super::{Fetcher, Resolver}; +use async_trait::async_trait; + +#[derive(Debug, Default)] +pub struct DidPlcResolver { + plc_url: String, + timeout: Option, + _fetcher: std::marker::PhantomData, +} + +impl DidPlcResolver { + pub fn new(plc_url: String, timeout: Option) -> Self { + Self { + plc_url, + timeout, + _fetcher: std::marker::PhantomData, + } + } +} + +#[async_trait] +impl Resolver for DidPlcResolver +where + T: Fetcher + Send + Sync, +{ + async fn resolve_no_check(&self, did: &str) -> Result>> { + unimplemented!() + } +} diff --git a/atrium-libs/src/identity/did/web_resolver.rs b/atrium-libs/src/identity/did/web_resolver.rs new file mode 100644 index 00000000..403a47a7 --- /dev/null +++ b/atrium-libs/src/identity/did/web_resolver.rs @@ -0,0 +1,52 @@ +use super::error::{Error, Result}; +use super::{Fetcher, Resolver}; +use async_trait::async_trait; +use std::marker::PhantomData; +use url::{Host, Url}; + +#[derive(Debug, Default)] +pub struct DidWebResolver { + timeout: Option, + _fetcher: PhantomData, +} + +impl DidWebResolver { + pub fn new(timeout: Option) -> Self { + Self { + timeout, + _fetcher: PhantomData, + } + } +} + +#[async_trait] +impl Resolver for DidWebResolver +where + T: Fetcher + Send + Sync, +{ + async fn resolve_no_check(&self, did: &str) -> Result>> { + let parts = did.splitn(3, ':').collect::>(); + if parts[2].is_empty() { + return Err(Error::PoorlyFormattedDid(did.to_string())); + } + if parts[2].contains(':') { + return Err(Error::UnsupportedDidWebPath(did.to_string())); + } + let mut url = Url::parse(&format!( + "https://{}/.well-known/did.json", + urlencoding::decode(parts[2])? + ))?; + if match url.host() { + Some(Host::Domain(domain)) if domain == "localhost" => true, + Some(Host::Ipv4(addr)) => addr.is_loopback(), + Some(Host::Ipv6(addr)) => addr.is_loopback(), + _ => false, + } { + url.set_scheme("http") + .expect("failed to set scheme to http"); + } + T::fetch(url.as_ref(), self.timeout) + .await + .map_err(Error::Fetch) + } +} diff --git a/atrium-libs/src/lib.rs b/atrium-libs/src/lib.rs index 4cee5812..8e7dfbf2 100644 --- a/atrium-libs/src/lib.rs +++ b/atrium-libs/src/lib.rs @@ -1,2 +1,4 @@ #[cfg(feature = "common-web")] mod common_web; +#[cfg(feature = "identity")] +mod identity; From 7761afa6506272a5ece1863f9e88e44aa21d6deb Mon Sep 17 00:00:00 2001 From: sugyan Date: Fri, 3 May 2024 22:40:04 +0900 Subject: [PATCH 03/17] Implement plc_resolver, add tests --- atrium-libs/src/identity/did.rs | 4 +- atrium-libs/src/identity/did/did_resolver.rs | 167 +++++++++++++++---- atrium-libs/src/identity/did/plc_resolver.rs | 12 +- atrium-libs/src/identity/did/web_resolver.rs | 6 +- 4 files changed, 145 insertions(+), 44 deletions(-) diff --git a/atrium-libs/src/identity/did.rs b/atrium-libs/src/identity/did.rs index ff5ca7d2..c1783ea4 100644 --- a/atrium-libs/src/identity/did.rs +++ b/atrium-libs/src/identity/did.rs @@ -8,7 +8,7 @@ use self::error::{Error, Result}; use async_trait::async_trait; #[async_trait] -pub trait Fetcher { +pub trait Fetch { async fn fetch( url: &str, timeout: Option, @@ -16,7 +16,7 @@ pub trait Fetcher { } #[async_trait] -pub trait Resolver { +pub trait Resolve { async fn resolve_no_check(&self, did: &str) -> Result>>; async fn resolve_no_cache(&self, did: &str) -> Result> { if let Some(got) = self.resolve_no_check(did).await? { diff --git a/atrium-libs/src/identity/did/did_resolver.rs b/atrium-libs/src/identity/did/did_resolver.rs index c2ab7ab1..35364dad 100644 --- a/atrium-libs/src/identity/did/did_resolver.rs +++ b/atrium-libs/src/identity/did/did_resolver.rs @@ -1,6 +1,6 @@ use super::error::{Error, Result}; use super::{plc_resolver::DidPlcResolver, web_resolver::DidWebResolver}; -use super::{Fetcher, Resolver}; +use super::{Fetch, Resolve}; use async_trait::async_trait; #[derive(Debug)] @@ -10,9 +10,9 @@ pub struct DidResolver { } #[async_trait] -impl Resolver for DidResolver +impl Resolve for DidResolver where - T: Fetcher + Send + Sync, + T: Fetch + Send + Sync, { async fn resolve_no_check(&self, did: &str) -> Result>> { let parts = did.split(':').collect::>(); @@ -41,15 +41,15 @@ impl Default for DidResolver { #[cfg(test)] mod tests { use super::*; - use crate::common_web::did_doc::{DidDocument, Service, VerificationMethod}; - use mockito::{Server, ServerGuard}; + use crate::common_web::did_doc::{DidDocument, Service}; + use mockito::{Matcher, Server, ServerGuard}; use reqwest::{header::CONTENT_TYPE, Client}; use std::time::Duration; struct ReqwestFetcher; #[async_trait] - impl Fetcher for ReqwestFetcher { + impl Fetch for ReqwestFetcher { async fn fetch( url: &str, timeout: Option, @@ -77,55 +77,152 @@ mod tests { fn did_doc_example() -> DidDocument { DidDocument { - context: Some(vec![ - String::from("https://www.w3.org/ns/did/v1"), - String::from("https://w3id.org/security/multikey/v1"), - String::from("https://w3id.org/security/suites/secp256k1-2019/v1"), - ]), - id: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp"), - also_known_as: Some(vec![String::from("at://sugyan.com")]), - verification_method: Some(vec![VerificationMethod { - id: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp#atproto"), - r#type: String::from("Multikey"), - controller: String::from("did:plc:4ee6oesrsbtmuln4gqsqf6fp"), - public_key_multibase: Some(String::from( - "zQ3shnw8ChQwGUE6gMghuvn5g7Q9YVej1MUJENqMsLmxZwRSz", - )), - }]), + context: None, + id: String::from("did:plc:234567abcdefghijklmnopqr"), + also_known_as: Some(vec![String::from("at://alice.test")]), + verification_method: None, service: Some(vec![Service { id: String::from("#atproto_pds"), r#type: String::from("AtprotoPersonalDataServer"), - service_endpoint: String::from("https://puffball.us-east.host.bsky.network"), + service_endpoint: String::from("https://service.test"), }]), } } - async fn server() -> ServerGuard { + async fn web_server() -> (ServerGuard, DidDocument) { + let mut did_doc = did_doc_example(); let mut server = Server::new_async().await; + did_doc.id = format!( + "did:web:{}", + urlencoding::encode(&server.host_with_port()).into_owned() + ); server .mock("GET", "/.well-known/did.json") .with_status(200) .with_header(CONTENT_TYPE.as_str(), "application/did+ld+json") + .with_body(serde_json::to_vec(&did_doc).expect("failed to serialize did_doc")) + .create(); + (server, did_doc) + } + + async fn plc_server() -> (ServerGuard, DidDocument) { + let did_doc = did_doc_example(); + let mut server = Server::new_async().await; + server + .mock( + "GET", + format!("/{}", urlencoding::encode(&did_doc.id)).as_str(), + ) + .with_status(200) + .with_header(CONTENT_TYPE.as_str(), "application/did+ld+json") .with_body(serde_json::to_vec(&did_doc_example()).expect("failed to serialize did_doc")) .create(); server + .mock("GET", Matcher::Regex(String::from(r"^/[^/]+$"))) + .with_status(404) + .create(); + (server, did_doc) + } + + fn resolver(plc_url: Option) -> DidResolver { + let timeout = Some(3000); + DidResolver { + plc: DidPlcResolver::new( + plc_url.unwrap_or(String::from("https://plc.directory")), + timeout, + ), + web: DidWebResolver::new(timeout), + } } #[tokio::test] - async fn resolve_valid_did_web() { - let server = server().await; - let resolver = DidResolver:: { - plc: DidPlcResolver::new("https://plc.directory".to_string(), Some(3000)), - web: DidWebResolver::new(Some(3000)), - }; - let web_did = format!( - "did:web:{}", - urlencoding::encode(&server.host_with_port()).into_owned() - ); + async fn resolve_did_web_valid() { + let (_server, did_doc) = web_server().await; + let resolver = resolver(None); let result = resolver - .ensure_resolve(&web_did, false) + .ensure_resolve(&did_doc.id, false) .await .expect("ensure_resolve shoud succeed with a valid did:web"); - assert_eq!(result, did_doc_example()); + assert_eq!(result, did_doc); + } + + #[tokio::test] + async fn resolve_did_web_malformed() { + let resolver = resolver(None); + + let err = resolver + .ensure_resolve("did:web:asdf", false) + .await + .expect_err("ensure_resolve should fail with a malformed did:web"); + assert!( + matches!(err, Error::Fetch(_)), + "error should be Fetch: {err:?}" + ); + + let err = resolver + .ensure_resolve("did:web:", false) + .await + .expect_err("ensure_resolve should fail with a malformed did:web"); + assert!( + matches!(err, Error::PoorlyFormattedDid(_)), + "error should be PoorlyFormattedDid: {err:?}" + ); + + let err = resolver + .ensure_resolve("", false) + .await + .expect_err("ensure_resolve should fail with a malformed did:web"); + assert!( + matches!(err, Error::PoorlyFormattedDid(_)), + "error should be PoorlyFormattedDid: {err:?}" + ); + } + + #[tokio::test] + async fn resolve_did_web_with_path_components() { + let resolver = resolver(None); + let err = resolver + .ensure_resolve("did:web:example.com:u:bob", false) + .await + .expect_err("ensure_resolve should fail with did:web with path components"); + assert!( + matches!(err, Error::UnsupportedDidWebPath(_)), + "error should be UnsupportedDidWebPath: {err:?}" + ); + } + + #[tokio::test] + async fn resolve_did_plc_valid() { + let (server, did_doc) = plc_server().await; + let resolver = resolver(Some(server.url())); + let result = resolver + .ensure_resolve(&did_doc.id, false) + .await + .expect("ensure_resolve shoud succeed with a valid did:plc"); + assert_eq!(result, did_doc); + } + + #[tokio::test] + async fn resolve_did_plc_malformed() { + let (server, _) = plc_server().await; + let resolver = resolver(Some(server.url())); + + let err = resolver + .ensure_resolve("did:plc:asdf", false) + .await + .expect_err("ensure_resolve should fail with a malformed did:plc"); + assert!( + matches!(err, Error::DidNotFound(_)), + "error should be DidNotFound: {err:?}" + ); + + let err = resolver + .ensure_resolve("did:plc", false) + .await + .expect_err("ensure_resolve should fail with a malformed did:plc"); + assert!( + matches!(err, Error::PoorlyFormattedDid(_)), + "error should be PoorlyFormattedDid: {err:?}" + ); } } diff --git a/atrium-libs/src/identity/did/plc_resolver.rs b/atrium-libs/src/identity/did/plc_resolver.rs index 78193494..7e9ddff4 100644 --- a/atrium-libs/src/identity/did/plc_resolver.rs +++ b/atrium-libs/src/identity/did/plc_resolver.rs @@ -1,6 +1,7 @@ use super::error::{Error, Result}; -use super::{Fetcher, Resolver}; +use super::{Fetch, Resolve}; use async_trait::async_trait; +use url::Url; #[derive(Debug, Default)] pub struct DidPlcResolver { @@ -20,11 +21,14 @@ impl DidPlcResolver { } #[async_trait] -impl Resolver for DidPlcResolver +impl Resolve for DidPlcResolver where - T: Fetcher + Send + Sync, + T: Fetch + Send + Sync, { async fn resolve_no_check(&self, did: &str) -> Result>> { - unimplemented!() + let url = Url::parse(&format!("{}/{}", self.plc_url, urlencoding::encode(did)))?; + T::fetch(url.as_ref(), self.timeout) + .await + .map_err(Error::Fetch) } } diff --git a/atrium-libs/src/identity/did/web_resolver.rs b/atrium-libs/src/identity/did/web_resolver.rs index 403a47a7..06d15723 100644 --- a/atrium-libs/src/identity/did/web_resolver.rs +++ b/atrium-libs/src/identity/did/web_resolver.rs @@ -1,5 +1,5 @@ use super::error::{Error, Result}; -use super::{Fetcher, Resolver}; +use super::{Fetch, Resolve}; use async_trait::async_trait; use std::marker::PhantomData; use url::{Host, Url}; @@ -20,9 +20,9 @@ impl DidWebResolver { } #[async_trait] -impl Resolver for DidWebResolver +impl Resolve for DidWebResolver where - T: Fetcher + Send + Sync, + T: Fetch + Send + Sync, { async fn resolve_no_check(&self, did: &str) -> Result>> { let parts = did.splitn(3, ':').collect::>(); From 8a92b2db84a88602d1a010906deac5f6133819cf Mon Sep 17 00:00:00 2001 From: sugyan Date: Thu, 9 May 2024 22:42:03 +0900 Subject: [PATCH 04/17] WIP: Add crypto --- Cargo.lock | 2 + Cargo.toml | 1 + atrium-libs/Cargo.toml | 7 +- atrium-libs/src/common_web/did_doc.rs | 69 +++++++++ atrium-libs/src/crypto.rs | 12 ++ atrium-libs/src/crypto/did.rs | 19 +++ atrium-libs/src/crypto/error.rs | 11 ++ atrium-libs/src/crypto/multibase.rs | 6 + atrium-libs/src/crypto/utils.rs | 1 + atrium-libs/src/identity/did.rs | 113 ++++++++++++++ atrium-libs/src/identity/did/atproto_data.rs | 150 +++++++++++++++++++ atrium-libs/src/identity/did/error.rs | 2 + atrium-libs/src/lib.rs | 2 + 13 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 atrium-libs/src/crypto.rs create mode 100644 atrium-libs/src/crypto/did.rs create mode 100644 atrium-libs/src/crypto/error.rs create mode 100644 atrium-libs/src/crypto/multibase.rs create mode 100644 atrium-libs/src/crypto/utils.rs create mode 100644 atrium-libs/src/identity/did/atproto_data.rs diff --git a/Cargo.lock b/Cargo.lock index 30f2c9e0..51a6bd33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,7 +169,9 @@ name = "atrium-libs" version = "0.1.0" dependencies = [ "async-trait", + "http 1.1.0", "mockito", + "multibase", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 295c2f5d..e6c4cf58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ serde_ipld_dagcbor = { version = "0.6.0", default-features = false, features = [ # Parsing and validation chrono = "0.4" langtag = "0.3" +multibase = "0.9.1" regex = "1" serde = "1.0.199" serde_bytes = "0.11.9" diff --git a/atrium-libs/Cargo.toml b/atrium-libs/Cargo.toml index 80d3fabb..5975fd9f 100644 --- a/atrium-libs/Cargo.toml +++ b/atrium-libs/Cargo.toml @@ -14,6 +14,8 @@ keywords.workspace = true [dependencies] async-trait = { workspace = true, optional = true } +http = { workspace = true, optional = true } +multibase = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } thiserror = { workspace = true, optional = true } @@ -27,6 +29,7 @@ serde_json = { workspace = true } tokio = { workspace = true, features = ["macros"] } [features] -default = [] -common-web = ["serde/derive"] +default = ["common-web", "crypto", "identity"] +common-web = ["http", "serde/derive"] +crypto = ["multibase"] identity = ["common-web", "async-trait", "serde_json", "thiserror", "url", "urlencoding"] diff --git a/atrium-libs/src/common_web/did_doc.rs b/atrium-libs/src/common_web/did_doc.rs index f0bf0687..50560700 100644 --- a/atrium-libs/src/common_web/did_doc.rs +++ b/atrium-libs/src/common_web/did_doc.rs @@ -37,6 +37,75 @@ pub struct Service { pub service_endpoint: String, } +impl TryFrom<&str> for DidDocument { + type Error = serde_json::Error; + + fn try_from(value: &str) -> Result { + serde_json::from_str(value) + } +} + +impl DidDocument { + pub fn get_did(&self) -> String { + self.id.clone() + } + pub fn get_handle(&self) -> Option { + if let Some(aka) = &self.also_known_as { + aka.iter() + .find_map(|name| name.strip_prefix("at://")) + .map(String::from) + } else { + None + } + } + pub fn get_signing_key(&self) -> Option<(String, String)> { + self.get_verification_material("#atproto") + } + pub fn get_pds_endpoint(&self) -> Option { + self.get_service_endpoint("#atproto_pds", "AtprotoPersonalDataServer") + } + fn get_verification_material(&self, id: &str) -> Option<(String, String)> { + let did = self.get_did(); + if let Some(keys) = &self.verification_method { + keys.iter().find_map(|key| { + if key.id == id || key.id == format!("{did}{id}") { + key.public_key_multibase + .as_ref() + .map(|multibase| (key.r#type.clone(), multibase.clone())) + } else { + None + } + }) + } else { + None + } + } + fn get_service_endpoint(&self, id: &str, r#type: &str) -> Option { + let did = self.get_did(); + if let Some(services) = &self.service { + services + .iter() + .find(|service| { + (service.id == id || service.id == format!("{did}{id}")) + && service.r#type == r#type + }) + .and_then(|service| Self::validate_url(&service.service_endpoint)) + } else { + None + } + } + fn validate_url(s: &str) -> Option { + s.parse::() + .ok() + .and_then(|uri| match (uri.scheme(), uri.host()) { + (Some(scheme), Some(_)) if (scheme == "https" || scheme == "http") => { + Some(s.to_string()) + } + _ => None, + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/atrium-libs/src/crypto.rs b/atrium-libs/src/crypto.rs new file mode 100644 index 00000000..a88f1fb5 --- /dev/null +++ b/atrium-libs/src/crypto.rs @@ -0,0 +1,12 @@ +pub mod did; +pub mod error; +pub mod multibase; +mod utils; + +const DID_KEY_PREFIX: &str = "did:key:"; + +#[derive(Debug)] +pub enum JwtAlg { + P256, + Secp256k1, +} diff --git a/atrium-libs/src/crypto/did.rs b/atrium-libs/src/crypto/did.rs new file mode 100644 index 00000000..32a91e79 --- /dev/null +++ b/atrium-libs/src/crypto/did.rs @@ -0,0 +1,19 @@ +use super::error::{Error, Result}; +use super::{JwtAlg, DID_KEY_PREFIX}; + +pub fn parse_multikey(multikey: &str) -> Result<(JwtAlg, Vec)> { + let (_, decoded) = multibase::decode(multikey)?; + match &decoded[..2] { + [0x80, 0x24] => Ok((JwtAlg::P256, decoded[2..].to_vec())), + [0xe7, 0x01] => Ok((JwtAlg::Secp256k1, decoded[2..].to_vec())), + _ => Err(Error::UnsupportedMultikeyType), + } +} + +pub fn format_did_key(jwt_alg: JwtAlg, key: &[u8]) -> String { + DID_KEY_PREFIX.to_string() + &format_multikey(jwt_alg, key) +} + +fn format_multikey(jwt_alg: JwtAlg, key: &[u8]) -> String { + todo!() +} diff --git a/atrium-libs/src/crypto/error.rs b/atrium-libs/src/crypto/error.rs new file mode 100644 index 00000000..7ffc6f59 --- /dev/null +++ b/atrium-libs/src/crypto/error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Unsupported key type")] + UnsupportedMultikeyType, + #[error(transparent)] + Multibase(#[from] multibase::Error), +} + +pub type Result = std::result::Result; diff --git a/atrium-libs/src/crypto/multibase.rs b/atrium-libs/src/crypto/multibase.rs new file mode 100644 index 00000000..03025d7c --- /dev/null +++ b/atrium-libs/src/crypto/multibase.rs @@ -0,0 +1,6 @@ +use super::error::Result; + +pub fn multibase_to_bytes(mb: &str) -> Result> { + let (_, bytes) = multibase::decode(mb)?; + Ok(bytes) +} diff --git a/atrium-libs/src/crypto/utils.rs b/atrium-libs/src/crypto/utils.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/atrium-libs/src/crypto/utils.rs @@ -0,0 +1 @@ + diff --git a/atrium-libs/src/identity/did.rs b/atrium-libs/src/identity/did.rs index c1783ea4..8f99befa 100644 --- a/atrium-libs/src/identity/did.rs +++ b/atrium-libs/src/identity/did.rs @@ -1,4 +1,6 @@ use crate::common_web::did_doc::DidDocument; +#[cfg(feature = "crypto")] +mod atproto_data; pub mod did_resolver; mod error; mod plc_resolver; @@ -18,6 +20,7 @@ pub trait Fetch { #[async_trait] pub trait Resolve { async fn resolve_no_check(&self, did: &str) -> Result>>; + async fn resolve_no_cache(&self, did: &str) -> Result> { if let Some(got) = self.resolve_no_check(did).await? { Ok(serde_json::from_slice(&got)?) @@ -41,3 +44,113 @@ pub trait Resolve { .ok_or_else(|| Error::DidNotFound(did.to_string())) } } + +pub fn validate_did_doc(did: &str, value: impl TryInto) -> Result { + if let Ok(did_doc) = value.try_into() { + if did_doc.get_did() == did { + return Ok(did_doc); + } + } + Err(Error::PoorlyFormattedDidDocument) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn validate_bad_did_doc() { + let err = validate_did_doc("did:plc:yk4dd2qkboz2yv6tpubpc6co", r##" + { + "ideep": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "blah": [ + "https://dholms.xyz" + ], + "zoot": [ + { + "id": "#elsewhere", + "type": "EcdsaSecp256k1VerificationKey2019", + "controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "publicKeyMultibase": "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR" + } + ], + "yarg": [ ] + } + "##, + ).expect_err("validation should fail with bad DID document"); + assert!(matches!(err, Error::PoorlyFormattedDidDocument)); + } + + #[test] + fn validate_legacy_format() { + let did_doc = validate_did_doc( + "did:plc:yk4dd2qkboz2yv6tpubpc6co", + r##" + { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "alsoKnownAs": [ + "at://dholms.xyz" + ], + "verificationMethod": [ + { + "id": "#atproto", + "type": "EcdsaSecp256k1VerificationKey2019", + "controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "publicKeyMultibase": "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR" + } + ], + "service": [ + { + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": "https://bsky.social" + } + ] + } + "##, + ) + .expect("validation should succeed with legacy DID format"); + assert_eq!(did_doc.get_did(), "did:plc:yk4dd2qkboz2yv6tpubpc6co"); + } + + #[test] + fn validate_newer_format() { + let did_doc = validate_did_doc( + "did:plc:yk4dd2qkboz2yv6tpubpc6co", + r##" + { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/multikey/v1", + "https://w3id.org/security/suites/secp256k1-2019/v1" + ], + "id": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "alsoKnownAs": [ + "at://dholms.xyz" + ], + "verificationMethod": [ + { + "id": "did:plc:yk4dd2qkboz2yv6tpubpc6co#atproto", + "type": "Multikey", + "controller": "did:plc:yk4dd2qkboz2yv6tpubpc6co", + "publicKeyMultibase": "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" + } + ], + "service": [ + { + "id": "#atproto_pds", + "type": "AtprotoPersonalDataServer", + "serviceEndpoint": "https://bsky.social" + } + ] + } + "##, + ) + .expect("validation should succeed with newer Multikey DID format"); + assert_eq!(did_doc.get_did(), "did:plc:yk4dd2qkboz2yv6tpubpc6co"); + } +} diff --git a/atrium-libs/src/identity/did/atproto_data.rs b/atrium-libs/src/identity/did/atproto_data.rs new file mode 100644 index 00000000..aa1d14b4 --- /dev/null +++ b/atrium-libs/src/identity/did/atproto_data.rs @@ -0,0 +1,150 @@ +use crate::common_web::did_doc::DidDocument; +use crate::crypto::did::{format_did_key, parse_multikey}; +use crate::crypto::multibase::multibase_to_bytes; +use crate::crypto::JwtAlg; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Could not parse signing key from doc: {0:?}")] + SigningKey(DidDocument), + #[error("Could not parse handle from doc: {0:?}")] + Handle(DidDocument), + #[error("Could not parse pds from doc: {0:?}")] + Pds(DidDocument), + #[error(transparent)] + Crypto(#[from] crate::crypto::error::Error), +} + +pub type Result = std::result::Result; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct AtprotoData { + pub did: String, + pub signing_key: String, + pub handle: String, + pub pds: String, +} + +pub fn ensure_atproto_data(did_doc: &DidDocument) -> Result { + Ok(AtprotoData { + did: did_doc.get_did(), + signing_key: get_key(did_doc)?.ok_or(Error::SigningKey(did_doc.clone()))?, + handle: did_doc.get_handle().ok_or(Error::Handle(did_doc.clone()))?, + pds: did_doc + .get_pds_endpoint() + .ok_or(Error::Pds(did_doc.clone()))?, + }) +} + +fn get_key(did_doc: &DidDocument) -> Result> { + if let Some((r#type, public_key_multibase)) = did_doc.get_signing_key() { + get_did_key_from_multibase(r#type, public_key_multibase) + } else { + Ok(None) + } +} + +fn get_did_key_from_multibase( + r#type: String, + public_key_multibase: String, +) -> Result> { + Ok(match r#type.as_str() { + "EcdsaSecp256r1VerificationKey2019" => Some(format_did_key( + JwtAlg::P256, + &multibase_to_bytes(&public_key_multibase)?, + )), + "EcdsaSecp256k1VerificationKey2019" => Some(format_did_key( + JwtAlg::Secp256k1, + &multibase_to_bytes(&public_key_multibase)?, + )), + "Multikey" => { + let (jwt_alg, key) = parse_multikey(&public_key_multibase)?; + Some(format_did_key(jwt_alg, &key)) + } + _ => None, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common_web::did_doc::{Service, VerificationMethod}; + + #[test] + fn extract_from_legacy_format() { + let did_doc = DidDocument { + context: Some(vec![ + String::from("https://www.w3.org/ns/did/v1"), + String::from("https://w3id.org/security/suites/secp256k1-2019/v1"), + ]), + id: String::from("did:plc:yk4dd2qkboz2yv6tpubpc6co"), + also_known_as: Some(vec![String::from("at://dholms.xyz")]), + verification_method: Some(vec![VerificationMethod { + id: String::from("#atproto"), + r#type: String::from("EcdsaSecp256k1VerificationKey2019"), + controller: String::from("did:plc:yk4dd2qkboz2yv6tpubpc6co"), + public_key_multibase: Some(String::from( + "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR", + )), + }]), + service: Some(vec![Service { + id: String::from("#atproto_pds"), + r#type: String::from("AtprotoPersonalDataServer"), + service_endpoint: String::from("https://bsky.social"), + }]), + }; + let atp_data = ensure_atproto_data(&did_doc) + .expect("ensure_atproto_data should succeed with legacy DID format"); + assert_eq!( + atp_data, + AtprotoData { + did: String::from("did:plc:yk4dd2qkboz2yv6tpubpc6co"), + signing_key: String::from( + "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" + ), + handle: String::from("dholms.xyz"), + pds: String::from("https://bsky.social"), + } + ); + } + + #[test] + fn extract_from_newer_format() { + let did_doc = DidDocument { + context: Some(vec![ + String::from("https://www.w3.org/ns/did/v1"), + String::from("https://w3id.org/security/multikey/v1"), + String::from("https://w3id.org/security/suites/secp256k1-2019/v1"), + ]), + id: String::from("did:plc:yk4dd2qkboz2yv6tpubpc6co"), + also_known_as: Some(vec![String::from("at://dholms.xyz")]), + verification_method: Some(vec![VerificationMethod { + id: String::from("did:plc:yk4dd2qkboz2yv6tpubpc6co#atproto"), + r#type: String::from("Multikey"), + controller: String::from("did:plc:yk4dd2qkboz2yv6tpubpc6co"), + public_key_multibase: Some(String::from( + "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF", + )), + }]), + service: Some(vec![Service { + id: String::from("#atproto_pds"), + r#type: String::from("AtprotoPersonalDataServer"), + service_endpoint: String::from("https://bsky.social"), + }]), + }; + let atp_data = ensure_atproto_data(&did_doc) + .expect("ensure_atproto_data should succeed with legacy DID format"); + assert_eq!( + atp_data, + AtprotoData { + did: String::from("did:plc:yk4dd2qkboz2yv6tpubpc6co"), + signing_key: String::from( + "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" + ), + handle: String::from("dholms.xyz"), + pds: String::from("https://bsky.social"), + } + ); + } +} diff --git a/atrium-libs/src/identity/did/error.rs b/atrium-libs/src/identity/did/error.rs index ad87c6b7..9351b3ea 100644 --- a/atrium-libs/src/identity/did/error.rs +++ b/atrium-libs/src/identity/did/error.rs @@ -15,6 +15,8 @@ pub enum Error { DidNotFound(String), #[error("Poorly formatted DID: {0}")] PoorlyFormattedDid(String), + #[error("Poorly formatted DID Document")] + PoorlyFormattedDidDocument, #[error("Unsupported DID method: {0}")] UnsupportedDidMethod(String), #[error("Unsupported did:web paths: {0}")] diff --git a/atrium-libs/src/lib.rs b/atrium-libs/src/lib.rs index 8e7dfbf2..fba3dcba 100644 --- a/atrium-libs/src/lib.rs +++ b/atrium-libs/src/lib.rs @@ -1,4 +1,6 @@ #[cfg(feature = "common-web")] mod common_web; +#[cfg(feature = "crypto")] +mod crypto; #[cfg(feature = "identity")] mod identity; From f53b03dd15b8f899652241ab4e6b63763966a785 Mon Sep 17 00:00:00 2001 From: sugyan Date: Fri, 10 May 2024 22:30:49 +0900 Subject: [PATCH 05/17] Implement format_did_key for Secp256k1 --- Cargo.lock | 287 +++++++++++++++++++ Cargo.toml | 6 + atrium-libs/Cargo.toml | 7 +- atrium-libs/src/crypto.rs | 10 +- atrium-libs/src/crypto/algorithm.rs | 39 +++ atrium-libs/src/crypto/did.rs | 69 ++++- atrium-libs/src/crypto/error.rs | 2 + atrium-libs/src/crypto/multibase.rs | 6 - atrium-libs/src/identity/did/atproto_data.rs | 24 +- 9 files changed, 413 insertions(+), 37 deletions(-) create mode 100644 atrium-libs/src/crypto/algorithm.rs delete mode 100644 atrium-libs/src/crypto/multibase.rs diff --git a/Cargo.lock b/Cargo.lock index 51a6bd33..d30de34c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,9 +169,13 @@ name = "atrium-libs" version = "0.1.0" dependencies = [ "async-trait", + "ecdsa", + "hex", "http 1.1.0", + "k256", "mockito", "multibase", + "p256", "reqwest", "serde", "serde_json", @@ -238,12 +242,24 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" @@ -256,6 +272,15 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -400,6 +425,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "core-foundation" version = "0.9.4" @@ -425,12 +456,43 @@ dependencies = [ "memchr", ] +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "curl" version = "0.4.46" @@ -488,6 +550,29 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + [[package]] name = "dirs" version = "5.0.1" @@ -509,6 +594,40 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.33" @@ -555,6 +674,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "fnv" version = "1.0.7" @@ -661,6 +790,17 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + [[package]] name = "getrandom" version = "0.2.12" @@ -678,6 +818,17 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.26" @@ -715,6 +866,21 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "http" version = "0.2.12" @@ -1000,6 +1166,20 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "k256" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "langtag" version = "0.3.4" @@ -1251,6 +1431,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "parking" version = "2.2.0" @@ -1280,6 +1472,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1318,6 +1519,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1346,6 +1557,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.78" @@ -1487,6 +1707,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.8" @@ -1588,6 +1818,20 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -1688,6 +1932,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -1697,6 +1952,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "similar" version = "2.4.0" @@ -1745,6 +2010,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "strsim" version = "0.10.0" @@ -1973,6 +2248,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -2041,6 +2322,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "waker-fn" version = "1.1.1" diff --git a/Cargo.toml b/Cargo.toml index e6c4cf58..ee440a8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ serde_ipld_dagcbor = { version = "0.6.0", default-features = false, features = [ # Parsing and validation chrono = "0.4" +hex = "0.4.3" langtag = "0.3" multibase = "0.9.1" regex = "1" @@ -45,6 +46,11 @@ serde_json = "1.0.96" serde_html_form = "0.2.6" urlencoding = "2.1.3" +# Cryptography +ecdsa = "0.16.9" +k256 = "0.13.3" +p256 = "0.13.2" + # Networking futures = { version = "0.3.30", default-features = false, features = ["alloc"] } http = "1.1.0" diff --git a/atrium-libs/Cargo.toml b/atrium-libs/Cargo.toml index 5975fd9f..dc7bfa13 100644 --- a/atrium-libs/Cargo.toml +++ b/atrium-libs/Cargo.toml @@ -14,6 +14,9 @@ keywords.workspace = true [dependencies] async-trait = { workspace = true, optional = true } +ecdsa = { workspace = true, optional = true } +k256 = { workspace = true, optional = true } +p256 = { workspace = true, optional = true } http = { workspace = true, optional = true } multibase = { workspace = true, optional = true } serde = { workspace = true, optional = true } @@ -23,6 +26,8 @@ url = { workspace = true, optional = true } urlencoding = { workspace = true, optional = true } [dev-dependencies] +ecdsa = { workspace = true, features = ["signing"] } +hex = { workspace = true } mockito = { workspace = true } reqwest = { workspace = true } serde_json = { workspace = true } @@ -31,5 +36,5 @@ tokio = { workspace = true, features = ["macros"] } [features] default = ["common-web", "crypto", "identity"] common-web = ["http", "serde/derive"] -crypto = ["multibase"] +crypto = ["ecdsa/verifying", "k256", "p256", "multibase"] identity = ["common-web", "async-trait", "serde_json", "thiserror", "url", "urlencoding"] diff --git a/atrium-libs/src/crypto.rs b/atrium-libs/src/crypto.rs index a88f1fb5..0f0f21b9 100644 --- a/atrium-libs/src/crypto.rs +++ b/atrium-libs/src/crypto.rs @@ -1,12 +1,8 @@ +mod algorithm; pub mod did; pub mod error; -pub mod multibase; mod utils; -const DID_KEY_PREFIX: &str = "did:key:"; +pub use algorithm::Algorithm; -#[derive(Debug)] -pub enum JwtAlg { - P256, - Secp256k1, -} +const DID_KEY_PREFIX: &str = "did:key:"; diff --git a/atrium-libs/src/crypto/algorithm.rs b/atrium-libs/src/crypto/algorithm.rs new file mode 100644 index 00000000..78080b7f --- /dev/null +++ b/atrium-libs/src/crypto/algorithm.rs @@ -0,0 +1,39 @@ +use super::error::Result; +use ecdsa::VerifyingKey; +use k256::Secp256k1; +use multibase::Base; + +#[derive(Debug)] +pub enum Algorithm { + P256, + Secp256k1, +} + +impl Algorithm { + const MULTICODE_PREFIX_P256: [u8; 2] = [0x80, 0x24]; + const MULTICODE_PREFIX_SECP256K1: [u8; 2] = [0xe7, 0x01]; + + pub fn from_prefix(prefix: [u8; 2]) -> Option { + match prefix { + Self::MULTICODE_PREFIX_P256 => Some(Self::P256), + Self::MULTICODE_PREFIX_SECP256K1 => Some(Self::Secp256k1), + _ => None, + } + } + pub fn format_multikey(&self, key: &[u8]) -> Result { + let prefixed_bytes = match self { + Algorithm::P256 => { + todo!() + } + Algorithm::Secp256k1 => { + let point = VerifyingKey::::from_sec1_bytes(key)?.to_encoded_point(true); + [ + Self::MULTICODE_PREFIX_SECP256K1.to_vec(), + point.as_bytes().to_vec(), + ] + .concat() + } + }; + Ok(multibase::encode(Base::Base58Btc, prefixed_bytes)) + } +} diff --git a/atrium-libs/src/crypto/did.rs b/atrium-libs/src/crypto/did.rs index 32a91e79..795d83c3 100644 --- a/atrium-libs/src/crypto/did.rs +++ b/atrium-libs/src/crypto/did.rs @@ -1,19 +1,68 @@ use super::error::{Error, Result}; -use super::{JwtAlg, DID_KEY_PREFIX}; +use super::{Algorithm, DID_KEY_PREFIX}; -pub fn parse_multikey(multikey: &str) -> Result<(JwtAlg, Vec)> { +pub fn parse_multikey(multikey: &str) -> Result<(Algorithm, Vec)> { let (_, decoded) = multibase::decode(multikey)?; - match &decoded[..2] { - [0x80, 0x24] => Ok((JwtAlg::P256, decoded[2..].to_vec())), - [0xe7, 0x01] => Ok((JwtAlg::Secp256k1, decoded[2..].to_vec())), - _ => Err(Error::UnsupportedMultikeyType), + if let Ok(prefix) = decoded[..2].try_into() { + if let Some(jwt_arg) = Algorithm::from_prefix(prefix) { + return Ok((jwt_arg, decoded[2..].to_vec())); + } } + Err(Error::UnsupportedMultikeyType) } -pub fn format_did_key(jwt_alg: JwtAlg, key: &[u8]) -> String { - DID_KEY_PREFIX.to_string() + &format_multikey(jwt_alg, key) +pub fn format_did_key_str(alg: Algorithm, s: &str) -> Result { + let (_, key) = multibase::decode(s)?; + format_did_key(alg, &key) } -fn format_multikey(jwt_alg: JwtAlg, key: &[u8]) -> String { - todo!() +pub fn format_did_key(alg: Algorithm, key: &[u8]) -> Result { + Ok(DID_KEY_PREFIX.to_string() + &alg.format_multikey(key)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use ecdsa::SigningKey; + use k256::Secp256k1; + + // did:key secp256k1 test vectors from W3C + // https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/secp256k1.json + fn secp256k1_vectors() -> Vec<(&'static str, &'static str)> { + vec![ + ( + "9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c", + "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme", + ), + ( + "f0f4df55a2b3ff13051ea814a8f24ad00f2e469af73c363ac7e9fb999a9072ed", + "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2", + ), + ( + "6b0b91287ae3348f8c2f2552d766f30e3604867e34adc37ccbb74a8e6b893e02", + "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N", + ), + ( + "c0a6a7c560d37d7ba81ecee9543721ff48fea3e0fb827d42c1868226540fac15", + "did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy", + ), + ( + "175a232d440be1e0788f25488a73d9416c04b6f924bea6354bf05dd2f1a75133", + "did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj", + ), + ] + } + + #[test] + fn secp256k1() { + for (seed, id) in secp256k1_vectors() { + let bytes = hex::decode(seed).expect("hex decoding should succeed"); + let sign = SigningKey::::from_slice(&bytes) + .expect("initializing signing key should succeed"); + let result = + format_did_key(Algorithm::Secp256k1, &sign.verifying_key().to_sec1_bytes()) + .expect("formatting DID key should succeed"); + assert_eq!(result, id); + } + } } diff --git a/atrium-libs/src/crypto/error.rs b/atrium-libs/src/crypto/error.rs index 7ffc6f59..0191e886 100644 --- a/atrium-libs/src/crypto/error.rs +++ b/atrium-libs/src/crypto/error.rs @@ -6,6 +6,8 @@ pub enum Error { UnsupportedMultikeyType, #[error(transparent)] Multibase(#[from] multibase::Error), + #[error(transparent)] + Signature(#[from] ecdsa::signature::Error), } pub type Result = std::result::Result; diff --git a/atrium-libs/src/crypto/multibase.rs b/atrium-libs/src/crypto/multibase.rs deleted file mode 100644 index 03025d7c..00000000 --- a/atrium-libs/src/crypto/multibase.rs +++ /dev/null @@ -1,6 +0,0 @@ -use super::error::Result; - -pub fn multibase_to_bytes(mb: &str) -> Result> { - let (_, bytes) = multibase::decode(mb)?; - Ok(bytes) -} diff --git a/atrium-libs/src/identity/did/atproto_data.rs b/atrium-libs/src/identity/did/atproto_data.rs index aa1d14b4..e82d4807 100644 --- a/atrium-libs/src/identity/did/atproto_data.rs +++ b/atrium-libs/src/identity/did/atproto_data.rs @@ -1,7 +1,6 @@ use crate::common_web::did_doc::DidDocument; -use crate::crypto::did::{format_did_key, parse_multikey}; -use crate::crypto::multibase::multibase_to_bytes; -use crate::crypto::JwtAlg; +use crate::crypto::did::{format_did_key, format_did_key_str, parse_multikey}; +use crate::crypto::Algorithm; use thiserror::Error; #[derive(Error, Debug)] @@ -50,17 +49,16 @@ fn get_did_key_from_multibase( public_key_multibase: String, ) -> Result> { Ok(match r#type.as_str() { - "EcdsaSecp256r1VerificationKey2019" => Some(format_did_key( - JwtAlg::P256, - &multibase_to_bytes(&public_key_multibase)?, - )), - "EcdsaSecp256k1VerificationKey2019" => Some(format_did_key( - JwtAlg::Secp256k1, - &multibase_to_bytes(&public_key_multibase)?, - )), + "EcdsaSecp256r1VerificationKey2019" => { + Some(format_did_key_str(Algorithm::P256, &public_key_multibase)?) + } + "EcdsaSecp256k1VerificationKey2019" => Some(format_did_key_str( + Algorithm::Secp256k1, + &public_key_multibase, + )?), "Multikey" => { - let (jwt_alg, key) = parse_multikey(&public_key_multibase)?; - Some(format_did_key(jwt_alg, &key)) + let (alg, key) = parse_multikey(&public_key_multibase)?; + Some(format_did_key(alg, &key)?) } _ => None, }) From 59bb483abcdd29b6670ce3941a4603ebcd759e75 Mon Sep 17 00:00:00 2001 From: sugyan Date: Sat, 11 May 2024 22:32:36 +0900 Subject: [PATCH 06/17] Implement format_did_key for P256 --- .github/workflows/libs.yml | 1 + atrium-libs/src/crypto.rs | 1 - atrium-libs/src/crypto/algorithm.rs | 24 +++++++++++-- atrium-libs/src/crypto/did.rs | 55 ++++++++++++++++++++++++++--- atrium-libs/src/crypto/error.rs | 2 ++ atrium-libs/src/crypto/utils.rs | 1 - 6 files changed, 76 insertions(+), 8 deletions(-) delete mode 100644 atrium-libs/src/crypto/utils.rs diff --git a/.github/workflows/libs.yml b/.github/workflows/libs.yml index 63c4630c..110a0e9a 100644 --- a/.github/workflows/libs.yml +++ b/.github/workflows/libs.yml @@ -18,6 +18,7 @@ jobs: run: | cargo build -p atrium-libs --verbose cargo build -p atrium-libs --verbose --features common-web + cargo build -p atrium-libs --verbose --features crypto cargo build -p atrium-libs --verbose --features identity cargo build -p atrium-libs --verbose --all-features - name: Run tests diff --git a/atrium-libs/src/crypto.rs b/atrium-libs/src/crypto.rs index 0f0f21b9..3fa764bf 100644 --- a/atrium-libs/src/crypto.rs +++ b/atrium-libs/src/crypto.rs @@ -1,7 +1,6 @@ mod algorithm; pub mod did; pub mod error; -mod utils; pub use algorithm::Algorithm; diff --git a/atrium-libs/src/crypto/algorithm.rs b/atrium-libs/src/crypto/algorithm.rs index 78080b7f..d1df07e8 100644 --- a/atrium-libs/src/crypto/algorithm.rs +++ b/atrium-libs/src/crypto/algorithm.rs @@ -2,8 +2,9 @@ use super::error::Result; use ecdsa::VerifyingKey; use k256::Secp256k1; use multibase::Base; +use p256::NistP256; -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Algorithm { P256, Secp256k1, @@ -23,7 +24,12 @@ impl Algorithm { pub fn format_multikey(&self, key: &[u8]) -> Result { let prefixed_bytes = match self { Algorithm::P256 => { - todo!() + let point = VerifyingKey::::from_sec1_bytes(key)?.to_encoded_point(true); + [ + Self::MULTICODE_PREFIX_P256.to_vec(), + point.as_bytes().to_vec(), + ] + .concat() } Algorithm::Secp256k1 => { let point = VerifyingKey::::from_sec1_bytes(key)?.to_encoded_point(true); @@ -36,4 +42,18 @@ impl Algorithm { }; Ok(multibase::encode(Base::Base58Btc, prefixed_bytes)) } + pub fn decompress_pubkey(&self, key: &[u8]) -> Result> { + let point = match self { + Algorithm::P256 => { + let point = VerifyingKey::::from_sec1_bytes(key)?.to_encoded_point(false); + point.to_bytes().to_vec() + } + Algorithm::Secp256k1 => { + let point = + VerifyingKey::::from_sec1_bytes(key)?.to_encoded_point(false); + point.to_bytes().to_vec() + } + }; + Ok(point) + } } diff --git a/atrium-libs/src/crypto/did.rs b/atrium-libs/src/crypto/did.rs index 795d83c3..fee7ac9b 100644 --- a/atrium-libs/src/crypto/did.rs +++ b/atrium-libs/src/crypto/did.rs @@ -4,8 +4,8 @@ use super::{Algorithm, DID_KEY_PREFIX}; pub fn parse_multikey(multikey: &str) -> Result<(Algorithm, Vec)> { let (_, decoded) = multibase::decode(multikey)?; if let Ok(prefix) = decoded[..2].try_into() { - if let Some(jwt_arg) = Algorithm::from_prefix(prefix) { - return Ok((jwt_arg, decoded[2..].to_vec())); + if let Some(alg) = Algorithm::from_prefix(prefix) { + return Ok((alg, alg.decompress_pubkey(&decoded[2..])?)); } } Err(Error::UnsupportedMultikeyType) @@ -16,6 +16,14 @@ pub fn format_did_key_str(alg: Algorithm, s: &str) -> Result { format_did_key(alg, &key) } +pub fn parse_did_key(did: &str) -> Result<(Algorithm, Vec)> { + if let Some(multikey) = did.strip_prefix(DID_KEY_PREFIX) { + parse_multikey(multikey) + } else { + Err(Error::IncorrectDIDKeyPrefix(did.to_string())) + } +} + pub fn format_did_key(alg: Algorithm, key: &[u8]) -> Result { Ok(DID_KEY_PREFIX.to_string() + &alg.format_multikey(key)?) } @@ -25,6 +33,8 @@ mod tests { use super::*; use ecdsa::SigningKey; use k256::Secp256k1; + use multibase::Base; + use p256::NistP256; // did:key secp256k1 test vectors from W3C // https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/secp256k1.json @@ -53,16 +63,53 @@ mod tests { ] } + // did:key p-256 test vectors from W3C + // https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/nist-curves.json + fn p256_vectors() -> Vec<(&'static str, &'static str)> { + vec![( + "9p4VRzdmhsnq869vQjVCTrRry7u4TtfRxhvBFJTGU2Cp", + "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb", + )] + } + #[test] fn secp256k1() { for (seed, id) in secp256k1_vectors() { let bytes = hex::decode(seed).expect("hex decoding should succeed"); let sign = SigningKey::::from_slice(&bytes) .expect("initializing signing key should succeed"); - let result = + let did_key = format_did_key(Algorithm::Secp256k1, &sign.verifying_key().to_sec1_bytes()) .expect("formatting DID key should succeed"); - assert_eq!(result, id); + assert_eq!(did_key, id); + + let (alg, key) = parse_did_key(&did_key).expect("parsing DID key should succeed"); + assert_eq!(alg, Algorithm::Secp256k1); + assert_eq!( + &key, + sign.verifying_key().to_encoded_point(false).as_bytes() + ); + } + } + + #[test] + fn p256() { + for (private_key_base58, id) in p256_vectors() { + let bytes = Base::Base58Btc + .decode(private_key_base58) + .expect("multibase decoding should succeed"); + let sign = SigningKey::::from_slice(&bytes) + .expect("initializing signing key should succeed"); + let did_key = format_did_key(Algorithm::P256, &sign.verifying_key().to_sec1_bytes()) + .expect("formatting DID key should succeed"); + assert_eq!(did_key, id); + + let (alg, key) = parse_did_key(&did_key).expect("parsing DID key should succeed"); + assert_eq!(alg, Algorithm::P256); + assert_eq!( + &key, + sign.verifying_key().to_encoded_point(false).as_bytes() + ); } } } diff --git a/atrium-libs/src/crypto/error.rs b/atrium-libs/src/crypto/error.rs index 0191e886..fdcbbf51 100644 --- a/atrium-libs/src/crypto/error.rs +++ b/atrium-libs/src/crypto/error.rs @@ -4,6 +4,8 @@ use thiserror::Error; pub enum Error { #[error("Unsupported key type")] UnsupportedMultikeyType, + #[error("Incorrect prefix for did:key: {0}")] + IncorrectDIDKeyPrefix(String), #[error(transparent)] Multibase(#[from] multibase::Error), #[error(transparent)] diff --git a/atrium-libs/src/crypto/utils.rs b/atrium-libs/src/crypto/utils.rs deleted file mode 100644 index 8b137891..00000000 --- a/atrium-libs/src/crypto/utils.rs +++ /dev/null @@ -1 +0,0 @@ - From 817baab889701af5cc1f1ced0db49eeb3a21f070 Mon Sep 17 00:00:00 2001 From: sugyan Date: Sun, 12 May 2024 15:48:39 +0900 Subject: [PATCH 07/17] Refactor crypto/algorithm --- atrium-libs/src/crypto/algorithm.rs | 52 ++++++++++++----------------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/atrium-libs/src/crypto/algorithm.rs b/atrium-libs/src/crypto/algorithm.rs index d1df07e8..34ef6a54 100644 --- a/atrium-libs/src/crypto/algorithm.rs +++ b/atrium-libs/src/crypto/algorithm.rs @@ -22,38 +22,28 @@ impl Algorithm { } } pub fn format_multikey(&self, key: &[u8]) -> Result { - let prefixed_bytes = match self { - Algorithm::P256 => { - let point = VerifyingKey::::from_sec1_bytes(key)?.to_encoded_point(true); - [ - Self::MULTICODE_PREFIX_P256.to_vec(), - point.as_bytes().to_vec(), - ] - .concat() - } - Algorithm::Secp256k1 => { - let point = VerifyingKey::::from_sec1_bytes(key)?.to_encoded_point(true); - [ - Self::MULTICODE_PREFIX_SECP256K1.to_vec(), - point.as_bytes().to_vec(), - ] - .concat() - } - }; - Ok(multibase::encode(Base::Base58Btc, prefixed_bytes)) + let mut bytes = match self { + Algorithm::P256 => Self::MULTICODE_PREFIX_P256, + Algorithm::Secp256k1 => Self::MULTICODE_PREFIX_SECP256K1, + } + .to_vec(); + bytes.extend(self.pubkey_bytes(key, true)?); + Ok(multibase::encode(Base::Base58Btc, bytes)) } pub fn decompress_pubkey(&self, key: &[u8]) -> Result> { - let point = match self { - Algorithm::P256 => { - let point = VerifyingKey::::from_sec1_bytes(key)?.to_encoded_point(false); - point.to_bytes().to_vec() - } - Algorithm::Secp256k1 => { - let point = - VerifyingKey::::from_sec1_bytes(key)?.to_encoded_point(false); - point.to_bytes().to_vec() - } - }; - Ok(point) + self.pubkey_bytes(key, false) + } + fn pubkey_bytes(&self, key: &[u8], compress: bool) -> Result> { + Ok(match self { + Algorithm::P256 => VerifyingKey::::from_sec1_bytes(key)? + .to_encoded_point(compress) + .as_bytes() + .to_vec(), + + Algorithm::Secp256k1 => VerifyingKey::::from_sec1_bytes(key)? + .to_encoded_point(compress) + .as_bytes() + .to_vec(), + }) } } From d64fcac88bb8e57e63d0425604a3c68b0267b72b Mon Sep 17 00:00:00 2001 From: sugyan Date: Sun, 12 May 2024 16:30:39 +0900 Subject: [PATCH 08/17] Separate crypto package --- .github/workflows/crypto.yml | 22 ++++ .github/workflows/libs.yml | 2 +- Cargo.lock | 16 ++- Cargo.toml | 2 + atrium-crypto/Cargo.toml | 24 ++++ atrium-crypto/README.md | 10 ++ atrium-crypto/src/algorithm.rs | 49 ++++++++ atrium-crypto/src/did.rs | 115 +++++++++++++++++++ atrium-crypto/src/error.rs | 15 +++ atrium-crypto/src/lib.rs | 8 ++ atrium-libs/Cargo.toml | 11 +- atrium-libs/src/identity/did.rs | 4 +- atrium-libs/src/identity/did/atproto_data.rs | 6 +- atrium-libs/src/lib.rs | 2 - 14 files changed, 266 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/crypto.yml create mode 100644 atrium-crypto/Cargo.toml create mode 100644 atrium-crypto/README.md create mode 100644 atrium-crypto/src/algorithm.rs create mode 100644 atrium-crypto/src/did.rs create mode 100644 atrium-crypto/src/error.rs create mode 100644 atrium-crypto/src/lib.rs diff --git a/.github/workflows/crypto.yml b/.github/workflows/crypto.yml new file mode 100644 index 00000000..20a49c0d --- /dev/null +++ b/.github/workflows/crypto.yml @@ -0,0 +1,22 @@ +name: Crypto + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: | + cargo build -p atrium-cypto --verbose + - name: Run tests + run: | + cargo test -p atrium-crypto --lib diff --git a/.github/workflows/libs.yml b/.github/workflows/libs.yml index 110a0e9a..c95e5f3d 100644 --- a/.github/workflows/libs.yml +++ b/.github/workflows/libs.yml @@ -17,8 +17,8 @@ jobs: - name: Build run: | cargo build -p atrium-libs --verbose + cargo build -p atrium-libs --verbose --features atproto-data cargo build -p atrium-libs --verbose --features common-web - cargo build -p atrium-libs --verbose --features crypto cargo build -p atrium-libs --verbose --features identity cargo build -p atrium-libs --verbose --all-features - name: Run tests diff --git a/Cargo.lock b/Cargo.lock index d30de34c..1f540e01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,17 +165,25 @@ dependencies = [ ] [[package]] -name = "atrium-libs" +name = "atrium-crypto" version = "0.1.0" dependencies = [ - "async-trait", "ecdsa", "hex", - "http 1.1.0", "k256", - "mockito", "multibase", "p256", + "thiserror", +] + +[[package]] +name = "atrium-libs" +version = "0.1.0" +dependencies = [ + "async-trait", + "atrium-crypto", + "http 1.1.0", + "mockito", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index ee440a8c..08e5c4cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "atrium-api", "atrium-cli", + "atrium-crypto", "atrium-libs", "atrium-xrpc", "atrium-xrpc-client", @@ -23,6 +24,7 @@ keywords = ["atproto", "bluesky"] [workspace.dependencies] # Intra-workspace dependencies atrium-api = { version = "0.21.0", path = "atrium-api" } +atrium-crypto = { version = "0.1.0", path = "atrium-crypto" } atrium-xrpc = { version = "0.10.5", path = "atrium-xrpc" } atrium-xrpc-client = { version = "0.5.2", path = "atrium-xrpc-client" } diff --git a/atrium-crypto/Cargo.toml b/atrium-crypto/Cargo.toml new file mode 100644 index 00000000..1345017e --- /dev/null +++ b/atrium-crypto/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "atrium-crypto" +version = "0.1.0" +authors = ["sugyan "] +edition.workspace = true +rust-version.workspace = true +description = "Cryptographic library providing basic helpers for AT Protocol" +readme = "README.md" +repository.workspace = true +license.workspace = true +keywords.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ecdsa = { workspace = true, features = ["verifying"] } +k256.workspace = true +p256.workspace = true +multibase.workspace = true +thiserror.workspace = true + +[dev-dependencies] +ecdsa = { workspace = true, features = ["signing"] } +hex = { workspace = true } diff --git a/atrium-crypto/README.md b/atrium-crypto/README.md new file mode 100644 index 00000000..7a27f3d9 --- /dev/null +++ b/atrium-crypto/README.md @@ -0,0 +1,10 @@ +# ATrium Crypto + +Cryptographic library providing basic helpers for AT Protocol. + +This package implements the two currently supported cryptographic systems: + +- [`p256`](https://crates.io/crates/p256) elliptic curve: aka "NIST P-256", aka `secp256r1` (note the `r`), aka `prime256v1` +- [`k256`](https://crates.io/crates/k256) elliptic curve: aka "NIST K-256", aka `secp256k1` (note the `k`) + +The details of cryptography in atproto are described in [the specification](https://atproto.com/specs/cryptography). This includes string encodings, validity of "low-S" signatures, byte representation "compression", hashing, and more. diff --git a/atrium-crypto/src/algorithm.rs b/atrium-crypto/src/algorithm.rs new file mode 100644 index 00000000..34ef6a54 --- /dev/null +++ b/atrium-crypto/src/algorithm.rs @@ -0,0 +1,49 @@ +use super::error::Result; +use ecdsa::VerifyingKey; +use k256::Secp256k1; +use multibase::Base; +use p256::NistP256; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Algorithm { + P256, + Secp256k1, +} + +impl Algorithm { + const MULTICODE_PREFIX_P256: [u8; 2] = [0x80, 0x24]; + const MULTICODE_PREFIX_SECP256K1: [u8; 2] = [0xe7, 0x01]; + + pub fn from_prefix(prefix: [u8; 2]) -> Option { + match prefix { + Self::MULTICODE_PREFIX_P256 => Some(Self::P256), + Self::MULTICODE_PREFIX_SECP256K1 => Some(Self::Secp256k1), + _ => None, + } + } + pub fn format_multikey(&self, key: &[u8]) -> Result { + let mut bytes = match self { + Algorithm::P256 => Self::MULTICODE_PREFIX_P256, + Algorithm::Secp256k1 => Self::MULTICODE_PREFIX_SECP256K1, + } + .to_vec(); + bytes.extend(self.pubkey_bytes(key, true)?); + Ok(multibase::encode(Base::Base58Btc, bytes)) + } + pub fn decompress_pubkey(&self, key: &[u8]) -> Result> { + self.pubkey_bytes(key, false) + } + fn pubkey_bytes(&self, key: &[u8], compress: bool) -> Result> { + Ok(match self { + Algorithm::P256 => VerifyingKey::::from_sec1_bytes(key)? + .to_encoded_point(compress) + .as_bytes() + .to_vec(), + + Algorithm::Secp256k1 => VerifyingKey::::from_sec1_bytes(key)? + .to_encoded_point(compress) + .as_bytes() + .to_vec(), + }) + } +} diff --git a/atrium-crypto/src/did.rs b/atrium-crypto/src/did.rs new file mode 100644 index 00000000..fee7ac9b --- /dev/null +++ b/atrium-crypto/src/did.rs @@ -0,0 +1,115 @@ +use super::error::{Error, Result}; +use super::{Algorithm, DID_KEY_PREFIX}; + +pub fn parse_multikey(multikey: &str) -> Result<(Algorithm, Vec)> { + let (_, decoded) = multibase::decode(multikey)?; + if let Ok(prefix) = decoded[..2].try_into() { + if let Some(alg) = Algorithm::from_prefix(prefix) { + return Ok((alg, alg.decompress_pubkey(&decoded[2..])?)); + } + } + Err(Error::UnsupportedMultikeyType) +} + +pub fn format_did_key_str(alg: Algorithm, s: &str) -> Result { + let (_, key) = multibase::decode(s)?; + format_did_key(alg, &key) +} + +pub fn parse_did_key(did: &str) -> Result<(Algorithm, Vec)> { + if let Some(multikey) = did.strip_prefix(DID_KEY_PREFIX) { + parse_multikey(multikey) + } else { + Err(Error::IncorrectDIDKeyPrefix(did.to_string())) + } +} + +pub fn format_did_key(alg: Algorithm, key: &[u8]) -> Result { + Ok(DID_KEY_PREFIX.to_string() + &alg.format_multikey(key)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use ecdsa::SigningKey; + use k256::Secp256k1; + use multibase::Base; + use p256::NistP256; + + // did:key secp256k1 test vectors from W3C + // https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/secp256k1.json + fn secp256k1_vectors() -> Vec<(&'static str, &'static str)> { + vec![ + ( + "9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c", + "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme", + ), + ( + "f0f4df55a2b3ff13051ea814a8f24ad00f2e469af73c363ac7e9fb999a9072ed", + "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2", + ), + ( + "6b0b91287ae3348f8c2f2552d766f30e3604867e34adc37ccbb74a8e6b893e02", + "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N", + ), + ( + "c0a6a7c560d37d7ba81ecee9543721ff48fea3e0fb827d42c1868226540fac15", + "did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy", + ), + ( + "175a232d440be1e0788f25488a73d9416c04b6f924bea6354bf05dd2f1a75133", + "did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj", + ), + ] + } + + // did:key p-256 test vectors from W3C + // https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/nist-curves.json + fn p256_vectors() -> Vec<(&'static str, &'static str)> { + vec![( + "9p4VRzdmhsnq869vQjVCTrRry7u4TtfRxhvBFJTGU2Cp", + "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb", + )] + } + + #[test] + fn secp256k1() { + for (seed, id) in secp256k1_vectors() { + let bytes = hex::decode(seed).expect("hex decoding should succeed"); + let sign = SigningKey::::from_slice(&bytes) + .expect("initializing signing key should succeed"); + let did_key = + format_did_key(Algorithm::Secp256k1, &sign.verifying_key().to_sec1_bytes()) + .expect("formatting DID key should succeed"); + assert_eq!(did_key, id); + + let (alg, key) = parse_did_key(&did_key).expect("parsing DID key should succeed"); + assert_eq!(alg, Algorithm::Secp256k1); + assert_eq!( + &key, + sign.verifying_key().to_encoded_point(false).as_bytes() + ); + } + } + + #[test] + fn p256() { + for (private_key_base58, id) in p256_vectors() { + let bytes = Base::Base58Btc + .decode(private_key_base58) + .expect("multibase decoding should succeed"); + let sign = SigningKey::::from_slice(&bytes) + .expect("initializing signing key should succeed"); + let did_key = format_did_key(Algorithm::P256, &sign.verifying_key().to_sec1_bytes()) + .expect("formatting DID key should succeed"); + assert_eq!(did_key, id); + + let (alg, key) = parse_did_key(&did_key).expect("parsing DID key should succeed"); + assert_eq!(alg, Algorithm::P256); + assert_eq!( + &key, + sign.verifying_key().to_encoded_point(false).as_bytes() + ); + } + } +} diff --git a/atrium-crypto/src/error.rs b/atrium-crypto/src/error.rs new file mode 100644 index 00000000..fdcbbf51 --- /dev/null +++ b/atrium-crypto/src/error.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Unsupported key type")] + UnsupportedMultikeyType, + #[error("Incorrect prefix for did:key: {0}")] + IncorrectDIDKeyPrefix(String), + #[error(transparent)] + Multibase(#[from] multibase::Error), + #[error(transparent)] + Signature(#[from] ecdsa::signature::Error), +} + +pub type Result = std::result::Result; diff --git a/atrium-crypto/src/lib.rs b/atrium-crypto/src/lib.rs new file mode 100644 index 00000000..8866b59c --- /dev/null +++ b/atrium-crypto/src/lib.rs @@ -0,0 +1,8 @@ +#![doc = include_str!("../README.md")] +mod algorithm; +pub mod did; +pub mod error; + +pub use algorithm::Algorithm; + +const DID_KEY_PREFIX: &str = "did:key:"; diff --git a/atrium-libs/Cargo.toml b/atrium-libs/Cargo.toml index dc7bfa13..43bffc78 100644 --- a/atrium-libs/Cargo.toml +++ b/atrium-libs/Cargo.toml @@ -13,12 +13,9 @@ keywords.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +atrium-crypto = { workspace = true, optional = true } async-trait = { workspace = true, optional = true } -ecdsa = { workspace = true, optional = true } -k256 = { workspace = true, optional = true } -p256 = { workspace = true, optional = true } http = { workspace = true, optional = true } -multibase = { workspace = true, optional = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } thiserror = { workspace = true, optional = true } @@ -26,15 +23,13 @@ url = { workspace = true, optional = true } urlencoding = { workspace = true, optional = true } [dev-dependencies] -ecdsa = { workspace = true, features = ["signing"] } -hex = { workspace = true } mockito = { workspace = true } reqwest = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["macros"] } [features] -default = ["common-web", "crypto", "identity"] +default = ["common-web", "identity"] +atproto-data = ["atrium-crypto"] common-web = ["http", "serde/derive"] -crypto = ["ecdsa/verifying", "k256", "p256", "multibase"] identity = ["common-web", "async-trait", "serde_json", "thiserror", "url", "urlencoding"] diff --git a/atrium-libs/src/identity/did.rs b/atrium-libs/src/identity/did.rs index 8f99befa..ebee1413 100644 --- a/atrium-libs/src/identity/did.rs +++ b/atrium-libs/src/identity/did.rs @@ -1,6 +1,6 @@ use crate::common_web::did_doc::DidDocument; -#[cfg(feature = "crypto")] -mod atproto_data; +#[cfg(feature = "atproto-data")] +pub mod atproto_data; pub mod did_resolver; mod error; mod plc_resolver; diff --git a/atrium-libs/src/identity/did/atproto_data.rs b/atrium-libs/src/identity/did/atproto_data.rs index e82d4807..2cf8d41d 100644 --- a/atrium-libs/src/identity/did/atproto_data.rs +++ b/atrium-libs/src/identity/did/atproto_data.rs @@ -1,6 +1,6 @@ use crate::common_web::did_doc::DidDocument; -use crate::crypto::did::{format_did_key, format_did_key_str, parse_multikey}; -use crate::crypto::Algorithm; +use atrium_crypto::did::{format_did_key, format_did_key_str, parse_multikey}; +use atrium_crypto::Algorithm; use thiserror::Error; #[derive(Error, Debug)] @@ -12,7 +12,7 @@ pub enum Error { #[error("Could not parse pds from doc: {0:?}")] Pds(DidDocument), #[error(transparent)] - Crypto(#[from] crate::crypto::error::Error), + Crypto(#[from] atrium_crypto::error::Error), } pub type Result = std::result::Result; diff --git a/atrium-libs/src/lib.rs b/atrium-libs/src/lib.rs index fba3dcba..8e7dfbf2 100644 --- a/atrium-libs/src/lib.rs +++ b/atrium-libs/src/lib.rs @@ -1,6 +1,4 @@ #[cfg(feature = "common-web")] mod common_web; -#[cfg(feature = "crypto")] -mod crypto; #[cfg(feature = "identity")] mod identity; From 19492c1a504e671dfcf53364c2a28b20fd349bbe Mon Sep 17 00:00:00 2001 From: sugyan Date: Sun, 12 May 2024 16:34:11 +0900 Subject: [PATCH 09/17] Fix workflows --- .github/workflows/crypto.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/crypto.yml b/.github/workflows/crypto.yml index 20a49c0d..41fc8a52 100644 --- a/.github/workflows/crypto.yml +++ b/.github/workflows/crypto.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v4 - name: Build run: | - cargo build -p atrium-cypto --verbose + cargo build -p atrium-crypto --verbose - name: Run tests run: | cargo test -p atrium-crypto --lib From 1cb62e7ee56f8190128cabe71a52161946ce5f68 Mon Sep 17 00:00:00 2001 From: sugyan Date: Sun, 12 May 2024 16:43:40 +0900 Subject: [PATCH 10/17] Remove crypto from libs --- atrium-libs/src/crypto.rs | 7 -- atrium-libs/src/crypto/algorithm.rs | 49 ------------ atrium-libs/src/crypto/did.rs | 115 ---------------------------- atrium-libs/src/crypto/error.rs | 15 ---- 4 files changed, 186 deletions(-) delete mode 100644 atrium-libs/src/crypto.rs delete mode 100644 atrium-libs/src/crypto/algorithm.rs delete mode 100644 atrium-libs/src/crypto/did.rs delete mode 100644 atrium-libs/src/crypto/error.rs diff --git a/atrium-libs/src/crypto.rs b/atrium-libs/src/crypto.rs deleted file mode 100644 index 3fa764bf..00000000 --- a/atrium-libs/src/crypto.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod algorithm; -pub mod did; -pub mod error; - -pub use algorithm::Algorithm; - -const DID_KEY_PREFIX: &str = "did:key:"; diff --git a/atrium-libs/src/crypto/algorithm.rs b/atrium-libs/src/crypto/algorithm.rs deleted file mode 100644 index 34ef6a54..00000000 --- a/atrium-libs/src/crypto/algorithm.rs +++ /dev/null @@ -1,49 +0,0 @@ -use super::error::Result; -use ecdsa::VerifyingKey; -use k256::Secp256k1; -use multibase::Base; -use p256::NistP256; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Algorithm { - P256, - Secp256k1, -} - -impl Algorithm { - const MULTICODE_PREFIX_P256: [u8; 2] = [0x80, 0x24]; - const MULTICODE_PREFIX_SECP256K1: [u8; 2] = [0xe7, 0x01]; - - pub fn from_prefix(prefix: [u8; 2]) -> Option { - match prefix { - Self::MULTICODE_PREFIX_P256 => Some(Self::P256), - Self::MULTICODE_PREFIX_SECP256K1 => Some(Self::Secp256k1), - _ => None, - } - } - pub fn format_multikey(&self, key: &[u8]) -> Result { - let mut bytes = match self { - Algorithm::P256 => Self::MULTICODE_PREFIX_P256, - Algorithm::Secp256k1 => Self::MULTICODE_PREFIX_SECP256K1, - } - .to_vec(); - bytes.extend(self.pubkey_bytes(key, true)?); - Ok(multibase::encode(Base::Base58Btc, bytes)) - } - pub fn decompress_pubkey(&self, key: &[u8]) -> Result> { - self.pubkey_bytes(key, false) - } - fn pubkey_bytes(&self, key: &[u8], compress: bool) -> Result> { - Ok(match self { - Algorithm::P256 => VerifyingKey::::from_sec1_bytes(key)? - .to_encoded_point(compress) - .as_bytes() - .to_vec(), - - Algorithm::Secp256k1 => VerifyingKey::::from_sec1_bytes(key)? - .to_encoded_point(compress) - .as_bytes() - .to_vec(), - }) - } -} diff --git a/atrium-libs/src/crypto/did.rs b/atrium-libs/src/crypto/did.rs deleted file mode 100644 index fee7ac9b..00000000 --- a/atrium-libs/src/crypto/did.rs +++ /dev/null @@ -1,115 +0,0 @@ -use super::error::{Error, Result}; -use super::{Algorithm, DID_KEY_PREFIX}; - -pub fn parse_multikey(multikey: &str) -> Result<(Algorithm, Vec)> { - let (_, decoded) = multibase::decode(multikey)?; - if let Ok(prefix) = decoded[..2].try_into() { - if let Some(alg) = Algorithm::from_prefix(prefix) { - return Ok((alg, alg.decompress_pubkey(&decoded[2..])?)); - } - } - Err(Error::UnsupportedMultikeyType) -} - -pub fn format_did_key_str(alg: Algorithm, s: &str) -> Result { - let (_, key) = multibase::decode(s)?; - format_did_key(alg, &key) -} - -pub fn parse_did_key(did: &str) -> Result<(Algorithm, Vec)> { - if let Some(multikey) = did.strip_prefix(DID_KEY_PREFIX) { - parse_multikey(multikey) - } else { - Err(Error::IncorrectDIDKeyPrefix(did.to_string())) - } -} - -pub fn format_did_key(alg: Algorithm, key: &[u8]) -> Result { - Ok(DID_KEY_PREFIX.to_string() + &alg.format_multikey(key)?) -} - -#[cfg(test)] -mod tests { - use super::*; - use ecdsa::SigningKey; - use k256::Secp256k1; - use multibase::Base; - use p256::NistP256; - - // did:key secp256k1 test vectors from W3C - // https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/secp256k1.json - fn secp256k1_vectors() -> Vec<(&'static str, &'static str)> { - vec![ - ( - "9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c", - "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme", - ), - ( - "f0f4df55a2b3ff13051ea814a8f24ad00f2e469af73c363ac7e9fb999a9072ed", - "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2", - ), - ( - "6b0b91287ae3348f8c2f2552d766f30e3604867e34adc37ccbb74a8e6b893e02", - "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N", - ), - ( - "c0a6a7c560d37d7ba81ecee9543721ff48fea3e0fb827d42c1868226540fac15", - "did:key:zQ3shadCps5JLAHcZiuX5YUtWHHL8ysBJqFLWvjZDKAWUBGzy", - ), - ( - "175a232d440be1e0788f25488a73d9416c04b6f924bea6354bf05dd2f1a75133", - "did:key:zQ3shptjE6JwdkeKN4fcpnYQY3m9Cet3NiHdAfpvSUZBFoKBj", - ), - ] - } - - // did:key p-256 test vectors from W3C - // https://github.com/w3c-ccg/did-method-key/blob/main/test-vectors/nist-curves.json - fn p256_vectors() -> Vec<(&'static str, &'static str)> { - vec![( - "9p4VRzdmhsnq869vQjVCTrRry7u4TtfRxhvBFJTGU2Cp", - "did:key:zDnaeTiq1PdzvZXUaMdezchcMJQpBdH2VN4pgrrEhMCCbmwSb", - )] - } - - #[test] - fn secp256k1() { - for (seed, id) in secp256k1_vectors() { - let bytes = hex::decode(seed).expect("hex decoding should succeed"); - let sign = SigningKey::::from_slice(&bytes) - .expect("initializing signing key should succeed"); - let did_key = - format_did_key(Algorithm::Secp256k1, &sign.verifying_key().to_sec1_bytes()) - .expect("formatting DID key should succeed"); - assert_eq!(did_key, id); - - let (alg, key) = parse_did_key(&did_key).expect("parsing DID key should succeed"); - assert_eq!(alg, Algorithm::Secp256k1); - assert_eq!( - &key, - sign.verifying_key().to_encoded_point(false).as_bytes() - ); - } - } - - #[test] - fn p256() { - for (private_key_base58, id) in p256_vectors() { - let bytes = Base::Base58Btc - .decode(private_key_base58) - .expect("multibase decoding should succeed"); - let sign = SigningKey::::from_slice(&bytes) - .expect("initializing signing key should succeed"); - let did_key = format_did_key(Algorithm::P256, &sign.verifying_key().to_sec1_bytes()) - .expect("formatting DID key should succeed"); - assert_eq!(did_key, id); - - let (alg, key) = parse_did_key(&did_key).expect("parsing DID key should succeed"); - assert_eq!(alg, Algorithm::P256); - assert_eq!( - &key, - sign.verifying_key().to_encoded_point(false).as_bytes() - ); - } - } -} diff --git a/atrium-libs/src/crypto/error.rs b/atrium-libs/src/crypto/error.rs deleted file mode 100644 index fdcbbf51..00000000 --- a/atrium-libs/src/crypto/error.rs +++ /dev/null @@ -1,15 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("Unsupported key type")] - UnsupportedMultikeyType, - #[error("Incorrect prefix for did:key: {0}")] - IncorrectDIDKeyPrefix(String), - #[error(transparent)] - Multibase(#[from] multibase::Error), - #[error(transparent)] - Signature(#[from] ecdsa::signature::Error), -} - -pub type Result = std::result::Result; From 30faf8e8e5b35b9cf543d1e73cae66054b6f38fd Mon Sep 17 00:00:00 2001 From: sugyan Date: Mon, 13 May 2024 23:34:28 +0900 Subject: [PATCH 11/17] Implement crypto/keypair --- Cargo.lock | 13 +-- Cargo.toml | 5 +- atrium-crypto/Cargo.toml | 10 +-- atrium-crypto/src/algorithm.rs | 25 +++++- atrium-crypto/src/did.rs | 18 ++-- atrium-crypto/src/keypair.rs | 151 +++++++++++++++++++++++++++++++++ atrium-crypto/src/lib.rs | 1 + 7 files changed, 194 insertions(+), 29 deletions(-) create mode 100644 atrium-crypto/src/keypair.rs diff --git a/Cargo.lock b/Cargo.lock index 1f540e01..c727dbc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,6 +173,7 @@ dependencies = [ "k256", "multibase", "p256", + "rand", "thiserror", ] @@ -565,7 +566,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", - "pem-rfc7468", "zeroize", ] @@ -628,7 +628,6 @@ dependencies = [ "ff", "generic-array", "group", - "pem-rfc7468", "pkcs8", "rand_core", "sec1", @@ -1185,7 +1184,6 @@ dependencies = [ "elliptic-curve", "once_cell", "sha2", - "signature", ] [[package]] @@ -1480,15 +1478,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.1" diff --git a/Cargo.toml b/Cargo.toml index 08e5c4cf..5ad538c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,9 @@ urlencoding = "2.1.3" # Cryptography ecdsa = "0.16.9" -k256 = "0.13.3" -p256 = "0.13.2" +k256 = { version = "0.13.3", default-features = false } +p256 = { version = "0.13.2", default-features = false } +rand = "0.8.5" # Networking futures = { version = "0.3.30", default-features = false, features = ["alloc"] } diff --git a/atrium-crypto/Cargo.toml b/atrium-crypto/Cargo.toml index 1345017e..f875349a 100644 --- a/atrium-crypto/Cargo.toml +++ b/atrium-crypto/Cargo.toml @@ -13,12 +13,12 @@ keywords.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ecdsa = { workspace = true, features = ["verifying"] } -k256.workspace = true -p256.workspace = true +ecdsa = { workspace = true, features = ["signing", "verifying"] } +k256 = { workspace = true, features = ["std", "ecdsa"] } +p256 = { workspace = true, features = ["std", "ecdsa"] } multibase.workspace = true thiserror.workspace = true [dev-dependencies] -ecdsa = { workspace = true, features = ["signing"] } -hex = { workspace = true } +hex.workspace = true +rand.workspace = true diff --git a/atrium-crypto/src/algorithm.rs b/atrium-crypto/src/algorithm.rs index 34ef6a54..89602bf0 100644 --- a/atrium-crypto/src/algorithm.rs +++ b/atrium-crypto/src/algorithm.rs @@ -1,4 +1,6 @@ -use super::error::Result; +use crate::error::{Error, Result}; +use crate::keypair::verify_signature; +use crate::DID_KEY_PREFIX; use ecdsa::VerifyingKey; use k256::Secp256k1; use multibase::Base; @@ -14,6 +16,12 @@ impl Algorithm { const MULTICODE_PREFIX_P256: [u8; 2] = [0x80, 0x24]; const MULTICODE_PREFIX_SECP256K1: [u8; 2] = [0xe7, 0x01]; + pub fn prefix(&self) -> [u8; 2] { + match self { + Self::P256 => Self::MULTICODE_PREFIX_P256, + Self::Secp256k1 => Self::MULTICODE_PREFIX_SECP256K1, + } + } pub fn from_prefix(prefix: [u8; 2]) -> Option { match prefix { Self::MULTICODE_PREFIX_P256 => Some(Self::P256), @@ -33,13 +41,26 @@ impl Algorithm { pub fn decompress_pubkey(&self, key: &[u8]) -> Result> { self.pubkey_bytes(key, false) } + pub fn verify_signature(&self, did: &str, msg: &[u8], signature: &[u8]) -> Result<()> { + if let Some(multikey) = did.strip_prefix(DID_KEY_PREFIX) { + let (_, decoded) = multibase::decode(multikey)?; + if decoded[..2] == self.prefix() { + return match self { + Algorithm::P256 => unimplemented!(), + Algorithm::Secp256k1 => { + verify_signature::(&decoded[2..], msg, signature) + } + }; + } + } + Err(Error::IncorrectDIDKeyPrefix(did.to_string())) + } fn pubkey_bytes(&self, key: &[u8], compress: bool) -> Result> { Ok(match self { Algorithm::P256 => VerifyingKey::::from_sec1_bytes(key)? .to_encoded_point(compress) .as_bytes() .to_vec(), - Algorithm::Secp256k1 => VerifyingKey::::from_sec1_bytes(key)? .to_encoded_point(compress) .as_bytes() diff --git a/atrium-crypto/src/did.rs b/atrium-crypto/src/did.rs index fee7ac9b..fe4903a7 100644 --- a/atrium-crypto/src/did.rs +++ b/atrium-crypto/src/did.rs @@ -76,18 +76,20 @@ mod tests { fn secp256k1() { for (seed, id) in secp256k1_vectors() { let bytes = hex::decode(seed).expect("hex decoding should succeed"); - let sign = SigningKey::::from_slice(&bytes) + let sig_key = SigningKey::::from_slice(&bytes) .expect("initializing signing key should succeed"); - let did_key = - format_did_key(Algorithm::Secp256k1, &sign.verifying_key().to_sec1_bytes()) - .expect("formatting DID key should succeed"); + let did_key = format_did_key( + Algorithm::Secp256k1, + &sig_key.verifying_key().to_sec1_bytes(), + ) + .expect("formatting DID key should succeed"); assert_eq!(did_key, id); let (alg, key) = parse_did_key(&did_key).expect("parsing DID key should succeed"); assert_eq!(alg, Algorithm::Secp256k1); assert_eq!( &key, - sign.verifying_key().to_encoded_point(false).as_bytes() + sig_key.verifying_key().to_encoded_point(false).as_bytes() ); } } @@ -98,9 +100,9 @@ mod tests { let bytes = Base::Base58Btc .decode(private_key_base58) .expect("multibase decoding should succeed"); - let sign = SigningKey::::from_slice(&bytes) + let sig_key = SigningKey::::from_slice(&bytes) .expect("initializing signing key should succeed"); - let did_key = format_did_key(Algorithm::P256, &sign.verifying_key().to_sec1_bytes()) + let did_key = format_did_key(Algorithm::P256, &sig_key.verifying_key().to_sec1_bytes()) .expect("formatting DID key should succeed"); assert_eq!(did_key, id); @@ -108,7 +110,7 @@ mod tests { assert_eq!(alg, Algorithm::P256); assert_eq!( &key, - sign.verifying_key().to_encoded_point(false).as_bytes() + sig_key.verifying_key().to_encoded_point(false).as_bytes() ); } } diff --git a/atrium-crypto/src/keypair.rs b/atrium-crypto/src/keypair.rs new file mode 100644 index 00000000..c9959975 --- /dev/null +++ b/atrium-crypto/src/keypair.rs @@ -0,0 +1,151 @@ +use crate::error::Result; +use ecdsa::elliptic_curve::{ + generic_array::ArrayLength, + ops::Invert, + point::PointCompression, + sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, + subtle::CtOption, + AffinePoint, CurveArithmetic, FieldBytesSize, PrimeCurve, Scalar, +}; +use ecdsa::hazmat::{DigestPrimitive, SignPrimitive, VerifyPrimitive}; +use ecdsa::signature::{ + rand_core::CryptoRngCore, + {Signer, Verifier}, +}; +use ecdsa::{Signature, SignatureSize, SigningKey, VerifyingKey}; +use k256::Secp256k1; + +pub struct Keypair +where + C: PrimeCurve + CurveArithmetic, + Scalar: Invert>> + SignPrimitive, + SignatureSize: ArrayLength, +{ + signing_key: SigningKey, +} + +impl Keypair +where + C: PrimeCurve + CurveArithmetic, + Scalar: Invert>> + SignPrimitive, + SignatureSize: ArrayLength, +{ + pub fn create(rng: &mut impl CryptoRngCore) -> Self { + Self { + signing_key: SigningKey::::random(rng), + } + } + pub fn import(priv_key: &[u8]) -> Result { + Ok(Self { + signing_key: SigningKey::from_slice(priv_key)?, + }) + } +} + +impl Keypair +where + C: PrimeCurve + CurveArithmetic + DigestPrimitive, + Scalar: Invert>> + SignPrimitive, + SignatureSize: ArrayLength, +{ + pub fn sign(&self, msg: &[u8]) -> Result> { + let signature: Signature<_> = self.signing_key.try_sign(msg)?; + Ok(signature.to_bytes().to_vec()) + } +} + +impl Keypair +where + C: PrimeCurve + CurveArithmetic + PointCompression, + Scalar: Invert>> + SignPrimitive, + SignatureSize: ArrayLength, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, +{ + pub fn public_key_bytes(&self) -> Vec { + self.signing_key.verifying_key().to_sec1_bytes().to_vec() + } +} + +pub trait Export { + fn export(&self) -> Vec; +} + +pub type Secp256k1Keypair = Keypair; + +impl Export for Secp256k1Keypair { + fn export(&self) -> Vec { + self.signing_key.to_bytes().to_vec() + } +} + +pub(crate) fn verify_signature(public_key: &[u8], msg: &[u8], signature: &[u8]) -> Result<()> +where + C: PrimeCurve + CurveArithmetic + DigestPrimitive, + AffinePoint: VerifyPrimitive + FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, + SignatureSize: ArrayLength, +{ + let verifying_key = VerifyingKey::::from_sec1_bytes(public_key)?; + let signature = Signature::from_slice(signature)?; + Ok(verifying_key.verify(msg, &signature)?) +} + +#[cfg(test)] +mod tests { + use super::Secp256k1Keypair; + use crate::{did::format_did_key, Algorithm}; + use rand::rngs::ThreadRng; + + #[test] + fn secp256k1_export() { + let keypair = Secp256k1Keypair::create(&mut ThreadRng::default()); + let exported = { + use super::Export; + keypair.export() + }; + let imported = + Secp256k1Keypair::import(&exported).expect("importing keypair should succeed"); + let did = format_did_key(Algorithm::Secp256k1, &keypair.public_key_bytes()) + .expect("formatting to did key should succeed"); + let imported_did = format_did_key(Algorithm::Secp256k1, &imported.public_key_bytes()) + .expect("formatting to did key should succeed"); + assert_eq!(did, imported_did); + } + + #[test] + fn secp256k1_verify() { + let keypair = Secp256k1Keypair::create(&mut ThreadRng::default()); + let did = format_did_key(Algorithm::Secp256k1, &keypair.public_key_bytes()) + .expect("formatting to did key should succeed"); + let msg = [1, 2, 3, 4, 5, 6, 7, 8]; + let signature = keypair.sign(&msg).expect("signing should succeed"); + let mut corrupted_signature = signature.clone(); + corrupted_signature[0] = corrupted_signature[0].wrapping_add(1); + + assert!( + Algorithm::Secp256k1 + .verify_signature(&did, &msg, &signature) + .is_ok(), + "verifying signature should succeed" + ); + assert!( + Algorithm::Secp256k1 + .verify_signature(&did, &msg[..7], &signature) + .is_err(), + "verifying signature should fail with incorrect message" + ); + assert!( + Algorithm::Secp256k1 + .verify_signature(&did, &msg, &corrupted_signature) + .is_err(), + "verifying signature should fail with incorrect signature" + ); + assert!( + Algorithm::P256 + .verify_signature(&did, &msg, &signature) + .is_err(), + "verifying signature should fail with incorrect algorithm" + ); + } +} diff --git a/atrium-crypto/src/lib.rs b/atrium-crypto/src/lib.rs index 8866b59c..07085637 100644 --- a/atrium-crypto/src/lib.rs +++ b/atrium-crypto/src/lib.rs @@ -2,6 +2,7 @@ mod algorithm; pub mod did; pub mod error; +pub mod keypair; pub use algorithm::Algorithm; From bf6ee071f5d5871859151507e5ea2a0454644f58 Mon Sep 17 00:00:00 2001 From: sugyan Date: Tue, 14 May 2024 16:37:02 +0900 Subject: [PATCH 12/17] Update crypto/keypair --- atrium-crypto/src/algorithm.rs | 33 +++--- atrium-crypto/src/did.rs | 9 +- atrium-crypto/src/keypair.rs | 192 +++++++++++++++++++++++++++------ 3 files changed, 181 insertions(+), 53 deletions(-) diff --git a/atrium-crypto/src/algorithm.rs b/atrium-crypto/src/algorithm.rs index 89602bf0..f1820061 100644 --- a/atrium-crypto/src/algorithm.rs +++ b/atrium-crypto/src/algorithm.rs @@ -1,6 +1,5 @@ -use crate::error::{Error, Result}; +use crate::error::Result; use crate::keypair::verify_signature; -use crate::DID_KEY_PREFIX; use ecdsa::VerifyingKey; use k256::Secp256k1; use multibase::Base; @@ -30,30 +29,22 @@ impl Algorithm { } } pub fn format_multikey(&self, key: &[u8]) -> Result { - let mut bytes = match self { - Algorithm::P256 => Self::MULTICODE_PREFIX_P256, - Algorithm::Secp256k1 => Self::MULTICODE_PREFIX_SECP256K1, - } - .to_vec(); - bytes.extend(self.pubkey_bytes(key, true)?); - Ok(multibase::encode(Base::Base58Btc, bytes)) + Ok(self.format_mulikey_compressed(&self.pubkey_bytes(key, true)?)) + } + pub(crate) fn format_mulikey_compressed(&self, key: &[u8]) -> String { + let mut v = Vec::with_capacity(2 + key.len()); + v.extend_from_slice(&self.prefix()); + v.extend_from_slice(key); + multibase::encode(Base::Base58Btc, v) } pub fn decompress_pubkey(&self, key: &[u8]) -> Result> { self.pubkey_bytes(key, false) } - pub fn verify_signature(&self, did: &str, msg: &[u8], signature: &[u8]) -> Result<()> { - if let Some(multikey) = did.strip_prefix(DID_KEY_PREFIX) { - let (_, decoded) = multibase::decode(multikey)?; - if decoded[..2] == self.prefix() { - return match self { - Algorithm::P256 => unimplemented!(), - Algorithm::Secp256k1 => { - verify_signature::(&decoded[2..], msg, signature) - } - }; - } + pub fn verify_signature(&self, public_key: &[u8], msg: &[u8], signature: &[u8]) -> Result<()> { + match self { + Algorithm::P256 => verify_signature::(public_key, msg, signature), + Algorithm::Secp256k1 => verify_signature::(public_key, msg, signature), } - Err(Error::IncorrectDIDKeyPrefix(did.to_string())) } fn pubkey_bytes(&self, key: &[u8], compress: bool) -> Result> { Ok(match self { diff --git a/atrium-crypto/src/did.rs b/atrium-crypto/src/did.rs index fe4903a7..39c9b2bb 100644 --- a/atrium-crypto/src/did.rs +++ b/atrium-crypto/src/did.rs @@ -25,7 +25,14 @@ pub fn parse_did_key(did: &str) -> Result<(Algorithm, Vec)> { } pub fn format_did_key(alg: Algorithm, key: &[u8]) -> Result { - Ok(DID_KEY_PREFIX.to_string() + &alg.format_multikey(key)?) + Ok(prefix_did_key(&alg.format_multikey(key)?)) +} + +pub(crate) fn prefix_did_key(multikey: &str) -> String { + let mut ret = String::with_capacity(DID_KEY_PREFIX.len() + multikey.len()); + ret.push_str(DID_KEY_PREFIX); + ret.push_str(multikey); + ret } #[cfg(test)] diff --git a/atrium-crypto/src/keypair.rs b/atrium-crypto/src/keypair.rs index c9959975..52f7d6de 100644 --- a/atrium-crypto/src/keypair.rs +++ b/atrium-crypto/src/keypair.rs @@ -1,8 +1,7 @@ -use crate::error::Result; +use crate::{did::prefix_did_key, error::Result, Algorithm}; use ecdsa::elliptic_curve::{ generic_array::ArrayLength, ops::Invert, - point::PointCompression, sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, subtle::CtOption, AffinePoint, CurveArithmetic, FieldBytesSize, PrimeCurve, Scalar, @@ -14,6 +13,7 @@ use ecdsa::signature::{ }; use ecdsa::{Signature, SignatureSize, SigningKey, VerifyingKey}; use k256::Secp256k1; +use p256::NistP256; pub struct Keypair where @@ -42,6 +42,22 @@ where } } +impl Keypair +where + C: PrimeCurve + CurveArithmetic, + Scalar: Invert>> + SignPrimitive, + SignatureSize: ArrayLength, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, +{ + fn compressed_public_key(&self) -> Box<[u8]> { + self.signing_key + .verifying_key() + .to_encoded_point(true) + .to_bytes() + } +} + impl Keypair where C: PrimeCurve + CurveArithmetic + DigestPrimitive, @@ -54,28 +70,40 @@ where } } -impl Keypair +pub trait Did { + fn did(&self) -> String; +} + +pub trait Export { + fn export(&self) -> Vec; +} + +impl Export for Keypair where - C: PrimeCurve + CurveArithmetic + PointCompression, + C: PrimeCurve + CurveArithmetic, Scalar: Invert>> + SignPrimitive, SignatureSize: ArrayLength, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, { - pub fn public_key_bytes(&self) -> Vec { - self.signing_key.verifying_key().to_sec1_bytes().to_vec() + fn export(&self) -> Vec { + self.signing_key.to_bytes().to_vec() } } -pub trait Export { - fn export(&self) -> Vec; +pub type P256Keypair = Keypair; + +impl Did for P256Keypair { + fn did(&self) -> String { + prefix_did_key(&Algorithm::P256.format_mulikey_compressed(&self.compressed_public_key())) + } } pub type Secp256k1Keypair = Keypair; -impl Export for Secp256k1Keypair { - fn export(&self) -> Vec { - self.signing_key.to_bytes().to_vec() +impl Did for Secp256k1Keypair { + fn did(&self) -> String { + prefix_did_key( + &Algorithm::Secp256k1.format_mulikey_compressed(&self.compressed_public_key()), + ) } } @@ -93,10 +121,77 @@ where #[cfg(test)] mod tests { - use super::Secp256k1Keypair; - use crate::{did::format_did_key, Algorithm}; + use super::{P256Keypair, Secp256k1Keypair}; + use crate::did::{format_did_key, parse_did_key}; + use crate::Algorithm; use rand::rngs::ThreadRng; + #[test] + fn p256_did() { + let keypair = P256Keypair::create(&mut ThreadRng::default()); + let did = { + use super::Did; + keypair.did() + }; + let formatted = format_did_key( + Algorithm::P256, + &keypair.signing_key.verifying_key().to_sec1_bytes(), + ) + .expect("formatting to did key should succeed"); + assert_eq!(did, formatted); + + let (alg, public_key) = parse_did_key(&did).expect("parsing did key should succeed"); + assert_eq!(alg, Algorithm::P256); + assert_eq!( + public_key, + keypair + .signing_key + .verifying_key() + .to_encoded_point(false) + .as_bytes() + ); + } + + #[test] + fn secp256k1_did() { + let keypair = Secp256k1Keypair::create(&mut ThreadRng::default()); + let did = { + use super::Did; + keypair.did() + }; + let formatted = format_did_key( + Algorithm::Secp256k1, + &keypair.signing_key.verifying_key().to_sec1_bytes(), + ) + .expect("formatting to did key should succeed"); + assert_eq!(did, formatted); + + let (alg, public_key) = parse_did_key(&did).expect("parsing did key should succeed"); + assert_eq!(alg, Algorithm::Secp256k1); + assert_eq!( + public_key, + keypair + .signing_key + .verifying_key() + .to_encoded_point(false) + .as_bytes() + ); + } + + #[test] + fn p256_export() { + let keypair = P256Keypair::create(&mut ThreadRng::default()); + let exported = { + use super::Export; + keypair.export() + }; + let imported = P256Keypair::import(&exported).expect("importing keypair should succeed"); + { + use super::Did; + assert_eq!(keypair.did(), imported.did()); + } + } + #[test] fn secp256k1_export() { let keypair = Secp256k1Keypair::create(&mut ThreadRng::default()); @@ -106,44 +201,79 @@ mod tests { }; let imported = Secp256k1Keypair::import(&exported).expect("importing keypair should succeed"); - let did = format_did_key(Algorithm::Secp256k1, &keypair.public_key_bytes()) - .expect("formatting to did key should succeed"); - let imported_did = format_did_key(Algorithm::Secp256k1, &imported.public_key_bytes()) - .expect("formatting to did key should succeed"); - assert_eq!(did, imported_did); + { + use super::Did; + assert_eq!(keypair.did(), imported.did()); + } + } + + #[test] + fn p256_verify() { + let keypair = P256Keypair::create(&mut ThreadRng::default()); + let did = { + use super::Did; + keypair.did() + }; + let (alg, public_key) = parse_did_key(&did).expect("parsing did key should succeed"); + assert_eq!(alg, Algorithm::P256); + + let msg = [1, 2, 3, 4, 5, 6, 7, 8]; + let signature = keypair.sign(&msg).expect("signing should succeed"); + let mut corrupted_signature = signature.clone(); + corrupted_signature[0] = corrupted_signature[0].wrapping_add(1); + assert!( + alg.verify_signature(&public_key, &msg, &signature).is_ok(), + "verifying signature should succeed" + ); + assert!( + alg.verify_signature(&public_key, &msg[..7], &signature) + .is_err(), + "verifying signature should fail with incorrect message" + ); + assert!( + alg.verify_signature(&public_key, &msg, &corrupted_signature) + .is_err(), + "verifying signature should fail with incorrect signature" + ); + assert!( + Algorithm::Secp256k1 + .verify_signature(&public_key, &msg, &signature) + .is_err(), + "verifying signature should fail with incorrect algorithm" + ); } #[test] fn secp256k1_verify() { let keypair = Secp256k1Keypair::create(&mut ThreadRng::default()); - let did = format_did_key(Algorithm::Secp256k1, &keypair.public_key_bytes()) - .expect("formatting to did key should succeed"); + let did = { + use super::Did; + keypair.did() + }; + let (alg, public_key) = parse_did_key(&did).expect("parsing did key should succeed"); + assert_eq!(alg, Algorithm::Secp256k1); + let msg = [1, 2, 3, 4, 5, 6, 7, 8]; let signature = keypair.sign(&msg).expect("signing should succeed"); let mut corrupted_signature = signature.clone(); corrupted_signature[0] = corrupted_signature[0].wrapping_add(1); - assert!( - Algorithm::Secp256k1 - .verify_signature(&did, &msg, &signature) - .is_ok(), + alg.verify_signature(&public_key, &msg, &signature).is_ok(), "verifying signature should succeed" ); assert!( - Algorithm::Secp256k1 - .verify_signature(&did, &msg[..7], &signature) + alg.verify_signature(&public_key, &msg[..7], &signature) .is_err(), "verifying signature should fail with incorrect message" ); assert!( - Algorithm::Secp256k1 - .verify_signature(&did, &msg, &corrupted_signature) + alg.verify_signature(&public_key, &msg, &corrupted_signature) .is_err(), "verifying signature should fail with incorrect signature" ); assert!( Algorithm::P256 - .verify_signature(&did, &msg, &signature) + .verify_signature(&public_key, &msg, &signature) .is_err(), "verifying signature should fail with incorrect algorithm" ); From 5e256e5210117c722524500a26a0754f385c5d5d Mon Sep 17 00:00:00 2001 From: sugyan Date: Tue, 14 May 2024 17:07:15 +0900 Subject: [PATCH 13/17] Add crypto/verify, Update README --- atrium-crypto/README.md | 30 ++++++++++++++++++++++++++++++ atrium-crypto/src/lib.rs | 1 + atrium-crypto/src/verify.rs | 6 ++++++ 3 files changed, 37 insertions(+) create mode 100644 atrium-crypto/src/verify.rs diff --git a/atrium-crypto/README.md b/atrium-crypto/README.md index 7a27f3d9..37548d5c 100644 --- a/atrium-crypto/README.md +++ b/atrium-crypto/README.md @@ -8,3 +8,33 @@ This package implements the two currently supported cryptographic systems: - [`k256`](https://crates.io/crates/k256) elliptic curve: aka "NIST K-256", aka `secp256k1` (note the `k`) The details of cryptography in atproto are described in [the specification](https://atproto.com/specs/cryptography). This includes string encodings, validity of "low-S" signatures, byte representation "compression", hashing, and more. + +## Usage + +```rust +use atrium_crypto::keypair::{Secp256k1Keypair, Did}; +use atrium_crypto::verify::verify_signature; +use rand::rngs::ThreadRng; + +fn main() -> Result<(), Box>{ + // generate a new random K-256 private key + let keypair = Secp256k1Keypair::create(&mut ThreadRng::default()); + + // sign binary data, resulting signature bytes. + // SHA-256 hash of data is what actually gets signed. + let msg = [1, 2, 3, 4, 5, 6, 7, 8]; + let signature = keypair.sign(&msg)?; + + // serialize the public key as a did:key string, which includes key type metadata + let pub_did_key = keypair.did(); + println!("{pub_did_key}"); + // output would look something like: 'did:key:zQ3shVRtgqTRHC7Lj4DYScoDgReNpsDp3HBnuKBKt1FSXKQ38' + + // verify signature using public key + match verify_signature(&pub_did_key, &msg, &signature) { + Ok(()) => println!("Success"), + Err(_) => panic!("Uh oh, something is fishy"), + } + Ok(()) +} +``` diff --git a/atrium-crypto/src/lib.rs b/atrium-crypto/src/lib.rs index 07085637..10da2f0a 100644 --- a/atrium-crypto/src/lib.rs +++ b/atrium-crypto/src/lib.rs @@ -3,6 +3,7 @@ mod algorithm; pub mod did; pub mod error; pub mod keypair; +pub mod verify; pub use algorithm::Algorithm; diff --git a/atrium-crypto/src/verify.rs b/atrium-crypto/src/verify.rs new file mode 100644 index 00000000..e05b48d3 --- /dev/null +++ b/atrium-crypto/src/verify.rs @@ -0,0 +1,6 @@ +use crate::{did::parse_did_key, error::Result}; + +pub fn verify_signature(did_key: &str, msg: &[u8], signature: &[u8]) -> Result<()> { + let (alg, public_key) = parse_did_key(did_key)?; + alg.verify_signature(&public_key, msg, signature) +} From 2d372e378acb243de2a7acf5f0c1a8209db40f60 Mon Sep 17 00:00:00 2001 From: sugyan Date: Tue, 14 May 2024 18:13:04 +0900 Subject: [PATCH 14/17] Add tests --- atrium-crypto/src/algorithm.rs | 82 +++++++++++++++++++++++++++++++++- atrium-crypto/src/did.rs | 10 ++--- 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/atrium-crypto/src/algorithm.rs b/atrium-crypto/src/algorithm.rs index f1820061..28ecb165 100644 --- a/atrium-crypto/src/algorithm.rs +++ b/atrium-crypto/src/algorithm.rs @@ -37,7 +37,7 @@ impl Algorithm { v.extend_from_slice(key); multibase::encode(Base::Base58Btc, v) } - pub fn decompress_pubkey(&self, key: &[u8]) -> Result> { + pub(crate) fn decompress_pubkey(&self, key: &[u8]) -> Result> { self.pubkey_bytes(key, false) } pub fn verify_signature(&self, public_key: &[u8], msg: &[u8], signature: &[u8]) -> Result<()> { @@ -59,3 +59,83 @@ impl Algorithm { }) } } + +#[cfg(test)] +mod tests { + use super::Algorithm; + use crate::did::parse_did_key; + use crate::keypair::{Did, P256Keypair, Secp256k1Keypair}; + use rand::rngs::ThreadRng; + + #[test] + fn p256_compress_decompress() { + let did = P256Keypair::create(&mut ThreadRng::default()).did(); + let (alg, key) = parse_did_key(&did).expect("parsing did key should succeed"); + assert_eq!(alg, Algorithm::P256); + // compress a key to the correct length + let compressed = alg + .pubkey_bytes(&key, true) + .expect("compressing public key should succeed"); + assert_eq!(compressed.len(), 33); + // decompress a key to the original + let decompressed = alg + .pubkey_bytes(&compressed, false) + .expect("decompressing public key should succeed"); + assert_eq!(decompressed.len(), 65); + assert_eq!(key, decompressed); + + // works consitesntly + let keys = (0..100) + .map(|_| { + let did = P256Keypair::create(&mut ThreadRng::default()).did(); + let (_, key) = parse_did_key(&did).expect("parsing did key should succeed"); + key + }) + .collect::>(); + let compressed = keys + .iter() + .filter_map(|key| alg.pubkey_bytes(key, true).ok()) + .collect::>(); + let decompressed = compressed + .iter() + .filter_map(|key| alg.pubkey_bytes(key, false).ok()) + .collect::>(); + assert_eq!(keys, decompressed); + } + + #[test] + fn secp256k1_compress_decompress() { + let did = Secp256k1Keypair::create(&mut ThreadRng::default()).did(); + let (alg, key) = parse_did_key(&did).expect("parsing did key should succeed"); + assert_eq!(alg, Algorithm::Secp256k1); + // compress a key to the correct length + let compressed = alg + .pubkey_bytes(&key, true) + .expect("compressing public key should succeed"); + assert_eq!(compressed.len(), 33); + // decompress a key to the original + let decompressed = alg + .pubkey_bytes(&compressed, false) + .expect("decompressing public key should succeed"); + assert_eq!(decompressed.len(), 65); + assert_eq!(key, decompressed); + + // works consitesntly + let keys = (0..100) + .map(|_| { + let did = Secp256k1Keypair::create(&mut ThreadRng::default()).did(); + let (_, key) = parse_did_key(&did).expect("parsing did key should succeed"); + key + }) + .collect::>(); + let compressed = keys + .iter() + .filter_map(|key| alg.pubkey_bytes(key, true).ok()) + .collect::>(); + let decompressed = compressed + .iter() + .filter_map(|key| alg.pubkey_bytes(key, false).ok()) + .collect::>(); + assert_eq!(keys, decompressed); + } +} diff --git a/atrium-crypto/src/did.rs b/atrium-crypto/src/did.rs index 39c9b2bb..7beaea4d 100644 --- a/atrium-crypto/src/did.rs +++ b/atrium-crypto/src/did.rs @@ -11,11 +11,6 @@ pub fn parse_multikey(multikey: &str) -> Result<(Algorithm, Vec)> { Err(Error::UnsupportedMultikeyType) } -pub fn format_did_key_str(alg: Algorithm, s: &str) -> Result { - let (_, key) = multibase::decode(s)?; - format_did_key(alg, &key) -} - pub fn parse_did_key(did: &str) -> Result<(Algorithm, Vec)> { if let Some(multikey) = did.strip_prefix(DID_KEY_PREFIX) { parse_multikey(multikey) @@ -24,6 +19,11 @@ pub fn parse_did_key(did: &str) -> Result<(Algorithm, Vec)> { } } +pub fn format_did_key_str(alg: Algorithm, s: &str) -> Result { + let (_, key) = multibase::decode(s)?; + format_did_key(alg, &key) +} + pub fn format_did_key(alg: Algorithm, key: &[u8]) -> Result { Ok(prefix_did_key(&alg.format_multikey(key)?)) } From d236db03e67da19b7b23b075982445a71c935e27 Mon Sep 17 00:00:00 2001 From: sugyan Date: Wed, 15 May 2024 23:07:51 +0900 Subject: [PATCH 15/17] Implement all tests for crypto --- Cargo.lock | 3 +- atrium-crypto/Cargo.toml | 8 +- atrium-crypto/src/algorithm.rs | 12 +- atrium-crypto/src/error.rs | 4 + atrium-crypto/src/keypair.rs | 54 ++--- atrium-crypto/src/verify.rs | 227 +++++++++++++++++- .../tests/data/signature-fixtures.json | 68 ++++++ 7 files changed, 333 insertions(+), 43 deletions(-) create mode 100644 atrium-crypto/tests/data/signature-fixtures.json diff --git a/Cargo.lock b/Cargo.lock index c727dbc9..c2639c01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -174,6 +174,8 @@ dependencies = [ "multibase", "p256", "rand", + "serde", + "serde_json", "thiserror", ] @@ -1182,7 +1184,6 @@ dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "once_cell", "sha2", ] diff --git a/atrium-crypto/Cargo.toml b/atrium-crypto/Cargo.toml index f875349a..99ac73ff 100644 --- a/atrium-crypto/Cargo.toml +++ b/atrium-crypto/Cargo.toml @@ -13,12 +13,14 @@ keywords.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ecdsa = { workspace = true, features = ["signing", "verifying"] } -k256 = { workspace = true, features = ["std", "ecdsa"] } -p256 = { workspace = true, features = ["std", "ecdsa"] } +ecdsa = { workspace = true, features = ["std", "signing", "verifying"] } +k256 = { workspace = true, features = ["ecdsa"] } +p256 = { workspace = true, features = ["ecdsa"] } multibase.workspace = true thiserror.workspace = true [dev-dependencies] hex.workspace = true rand.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true diff --git a/atrium-crypto/src/algorithm.rs b/atrium-crypto/src/algorithm.rs index 28ecb165..7c6e85f1 100644 --- a/atrium-crypto/src/algorithm.rs +++ b/atrium-crypto/src/algorithm.rs @@ -1,5 +1,4 @@ use crate::error::Result; -use crate::keypair::verify_signature; use ecdsa::VerifyingKey; use k256::Secp256k1; use multibase::Base; @@ -29,7 +28,7 @@ impl Algorithm { } } pub fn format_multikey(&self, key: &[u8]) -> Result { - Ok(self.format_mulikey_compressed(&self.pubkey_bytes(key, true)?)) + Ok(self.format_mulikey_compressed(&self.compress_pubkey(key)?)) } pub(crate) fn format_mulikey_compressed(&self, key: &[u8]) -> String { let mut v = Vec::with_capacity(2 + key.len()); @@ -37,15 +36,12 @@ impl Algorithm { v.extend_from_slice(key); multibase::encode(Base::Base58Btc, v) } + pub(crate) fn compress_pubkey(&self, key: &[u8]) -> Result> { + self.pubkey_bytes(key, true) + } pub(crate) fn decompress_pubkey(&self, key: &[u8]) -> Result> { self.pubkey_bytes(key, false) } - pub fn verify_signature(&self, public_key: &[u8], msg: &[u8], signature: &[u8]) -> Result<()> { - match self { - Algorithm::P256 => verify_signature::(public_key, msg, signature), - Algorithm::Secp256k1 => verify_signature::(public_key, msg, signature), - } - } fn pubkey_bytes(&self, key: &[u8], compress: bool) -> Result> { Ok(match self { Algorithm::P256 => VerifyingKey::::from_sec1_bytes(key)? diff --git a/atrium-crypto/src/error.rs b/atrium-crypto/src/error.rs index fdcbbf51..0036e25e 100644 --- a/atrium-crypto/src/error.rs +++ b/atrium-crypto/src/error.rs @@ -6,6 +6,10 @@ pub enum Error { UnsupportedMultikeyType, #[error("Incorrect prefix for did:key: {0}")] IncorrectDIDKeyPrefix(String), + #[error("Low-S Signature is not allowed")] + LowSSignatureNotAllowed, + #[error("Signature is invalid")] + InvalidSignature, #[error(transparent)] Multibase(#[from] multibase::Error), #[error(transparent)] diff --git a/atrium-crypto/src/keypair.rs b/atrium-crypto/src/keypair.rs index 52f7d6de..867e33fe 100644 --- a/atrium-crypto/src/keypair.rs +++ b/atrium-crypto/src/keypair.rs @@ -6,12 +6,9 @@ use ecdsa::elliptic_curve::{ subtle::CtOption, AffinePoint, CurveArithmetic, FieldBytesSize, PrimeCurve, Scalar, }; -use ecdsa::hazmat::{DigestPrimitive, SignPrimitive, VerifyPrimitive}; -use ecdsa::signature::{ - rand_core::CryptoRngCore, - {Signer, Verifier}, -}; -use ecdsa::{Signature, SignatureSize, SigningKey, VerifyingKey}; +use ecdsa::hazmat::{DigestPrimitive, SignPrimitive}; +use ecdsa::signature::{rand_core::CryptoRngCore, Signer}; +use ecdsa::{Signature, SignatureSize, SigningKey}; use k256::Secp256k1; use p256::NistP256; @@ -66,7 +63,11 @@ where { pub fn sign(&self, msg: &[u8]) -> Result> { let signature: Signature<_> = self.signing_key.try_sign(msg)?; - Ok(signature.to_bytes().to_vec()) + Ok(signature + .normalize_s() + .unwrap_or(signature) + .to_bytes() + .to_vec()) } } @@ -107,22 +108,11 @@ impl Did for Secp256k1Keypair { } } -pub(crate) fn verify_signature(public_key: &[u8], msg: &[u8], signature: &[u8]) -> Result<()> -where - C: PrimeCurve + CurveArithmetic + DigestPrimitive, - AffinePoint: VerifyPrimitive + FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, - SignatureSize: ArrayLength, -{ - let verifying_key = VerifyingKey::::from_sec1_bytes(public_key)?; - let signature = Signature::from_slice(signature)?; - Ok(verifying_key.verify(msg, &signature)?) -} - #[cfg(test)] mod tests { use super::{P256Keypair, Secp256k1Keypair}; use crate::did::{format_did_key, parse_did_key}; + use crate::verify::Verifier; use crate::Algorithm; use rand::rngs::ThreadRng; @@ -217,27 +207,30 @@ mod tests { let (alg, public_key) = parse_did_key(&did).expect("parsing did key should succeed"); assert_eq!(alg, Algorithm::P256); + let verifier = Verifier::default(); let msg = [1, 2, 3, 4, 5, 6, 7, 8]; let signature = keypair.sign(&msg).expect("signing should succeed"); let mut corrupted_signature = signature.clone(); corrupted_signature[0] = corrupted_signature[0].wrapping_add(1); assert!( - alg.verify_signature(&public_key, &msg, &signature).is_ok(), + verifier.verify(alg, &public_key, &msg, &signature).is_ok(), "verifying signature should succeed" ); assert!( - alg.verify_signature(&public_key, &msg[..7], &signature) + verifier + .verify(alg, &public_key, &msg[..7], &signature) .is_err(), "verifying signature should fail with incorrect message" ); assert!( - alg.verify_signature(&public_key, &msg, &corrupted_signature) + verifier + .verify(alg, &public_key, &msg, &corrupted_signature) .is_err(), "verifying signature should fail with incorrect signature" ); assert!( - Algorithm::Secp256k1 - .verify_signature(&public_key, &msg, &signature) + verifier + .verify(Algorithm::Secp256k1, &public_key, &msg, &signature) .is_err(), "verifying signature should fail with incorrect algorithm" ); @@ -253,27 +246,30 @@ mod tests { let (alg, public_key) = parse_did_key(&did).expect("parsing did key should succeed"); assert_eq!(alg, Algorithm::Secp256k1); + let verifier = Verifier::default(); let msg = [1, 2, 3, 4, 5, 6, 7, 8]; let signature = keypair.sign(&msg).expect("signing should succeed"); let mut corrupted_signature = signature.clone(); corrupted_signature[0] = corrupted_signature[0].wrapping_add(1); assert!( - alg.verify_signature(&public_key, &msg, &signature).is_ok(), + verifier.verify(alg, &public_key, &msg, &signature).is_ok(), "verifying signature should succeed" ); assert!( - alg.verify_signature(&public_key, &msg[..7], &signature) + verifier + .verify(alg, &public_key, &msg[..7], &signature) .is_err(), "verifying signature should fail with incorrect message" ); assert!( - alg.verify_signature(&public_key, &msg, &corrupted_signature) + verifier + .verify(alg, &public_key, &msg, &corrupted_signature) .is_err(), "verifying signature should fail with incorrect signature" ); assert!( - Algorithm::P256 - .verify_signature(&public_key, &msg, &signature) + verifier + .verify(Algorithm::P256, &public_key, &msg, &signature) .is_err(), "verifying signature should fail with incorrect algorithm" ); diff --git a/atrium-crypto/src/verify.rs b/atrium-crypto/src/verify.rs index e05b48d3..d31d4cd2 100644 --- a/atrium-crypto/src/verify.rs +++ b/atrium-crypto/src/verify.rs @@ -1,6 +1,229 @@ -use crate::{did::parse_did_key, error::Result}; +use crate::{ + did::parse_did_key, + error::{Error, Result}, + Algorithm, +}; +use ecdsa::elliptic_curve::{ + generic_array::ArrayLength, + sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, + AffinePoint, CurveArithmetic, FieldBytesSize, PrimeCurve, +}; +use ecdsa::hazmat::{DigestPrimitive, VerifyPrimitive}; +use ecdsa::{SignatureSize, VerifyingKey}; +use k256::Secp256k1; +use p256::NistP256; pub fn verify_signature(did_key: &str, msg: &[u8], signature: &[u8]) -> Result<()> { let (alg, public_key) = parse_did_key(did_key)?; - alg.verify_signature(&public_key, msg, signature) + Verifier::default().verify(alg, &public_key, msg, signature) +} + +#[derive(Debug, Default)] +pub struct Verifier { + allow_malleable: bool, +} + +use ecdsa::der::{MaxOverhead, MaxSize}; +use std::ops::Add; +impl Verifier { + pub fn new(allow_malleable: bool) -> Self { + Self { allow_malleable } + } + pub fn verify( + &self, + algorithm: Algorithm, + public_key: &[u8], + msg: &[u8], + signature: &[u8], + ) -> Result<()> { + match algorithm { + Algorithm::P256 => self.verify_inner::(public_key, msg, signature), + Algorithm::Secp256k1 => self.verify_inner::(public_key, msg, signature), + } + } + fn verify_inner(&self, public_key: &[u8], msg: &[u8], signature: &[u8]) -> Result<()> + where + C: PrimeCurve + CurveArithmetic + DigestPrimitive, + AffinePoint: VerifyPrimitive + FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, + SignatureSize: ArrayLength, + + MaxSize: ArrayLength, + as Add>::Output: Add + ArrayLength, + { + let verifying_key = VerifyingKey::::from_sec1_bytes(public_key)?; + if let Ok(mut signature) = ecdsa::Signature::from_slice(signature) { + if let Some(normalized) = signature.normalize_s() { + if !self.allow_malleable { + return Err(Error::LowSSignatureNotAllowed); + } + signature = normalized + } + Ok(ecdsa::signature::Verifier::verify( + &verifying_key, + msg, + &signature, + )?) + } else if self.allow_malleable { + let signature = ecdsa::der::Signature::from_bytes(signature)?; + Ok(ecdsa::signature::Verifier::verify( + &verifying_key, + msg, + &signature, + )?) + } else { + Err(Error::InvalidSignature) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use multibase::Base; + use serde::{Deserialize, Serialize}; + use std::{fs::File, path::PathBuf}; + + #[derive(Debug, Serialize, Deserialize)] + enum Algorithm { + ES256, + ES256K, + } + + #[derive(Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + struct TestVector { + comment: String, + message_base64: String, + algorithm: Algorithm, + public_key_multibase: String, + public_key_did: String, + signature_base64: String, + valid_signature: bool, + tags: Vec, + } + + fn test_vectors(cond: Option<&str>) -> Vec { + let data_path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/signature-fixtures.json"); + let file = File::open(data_path).expect("opening test data should succeed"); + let v = serde_json::from_reader::<_, Vec>(file) + .expect("parsing test data should succeed"); + v.into_iter() + .filter(|v| { + if let Some(s) = cond { + v.tags.contains(&s.to_string()) + } else { + true + } + }) + .collect() + } + + #[test] + fn verify() { + let vectors = test_vectors(None); + assert!(!vectors.is_empty()); + let verifier = Verifier::default(); + for vector in vectors { + let message = Base::Base64 + .decode(vector.message_base64) + .expect("decoding message should succeed"); + let signature = Base::Base64 + .decode(vector.signature_base64) + .expect("decoding signature should succeed"); + + let (base, decoded_key) = multibase::decode(vector.public_key_multibase) + .expect("decoding multibase public key should succeed"); + assert_eq!(base, Base::Base58Btc); + let (alg, parsed_key) = + parse_did_key(&vector.public_key_did).expect("parsing DID key should succeed"); + + // assert_eq!(decoded_key, parsed_key); + match vector.algorithm { + Algorithm::ES256 => assert_eq!(alg, crate::Algorithm::P256), + Algorithm::ES256K => assert_eq!(alg, crate::Algorithm::Secp256k1), + } + assert_eq!( + verifier + .verify(alg, &decoded_key, &message, &signature) + .is_ok(), + vector.valid_signature + ); + assert_eq!( + verifier + .verify(alg, &parsed_key, &message, &signature) + .is_ok(), + vector.valid_signature + ); + } + } + + #[test] + fn verify_high_s_signatures() { + let vectors = test_vectors(Some("high-s")); + assert!(vectors.len() >= 2); + let verifier = Verifier::new(true); + for vector in vectors { + let message = Base::Base64 + .decode(vector.message_base64) + .expect("decoding message should succeed"); + let signature = Base::Base64 + .decode(vector.signature_base64) + .expect("decoding signature should succeed"); + + let (base, decoded_key) = multibase::decode(vector.public_key_multibase) + .expect("decoding multibase public key should succeed"); + assert_eq!(base, Base::Base58Btc); + let (alg, parsed_key) = + parse_did_key(&vector.public_key_did).expect("parsing DID key should succeed"); + + // assert_eq!(decoded_key, parsed_key); + match vector.algorithm { + Algorithm::ES256 => assert_eq!(alg, crate::Algorithm::P256), + Algorithm::ES256K => assert_eq!(alg, crate::Algorithm::Secp256k1), + } + assert!(!vector.valid_signature); + assert!(verifier + .verify(alg, &decoded_key, &message, &signature) + .is_ok()); + assert!(verifier + .verify(alg, &parsed_key, &message, &signature) + .is_ok()); + } + } + + #[test] + fn verify_der_encoded() { + let vectors = test_vectors(Some("der-encoded")); + assert!(vectors.len() >= 2); + let verifier = Verifier::new(true); + for vector in vectors { + let message = Base::Base64 + .decode(vector.message_base64) + .expect("decoding message should succeed"); + let signature = Base::Base64 + .decode(vector.signature_base64) + .expect("decoding signature should succeed"); + + let (base, decoded_key) = multibase::decode(vector.public_key_multibase) + .expect("decoding multibase public key should succeed"); + assert_eq!(base, Base::Base58Btc); + let (alg, parsed_key) = + parse_did_key(&vector.public_key_did).expect("parsing DID key should succeed"); + + // assert_eq!(decoded_key, parsed_key); + match vector.algorithm { + Algorithm::ES256 => assert_eq!(alg, crate::Algorithm::P256), + Algorithm::ES256K => assert_eq!(alg, crate::Algorithm::Secp256k1), + } + assert!(!vector.valid_signature); + assert!(verifier + .verify(alg, &decoded_key, &message, &signature) + .is_ok()); + assert!(verifier + .verify(alg, &parsed_key, &message, &signature) + .is_ok()); + } + } } diff --git a/atrium-crypto/tests/data/signature-fixtures.json b/atrium-crypto/tests/data/signature-fixtures.json new file mode 100644 index 00000000..2e41be58 --- /dev/null +++ b/atrium-crypto/tests/data/signature-fixtures.json @@ -0,0 +1,68 @@ +[ + { + "comment": "valid P-256 key and signature, with low-S signature", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256", + "didDocSuite": "EcdsaSecp256r1VerificationKey2019", + "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", + "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", + "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoJWExHptCfduPleDbG3rko3YZnn9Lw0IjpixVmexJDegg", + "validSignature": true, + "tags": [] + }, + { + "comment": "valid K-256 key and signature, with low-S signature", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256K", + "didDocSuite": "EcdsaSecp256k1VerificationKey2019", + "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", + "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", + "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdUn/FEznOndsz/qgiYb89zwxYCbB71f7yQK5Lr7NasfoA", + "validSignature": true, + "tags": [] + }, + { + "comment": "P-256 key and signature, with non-low-S signature which is invalid in atproto", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256", + "didDocSuite": "EcdsaSecp256r1VerificationKey2019", + "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", + "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", + "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoKp7O4VS9giSAah8k5IUbXIW00SuOrjfEqQ9HEkN9JGzw", + "validSignature": false, + "tags": ["high-s"] + }, + { + "comment": "K-256 key and signature, with non-low-S signature which is invalid in atproto", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256K", + "didDocSuite": "EcdsaSecp256k1VerificationKey2019", + "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", + "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", + "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdXYA67MYxYiTMAVfdnkDCMN9S5B3vHosRe07aORmoshoQ", + "validSignature": false, + "tags": ["high-s"] + }, + { + "comment": "P-256 key and signature, with DER-encoded signature which is invalid in atproto", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256", + "didDocSuite": "EcdsaSecp256r1VerificationKey2019", + "publicKeyDid": "did:key:zDnaeT6hL2RnTdUhAPLij1QBkhYZnmuKyM7puQLW1tkF4Zkt8", + "publicKeyMultibase": "ze8N2PPxnu19hmBQ58t5P3E9Yj6CqakJmTVCaKvf9Byq2", + "signatureBase64": "MEQCIFxYelWJ9lNcAVt+jK0y/T+DC/X4ohFZ+m8f9SEItkY1AiACX7eXz5sgtaRrz/SdPR8kprnbHMQVde0T2R8yOTBweA", + "validSignature": false, + "tags": ["der-encoded"] + }, + { + "comment": "K-256 key and signature, with DER-encoded signature which is invalid in atproto", + "messageBase64": "oWVoZWxsb2V3b3JsZA", + "algorithm": "ES256K", + "didDocSuite": "EcdsaSecp256k1VerificationKey2019", + "publicKeyDid": "did:key:zQ3shnriYMXc8wvkbJqfNWh5GXn2bVAeqTC92YuNbek4npqGF", + "publicKeyMultibase": "z22uZXWP8fdHXi4jyx8cCDiBf9qQTsAe6VcycoMQPfcMQX", + "signatureBase64": "MEUCIQCWumUqJqOCqInXF7AzhIRg2MhwRz2rWZcOEsOjPmNItgIgXJH7RnqfYY6M0eg33wU0sFYDlprwdOcpRn78Sz5ePgk", + "validSignature": false, + "tags": ["der-encoded"] + } +] From a4355101d66c1b26ed8d9845a7d9cafc1b36e5cf Mon Sep 17 00:00:00 2001 From: sugyan Date: Thu, 16 May 2024 10:58:25 +0900 Subject: [PATCH 16/17] Add crypto/encoding, move logic and tests --- atrium-crypto/src/algorithm.rs | 105 --------------------------------- atrium-crypto/src/did.rs | 16 +++-- atrium-crypto/src/encoding.rs | 100 +++++++++++++++++++++++++++++++ atrium-crypto/src/keypair.rs | 4 +- atrium-crypto/src/lib.rs | 2 + atrium-crypto/src/verify.rs | 15 ++--- 6 files changed, 118 insertions(+), 124 deletions(-) create mode 100644 atrium-crypto/src/encoding.rs diff --git a/atrium-crypto/src/algorithm.rs b/atrium-crypto/src/algorithm.rs index 7c6e85f1..884421b1 100644 --- a/atrium-crypto/src/algorithm.rs +++ b/atrium-crypto/src/algorithm.rs @@ -1,8 +1,4 @@ -use crate::error::Result; -use ecdsa::VerifyingKey; -use k256::Secp256k1; use multibase::Base; -use p256::NistP256; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Algorithm { @@ -27,111 +23,10 @@ impl Algorithm { _ => None, } } - pub fn format_multikey(&self, key: &[u8]) -> Result { - Ok(self.format_mulikey_compressed(&self.compress_pubkey(key)?)) - } pub(crate) fn format_mulikey_compressed(&self, key: &[u8]) -> String { let mut v = Vec::with_capacity(2 + key.len()); v.extend_from_slice(&self.prefix()); v.extend_from_slice(key); multibase::encode(Base::Base58Btc, v) } - pub(crate) fn compress_pubkey(&self, key: &[u8]) -> Result> { - self.pubkey_bytes(key, true) - } - pub(crate) fn decompress_pubkey(&self, key: &[u8]) -> Result> { - self.pubkey_bytes(key, false) - } - fn pubkey_bytes(&self, key: &[u8], compress: bool) -> Result> { - Ok(match self { - Algorithm::P256 => VerifyingKey::::from_sec1_bytes(key)? - .to_encoded_point(compress) - .as_bytes() - .to_vec(), - Algorithm::Secp256k1 => VerifyingKey::::from_sec1_bytes(key)? - .to_encoded_point(compress) - .as_bytes() - .to_vec(), - }) - } -} - -#[cfg(test)] -mod tests { - use super::Algorithm; - use crate::did::parse_did_key; - use crate::keypair::{Did, P256Keypair, Secp256k1Keypair}; - use rand::rngs::ThreadRng; - - #[test] - fn p256_compress_decompress() { - let did = P256Keypair::create(&mut ThreadRng::default()).did(); - let (alg, key) = parse_did_key(&did).expect("parsing did key should succeed"); - assert_eq!(alg, Algorithm::P256); - // compress a key to the correct length - let compressed = alg - .pubkey_bytes(&key, true) - .expect("compressing public key should succeed"); - assert_eq!(compressed.len(), 33); - // decompress a key to the original - let decompressed = alg - .pubkey_bytes(&compressed, false) - .expect("decompressing public key should succeed"); - assert_eq!(decompressed.len(), 65); - assert_eq!(key, decompressed); - - // works consitesntly - let keys = (0..100) - .map(|_| { - let did = P256Keypair::create(&mut ThreadRng::default()).did(); - let (_, key) = parse_did_key(&did).expect("parsing did key should succeed"); - key - }) - .collect::>(); - let compressed = keys - .iter() - .filter_map(|key| alg.pubkey_bytes(key, true).ok()) - .collect::>(); - let decompressed = compressed - .iter() - .filter_map(|key| alg.pubkey_bytes(key, false).ok()) - .collect::>(); - assert_eq!(keys, decompressed); - } - - #[test] - fn secp256k1_compress_decompress() { - let did = Secp256k1Keypair::create(&mut ThreadRng::default()).did(); - let (alg, key) = parse_did_key(&did).expect("parsing did key should succeed"); - assert_eq!(alg, Algorithm::Secp256k1); - // compress a key to the correct length - let compressed = alg - .pubkey_bytes(&key, true) - .expect("compressing public key should succeed"); - assert_eq!(compressed.len(), 33); - // decompress a key to the original - let decompressed = alg - .pubkey_bytes(&compressed, false) - .expect("decompressing public key should succeed"); - assert_eq!(decompressed.len(), 65); - assert_eq!(key, decompressed); - - // works consitesntly - let keys = (0..100) - .map(|_| { - let did = Secp256k1Keypair::create(&mut ThreadRng::default()).did(); - let (_, key) = parse_did_key(&did).expect("parsing did key should succeed"); - key - }) - .collect::>(); - let compressed = keys - .iter() - .filter_map(|key| alg.pubkey_bytes(key, true).ok()) - .collect::>(); - let decompressed = compressed - .iter() - .filter_map(|key| alg.pubkey_bytes(key, false).ok()) - .collect::>(); - assert_eq!(keys, decompressed); - } } diff --git a/atrium-crypto/src/did.rs b/atrium-crypto/src/did.rs index 7beaea4d..9635c600 100644 --- a/atrium-crypto/src/did.rs +++ b/atrium-crypto/src/did.rs @@ -1,11 +1,12 @@ -use super::error::{Error, Result}; -use super::{Algorithm, DID_KEY_PREFIX}; +use crate::encoding::{compress_pubkey, decompress_pubkey}; +use crate::error::{Error, Result}; +use crate::{Algorithm, DID_KEY_PREFIX}; pub fn parse_multikey(multikey: &str) -> Result<(Algorithm, Vec)> { let (_, decoded) = multibase::decode(multikey)?; if let Ok(prefix) = decoded[..2].try_into() { if let Some(alg) = Algorithm::from_prefix(prefix) { - return Ok((alg, alg.decompress_pubkey(&decoded[2..])?)); + return Ok((alg, decompress_pubkey(alg, &decoded[2..])?)); } } Err(Error::UnsupportedMultikeyType) @@ -19,13 +20,10 @@ pub fn parse_did_key(did: &str) -> Result<(Algorithm, Vec)> { } } -pub fn format_did_key_str(alg: Algorithm, s: &str) -> Result { - let (_, key) = multibase::decode(s)?; - format_did_key(alg, &key) -} - pub fn format_did_key(alg: Algorithm, key: &[u8]) -> Result { - Ok(prefix_did_key(&alg.format_multikey(key)?)) + Ok(prefix_did_key( + &alg.format_mulikey_compressed(&compress_pubkey(alg, key)?), + )) } pub(crate) fn prefix_did_key(multikey: &str) -> String { diff --git a/atrium-crypto/src/encoding.rs b/atrium-crypto/src/encoding.rs new file mode 100644 index 00000000..50a43c96 --- /dev/null +++ b/atrium-crypto/src/encoding.rs @@ -0,0 +1,100 @@ +use crate::{error::Result, Algorithm}; +use ecdsa::VerifyingKey; +use k256::Secp256k1; +use p256::NistP256; + +pub(crate) fn compress_pubkey(alg: Algorithm, key: &[u8]) -> Result> { + pubkey_bytes(alg, key, true) +} + +pub(crate) fn decompress_pubkey(alg: Algorithm, key: &[u8]) -> Result> { + pubkey_bytes(alg, key, false) +} + +fn pubkey_bytes(alg: Algorithm, key: &[u8], compress: bool) -> Result> { + Ok(match alg { + Algorithm::P256 => VerifyingKey::::from_sec1_bytes(key)? + .to_encoded_point(compress) + .as_bytes() + .to_vec(), + Algorithm::Secp256k1 => VerifyingKey::::from_sec1_bytes(key)? + .to_encoded_point(compress) + .as_bytes() + .to_vec(), + }) +} + +#[cfg(test)] +mod tests { + use super::{compress_pubkey, decompress_pubkey}; + use crate::did::parse_did_key; + use crate::keypair::{Did, P256Keypair, Secp256k1Keypair}; + use crate::Algorithm; + use rand::rngs::ThreadRng; + + #[test] + fn p256_compress_decompress() { + let did = P256Keypair::create(&mut ThreadRng::default()).did(); + let (alg, key) = parse_did_key(&did).expect("parsing did key should succeed"); + assert_eq!(alg, Algorithm::P256); + // compress a key to the correct length + let compressed = compress_pubkey(alg, &key).expect("compressing public key should succeed"); + assert_eq!(compressed.len(), 33); + // decompress a key to the original + let decompressed = + decompress_pubkey(alg, &compressed).expect("decompressing public key should succeed"); + assert_eq!(decompressed.len(), 65); + assert_eq!(key, decompressed); + + // works consitesntly + let keys = (0..100) + .map(|_| { + let did = P256Keypair::create(&mut ThreadRng::default()).did(); + let (_, key) = parse_did_key(&did).expect("parsing did key should succeed"); + key + }) + .collect::>(); + let compressed = keys + .iter() + .filter_map(|key| compress_pubkey(alg, &key).ok()) + .collect::>(); + let decompressed = compressed + .iter() + .filter_map(|key| decompress_pubkey(alg, &key).ok()) + .collect::>(); + assert_eq!(keys, decompressed); + } + + #[test] + fn secp256k1_compress_decompress() { + let did = Secp256k1Keypair::create(&mut ThreadRng::default()).did(); + let (alg, key) = parse_did_key(&did).expect("parsing did key should succeed"); + assert_eq!(alg, Algorithm::Secp256k1); + // compress a key to the correct length + let compressed = compress_pubkey(alg, &key).expect("compressing public key should succeed"); + assert_eq!(compressed.len(), 33); + // decompress a key to the original + let decompressed = + decompress_pubkey(alg, &compressed).expect("decompressing public key should succeed"); + assert_eq!(decompressed.len(), 65); + assert_eq!(key, decompressed); + + // works consitesntly + let keys = (0..100) + .map(|_| { + let did = Secp256k1Keypair::create(&mut ThreadRng::default()).did(); + let (_, key) = parse_did_key(&did).expect("parsing did key should succeed"); + key + }) + .collect::>(); + let compressed = keys + .iter() + .filter_map(|key| compress_pubkey(alg, key).ok()) + .collect::>(); + let decompressed = compressed + .iter() + .filter_map(|key| decompress_pubkey(alg, key).ok()) + .collect::>(); + assert_eq!(keys, decompressed); + } +} diff --git a/atrium-crypto/src/keypair.rs b/atrium-crypto/src/keypair.rs index 867e33fe..e5ca79fc 100644 --- a/atrium-crypto/src/keypair.rs +++ b/atrium-crypto/src/keypair.rs @@ -1,4 +1,6 @@ -use crate::{did::prefix_did_key, error::Result, Algorithm}; +use crate::did::prefix_did_key; +use crate::error::Result; +use crate::Algorithm; use ecdsa::elliptic_curve::{ generic_array::ArrayLength, ops::Invert, diff --git a/atrium-crypto/src/lib.rs b/atrium-crypto/src/lib.rs index 10da2f0a..ae973a3d 100644 --- a/atrium-crypto/src/lib.rs +++ b/atrium-crypto/src/lib.rs @@ -1,10 +1,12 @@ #![doc = include_str!("../README.md")] mod algorithm; pub mod did; +mod encoding; pub mod error; pub mod keypair; pub mod verify; pub use algorithm::Algorithm; +pub use multibase; const DID_KEY_PREFIX: &str = "did:key:"; diff --git a/atrium-crypto/src/verify.rs b/atrium-crypto/src/verify.rs index d31d4cd2..8f800438 100644 --- a/atrium-crypto/src/verify.rs +++ b/atrium-crypto/src/verify.rs @@ -1,8 +1,7 @@ -use crate::{ - did::parse_did_key, - error::{Error, Result}, - Algorithm, -}; +use crate::did::parse_did_key; +use crate::error::{Error, Result}; +use crate::Algorithm; +use ecdsa::der::{MaxOverhead, MaxSize}; use ecdsa::elliptic_curve::{ generic_array::ArrayLength, sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, @@ -12,6 +11,7 @@ use ecdsa::hazmat::{DigestPrimitive, VerifyPrimitive}; use ecdsa::{SignatureSize, VerifyingKey}; use k256::Secp256k1; use p256::NistP256; +use std::ops::Add; pub fn verify_signature(did_key: &str, msg: &[u8], signature: &[u8]) -> Result<()> { let (alg, public_key) = parse_did_key(did_key)?; @@ -23,8 +23,6 @@ pub struct Verifier { allow_malleable: bool, } -use ecdsa::der::{MaxOverhead, MaxSize}; -use std::ops::Add; impl Verifier { pub fn new(allow_malleable: bool) -> Self { Self { allow_malleable } @@ -47,7 +45,6 @@ impl Verifier { AffinePoint: VerifyPrimitive + FromEncodedPoint + ToEncodedPoint, FieldBytesSize: ModulusSize, SignatureSize: ArrayLength, - MaxSize: ArrayLength, as Add>::Output: Add + ArrayLength, { @@ -160,7 +157,7 @@ mod tests { } #[test] - fn verify_high_s_signatures() { + fn verify_high_s() { let vectors = test_vectors(Some("high-s")); assert!(vectors.len() >= 2); let verifier = Verifier::new(true); From 438f0012d5b9af0a05dfb1b3be8c00bfb0c46cec Mon Sep 17 00:00:00 2001 From: sugyan Date: Thu, 16 May 2024 23:13:06 +0900 Subject: [PATCH 17/17] Update documents --- atrium-crypto/src/algorithm.rs | 7 +- atrium-crypto/src/did.rs | 90 +++++++++++++++++--- atrium-crypto/src/error.rs | 10 ++- atrium-crypto/src/keypair.rs | 24 +++++- atrium-crypto/src/lib.rs | 5 +- atrium-crypto/src/verify.rs | 38 ++++++++- atrium-libs/src/identity/did/atproto_data.rs | 18 ++-- 7 files changed, 163 insertions(+), 29 deletions(-) diff --git a/atrium-crypto/src/algorithm.rs b/atrium-crypto/src/algorithm.rs index 884421b1..d65f4898 100644 --- a/atrium-crypto/src/algorithm.rs +++ b/atrium-crypto/src/algorithm.rs @@ -1,8 +1,11 @@ use multibase::Base; +/// Supported algorithms (elliptic curves) for atproto cryptography. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Algorithm { + /// [`p256`] elliptic curve: aka "NIST P-256", aka `secp256r1` (note the `r`), aka `prime256v1`. P256, + /// [`k256`] elliptic curve: aka "NIST K-256", aka `secp256k1` (note the `k`). Secp256k1, } @@ -10,13 +13,13 @@ impl Algorithm { const MULTICODE_PREFIX_P256: [u8; 2] = [0x80, 0x24]; const MULTICODE_PREFIX_SECP256K1: [u8; 2] = [0xe7, 0x01]; - pub fn prefix(&self) -> [u8; 2] { + pub(crate) fn prefix(&self) -> [u8; 2] { match self { Self::P256 => Self::MULTICODE_PREFIX_P256, Self::Secp256k1 => Self::MULTICODE_PREFIX_SECP256K1, } } - pub fn from_prefix(prefix: [u8; 2]) -> Option { + pub(crate) fn from_prefix(prefix: [u8; 2]) -> Option { match prefix { Self::MULTICODE_PREFIX_P256 => Some(Self::P256), Self::MULTICODE_PREFIX_SECP256K1 => Some(Self::Secp256k1), diff --git a/atrium-crypto/src/did.rs b/atrium-crypto/src/did.rs index 9635c600..071a8602 100644 --- a/atrium-crypto/src/did.rs +++ b/atrium-crypto/src/did.rs @@ -1,17 +1,59 @@ +//! Functions for parsing and formatting DID keys. use crate::encoding::{compress_pubkey, decompress_pubkey}; use crate::error::{Error, Result}; use crate::{Algorithm, DID_KEY_PREFIX}; -pub fn parse_multikey(multikey: &str) -> Result<(Algorithm, Vec)> { - let (_, decoded) = multibase::decode(multikey)?; - if let Ok(prefix) = decoded[..2].try_into() { - if let Some(alg) = Algorithm::from_prefix(prefix) { - return Ok((alg, decompress_pubkey(alg, &decoded[2..])?)); - } - } - Err(Error::UnsupportedMultikeyType) +/// Format a public key as a DID key string. +/// +/// The public key will be compressed and encoded with multibase and multicode. +/// The resulting string will start with `did:key:`. +/// +/// Details: +/// [https://atproto.com/specs/cryptography#public-key-encoding](https://atproto.com/specs/cryptography#public-key-encoding) +/// +/// # Examples +/// +/// ``` +/// use atrium_crypto::Algorithm; +/// use atrium_crypto::did::format_did_key; +/// +/// # fn main() -> atrium_crypto::Result<()> { +/// let signing_key = ecdsa::SigningKey::::from_slice( +/// &hex::decode("9085d2bef69286a6cbb51623c8fa258629945cd55ca705cc4e66700396894e0c").unwrap() +/// )?; +/// let public_key = signing_key.verifying_key(); +/// let did_key = format_did_key(Algorithm::Secp256k1, &public_key.to_sec1_bytes())?; +/// assert_eq!(did_key, "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme"); +/// # Ok(()) +/// # } +/// ``` +pub fn format_did_key(alg: Algorithm, key: &[u8]) -> Result { + Ok(prefix_did_key( + &alg.format_mulikey_compressed(&compress_pubkey(alg, key)?), + )) } +/// Parse a DID key string. +/// +/// Input should be a string starting with `did:key:`. +/// The rest of the string is the multibase and multicode encoded public key, +/// which will be parsed with [`parse_multikey`]. +/// +/// Returns the parsed [`Algorithm`] and bytes of the public key. +/// +/// # Examples +/// +/// ``` +/// use atrium_crypto::Algorithm; +/// use atrium_crypto::did::parse_did_key; +/// +/// # fn main() -> atrium_crypto::Result<()> { +/// let (alg, key): (Algorithm, Vec) = parse_did_key("did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme")?; +/// assert_eq!(alg, Algorithm::Secp256k1); +/// assert_eq!(key.len(), 65); +/// # Ok(()) +/// # } +/// ``` pub fn parse_did_key(did: &str) -> Result<(Algorithm, Vec)> { if let Some(multikey) = did.strip_prefix(DID_KEY_PREFIX) { parse_multikey(multikey) @@ -20,10 +62,34 @@ pub fn parse_did_key(did: &str) -> Result<(Algorithm, Vec)> { } } -pub fn format_did_key(alg: Algorithm, key: &[u8]) -> Result { - Ok(prefix_did_key( - &alg.format_mulikey_compressed(&compress_pubkey(alg, key)?), - )) +/// Parse a multibase and multicode encoded public key string. +/// +/// Details: +/// [https://atproto.com/specs/cryptography#public-key-encoding](https://atproto.com/specs/cryptography#public-key-encoding) +/// +/// Returns the parsed [`Algorithm`] and bytes of the public key. +/// +/// # Examples +/// +/// ``` +/// use atrium_crypto::Algorithm; +/// use atrium_crypto::did::parse_multikey; +/// +/// # fn main() -> atrium_crypto::Result<()> { +/// let (alg, key): (Algorithm, Vec) = parse_multikey("zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme")?; +/// assert_eq!(alg, Algorithm::Secp256k1); +/// assert_eq!(key.len(), 65); +/// # Ok(()) +/// # } +/// ``` +pub fn parse_multikey(multikey: &str) -> Result<(Algorithm, Vec)> { + let (_, decoded) = multibase::decode(multikey)?; + if let Ok(prefix) = decoded[..2].try_into() { + if let Some(alg) = Algorithm::from_prefix(prefix) { + return Ok((alg, decompress_pubkey(alg, &decoded[2..])?)); + } + } + Err(Error::UnsupportedMultikeyType) } pub(crate) fn prefix_did_key(multikey: &str) -> String { diff --git a/atrium-crypto/src/error.rs b/atrium-crypto/src/error.rs index 0036e25e..f2cb74c0 100644 --- a/atrium-crypto/src/error.rs +++ b/atrium-crypto/src/error.rs @@ -1,19 +1,27 @@ use thiserror::Error; +/// Error types. #[derive(Error, Debug)] pub enum Error { + /// Unsupported multikey type. #[error("Unsupported key type")] UnsupportedMultikeyType, + /// Incorrect prefix for DID key. #[error("Incorrect prefix for did:key: {0}")] IncorrectDIDKeyPrefix(String), - #[error("Low-S Signature is not allowed")] + /// Low-S signature is not allowed. + #[error("Low-S signature is not allowed")] LowSSignatureNotAllowed, + /// Signature is invalid. #[error("Signature is invalid")] InvalidSignature, + /// Error in [`multibase`] encoding or decoding. #[error(transparent)] Multibase(#[from] multibase::Error), + /// Error in [`ecdsa::signature`]. #[error(transparent)] Signature(#[from] ecdsa::signature::Error), } +/// Type alias to use this library's [`Error`](crate::Error) type in a [`Result`](core::result::Result). pub type Result = std::result::Result; diff --git a/atrium-crypto/src/keypair.rs b/atrium-crypto/src/keypair.rs index e5ca79fc..a3ce229a 100644 --- a/atrium-crypto/src/keypair.rs +++ b/atrium-crypto/src/keypair.rs @@ -1,3 +1,4 @@ +//! Keypair structs for signing, and utility trait implementations. use crate::did::prefix_did_key; use crate::error::Result; use crate::Algorithm; @@ -14,6 +15,7 @@ use ecdsa::{Signature, SignatureSize, SigningKey}; use k256::Secp256k1; use p256::NistP256; +/// A keypair for signing messages. pub struct Keypair where C: PrimeCurve + CurveArithmetic, @@ -29,14 +31,22 @@ where Scalar: Invert>> + SignPrimitive, SignatureSize: ArrayLength, { + /// Generate a cryptographically random [`SigningKey`]. + /// + /// ``` + /// use atrium_crypto::keypair::Keypair; + /// + /// let keypair = Keypair::::create(&mut rand::thread_rng()); + /// ``` pub fn create(rng: &mut impl CryptoRngCore) -> Self { Self { signing_key: SigningKey::::random(rng), } } - pub fn import(priv_key: &[u8]) -> Result { + /// Initialize signing key from a raw scalar serialized as a byte slice. + pub fn import(bytes: &[u8]) -> Result { Ok(Self { - signing_key: SigningKey::from_slice(priv_key)?, + signing_key: SigningKey::from_slice(bytes)?, }) } } @@ -63,6 +73,12 @@ where Scalar: Invert>> + SignPrimitive, SignatureSize: ArrayLength, { + /// Sign a message with the keypair. + /// + /// Returns the signature as a byte vector of the "low-S" form. + /// + /// Details: + /// [https://atproto.com/specs/cryptography#ecdsa-signature-malleability](https://atproto.com/specs/cryptography#ecdsa-signature-malleability) pub fn sign(&self, msg: &[u8]) -> Result> { let signature: Signature<_> = self.signing_key.try_sign(msg)?; Ok(signature @@ -73,10 +89,12 @@ where } } +/// Generate a DID key string from a keypair. pub trait Did { fn did(&self) -> String; } +/// Export a keypair as a byte vector. pub trait Export { fn export(&self) -> Vec; } @@ -92,6 +110,7 @@ where } } +/// Type alias for a P-256 keypair. pub type P256Keypair = Keypair; impl Did for P256Keypair { @@ -100,6 +119,7 @@ impl Did for P256Keypair { } } +/// Type alias for a secp256k1 keypair. pub type Secp256k1Keypair = Keypair; impl Did for Secp256k1Keypair { diff --git a/atrium-crypto/src/lib.rs b/atrium-crypto/src/lib.rs index ae973a3d..dd1bcfa5 100644 --- a/atrium-crypto/src/lib.rs +++ b/atrium-crypto/src/lib.rs @@ -2,11 +2,12 @@ mod algorithm; pub mod did; mod encoding; -pub mod error; +mod error; pub mod keypair; pub mod verify; -pub use algorithm::Algorithm; +pub use crate::algorithm::Algorithm; +pub use crate::error::{Error, Result}; pub use multibase; const DID_KEY_PREFIX: &str = "did:key:"; diff --git a/atrium-crypto/src/verify.rs b/atrium-crypto/src/verify.rs index 8f800438..34dcf318 100644 --- a/atrium-crypto/src/verify.rs +++ b/atrium-crypto/src/verify.rs @@ -1,3 +1,4 @@ +//! Verifies a signature for a message using a public key. use crate::did::parse_did_key; use crate::error::{Error, Result}; use crate::Algorithm; @@ -13,20 +14,49 @@ use k256::Secp256k1; use p256::NistP256; use std::ops::Add; +/// Verify a signature for a message using the given DID key formatted public key. +/// +/// This function verifies a signature using [`Verifier::default()`]. +/// +/// # Examples +/// +/// ``` +/// use atrium_crypto::verify::verify_signature; +/// +/// # fn main() -> atrium_crypto::Result<()> { +/// let did_key = "did:key:zQ3shtNTBUUCARYFEkRPZQ9NCaM5i5hVHPeEsEKXpmVkR2Upq"; +/// let signature = hex::decode( +/// "fdaa28ab03d6767c11d71fa39627c770ff62f91ca9661401ca0e2c475ae96a8c27064fbde3c355fa8121d2e8bbcf87a2de308e1d72b9bf4270f1e7cd8a1575ab" +/// ).unwrap(); +/// assert!(verify_signature(did_key, b"Hello, world!", &signature).is_ok()); +/// assert!(verify_signature(did_key, b"Hello, world?", &signature).is_err()); +/// # Ok(()) +/// # } +/// ``` pub fn verify_signature(did_key: &str, msg: &[u8], signature: &[u8]) -> Result<()> { let (alg, public_key) = parse_did_key(did_key)?; Verifier::default().verify(alg, &public_key, msg, signature) } +/// Verifier for verifying signatures for a message using a public key. +/// +/// This verifier can be configured to `allow_malleable` mode, which allows +/// verifying signatures with "high-S" or DER-encoded ones. +/// By default, this verifier allows only "low-S" signatures. +/// +/// See also: [https://github.com/bluesky-social/atproto/pull/1839](https://github.com/bluesky-social/atproto/pull/1839) #[derive(Debug, Default)] pub struct Verifier { allow_malleable: bool, } impl Verifier { + /// Create a new verifier with the given malleable mode. pub fn new(allow_malleable: bool) -> Self { Self { allow_malleable } } + /// Verify a signature for a message using the given public key. + /// The `algorithm` is used to determine the curve for the public key. pub fn verify( &self, algorithm: Algorithm, @@ -39,7 +69,9 @@ impl Verifier { Algorithm::Secp256k1 => self.verify_inner::(public_key, msg, signature), } } - fn verify_inner(&self, public_key: &[u8], msg: &[u8], signature: &[u8]) -> Result<()> + /// Verify a signature for a message using the given public key. + /// Any elliptic curve of the generics implementation of [`ECDSA`](ecdsa) can be used for parameter `C`. + pub fn verify_inner(&self, public_key: &[u8], msg: &[u8], bytes: &[u8]) -> Result<()> where C: PrimeCurve + CurveArithmetic + DigestPrimitive, AffinePoint: VerifyPrimitive + FromEncodedPoint + ToEncodedPoint, @@ -49,7 +81,7 @@ impl Verifier { as Add>::Output: Add + ArrayLength, { let verifying_key = VerifyingKey::::from_sec1_bytes(public_key)?; - if let Ok(mut signature) = ecdsa::Signature::from_slice(signature) { + if let Ok(mut signature) = ecdsa::Signature::from_slice(bytes) { if let Some(normalized) = signature.normalize_s() { if !self.allow_malleable { return Err(Error::LowSSignatureNotAllowed); @@ -62,7 +94,7 @@ impl Verifier { &signature, )?) } else if self.allow_malleable { - let signature = ecdsa::der::Signature::from_bytes(signature)?; + let signature = ecdsa::der::Signature::from_bytes(bytes)?; Ok(ecdsa::signature::Verifier::verify( &verifying_key, msg, diff --git a/atrium-libs/src/identity/did/atproto_data.rs b/atrium-libs/src/identity/did/atproto_data.rs index 2cf8d41d..ad95e2cd 100644 --- a/atrium-libs/src/identity/did/atproto_data.rs +++ b/atrium-libs/src/identity/did/atproto_data.rs @@ -1,5 +1,6 @@ use crate::common_web::did_doc::DidDocument; -use atrium_crypto::did::{format_did_key, format_did_key_str, parse_multikey}; +use atrium_crypto::did::{format_did_key, parse_multikey}; +use atrium_crypto::multibase; use atrium_crypto::Algorithm; use thiserror::Error; @@ -12,7 +13,9 @@ pub enum Error { #[error("Could not parse pds from doc: {0:?}")] Pds(DidDocument), #[error(transparent)] - Crypto(#[from] atrium_crypto::error::Error), + Crypto(#[from] atrium_crypto::Error), + #[error(transparent)] + Multibase(#[from] multibase::Error), } pub type Result = std::result::Result; @@ -50,12 +53,13 @@ fn get_did_key_from_multibase( ) -> Result> { Ok(match r#type.as_str() { "EcdsaSecp256r1VerificationKey2019" => { - Some(format_did_key_str(Algorithm::P256, &public_key_multibase)?) + let (_, key) = multibase::decode(public_key_multibase)?; + Some(format_did_key(Algorithm::P256, &key)?) + } + "EcdsaSecp256k1VerificationKey2019" => { + let (_, key) = multibase::decode(public_key_multibase)?; + Some(format_did_key(Algorithm::Secp256k1, &key)?) } - "EcdsaSecp256k1VerificationKey2019" => Some(format_did_key_str( - Algorithm::Secp256k1, - &public_key_multibase, - )?), "Multikey" => { let (alg, key) = parse_multikey(&public_key_multibase)?; Some(format_did_key(alg, &key)?)