diff --git a/.github/workflows/cargo.yaml b/.github/workflows/cargo.yaml new file mode 100644 index 0000000..45396e4 --- /dev/null +++ b/.github/workflows/cargo.yaml @@ -0,0 +1,15 @@ +on: [push] + +name: CI + +jobs: + build_and_test: + name: Rust project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + - run: cargo build --release + - run: cargo test \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index d379e64..13db738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,8 @@ [package] -name = "example-rs" +name = "infra-did" version = "0.1.0" -authors = ["Brick Pop "] -edition = "2018" - -[lib] -name = "example" -crate-type = ["staticlib", "cdylib"] +authors = ["InfraBlockchain"] +edition = "2021" [dependencies] hex = "0.4.3" @@ -23,3 +19,21 @@ blake2-rfc = "0.2.18" rustc-hex = "2.0.1" ed25519-dalek = "1.0.1" bs58 = "0.4.0" +rand = "0.7.0" +chrono = { version = "0.4", features = ["serde"] } +thiserror = "1.0.40" +iref = { version = "2.2.2", features = ["serde"] } +static-iref = "2.0.0" +multibase = "0.9.1" +ssi-vc = "0.2.1" +ssi = "0.7.0" +ssi-json-ld = "0.2.2" +ssi-ldp = "0.3.2" +base64 = "0.12.3" +ssi-jws = "0.1.1" +ssi-dids = { version = "0.1.1", features = ["example"] } +async-std = { version = "1.9", features = ["attributes"] } +async-trait = "0.1.68" +serde_urlencoded = "0.7.1" +percent-encoding = "2.2.0" +anyhow = "1.0.70" diff --git a/Makefile b/Makefile deleted file mode 100644 index 52ca3b6..0000000 --- a/Makefile +++ /dev/null @@ -1,129 +0,0 @@ -.DEFAULT_GOAL := help -PROJECTNAME=$(shell basename "$(PWD)") -SOURCES=$(sort $(wildcard ./src/*.rs ./src/**/*.rs)) - -OS_NAME=$(shell uname | tr '[:upper:]' '[:lower:]') -PATH := $(ANDROID_NDK_HOME)/toolchains/llvm/prebuilt/$(OS_NAME)-x86_64/bin:$(PATH) - -ANDROID_AARCH64_LINKER=$(ANDROID_NDK_HOME)/toolchains/llvm/prebuilt/$(OS_NAME)-x86_64/bin/aarch64-linux-android29-clang -ANDROID_ARMV7_LINKER=$(ANDROID_NDK_HOME)/toolchains/llvm/prebuilt/$(OS_NAME)-x86_64/bin/armv7a-linux-androideabi29-clang -ANDROID_I686_LINKER=$(ANDROID_NDK_HOME)/toolchains/llvm/prebuilt/$(OS_NAME)-x86_64/bin/i686-linux-android29-clang -ANDROID_X86_64_LINKER=$(ANDROID_NDK_HOME)/toolchains/llvm/prebuilt/$(OS_NAME)-x86_64/bin/x86_64-linux-android29-clang - -SHELL := /bin/bash - -# ############################################################################## -# # GENERAL -# ############################################################################## - -.PHONY: help -help: makefile - @echo - @echo " Available actions in "$(PROJECTNAME)":" - @echo - @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' - @echo - -## init: Install missing dependencies. -.PHONY: init -init: - rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim - rustup target add aarch64-apple-darwin x86_64-apple-darwin - #rustup target add armv7-apple-ios armv7s-apple-ios i386-apple-ios ## deprecated - rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android - @if [ $$(uname) == "Darwin" ] ; then cargo install cargo-lipo ; fi - cargo install cbindgen - -## : - -# ############################################################################## -# # RECIPES -# ############################################################################## - -## all: Compile iOS, Android and bindings targets -all: ios macos android bindings - -## ios: Compile the iOS universal library -ios: target/universal/release/libexample.a - -target/universal/release/libexample.a: $(SOURCES) ndk-home - @if [ $$(uname) == "Darwin" ] ; then \ - cargo lipo --release ; \ - else echo "Skipping iOS compilation on $$(uname)" ; \ - fi - @echo "[DONE] $@" - -## macos: Compile the macOS libraries -macos: target/x86_64-apple-darwin/release/libexample.dylib target/aarch64-apple-darwin/release/libexample.dylib - -target/x86_64-apple-darwin/release/libexample.dylib: $(SOURCES) - @if [ $$(uname) == "Darwin" ] ; then \ - cargo lipo --release --targets x86_64-apple-darwin ; \ - else echo "Skipping macOS compilation on $$(uname)" ; \ - fi - @echo "[DONE] $@" - -target/aarch64-apple-darwin/release/libexample.dylib: $(SOURCES) - @if [ $$(uname) == "Darwin" ] ; then \ - cargo lipo --release --targets aarch64-apple-darwin ; \ - else echo "Skipping macOS compilation on $$(uname)" ; \ - fi - @echo "[DONE] $@" - -## android: Compile the android targets (arm64, armv7 and i686) -android: target/aarch64-linux-android/release/libexample.so target/armv7-linux-androideabi/release/libexample.so target/i686-linux-android/release/libexample.so target/x86_64-linux-android/release/libexample.so - -target/aarch64-linux-android/release/libexample.so: $(SOURCES) ndk-home - CC_aarch64_linux_android=$(ANDROID_AARCH64_LINKER) \ - CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$(ANDROID_AARCH64_LINKER) \ - cargo build --target aarch64-linux-android --release - @echo "[DONE] $@" - -target/armv7-linux-androideabi/release/libexample.so: $(SOURCES) ndk-home - CC_armv7_linux_androideabi=$(ANDROID_ARMV7_LINKER) \ - CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=$(ANDROID_ARMV7_LINKER) \ - cargo build --target armv7-linux-androideabi --release - @echo "[DONE] $@" - -target/i686-linux-android/release/libexample.so: $(SOURCES) ndk-home - CC_i686_linux_android=$(ANDROID_I686_LINKER) \ - CARGO_TARGET_I686_LINUX_ANDROID_LINKER=$(ANDROID_I686_LINKER) \ - cargo build --target i686-linux-android --release - @echo "[DONE] $@" - -target/x86_64-linux-android/release/libexample.so: $(SOURCES) ndk-home - CC_x86_64_linux_android=$(ANDROID_X86_64_LINKER) \ - CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=$(ANDROID_X86_64_LINKER) \ - cargo build --target x86_64-linux-android --release - @echo "[DONE] $@" - -.PHONY: ndk-home -ndk-home: - @if [ ! -d "${ANDROID_NDK_HOME}" ] ; then \ - echo "Error: Please, set the ANDROID_NDK_HOME env variable to point to your NDK folder" ; \ - exit 1 ; \ - fi - -## bindings: Generate the .h file for iOS -bindings: target/bindings.h - -target/bindings.h: $(SOURCES) - cbindgen ./src/lib.rs -c cbindgen.toml | grep -v \#include | uniq > $@ - @echo "[DONE] $@" - -## : - -# ############################################################################## -# # OTHER -# ############################################################################## - -## clean: -.PHONY: clean -clean: - cargo clean - rm -f target/bindings.h target/bindings.src.h - -## test: -.PHONY: test -test: - cargo test diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbcb853 --- /dev/null +++ b/README.md @@ -0,0 +1,230 @@ +# infra-did-rust + +**This Library is rust version of ss58 based did in infra-did-js** + +- Infra DID Method Spec + + - https://github.com/InfraBlockchain/infra-did-method-specs/blob/main/docs/Infra-DID-method-spec.md + +- Infra DID Registry Smart Contract on InfraBlockchain + + - https://github.com/InfraBlockchain/infra-did-registry + +- Infra DID Resolver (DIF javascript universal resolver compatible) + - https://github.com/InfraBlockchain/infra-did-resolver + +Feature provided by infra-did-rust Library : + +- Infra DID Creation (SS58) +- Resolve Infra DID (SS58) +- JSON-LD VC/VP creation/verification + +## Installation + +- **Using [crates](https://crates.io/)**: + +```sh +cargo add infra-did +``` + +### Infra DID Creation + +currently ed25519 curve is supported + +```rust + println!("{:?}", generate_ss58_did("01".to_string(), AddressType::Ed25519)); + /* + { + "address":"5FEWTgcxvefibCpoy7rfPx7WKimuWAuRx7JZmhwRTvQosZse","did":"did:infra:01:5FEWTgcxvefibCpoy7rfPx7WKimuWAuRx7JZmhwRTvQosZse", + "mnemonic":"second lucky rifle size spray advance approve view melody carpet offer thumb","private_key":"0177ced8efef49f17fec276d56de1b2037fbcc6348693d22436633043247a942","public_key":"8c2eca839176ba7b2ee50aa8aa1dc406abb89a4ebe2d90fcda489fee29795c94" + } + */ +``` + +### Issuing and Verifying W3C Verifiable Credential (VC), Verifiable Presentation (VP) + +#### DID Resolver + +```rust + let resolver = InfraDIDResolver::default(); + let (_, doc, _) = resolver + .resolve( + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + &ResolutionInputMetadata::default(), + ) + .await; + println!("{:?}", serde_json::to_string_pretty(&doc).unwrap()); + /* + { + "@context": "https://www.w3.org/ns/did/v1", + "id": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "verificationMethod": [ + { + "id": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "publicKeyBase58": "F9JHKboDqg3tK9wnrt8z8xwZRnoZCJAHTdxXVuUMW8z2" + }, + { + "id": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "publicKeyMultibase": "z6MktbZKur3fBDYMRenVYT6pz4VZFN5QcBQe9esTLBSNRMmQ" + } + ], + "authentication": [ + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2" + ], + "assertionMethod": [ + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2" + ] + } + */ +``` + +#### Create and Verify Verifiable Credential JSON-LD + +```rust + let did = "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW".to_string(); + let hex_secret_key = + "8006aaa5985f1d72e916167bdcbc663232cef5823209b1246728f73137888170".to_string(); + let vc_str = r###"{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5FDseiC76zPek2YYkuyenu4ZgxZ7PUWXt9d19HNB5CaQXt5U", + "type": [ + "VerifiableCredential" + ], + "credentialSubject": [ + { + "id": "did:example:d23dd687a7dc6787646f2eb98d0" + } + ], + "issuanceDate": "2023-04-24T06:08:03.039Z", + "issuer": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW" + }"###; + + let vc = issue_credential(did, hex_secret_key, vc_str.to_string()).await; + let verify = verify_credential(vc.to_string()).await.unwrap(); + assert_eq!(verify, "true".to_string()); +``` + +Verified Credential Result + +```json + { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5FDseiC76zPek2YYkuyenu4ZgxZ7PUWXt9d19HNB5CaQXt5U", + "type": [ + "VerifiableCredential" + ], + "credentialSubject": [ + { + "id": "did:example:d23dd687a7dc6787646f2eb98d0" + } + ], + "issuer": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "issuanceDate": "2023-04-24T06:08:03.039Z", + "proof": { + "@context": [ + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": "Ed25519Signature2020", + "proofPurpose": "assertionMethod", + "proofValue": "z5LPkbsnBbYTAJJ3fcwkEBtbfkT2wnLhLNmcSwj2e8FSYfMrrWoFey6958gm7G93UfTu6qkLkD1nwgzbzSihbu3jw", + "verificationMethod": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2", + "created": "2023-04-30T23:53:20.028Z" + } + } +``` + +#### Create and Verify Verifiable Presentation JSON-LD + +```rust + let did = "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW".to_string(); + let hex_secret_key = "8006aaa5985f1d72e916167bdcbc663232cef5823209b1246728f73137888170".to_string(); + let vc_str = r###"{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5FDseiC76zPek2YYkuyenu4ZgxZ7PUWXt9d19HNB5CaQXt5U", + "type": [ + "VerifiableCredential" + ], + "credentialSubject": [ + { + "id": "did:example:d23dd687a7dc6787646f2eb98d0" + } + ], + "issuer": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "issuanceDate": "2023-04-24T06:08:03.039Z", + "proof": { + "@context": [ + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": "Ed25519Signature2020", + "proofPurpose": "assertionMethod", + "proofValue": "z3gFJvCvNYTVQJ7R7tXzbmAyZ62g3ZymbzwTrWJhgwatJouope5GnQmz7NW2zAVVYbor5KUW8TUa1V5KADPp8kBog", + "verificationMethod": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2", + "created": "2023-04-25T23:52:13.770Z" + } + }"###; + + let vp = issue_presentation(did, hex_secret_key, vc_str.to_string()).await; + let verify: String = verify_presentation(vp.to_string()).await.unwrap(); + assert_eq!(verify, "true".to_string()); +``` + +Verified Presentation Result + +```json + { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5D7nQ3WTPtJx79ywCbLum7fyVt1DKcg32jB6SdABhhwpzT9a", + "type": "VerifiablePresentation", + "verifiableCredential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5FDseiC76zPek2YYkuyenu4ZgxZ7PUWXt9d19HNB5CaQXt5U", + "type": [ + "VerifiableCredential" + ], + "credentialSubject": [ + { + "id": "did:example:d23dd687a7dc6787646f2eb98d0" + } + ], + "issuer": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "issuanceDate": "2023-04-24T06:08:03.039Z", + "proof": { + "@context": [ + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": "Ed25519Signature2020", + "proofPurpose": "assertionMethod", + "proofValue": "z3gFJvCvNYTVQJ7R7tXzbmAyZ62g3ZymbzwTrWJhgwatJouope5GnQmz7NW2zAVVYbor5KUW8TUa1V5KADPp8kBog", + "verificationMethod": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2", + "created": "2023-04-25T23:52:13.770Z" + } + }, + "proof": { + "@context": [ + "https://w3id.org/security/suites/ed25519-2020/v1" + ], + "type": "Ed25519Signature2020", + "proofPurpose": "assertionMethod", + "proofValue": "zWPTW2TC7WcEUk1F25saJxHKKt2HjsdSW3GEk12d2mJbUN2dJntEBng9N1RmZz6XuHqNuh7Dq1d4DTpyZ1GEokRq", + "verificationMethod": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2", + "created": "2023-05-01T00:03:59.511Z" + }, + "holder": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW" + } +``` \ No newline at end of file diff --git a/cbindgen.toml b/cbindgen.toml deleted file mode 100644 index 296d3de..0000000 --- a/cbindgen.toml +++ /dev/null @@ -1,8 +0,0 @@ -language = "C" -autogen_warning = "// NOTE: Append the lines below to ios/Classes/Plugin.h" -#namespace = "ffi" -#include_guard = "CBINDGEN_BINDINGS_H" - -[defines] -"target_os = ios" = "TARGET_OS_IOS" -"target_os = macos" = "TARGET_OS_MACOS" \ No newline at end of file diff --git a/src/crypto/ed25519.rs b/src/crypto/ed25519.rs new file mode 100644 index 0000000..9e65fc1 --- /dev/null +++ b/src/crypto/ed25519.rs @@ -0,0 +1,200 @@ +use base58::ToBase58; +use bip39::{Language, Mnemonic}; +use ed25519_dalek::{ + Keypair, PublicKey, SecretKey, Signature, Signer, Verifier, KEYPAIR_LENGTH, SECRET_KEY_LENGTH, +}; +use rand::rngs::OsRng; +use substrate_bip39::mini_secret_from_entropy; + +pub struct Ed25519KeyPair(ed25519_dalek::Keypair); + +impl Ed25519KeyPair { + pub fn generate() -> Ed25519KeyPair { + let mut csprng = OsRng {}; + let keypair: Keypair = Keypair::generate(&mut csprng); + Ed25519KeyPair(keypair) + } + + pub fn from_bip39_phrase(phrase: &str, password: Option<&str>) -> Ed25519KeyPair { + let mnemonic = Mnemonic::from_phrase(phrase, Language::English).unwrap(); + let mini_secret_key = + mini_secret_from_entropy(mnemonic.entropy(), password.unwrap_or("")).unwrap(); + + let secret_key: SecretKey = SecretKey::from_bytes(mini_secret_key.as_bytes()).unwrap(); + let public_key: PublicKey = PublicKey::from(&secret_key).into(); + + let secret = secret_key.to_bytes(); + let public = public_key.to_bytes(); + + let mut keypair_bytes: [u8; KEYPAIR_LENGTH] = [0u8; KEYPAIR_LENGTH]; + + keypair_bytes[..SECRET_KEY_LENGTH].copy_from_slice(&secret); + keypair_bytes[SECRET_KEY_LENGTH..].copy_from_slice(&public); + + let keypair = ed25519_dalek::Keypair::from_bytes(&keypair_bytes).ok(); + Ed25519KeyPair(keypair.unwrap()) + } + + pub fn from_secret_key_bytes(bytes: &[u8]) -> Ed25519KeyPair { + let secret_key: SecretKey = SecretKey::from_bytes(bytes).unwrap(); + let public_key: PublicKey = (&secret_key).into(); + + let secret = secret_key.to_bytes(); + let public = public_key.to_bytes(); + + let mut keypair_bytes: [u8; KEYPAIR_LENGTH] = [0u8; KEYPAIR_LENGTH]; + + keypair_bytes[..SECRET_KEY_LENGTH].copy_from_slice(&secret); + keypair_bytes[SECRET_KEY_LENGTH..].copy_from_slice(&public); + + let keypair = ed25519_dalek::Keypair::from_bytes(&keypair_bytes).ok(); + Ed25519KeyPair(keypair.unwrap()) + } + + pub fn from_public_key_bytes(bytes: &[u8]) -> Ed25519KeyPair { + let public_key: PublicKey = PublicKey::from_bytes(bytes).unwrap(); + + let public = public_key.to_bytes(); + + let mut keypair_bytes: [u8; KEYPAIR_LENGTH] = [0u8; KEYPAIR_LENGTH]; + + keypair_bytes[SECRET_KEY_LENGTH..].copy_from_slice(&public); + + let keypair = ed25519_dalek::Keypair::from_bytes(&keypair_bytes).ok(); + Ed25519KeyPair(keypair.unwrap()) + } + + pub fn to_public_key_bytes(&self) -> [u8; 32] { + self.0.public.to_bytes() + } + + pub fn to_secret_key_bytes(&self) -> [u8; 32] { + self.0.secret.to_bytes() + } + + pub fn ss58_address(&self, prefix: u8) -> String { + let mut v = vec![prefix]; + v.extend_from_slice(&self.0.public.to_bytes()); + let r = ss58hash(&v); + v.extend_from_slice(&r.as_bytes()[0..2]); + v.to_base58() + } + + pub fn sign(&self, message: &[u8]) -> Signature { + let signature: Signature = self.0.sign(message); + signature + } + + pub fn verify_signature(&self, message: &[u8], signature: &[u8]) -> bool { + let signature = ed25519_dalek::Signature::from_bytes(signature).unwrap(); + let public_key: PublicKey = self.0.public; + let verified: bool = public_key.verify(message, &signature).is_ok(); + verified + } +} + +fn ss58hash(data: &[u8]) -> blake2_rfc::blake2b::Blake2bResult { + const PREFIX: &[u8] = b"SS58PRE"; + + let mut context = blake2_rfc::blake2b::Blake2b::new(64); + context.update(PREFIX); + context.update(data); + context.finalize() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate() { + let keypair = Ed25519KeyPair::generate(); + match keypair { + Ed25519KeyPair(keypair) => { + let bytes = keypair.to_bytes(); + let secret_key_bytes = &bytes[..SECRET_KEY_LENGTH]; + let public_key_bytes = &bytes[SECRET_KEY_LENGTH..]; + println!("{:?}", keypair.to_bytes()); + println!("{:?}", hex::encode(secret_key_bytes)); + println!("{:?}", hex::encode(public_key_bytes)); + } + } + } + + #[test] + fn test_from_bip39_phrase() { + let keypair = Ed25519KeyPair::from_bip39_phrase( + "caution juice atom organ advance problem want pledge someone senior holiday very", + Some(""), + ); + match keypair { + Ed25519KeyPair(keypair) => { + let keypair_bytes = keypair.to_bytes(); + let secret_key_bytes = &keypair_bytes[..SECRET_KEY_LENGTH]; + let publuc_key_bytes = &keypair_bytes[SECRET_KEY_LENGTH..]; + assert_eq!( + hex::encode(secret_key_bytes), + "c8fa03532fb22ee1f7f6908b9c02b4e72483f0dbd66e4cd456b8f34c6230b849" + ); + assert_eq!( + hex::encode(publuc_key_bytes), + "bd7436a22571207d018ffe83f5dc77d0750b7777f1eb169053d40201d6c68d53" + ); + } + } + } + + #[test] + fn test_from_secret_key_bytes() { + // https://datatracker.ietf.org/doc/html/rfc8032#section-7.1 + let bytes = [ + 157, 97, 177, 157, 239, 253, 90, 96, 186, 132, 74, 244, 146, 236, 44, 196, 68, 73, 197, + 105, 123, 50, 105, 25, 112, 59, 172, 3, 28, 174, 127, 96, + ]; + let keypair = Ed25519KeyPair::from_secret_key_bytes(&bytes); + match keypair { + Ed25519KeyPair(keypair) => { + let bytes = keypair.to_bytes(); + let secret_key_bytes = &bytes[..SECRET_KEY_LENGTH]; + let public_key_bytes = &bytes[SECRET_KEY_LENGTH..]; + assert_eq!( + hex::encode(secret_key_bytes), + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + ); + assert_eq!( + hex::encode(public_key_bytes), + "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + ); + } + } + } + + #[test] + fn test_sign() { + let bytes = [ + 157, 97, 177, 157, 239, 253, 90, 96, 186, 132, 74, 244, 146, 236, 44, 196, 68, 73, 197, + 105, 123, 50, 105, 25, 112, 59, 172, 3, 28, 174, 127, 96, + ]; + let keypair = Ed25519KeyPair::from_secret_key_bytes(&bytes); + println!("{:?}", keypair.to_secret_key_bytes()); + println!("{:?}", keypair.to_public_key_bytes()); + let message = []; + let signature = keypair.sign(&message); + let sig_multibase = multibase::encode(multibase::Base::Base58Btc, signature); + assert_eq!(sig_multibase,"z5awYiUvGiDFA33EJjj4TXJG44a5afJc8QjWRpGgQiu6b23jCr7yndW2fmp9ujwqJVe32J456wV3VF78Asb1obnTc"); + } + + #[test] + fn test_verify_signature() { + let bytes = [ + 215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114, + 243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26, + ]; + let keypair = Ed25519KeyPair::from_public_key_bytes(&bytes); + let message = []; + let sig_multibase = "z5awYiUvGiDFA33EJjj4TXJG44a5afJc8QjWRpGgQiu6b23jCr7yndW2fmp9ujwqJVe32J456wV3VF78Asb1obnTc"; + let (_base, sig) = multibase::decode(sig_multibase).unwrap(); + let verify = keypair.verify_signature(&message, &sig); + assert_eq!(verify, true); + } +} diff --git a/src/crypto/keytype.rs b/src/crypto/keytype.rs new file mode 100644 index 0000000..51f5d69 --- /dev/null +++ b/src/crypto/keytype.rs @@ -0,0 +1,6 @@ +use super::{ed25519::Ed25519KeyPair, sr25519::Sr25519KeyPair}; + +pub enum KeyType { + Ed25519(Ed25519KeyPair), + Sr25519(Sr25519KeyPair), +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs new file mode 100644 index 0000000..e98cb7e --- /dev/null +++ b/src/crypto/mod.rs @@ -0,0 +1,3 @@ +pub mod ed25519; +pub mod keytype; +pub mod sr25519; diff --git a/src/crypto/sr25519.rs b/src/crypto/sr25519.rs new file mode 100644 index 0000000..86b7661 --- /dev/null +++ b/src/crypto/sr25519.rs @@ -0,0 +1,365 @@ +use base58::ToBase58; +use bip39::{Language, Mnemonic}; +use codec::{Decode, Encode}; +use regex::Regex; +use schnorrkel::derive::{ChainCode, Derivation}; +use schnorrkel::{ + ExpansionMode, MiniSecretKey, PublicKey, SecretKey, Signature, KEYPAIR_LENGTH, + SECRET_KEY_LENGTH, +}; +use substrate_bip39::mini_secret_from_entropy; + +use lazy_static::lazy_static; + +pub struct Sr25519KeyPair(schnorrkel::Keypair); + +const SIGNING_CTX: &[u8] = b"substrate"; +const JUNCTION_ID_LEN: usize = 32; +const CHAIN_CODE_LENGTH: usize = 32; + +impl Sr25519KeyPair { + pub fn from_bip39_phrase(phrase: &str, password: Option<&str>) -> Option { + let mnemonic = Mnemonic::from_phrase(phrase, Language::English).ok()?; + let mini_secret_key = + mini_secret_from_entropy(mnemonic.entropy(), password.unwrap_or("")).ok()?; + + Some(Sr25519KeyPair( + mini_secret_key.expand_to_keypair(ExpansionMode::Ed25519), + )) + } + + // Should match implementation at https://github.com/paritytech/substrate/blob/master/core/primitives/src/crypto.rs#L653-L682 + pub fn from_suri(suri: &str) -> Option { + lazy_static! { + static ref RE_SURI: Regex = { + Regex::new(r"^(?P\w+( \w+)*)?(?P(//?[^/]+)*)(///(?P.*))?$") + .expect("constructed from known-good static value; qed") + }; + static ref RE_JUNCTION: Regex = + Regex::new(r"/(/?[^/]+)").expect("constructed from known-good static value; qed"); + } + + let cap = RE_SURI.captures(suri)?; + let path = RE_JUNCTION + .captures_iter(&cap["path"]) + .map(|j| DeriveJunction::from(&j[1])); + + let pair = Self::from_bip39_phrase( + cap.name("phrase").map(|p| p.as_str())?, + cap.name("password").map(|p| p.as_str()), + )?; + + Some(pair.derive(path)) + } + + pub fn from_mini_secret_key_bytes(bytes: &[u8; 32]) -> Option { + let mini_secret_key = MiniSecretKey::from_bytes(bytes).unwrap(); + let public_key = mini_secret_key.expand_to_public(ExpansionMode::Ed25519); + println!("{:?}", public_key); + Some(Sr25519KeyPair( + mini_secret_key.expand_to_keypair(ExpansionMode::Ed25519), + )) + } + + pub fn from_secret_key_bytes(bytes: &[u8; SECRET_KEY_LENGTH]) -> Option { + let secret_key: SecretKey = SecretKey::from_bytes(bytes).ok()?; + let public_key: PublicKey = secret_key.to_public(); + + let secret = secret_key.to_bytes(); + let public = public_key.to_bytes(); + + let mut keypair_bytes: [u8; KEYPAIR_LENGTH] = [0u8; KEYPAIR_LENGTH]; + keypair_bytes[..SECRET_KEY_LENGTH].copy_from_slice(&secret); + keypair_bytes[SECRET_KEY_LENGTH..].copy_from_slice(&public); + + let keypair = schnorrkel::Keypair::from_bytes(&keypair_bytes).ok(); + Some(Sr25519KeyPair(keypair.unwrap())) + } + + pub fn from_public_key_bytes(bytes: &[u8]) -> Option { + let public_key: PublicKey = PublicKey::from_bytes(bytes).unwrap(); + + let public = public_key.to_bytes(); + + let mut keypair_bytes: [u8; KEYPAIR_LENGTH] = [0u8; KEYPAIR_LENGTH]; + keypair_bytes[SECRET_KEY_LENGTH..].copy_from_slice(&public); + let keypair = schnorrkel::Keypair::from_bytes(&keypair_bytes).ok(); + Some(Sr25519KeyPair(keypair.unwrap())) + } + + pub fn to_public_key_bytes(&self) -> [u8; 32] { + self.0.public.to_bytes() + } + + pub fn to_secret_key_bytes(&self) -> [u8; 64] { + self.0.secret.to_bytes() + } + + pub fn to_ed25519_key_bytes(&self) -> [u8; KEYPAIR_LENGTH] { + self.0.to_half_ed25519_bytes() + } + + fn derive(&self, path: impl Iterator) -> Self { + let init = self.0.secret.clone(); + let result = path.fold(init, |acc, j| match j { + DeriveJunction::Soft(cc) => acc.derived_key_simple(ChainCode(cc), &[]).0, + DeriveJunction::Hard(cc) => derive_hard_junction(&acc, cc), + }); + + Sr25519KeyPair(result.to_keypair()) + } + + pub fn ss58_address(&self, prefix: u8) -> String { + let mut v = vec![prefix]; + v.extend_from_slice(&self.0.public.to_bytes()); + let r = ss58hash(&v); + v.extend_from_slice(&r.as_bytes()[0..2]); + v.to_base58() + } + + pub fn sign(&self, message: &[u8]) -> [u8; 64] { + let context = schnorrkel::signing_context(SIGNING_CTX); + self.0.sign(context.bytes(message)).to_bytes() + } + + pub fn verify_signature(&self, message: &[u8], signature: &[u8]) -> Option { + let context = schnorrkel::signing_context(SIGNING_CTX); + + let signature = Signature::from_bytes(signature).ok()?; + + Some(self.0.verify(context.bytes(&message), &signature).is_ok()) + } +} + +fn derive_hard_junction(secret: &SecretKey, cc: [u8; CHAIN_CODE_LENGTH]) -> SecretKey { + secret + .hard_derive_mini_secret_key(Some(ChainCode(cc)), b"") + .0 + .expand(ExpansionMode::Ed25519) +} + +/// A since derivation junction description. It is the single parameter used when creating +/// a new secret key from an existing secret key and, in the case of `SoftRaw` and `SoftIndex` +/// a new public key from an existing public key. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Encode, Decode)] +enum DeriveJunction { + /// Soft (vanilla) derivation. Public keys have a correspondent derivation. + Soft([u8; JUNCTION_ID_LEN]), + /// Hard ("hardened") derivation. Public keys do not have a correspondent derivation. + Hard([u8; JUNCTION_ID_LEN]), +} + +impl DeriveJunction { + /// Consume self to return a hard derive junction with the same chain code. + fn harden(self) -> Self { + DeriveJunction::Hard(self.unwrap_inner()) + } + + /// Create a new soft (vanilla) DeriveJunction from a given, encodable, value. + /// + /// If you need a hard junction, use `hard()`. + fn soft(index: T) -> Self { + let mut cc: [u8; JUNCTION_ID_LEN] = Default::default(); + index.using_encoded(|data| { + if data.len() > JUNCTION_ID_LEN { + let hash_result = blake2_rfc::blake2b::blake2b(JUNCTION_ID_LEN, &[], data); + let hash = hash_result.as_bytes(); + cc.copy_from_slice(hash); + } else { + cc[0..data.len()].copy_from_slice(data); + } + }); + DeriveJunction::Soft(cc) + } + + /// Consume self to return the chain code. + fn unwrap_inner(self) -> [u8; JUNCTION_ID_LEN] { + match self { + DeriveJunction::Hard(c) | DeriveJunction::Soft(c) => c, + } + } +} + +impl> From for DeriveJunction { + fn from(j: T) -> DeriveJunction { + let j = j.as_ref(); + let (code, hard) = if j.starts_with("/") { + (&j[1..], true) + } else { + (j, false) + }; + + let res = if let Ok(n) = str::parse::(code) { + // number + DeriveJunction::soft(n) + } else { + // something else + DeriveJunction::soft(code) + }; + + if hard { + res.harden() + } else { + res + } + } +} + +fn ss58hash(data: &[u8]) -> blake2_rfc::blake2b::Blake2bResult { + const PREFIX: &[u8] = b"SS58PRE"; + + let mut context = blake2_rfc::blake2b::Blake2b::new(64); + context.update(PREFIX); + context.update(data); + context.finalize() +} + +#[cfg(test)] +mod tests { + use schnorrkel::SECRET_KEY_LENGTH; + + use super::*; + + #[test] + fn test_from_bip39_phrase() { + let keypair = Sr25519KeyPair::from_bip39_phrase( + "true crowd stereo border country ocean mountain sadness term stumble media glory", + Some(""), + ) + .unwrap(); + match keypair { + Sr25519KeyPair(keypair) => { + let keypair_bytes = keypair.to_bytes(); + let secret_key_bytes = &keypair_bytes[..SECRET_KEY_LENGTH]; + let publuc_key_bytes = &keypair_bytes[SECRET_KEY_LENGTH..]; + assert_eq!(hex::encode(secret_key_bytes),"b3370307d69f13cece7c28b2fa6380bcd56e9f32c9daa5a7be545efb65bc370dbab0ac540259f83925afca9192fa73f99f3ec9ca1c8da3297b0e05a87fee3df3"); + assert_eq!( + hex::encode(publuc_key_bytes), + "f02283ff600d00613244e1e43dc88d56fec666223de7ebeb3f32e93a375fe12b" + ); + } + } + } + + #[test] + fn test_from_suri() { + let keypair = Sr25519KeyPair::from_suri( + "true crowd stereo border country ocean mountain sadness term stumble media glory", + ) + .unwrap(); + match keypair { + Sr25519KeyPair(keypair) => { + let keypair_bytes = keypair.to_bytes(); + let secret_key_bytes = &keypair_bytes[..SECRET_KEY_LENGTH]; + let publuc_key_bytes = &keypair_bytes[SECRET_KEY_LENGTH..]; + assert_eq!(hex::encode(secret_key_bytes),"b3370307d69f13cece7c28b2fa6380bcd56e9f32c9daa5a7be545efb65bc370dbab0ac540259f83925afca9192fa73f99f3ec9ca1c8da3297b0e05a87fee3df3"); + assert_eq!( + hex::encode(publuc_key_bytes), + "f02283ff600d00613244e1e43dc88d56fec666223de7ebeb3f32e93a375fe12b" + ); + } + } + } + + #[test] + fn test_from_mini_secret_key_bytes() { + let bytes = [ + 200, 227, 122, 92, 143, 199, 212, 106, 232, 198, 122, 58, 208, 6, 178, 26, 220, 207, + 30, 15, 194, 115, 193, 89, 75, 112, 249, 220, 241, 127, 234, 214, + ]; + let keypair = Sr25519KeyPair::from_mini_secret_key_bytes(&bytes); + match keypair { + Some(Sr25519KeyPair(keypair)) => { + let bytes = keypair.to_bytes(); + let secret_key_bytes = &bytes[..SECRET_KEY_LENGTH]; + let public_key_bytes = &bytes[SECRET_KEY_LENGTH..]; + assert_eq!( + hex::encode(secret_key_bytes), + "b3370307d69f13cece7c28b2fa6380bcd56e9f32c9daa5a7be545efb65bc370dbab0ac540259f83925afca9192fa73f99f3ec9ca1c8da3297b0e05a87fee3df3" + ); + assert_eq!( + hex::encode(public_key_bytes), + "f02283ff600d00613244e1e43dc88d56fec666223de7ebeb3f32e93a375fe12b" + ); + } + None => assert!(false), + } + } + + #[test] + fn test_from_secret_key_bytes() { + let bytes = [ + 179, 55, 3, 7, 214, 159, 19, 206, 206, 124, 40, 178, 250, 99, 128, 188, 213, 110, 159, + 50, 201, 218, 165, 167, 190, 84, 94, 251, 101, 188, 55, 13, 186, 176, 172, 84, 2, 89, + 248, 57, 37, 175, 202, 145, 146, 250, 115, 249, 159, 62, 201, 202, 28, 141, 163, 41, + 123, 14, 5, 168, 127, 238, 61, 243, + ]; + let keypair = Sr25519KeyPair::from_secret_key_bytes(&bytes); + match keypair { + Some(Sr25519KeyPair(keypair)) => { + let bytes = keypair.to_bytes(); + let secret_key_bytes = &bytes[..SECRET_KEY_LENGTH]; + let public_key_bytes = &bytes[SECRET_KEY_LENGTH..]; + assert_eq!( + hex::encode(secret_key_bytes), + "b3370307d69f13cece7c28b2fa6380bcd56e9f32c9daa5a7be545efb65bc370dbab0ac540259f83925afca9192fa73f99f3ec9ca1c8da3297b0e05a87fee3df3" + ); + assert_eq!( + hex::encode(public_key_bytes), + "f02283ff600d00613244e1e43dc88d56fec666223de7ebeb3f32e93a375fe12b" + ); + } + None => assert!(false), + } + } + + #[test] + fn test_from_public_key_bytes() { + let bytes = [ + 240, 34, 131, 255, 96, 13, 0, 97, 50, 68, 225, 228, 61, 200, 141, 86, 254, 198, 102, + 34, 61, 231, 235, 235, 63, 50, 233, 58, 55, 95, 225, 43, + ]; + let keypair = Sr25519KeyPair::from_public_key_bytes(&bytes).unwrap(); + assert_eq!(keypair.to_public_key_bytes(), bytes); + } + + #[test] + fn test_ss58_address() { + let keypair = Sr25519KeyPair::from_suri( + "true crowd stereo border country ocean mountain sadness term stumble media glory", + ) + .unwrap(); + let address = keypair.ss58_address(42); + assert_eq!(address, "5HVZbuy7bpM8NX7VXTyxoL5dvk5W3496vkknoWtVhF7cRjc3"); + } + + #[test] + fn test_sign() { + let keypair = Sr25519KeyPair::from_suri( + "true crowd stereo border country ocean mountain sadness term stumble media glory", + ) + .unwrap(); + let message = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let signature = keypair.sign(&message); + // sr25519 signature is non-deterministic + assert_eq!(hex::encode(signature).len(), 128); + } + + #[test] + fn test_verify_signature() { + let public_key_bytes = [ + 240, 34, 131, 255, 96, 13, 0, 97, 50, 68, 225, 228, 61, 200, 141, 86, 254, 198, 102, + 34, 61, 231, 235, 235, 63, 50, 233, 58, 55, 95, 225, 43, + ]; + let keypair = Sr25519KeyPair::from_public_key_bytes(&public_key_bytes).unwrap(); + let message = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + let signature = [ + 80, 147, 218, 23, 52, 24, 12, 20, 87, 87, 240, 184, 36, 197, 125, 76, 121, 152, 133, + 133, 226, 196, 178, 32, 112, 254, 10, 160, 116, 123, 149, 57, 11, 223, 29, 28, 192, 78, + 190, 6, 248, 99, 45, 96, 43, 87, 164, 205, 213, 177, 62, 199, 240, 195, 50, 21, 209, + 155, 206, 38, 7, 23, 245, 143, + ]; + let verify = keypair.verify_signature(&message, &signature).unwrap(); + assert_eq!(verify, true); + } +} diff --git a/src/did/mod.rs b/src/did/mod.rs new file mode 100644 index 0000000..11f8bb1 --- /dev/null +++ b/src/did/mod.rs @@ -0,0 +1,236 @@ +use bip39::{Language, Mnemonic, MnemonicType}; +use schnorrkel::ExpansionMode; +use serde_json::json; +use substrate_bip39::mini_secret_from_entropy; + +use crate::{ + crypto::{ed25519::Ed25519KeyPair, sr25519::Sr25519KeyPair}, + Error, +}; + +pub enum AddressType { + Ed25519, + Sr25519, +} +pub fn random_phrase(words_number: u32) -> String { + let mnemonic_type = match MnemonicType::for_word_count(words_number as usize) { + Ok(t) => t, + Err(_e) => MnemonicType::Words24, + }; + let mnemonic = Mnemonic::new(mnemonic_type, Language::English); + + mnemonic.into_phrase() +} + +pub fn generate_ss58_did(network_id: String, address_type: AddressType) -> Result { + let mnemonic_type = MnemonicType::for_word_count(12)?; + let mnemonic = Mnemonic::new(mnemonic_type, Language::English); + + match address_type { + AddressType::Ed25519 => { + let keypair: Ed25519KeyPair = Ed25519KeyPair::from_bip39_phrase( + mnemonic.clone().into_phrase().as_str(), + Some(""), + ); + + let address = keypair.ss58_address(42); + let did = format!("did:infra:{}:{}", network_id, address.clone()); + + let result = serde_json::to_string(&json!({ + "mnemonic": mnemonic.into_phrase(), + "private_key": hex::encode(keypair.to_secret_key_bytes()), + "public_key": hex::encode(keypair.to_public_key_bytes()), + "address": address.clone(), + "did": did + }))?; + Ok(result) + } + AddressType::Sr25519 => { + let keypair_option: Option = + Sr25519KeyPair::from_suri(mnemonic.clone().into_phrase().as_str()); + + let keypair = keypair_option.ok_or(Error::InvalidKeypair)?; + + let address = keypair.ss58_address(42); + let did = format!("did:infra:{}:{}", network_id, address.clone()); + + let mini_secret_key = mini_secret_from_entropy(mnemonic.entropy(), "").ok(); + + let secret_key = mini_secret_key.ok_or(Error::InvalidSecretKey)?; + let public_key = secret_key.expand_to_public(ExpansionMode::Ed25519); + + let result = serde_json::to_string(&json!({ + "mnemonic": mnemonic.into_phrase(), + "private_key": hex::encode(secret_key.to_bytes()), + "public_key": hex::encode(public_key.to_bytes()), + "address": address.clone(), + "did": did + }))?; + Ok(result) + } + } +} + +pub fn generate_ss58_did_from_phrase( + suri: String, + network_id: String, + address_type: AddressType, +) -> Result { + match address_type { + AddressType::Ed25519 => { + let keypair: Ed25519KeyPair = + Ed25519KeyPair::from_bip39_phrase(suri.clone().as_str(), Some("")); + + let address = keypair.ss58_address(42); + let did = format!("did:infra:{}:{}", network_id, address.clone()); + + let result = serde_json::to_string(&json!({ + "mnemonic": suri, + "private_key": hex::encode(keypair.to_secret_key_bytes()), + "public_key": hex::encode(keypair.to_public_key_bytes()), + "address": address.clone(), + "did": did + }))?; + Ok(result) + } + AddressType::Sr25519 => { + let keypair_option: Option = + Sr25519KeyPair::from_suri(suri.clone().as_str()); + + let keypair: Sr25519KeyPair = keypair_option.ok_or(Error::InvalidKeypair)?; + + let address = keypair.ss58_address(42); + let did = format!("did:infra:{}:{}", network_id, address.clone()); + + let mnemonic = Mnemonic::from_phrase(&suri, Language::English)?; + let mini_secret_key = mini_secret_from_entropy(mnemonic.entropy(), "").ok(); + + let secret_key = mini_secret_key.ok_or(Error::InvalidSecretKey)?; + let public_key = secret_key.expand_to_public(ExpansionMode::Ed25519); + + let result = serde_json::to_string(&json!({ + "mnemonic": suri, + "private_key": hex::encode(secret_key.to_bytes()), + "public_key": hex::encode(public_key.to_bytes()), + "address": address.clone(), + "did": did + }))?; + Ok(result) + } + } +} + +pub fn did_to_hex_public_key(did: String, address_type: AddressType) -> Result { + let splited_did: Vec<&str> = did.split(":").collect(); + let address = splited_did[3]; + + let decoded_address = bs58::decode(address).into_vec()?; + + let public_key_bytes: [u8; 32] = match address_type { + AddressType::Ed25519 => { + let public_key: ed25519_dalek::PublicKey = + ed25519_dalek::PublicKey::from_bytes(&decoded_address[1..33])?; + public_key.to_bytes() + } + AddressType::Sr25519 => { + let public_key: schnorrkel::PublicKey = + schnorrkel::PublicKey::from_bytes(&decoded_address[1..33]).unwrap(); + public_key.to_bytes() + } + }; + + Ok(hex::encode(public_key_bytes)) +} + +pub fn ss58_address_to_did(address: String, network_id: String) -> Result { + let did = format!("did:infra:{}:{}", network_id, address); + Ok(did) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn testa() { + println!( + "{:?}", + hex::decode("8006aaa5985f1d72e916167bdcbc663232cef5823209b1246728f73137888170") + ); + } + + #[test] + fn test_generate_random_phrase() { + println!("{:?}", random_phrase(12)); + } + + #[test] + fn test_generate_ss58_did() { + println!( + "{:?}", + generate_ss58_did("01".to_string(), AddressType::Ed25519) + ); + println!( + "{:?}", + generate_ss58_did("01".to_string(), AddressType::Sr25519) + ); + } + + #[test] + fn test_generate_ss58_did_from_phrase() { + assert_eq!( + r###"{"address":"5GM7RtekqU8cGiS4MKQ7tufoH4Q1itzmoFpVcvcPfjksyPrw","did":"did:infra:01:5GM7RtekqU8cGiS4MKQ7tufoH4Q1itzmoFpVcvcPfjksyPrw","mnemonic":"caution juice atom organ advance problem want pledge someone senior holiday very","private_key":"c8fa03532fb22ee1f7f6908b9c02b4e72483f0dbd66e4cd456b8f34c6230b849","public_key":"bd7436a22571207d018ffe83f5dc77d0750b7777f1eb169053d40201d6c68d53"}"###, + generate_ss58_did_from_phrase( + "caution juice atom organ advance problem want pledge someone senior holiday very" + .to_string(), + "01".to_string(), + AddressType::Ed25519 + ) + .unwrap() + ); + + assert_eq!( + r###"{"address":"5Gv8YYFu8H1btvmrJy9FjjAWfb99wrhV3uhPFoNEr918utyR","did":"did:infra:01:5Gv8YYFu8H1btvmrJy9FjjAWfb99wrhV3uhPFoNEr918utyR","mnemonic":"caution juice atom organ advance problem want pledge someone senior holiday very","private_key":"c8fa03532fb22ee1f7f6908b9c02b4e72483f0dbd66e4cd456b8f34c6230b849","public_key":"d6a3105d6768e956e9e5d41050ac29843f98561410d3a47f9dd5b3b227ab8746"}"###, + generate_ss58_did_from_phrase( + "caution juice atom organ advance problem want pledge someone senior holiday very" + .to_string(), + "01".to_string(), + AddressType::Sr25519 + ) + .unwrap() + ); + } + + #[test] + fn test_did_to_hex_public_key() { + assert_eq!( + did_to_hex_public_key( + "did:infra:01:5GM7RtekqU8cGiS4MKQ7tufoH4Q1itzmoFpVcvcPfjksyPrw".to_string(), + AddressType::Ed25519 + ) + .unwrap(), + "bd7436a22571207d018ffe83f5dc77d0750b7777f1eb169053d40201d6c68d53".to_string() + ); + + assert_eq!( + did_to_hex_public_key( + "did:infra:01:5Gv8YYFu8H1btvmrJy9FjjAWfb99wrhV3uhPFoNEr918utyR".to_string(), + AddressType::Sr25519 + ) + .unwrap(), + "d6a3105d6768e956e9e5d41050ac29843f98561410d3a47f9dd5b3b227ab8746".to_string() + ); + } + + #[test] + fn test_ss58_address_to_did() { + assert_eq!( + ss58_address_to_did( + "5H6PhTQ1ukXBE1pqYVt2BMLjiKD9pqVsoppp2g8eM4EENAfL".to_string(), + "01".to_string() + ) + .unwrap(), + "did:infra:01:5H6PhTQ1ukXBE1pqYVt2BMLjiKD9pqVsoppp2g8eM4EENAfL".to_string() + ); + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..60bd640 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,66 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum Error { + #[error(transparent)] + LDP(#[from] ssi_ldp::Error), + #[error(transparent)] + JWS(#[from] ssi_jws::Error), + #[error(transparent)] + DID(#[from] ssi_dids::Error), + #[error(transparent)] + VC(#[from] ssi_vc::Error), + #[error(transparent)] + Hex(#[from] hex::FromHexError), + #[error(transparent)] + Base64(#[from] base64::DecodeError), + #[error(transparent)] + Mnemonic(#[from] anyhow::Error), + #[error(transparent)] + Ed25519(#[from] ed25519_dalek::ed25519::Error), + #[error(transparent)] + Base58(#[from] bs58::decode::Error), + #[error("Invalid DID")] + InvalidDID, + #[error("Invalid Keypair")] + InvalidKeypair, + #[error("Invalid Secret key")] + InvalidSecretKey, + #[error("Invalid Proof")] + InvalidProof, + #[error("Missing proof")] + MissingProof, + #[error("Missing credential schema")] + MissingCredentialSchema, + #[error("Missing credential")] + MissingCredential, + #[error("Missing presentation")] + MissingPresentation, + #[error("Invalid issuer")] + InvalidIssuer, + #[error("Missing holder property")] + MissingHolder, + #[error("Unsupported Holder Binding")] + UnsupportedHolderBinding, + #[error("Missing issuance date")] + MissingIssuanceDate, + #[error("Missing type VerifiableCredential")] + MissingTypeVerifiableCredential, + #[error("Missing type VerifiablePresentation")] + MissingTypeVerifiablePresentation, + #[error("Invalid subject")] + InvalidSubject, + #[error("Unable to convert date/time")] + TimeError, + #[error("Empty credential subject")] + EmptyCredentialSubject, + /// Verification method id does not match JWK id + #[error("Verification method id does not match JWK id. VM id: {0}, JWK key id: {1}")] + KeyIdVMMismatch(String, String), + /// Linked data proof option unencodable as JWT claim + #[error("Linked data proof option unencodable as JWT claim: {0}")] + UnencodableOptionClaim(String), + #[error(transparent)] + Json(#[from] serde_json::Error), +} diff --git a/src/lib.rs b/src/lib.rs index 4fa61b5..443c77e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,142 +1,7 @@ -use bip39::{Language, Mnemonic, MnemonicType, Seed}; -use schnorrkel::ExpansionMode; -use serde_json::json; -use sr25519::KeyPair; -use std::ffi::{CStr, CString}; -use std::os::raw::c_char; -use substrate_bip39::mini_secret_from_entropy; +pub mod crypto; +pub mod did; +pub mod resolver; +pub mod verifiable; -mod sr25519; - -fn get_str(rust_ptr: *const c_char) -> String { - let c_str = unsafe { CStr::from_ptr(rust_ptr) }; - let result_string = match c_str.to_str() { - Err(_) => "input string error", - Ok(string) => string, - }; - return String::from(result_string); -} - -fn get_ptr(rust_string: &str) -> *mut c_char { - CString::new(rust_string).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn random_phrase(words_number: u32) -> *mut c_char { - let mnemonic_type = match MnemonicType::for_word_count(words_number as usize) { - Ok(t) => t, - Err(_e) => MnemonicType::Words24, - }; - let mnemonic = Mnemonic::new(mnemonic_type, Language::English); - - get_ptr(&mnemonic.into_phrase()) -} - -#[no_mangle] -pub extern "C" fn substrate_address(suri: *const c_char, prefix: u8) -> *mut c_char { - let keypair_option = KeyPair::from_suri(&get_str(suri)); - let keypair = match keypair_option { - Some(c) => c, - _ => return get_ptr(""), - }; - - let rust_string = keypair.ss58_address(prefix); - get_ptr(&rust_string) -} - -#[no_mangle] -pub extern "C" fn generate_ss58_did(network_id: *const c_char) -> *mut c_char { - let mnemonic_type = MnemonicType::for_word_count(12).unwrap(); - let mnemonic = Mnemonic::new(mnemonic_type, Language::English); - - let keypair_option = KeyPair::from_suri(mnemonic.clone().into_phrase().as_str()); - - let keypair = match keypair_option { - Some(c) => c, - _ => return get_ptr(""), - }; - - let seed = Seed::new(&mnemonic, ""); - - let network_id_string = get_str(network_id); - let address = keypair.ss58_address(42); - let did = format!("did:infra:{}:{}", network_id_string, address.clone()); - - let mini_secret_key = mini_secret_from_entropy(mnemonic.entropy(), "").unwrap(); - - let secret_key: schnorrkel::SecretKey = mini_secret_key.expand(ExpansionMode::Ed25519); - let public_key: schnorrkel::PublicKey = secret_key.to_public(); - - let result = serde_json::to_string(&json!({ - "mnemonic": mnemonic.into_phrase(), - "seed": hex::encode(seed.clone()), - "private_key": hex::encode(secret_key.to_bytes()), - "public_key": hex::encode(public_key.to_bytes()), - "address": address.clone(), - "did": did - })); - - get_ptr(&result.unwrap()) -} - -#[no_mangle] -pub extern "C" fn did_to_hex_public_key(did: *mut c_char) -> *mut c_char { - let did_string = get_str(did); - let splited_did: Vec<&str> = did_string.split(":").collect(); - let address = splited_did[3]; - - let decoded_address = bs58::decode(address).into_vec().unwrap(); - - let public_key: schnorrkel::PublicKey = - schnorrkel::PublicKey::from_bytes(&decoded_address[1..33]).unwrap(); - - get_ptr(&hex::encode(public_key.to_bytes())) -} - -#[no_mangle] -pub extern "C" fn ss58_address_to_did( - address: *mut c_char, - network_id: *mut c_char, -) -> *mut c_char { - let address_string = get_str(address); - let network_id_string = get_str(network_id); - - let did = format!("did:infra:{}:{}", network_id_string, address_string); - get_ptr(&did) -} - -#[no_mangle] -pub extern "C" fn rust_cstr_free(s: *mut c_char) { - unsafe { - if s.is_null() { - return; - } - CString::from_raw(s) - }; -} - -#[test] -fn test_generate_ss58_did() { - println!("{:?}", generate_ss58_did(get_ptr("01"))); -} - -#[test] -fn test_did_to_hex_public_key() { - assert_eq!( - get_str(did_to_hex_public_key(get_ptr( - "did:infra:01:5H6PhTQ1ukXBE1pqYVt2BMLjiKD9pqVsoppp2g8eM4EENAfL" - ))), - "de7687abb0442514b3f765e17f6cde78227e3b5afa45627f12d805fb5c5e473a" - ); -} - -#[test] -fn test_ss58_address_to_did() { - assert_eq!( - get_str(ss58_address_to_did( - get_ptr("5H6PhTQ1ukXBE1pqYVt2BMLjiKD9pqVsoppp2g8eM4EENAfL"), - get_ptr("01") - )), - "did:infra:01:5H6PhTQ1ukXBE1pqYVt2BMLjiKD9pqVsoppp2g8eM4EENAfL" - ); -} +pub mod error; +pub use error::Error; diff --git a/src/resolver/mod.rs b/src/resolver/mod.rs new file mode 100644 index 0000000..e755804 --- /dev/null +++ b/src/resolver/mod.rs @@ -0,0 +1 @@ +pub mod resolver; diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs new file mode 100644 index 0000000..7206165 --- /dev/null +++ b/src/resolver/resolver.rs @@ -0,0 +1,204 @@ +use async_trait::async_trait; +use serde_json::json; +use ssi::did_resolve::{ + DIDResolver, DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, TYPE_DID_LD_JSON, +}; +use ssi::jwk::{Base64urlUInt, OctetParams, Params, JWK}; +use ssi_dids::{Document, VerificationMethod, VerificationMethodMap, DIDURL}; + +use crate::did::{did_to_hex_public_key, AddressType}; + +const DID_KEY_ED25519_PREFIX: [u8; 2] = [0xed, 0x01]; + +pub const ERROR_NOT_FOUND: &str = "notFound"; +const DOC_JSON_FOO: &str = include_str!("../../tests/did-example-foo.json"); +const DOC_JSON_INFRA: &str = include_str!("../../tests/did-infra-space.json"); + +/// A DID Resolver implementing a client for the [DID Resolution HTTP(S) +/// Binding](https://w3c-ccg.github.io/did-resolution/#bindings-https). +#[derive(Debug, Clone, Default)] +pub struct InfraDIDResolver { + /// HTTP(S) URL for DID resolver HTTP(S) endpoint. + pub endpoint: String, +} + +impl InfraDIDResolver { + /// Construct a new HTTP DID Resolver with a given [endpoint][InfraDIDResolver::endpoint] URL. + pub fn new(url: &str) -> Self { + Self { + endpoint: url.to_string(), + } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl DIDResolver for InfraDIDResolver { + /// Resolve a DID over HTTP(S), using the [DID Resolution HTTP(S) Binding](https://w3c-ccg.github.io/did-resolution/#bindings-https). + async fn resolve( + &self, + did: &str, + _input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Option, + Option, + ) { + let hex_public_key = did_to_hex_public_key(did.to_string(), AddressType::Ed25519).unwrap(); + let public_key_bytes = hex::decode(hex_public_key).unwrap(); + + let jwk: JWK = JWK::from(Params::OKP(OctetParams { + curve: "Ed25519".to_string(), + public_key: Base64urlUInt(public_key_bytes.clone()), + private_key: None, + })); + + let vms = vec![ + VerificationMethod::Map(VerificationMethodMap { + id: did.to_string() + "#keys-1", + type_: "Ed25519VerificationKey2018".to_string(), + controller: did.to_string(), + public_key_base58: Some(bs58::encode(public_key_bytes.clone()).into_string()), + ..Default::default() + }), + VerificationMethod::Map(VerificationMethodMap { + id: did.to_string() + "#keys-2", + type_: "Ed25519VerificationKey2020".to_string(), + controller: did.to_string(), + property_set: serde_json::from_value(json!({ + "publicKeyMultibase": multibase::encode( + multibase::Base::Base58Btc, + [ + DID_KEY_ED25519_PREFIX.to_vec(), + public_key_bytes.clone() + ] + .concat() + ), + })) + .unwrap(), + ..Default::default() + }), + VerificationMethod::Map(VerificationMethodMap { + id: did.to_string() + "#keys-3", + type_: "JsonWebKey2020".to_string(), + controller: did.to_string(), + public_key_jwk: Some(jwk), + ..Default::default() + }), + ]; + + let vm_urls = vec![ + VerificationMethod::DIDURL(DIDURL { + did: did.to_string() + "#keys-1", + ..Default::default() + }), + VerificationMethod::DIDURL(DIDURL { + did: did.to_string() + "#keys-2", + ..Default::default() + }), + VerificationMethod::DIDURL(DIDURL { + did: did.to_string() + "#keys-3", + ..Default::default() + }), + ]; + + let doc = Document { + context: ssi_dids::Contexts::Many(vec![ssi_dids::Context::URI( + ssi_dids::DEFAULT_CONTEXT.into(), + )]), + id: did.to_string(), + verification_method: Some(vms), + authentication: Some(vm_urls.clone()), + assertion_method: Some(vm_urls), + ..Default::default() + }; + ( + ResolutionMetadata { + error: None, + content_type: Some(TYPE_DID_LD_JSON.to_string()), + property_set: None, + }, + Some(doc), + Some(DocumentMetadata::default()), + ) + } +} + +/// A DID Resolver implementing a client for the [DID Resolution HTTP(S) +/// Binding](https://w3c-ccg.github.io/did-resolution/#bindings-https). +#[derive(Debug, Clone, Default)] +pub struct TestDIDResolver { + /// HTTP(S) URL for DID resolver HTTP(S) endpoint. + pub endpoint: String, +} + +impl TestDIDResolver { + /// Construct a new HTTP DID Resolver with a given [endpoint][HTTPDIDResolver::endpoint] URL. + pub fn new(url: &str) -> Self { + Self { + endpoint: url.to_string(), + } + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl DIDResolver for TestDIDResolver { + /// Resolve a DID over HTTP(S), using the [DID Resolution HTTP(S) Binding](https://w3c-ccg.github.io/did-resolution/#bindings-https). + async fn resolve( + &self, + did: &str, + _input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Option, + Option, + ) { + let doc_str = match did { + "did:example:foo" => DOC_JSON_FOO, + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW" => DOC_JSON_INFRA, + _ => return (ResolutionMetadata::from_error(ERROR_NOT_FOUND), None, None), + }; + let doc: Document = match serde_json::from_str(doc_str) { + Ok(doc) => doc, + Err(err) => { + return (ResolutionMetadata::from_error(&err.to_string()), None, None); + } + }; + ( + ResolutionMetadata { + error: None, + content_type: Some(TYPE_DID_LD_JSON.to_string()), + property_set: None, + }, + Some(doc), + Some(DocumentMetadata::default()), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[async_std::test] + async fn test_resolve() { + let resolver = TestDIDResolver::default(); + let (_, doc, _) = resolver + .resolve("did:example:foo", &ResolutionInputMetadata::default()) + .await; + println!("{:?}", serde_json::to_string_pretty(&doc).unwrap()); + } + + #[async_std::test] + async fn test_infra_resolve() { + let resolver = InfraDIDResolver::default(); + let (_, doc, _) = resolver + .resolve( + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + &ResolutionInputMetadata::default(), + ) + .await; + println!("{:?}", serde_json::to_string_pretty(&doc).unwrap()); + } +} diff --git a/src/sr25519.rs b/src/sr25519.rs deleted file mode 100644 index 212cfbe..0000000 --- a/src/sr25519.rs +++ /dev/null @@ -1,165 +0,0 @@ -use base58::ToBase58; -use bip39::{Language, Mnemonic}; -use codec::{Decode, Encode}; -use regex::Regex; -use schnorrkel::derive::{ChainCode, Derivation}; -use schnorrkel::{ExpansionMode, SecretKey, Signature}; -use substrate_bip39::mini_secret_from_entropy; - -use lazy_static::lazy_static; - -pub struct KeyPair(schnorrkel::Keypair); - -const SIGNING_CTX: &[u8] = b"substrate"; -const JUNCTION_ID_LEN: usize = 32; -const CHAIN_CODE_LENGTH: usize = 32; - -impl KeyPair { - pub fn from_bip39_phrase(phrase: &str, password: Option<&str>) -> Option { - let mnemonic = Mnemonic::from_phrase(phrase, Language::English).ok()?; - let mini_secret_key = - mini_secret_from_entropy(mnemonic.entropy(), password.unwrap_or("")).ok()?; - - Some(KeyPair( - mini_secret_key.expand_to_keypair(ExpansionMode::Ed25519), - )) - } - - // Should match implementation at https://github.com/paritytech/substrate/blob/master/core/primitives/src/crypto.rs#L653-L682 - pub fn from_suri(suri: &str) -> Option { - lazy_static! { - static ref RE_SURI: Regex = { - Regex::new(r"^(?P\w+( \w+)*)?(?P(//?[^/]+)*)(///(?P.*))?$") - .expect("constructed from known-good static value; qed") - }; - static ref RE_JUNCTION: Regex = - Regex::new(r"/(/?[^/]+)").expect("constructed from known-good static value; qed"); - } - - let cap = RE_SURI.captures(suri)?; - let path = RE_JUNCTION - .captures_iter(&cap["path"]) - .map(|j| DeriveJunction::from(&j[1])); - - let pair = Self::from_bip39_phrase( - cap.name("phrase").map(|p| p.as_str())?, - cap.name("password").map(|p| p.as_str()), - )?; - - Some(pair.derive(path)) - } - - fn derive(&self, path: impl Iterator) -> Self { - let init = self.0.secret.clone(); - let result = path.fold(init, |acc, j| match j { - DeriveJunction::Soft(cc) => acc.derived_key_simple(ChainCode(cc), &[]).0, - DeriveJunction::Hard(cc) => derive_hard_junction(&acc, cc), - }); - - KeyPair(result.to_keypair()) - } - - pub fn ss58_address(&self, prefix: u8) -> String { - let mut v = vec![prefix]; - v.extend_from_slice(&self.0.public.to_bytes()); - let r = ss58hash(&v); - v.extend_from_slice(&r.as_bytes()[0..2]); - v.to_base58() - } - - pub fn sign(&self, message: &[u8]) -> [u8; 64] { - let context = schnorrkel::signing_context(SIGNING_CTX); - self.0.sign(context.bytes(message)).to_bytes() - } - - pub fn verify_signature(&self, message: &[u8], signature: &[u8]) -> Option { - let context = schnorrkel::signing_context(SIGNING_CTX); - - let signature = Signature::from_bytes(signature).ok()?; - - Some(self.0.verify(context.bytes(&message), &signature).is_ok()) - } -} - -fn derive_hard_junction(secret: &SecretKey, cc: [u8; CHAIN_CODE_LENGTH]) -> SecretKey { - secret - .hard_derive_mini_secret_key(Some(ChainCode(cc)), b"") - .0 - .expand(ExpansionMode::Ed25519) -} - -/// A since derivation junction description. It is the single parameter used when creating -/// a new secret key from an existing secret key and, in the case of `SoftRaw` and `SoftIndex` -/// a new public key from an existing public key. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Encode, Decode)] -enum DeriveJunction { - /// Soft (vanilla) derivation. Public keys have a correspondent derivation. - Soft([u8; JUNCTION_ID_LEN]), - /// Hard ("hardened") derivation. Public keys do not have a correspondent derivation. - Hard([u8; JUNCTION_ID_LEN]), -} - -impl DeriveJunction { - /// Consume self to return a hard derive junction with the same chain code. - fn harden(self) -> Self { - DeriveJunction::Hard(self.unwrap_inner()) - } - - /// Create a new soft (vanilla) DeriveJunction from a given, encodable, value. - /// - /// If you need a hard junction, use `hard()`. - fn soft(index: T) -> Self { - let mut cc: [u8; JUNCTION_ID_LEN] = Default::default(); - index.using_encoded(|data| { - if data.len() > JUNCTION_ID_LEN { - let hash_result = blake2_rfc::blake2b::blake2b(JUNCTION_ID_LEN, &[], data); - let hash = hash_result.as_bytes(); - cc.copy_from_slice(hash); - } else { - cc[0..data.len()].copy_from_slice(data); - } - }); - DeriveJunction::Soft(cc) - } - - /// Consume self to return the chain code. - fn unwrap_inner(self) -> [u8; JUNCTION_ID_LEN] { - match self { - DeriveJunction::Hard(c) | DeriveJunction::Soft(c) => c, - } - } -} - -impl> From for DeriveJunction { - fn from(j: T) -> DeriveJunction { - let j = j.as_ref(); - let (code, hard) = if j.starts_with("/") { - (&j[1..], true) - } else { - (j, false) - }; - - let res = if let Ok(n) = str::parse::(code) { - // number - DeriveJunction::soft(n) - } else { - // something else - DeriveJunction::soft(code) - }; - - if hard { - res.harden() - } else { - res - } - } -} - -fn ss58hash(data: &[u8]) -> blake2_rfc::blake2b::Blake2bResult { - const PREFIX: &[u8] = b"SS58PRE"; - - let mut context = blake2_rfc::blake2b::Blake2b::new(64); - context.update(PREFIX); - context.update(data); - context.finalize() -} diff --git a/src/verifiable/credential/mod.rs b/src/verifiable/credential/mod.rs new file mode 100644 index 0000000..7581a7b --- /dev/null +++ b/src/verifiable/credential/mod.rs @@ -0,0 +1,137 @@ +use ssi::jwk::{Base64urlUInt, OctetParams, Params, JWK}; +use ssi_ldp::{ProofSuite, ProofSuiteType}; +use ssi_vc::{Credential, LinkedDataProofOptions, ProofPurpose, URI}; + +use crate::{crypto::ed25519::Ed25519KeyPair, resolver::resolver::InfraDIDResolver, Error}; + +pub async fn issue_credential( + did: String, + hex_secret_key: String, + credential_string: String, +) -> Result { + let secret_key_bytes = hex::decode(hex_secret_key)?; + + let keypair = Ed25519KeyPair::from_secret_key_bytes(&secret_key_bytes); + + let key: JWK = JWK::from(Params::OKP(OctetParams { + curve: "Ed25519".to_string(), + public_key: Base64urlUInt(keypair.to_public_key_bytes().to_vec()), + private_key: Some(Base64urlUInt(keypair.to_secret_key_bytes().to_vec())), + })); + + let mut vc: Credential = Credential::from_json_unsigned(credential_string.as_str())?; + + let resolver = InfraDIDResolver::default(); + + let mut context_loader = ssi_json_ld::ContextLoader::default(); + let issue_options: LinkedDataProofOptions = LinkedDataProofOptions { + type_: Some(ProofSuiteType::Ed25519Signature2018), + proof_purpose: Some(ProofPurpose::AssertionMethod), + verification_method: Some(URI::String(did + "#keys-1")), + ..Default::default() + }; + + let proof = ProofSuiteType::Ed25519Signature2018 + .sign( + &vc, + &issue_options, + &resolver, + &mut context_loader, + &key, + None, + ) + .await?; + vc.add_proof(proof); + vc.validate()?; + + let verification_result = vc.verify(None, &resolver, &mut context_loader).await; + if verification_result.errors.is_empty() { + Ok(serde_json::to_string_pretty(&vc)?) + } else { + Err(Error::InvalidProof) + } +} + +pub async fn verify_credential(credential_string: String) -> Result { + let vc: Credential = Credential::from_json(credential_string.as_str())?; + let issuer = vc.clone().issuer.ok_or(Error::InvalidIssuer)?; + let resolver = InfraDIDResolver::default(); + + let mut context_loader = ssi_json_ld::ContextLoader::default(); + + let options: LinkedDataProofOptions = LinkedDataProofOptions { + proof_purpose: Some(ProofPurpose::AssertionMethod), + verification_method: Some(URI::String(issuer.get_id() + "#keys-1")), + ..Default::default() + }; + + let verification_result = vc + .verify(Some(options), &resolver, &mut context_loader) + .await; + if verification_result.errors.is_empty() { + Ok("true".to_string()) + } else { + Ok("false".to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[async_std::test] + async fn test_sign_credential_ed25519() { + let did = "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW".to_string(); + let hex_secret_key = + "8006aaa5985f1d72e916167bdcbc663232cef5823209b1246728f73137888170".to_string(); + let vc_str = r###"{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5FDseiC76zPek2YYkuyenu4ZgxZ7PUWXt9d19HNB5CaQXt5U", + "type": [ + "VerifiableCredential" + ], + "credentialSubject": [ + { + "id": "did:example:d23dd687a7dc6787646f2eb98d0" + } + ], + "issuanceDate": "2023-04-24T06:08:03.039Z", + "issuer": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW" + }"###; + + let vc = issue_credential(did, hex_secret_key, vc_str.to_string()).await; + println!("{:?}", vc); + } + + #[async_std::test] + async fn test_verify_credential_ed25519() { + let vc_str = r###"{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5FDseiC76zPek2YYkuyenu4ZgxZ7PUWXt9d19HNB5CaQXt5U", + "type": [ + "VerifiableCredential" + ], + "credentialSubject": [ + { + "id": "did:example:d23dd687a7dc6787646f2eb98d0" + } + ], + "issuer": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "issuanceDate": "2023-04-24T06:08:03.039Z", + "proof": { + "type": "Ed25519Signature2018", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "created": "2024-04-03T01:13:18.220667Z", + "jws": "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..VZU_0mj3fD-Nrcq1Zu4r_tqOhQERfI8RMpPeDHX3dQkmTyvOG5AUFtgebrr-wS1RqHIRgvxqIBaSE51dHwUtBA" + } + }"###; + + let verify = verify_credential(vc_str.to_string()).await.unwrap(); + assert_eq!(verify, "true".to_string()); + } +} diff --git a/src/verifiable/mod.rs b/src/verifiable/mod.rs new file mode 100644 index 0000000..1420934 --- /dev/null +++ b/src/verifiable/mod.rs @@ -0,0 +1,2 @@ +pub mod credential; +pub mod presentation; diff --git a/src/verifiable/presentation/mod.rs b/src/verifiable/presentation/mod.rs new file mode 100644 index 0000000..f826539 --- /dev/null +++ b/src/verifiable/presentation/mod.rs @@ -0,0 +1,190 @@ +use ssi::jwk::{Base64urlUInt, OctetParams, Params, JWK}; +use ssi_ldp::ProofSuiteType; +use ssi_vc::{ + Credential, CredentialOrJWT, LinkedDataProofOptions, OneOrMany, Presentation, ProofPurpose, + StringOrURI, DEFAULT_CONTEXT, URI, +}; + +use crate::{ + crypto::ed25519::Ed25519KeyPair, did::random_phrase, error::Error, + resolver::resolver::InfraDIDResolver, +}; + +pub async fn issue_presentation( + did: String, + hex_secret_key: String, + credential_string: String, +) -> Result { + let secret_key_bytes = hex::decode(hex_secret_key)?; + + let keypair = Ed25519KeyPair::from_secret_key_bytes(&secret_key_bytes); + + let key: JWK = JWK::from(Params::OKP(OctetParams { + curve: "Ed25519".to_string(), + public_key: Base64urlUInt(keypair.to_public_key_bytes().to_vec()), + private_key: Some(Base64urlUInt(keypair.to_secret_key_bytes().to_vec())), + })); + + let vc: Credential = Credential::from_json(credential_string.as_str())?; + + let resolver = InfraDIDResolver::default(); + + let id = { + let mnemonic = random_phrase(12); + let keypair: Ed25519KeyPair = + Ed25519KeyPair::from_bip39_phrase(mnemonic.as_str(), Some("")); + let address = keypair.ss58_address(42); + let did = format!("did:infra:{}:{}", "01", address.clone()); + did + }; + + let mut vp = Presentation { + context: ssi_vc::Contexts::Many(vec![ssi_vc::Context::URI(ssi_vc::URI::String( + DEFAULT_CONTEXT.to_string(), + ))]), + id: Some(StringOrURI::String(id.to_string())), + type_: OneOrMany::One("VerifiablePresentation".to_string()), + verifiable_credential: Some(OneOrMany::One(CredentialOrJWT::Credential(vc))), + proof: None, + holder: Some(URI::String(did.to_string())), + property_set: None, + holder_binding: None, + }; + + let vp_issue_options: LinkedDataProofOptions = LinkedDataProofOptions { + type_: Some(ProofSuiteType::Ed25519Signature2018), + proof_purpose: Some(ProofPurpose::AssertionMethod), + verification_method: Some(URI::String(did + "#keys-1")), + ..Default::default() + }; + + let mut context_loader = ssi_json_ld::ContextLoader::default(); + + let vp_proof = vp + .generate_proof(&key, &vp_issue_options, &resolver, &mut context_loader) + .await?; + vp.add_proof(vp_proof); + vp.validate()?; + + let vp_verification_result = vp + .verify( + Some(vp_issue_options.clone()), + &resolver, + &mut context_loader, + ) + .await; + + if vp_verification_result.errors.is_empty() { + Ok(serde_json::to_string_pretty(&vp)?) + } else { + Err(Error::InvalidProof) + } +} + +pub async fn verify_presentation(presentation_string: String) -> Result { + let vp: Presentation = Presentation::from_json(presentation_string.as_str())?; + let holder = vp.clone().holder.ok_or(Error::MissingHolder)?; + + let resolver = InfraDIDResolver::default(); + + let mut context_loader = ssi_json_ld::ContextLoader::default(); + + let options: LinkedDataProofOptions = LinkedDataProofOptions { + proof_purpose: Some(ProofPurpose::AssertionMethod), + verification_method: Some(URI::String(holder.as_str().to_string() + "#keys-1")), + ..Default::default() + }; + + let vp_verification_result = vp + .verify(Some(options), &resolver, &mut context_loader) + .await; + + if vp_verification_result.errors.is_empty() { + Ok("true".to_string()) + } else { + Ok("false".to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[async_std::test] + async fn test_sign_presentation_ed25519() { + let did = "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW".to_string(); + let hex_secret_key = + "8006aaa5985f1d72e916167bdcbc663232cef5823209b1246728f73137888170".to_string(); + let vc_str = r###"{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5FDseiC76zPek2YYkuyenu4ZgxZ7PUWXt9d19HNB5CaQXt5U", + "type": [ + "VerifiableCredential" + ], + "credentialSubject": [ + { + "id": "did:example:d23dd687a7dc6787646f2eb98d0" + } + ], + "issuer": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "issuanceDate": "2023-04-24T06:08:03.039Z", + "proof": { + "type": "Ed25519Signature2018", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "created": "2024-04-03T01:13:18.220667Z", + "jws": "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..VZU_0mj3fD-Nrcq1Zu4r_tqOhQERfI8RMpPeDHX3dQkmTyvOG5AUFtgebrr-wS1RqHIRgvxqIBaSE51dHwUtBA" + } + }"###; + + let vc = issue_presentation(did, hex_secret_key, vc_str.to_string()).await; + println!("{:?}", vc); + } + + #[async_std::test] + async fn test_verify_presentation_ed25519() { + let vp_str = r###"{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5F9myCAKW52XUU38Z4uhttmYYLoLFWe9AnEVpv1aGpx9Q3Bp", + "type": "VerifiablePresentation", + "verifiableCredential": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "did:infra:01:5FDseiC76zPek2YYkuyenu4ZgxZ7PUWXt9d19HNB5CaQXt5U", + "type": [ + "VerifiableCredential" + ], + "credentialSubject": [ + { + "id": "did:example:d23dd687a7dc6787646f2eb98d0" + } + ], + "issuer": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "issuanceDate": "2023-04-24T06:08:03.039Z", + "proof": { + "type": "Ed25519Signature2018", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "created": "2024-04-03T01:13:18.220667Z", + "jws": "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..VZU_0mj3fD-Nrcq1Zu4r_tqOhQERfI8RMpPeDHX3dQkmTyvOG5AUFtgebrr-wS1RqHIRgvxqIBaSE51dHwUtBA" + } + }, + "proof": { + "type": "Ed25519Signature2018", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "created": "2024-04-03T01:16:09.837873Z", + "jws": "eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..XMUnK1nLJI3jahunuS-ooEVWAKgN3VwiUc0cm2xiFNMdgnBqYi6-n-uPdpDJls6-7BXlLhR4W4nGlPrptQFTBA" + }, + "holder": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW" + }"###; + + let verify: String = verify_presentation(vp_str.to_string()).await.unwrap(); + assert_eq!(verify, "true".to_string()); + } +} diff --git a/tests/did-example-foo.json b/tests/did-example-foo.json new file mode 100644 index 0000000..91f5b88 --- /dev/null +++ b/tests/did-example-foo.json @@ -0,0 +1,50 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": "did:example:foo", + "verificationMethod": [ + { + "id": "did:example:foo#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:example:foo", + "publicKeyBase58": "F9JHKboDqg3tK9wnrt8z8xwZRnoZCJAHTdxXVuUMW8z2" + }, + { + "id": "did:example:foo#keys-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:example:foo", + "publicKeyMultibase": "z6MktbZKur3fBDYMRenVYT6pz4VZFN5QcBQe9esTLBSNRMmQ" + }, + { + "id": "did:example:foo#keys-3", + "type": "JsonWebKey2020", + "controller": "did:example:foo", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "0iPNtvMisdUWHTxJlYR9w0cvQ2NmEQ-2cu4B0x60648" + } + } + ], + "assertionMethod": [ + "did:example:foo#keys-1", + "did:example:foo#keys-2", + "did:example:foo#keys-3" + ], + "authentication": [ + "did:example:foo#keys-1", + "did:example:foo#keys-2", + "did:example:foo#keys-3" + ], + "capabilityDelegation": [ + "did:example:foo#keys-1", + "did:example:foo#keys-2", + "did:example:foo#keys-3" + ], + "capabilityInvocation": [ + "did:example:foo#keys-1", + "did:example:foo#keys-2", + "did:example:foo#keys-3" + ] +} \ No newline at end of file diff --git a/tests/did-infra-space.json b/tests/did-infra-space.json new file mode 100644 index 0000000..e7f82f7 --- /dev/null +++ b/tests/did-infra-space.json @@ -0,0 +1,40 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "id": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "verificationMethod": [ + { + "id": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "type": "Ed25519VerificationKey2018", + "controller": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "publicKeyBase58": "F9JHKboDqg3tK9wnrt8z8xwZRnoZCJAHTdxXVuUMW8z2" + }, + { + "id": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2", + "type": "Ed25519VerificationKey2020", + "controller": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "publicKeyMultibase": "z6MktbZKur3fBDYMRenVYT6pz4VZFN5QcBQe9esTLBSNRMmQ" + }, + { + "id": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-3", + "type": "JsonWebKey2020", + "controller": "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW", + "publicKeyJwk": { + "kty": "OKP", + "crv": "Ed25519", + "x": "0iPNtvMisdUWHTxJlYR9w0cvQ2NmEQ-2cu4B0x60648" + } + } + ], + "authentication": [ + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2", + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-3" + ], + "assertionMethod": [ + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-1", + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-2", + "did:infra:01:5GpEYnXBoLgvzyWe4Defitp5UV25xZUiUCJM2xNgkDXkM4NW#keys-3" + ] +} \ No newline at end of file