diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15f729d4b..9ad7dc9b5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,3 +69,6 @@ jobs: run: | rustup target add wasm32-unknown-unknown cargo check --workspace --target wasm32-unknown-unknown + + - name: Test Aleo signatures + run: cargo test --features=aleosig diff --git a/Cargo.toml b/Cargo.toml index 4ed3fd1f0..cb5500401 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ http-did = ["hyper", "hyper-tls", "http", "percent-encoding", "tokio"] libsecp256k1 = ["secp256k1"] # backward compatibility secp256k1 = ["k256", "rand", "k256/keccak256"] secp256r1 = ["p256", "rand"] +aleosig = ["rand", "blake2", "snarkvm-dpc", "snarkvm-algorithms", "snarkvm-curves", "snarkvm-utilities", "snarkvm-parameters"] ripemd-160 = ["ripemd160", "secp256k1"] # TODO handle better keccak and sha keccak = ["keccak-hash", "secp256k1", "k256/keccak256"] @@ -69,6 +70,7 @@ serde_urlencoded = "0.7" percent-encoding = { version = "2.1", optional = true } tokio = { version = "1.0", optional = true, features = ["macros"] } blake2b_simd = "0.5" +blake2 = { version = "0.9", optional = true } bs58 = { version = "0.4", features = ["check"] } thiserror = "1.0" keccak-hash = { version = "0.7", optional = true } @@ -85,6 +87,13 @@ flate2 = "1.0" bitvec = "0.20" clear_on_drop = "0.2.4" url = { version = "2.2", features = ["serde"] } +rand_xorshift = "0.3" +bech32 = "0.8" +snarkvm-dpc = { version = "0.7.9", optional = true } +snarkvm-algorithms = { version= "0.7.9", optional = true } +snarkvm-curves = { version= "0.7.9", optional = true } +snarkvm-utilities = { version = "0.7.9", optional = true } +snarkvm-parameters = { version = "0.7.9", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] chrono = { version = "0.4", features = ["serde"] } @@ -109,7 +118,7 @@ members = [ ] [dev-dependencies] -blake2 = "0.8" # for bbs doctest +blake2_old = { package = "blake2", version = "0.8" } # for bbs doctest uuid = { version = "0.8", features = ["v4", "serde"] } difference = "2.0" did-method-key = { path = "./did-key" } diff --git a/contexts/aleovm.jsonld b/contexts/aleovm.jsonld new file mode 100644 index 000000000..39d2a1a8e --- /dev/null +++ b/contexts/aleovm.jsonld @@ -0,0 +1,76 @@ +{ + "AleoMethod2021": { + "@id": "https://w3id.org/security#AleoMethod2021", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "blockchainAccountId": "https://w3id.org/security#blockchainAccountId" + } + }, + "AleoSignature2021": { + "@id": "https://w3id.org/security#AleoSignature2021", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": { + "@id": "https://w3id.org/security#proofValue", + "@type": "https://w3id.org/security#multibase" + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } +} diff --git a/contexts/src/lib.rs b/contexts/src/lib.rs index ef00eb4fe..41d8c506f 100644 --- a/contexts/src/lib.rs +++ b/contexts/src/lib.rs @@ -54,3 +54,4 @@ pub const TZJCSVM_V1: &str = include_str!("../tzjcsvm-2021-v1.jsonld"); pub const EIP712VM: &str = include_str!("../eip712vm.jsonld"); pub const EPSIG_V0_1: &str = include_str!("../epsig-v0.1.jsonld"); pub const SOLVM: &str = include_str!("../solvm.jsonld"); +pub const ALEOVM: &str = include_str!("../aleovm.jsonld"); diff --git a/examples/genaleojwk.rs b/examples/genaleojwk.rs new file mode 100644 index 000000000..55361e891 --- /dev/null +++ b/examples/genaleojwk.rs @@ -0,0 +1,11 @@ +#[async_std::main] +#[ignore] // Skip expensive key generation +async fn main() -> Result<(), ssi::error::Error> { + #[cfg(feature = "aleosig")] + { + let jwk = ssi::jwk::JWK::generate_aleo()?; + let writer = std::io::BufWriter::new(std::io::stdout()); + serde_json::to_writer_pretty(writer, &jwk).unwrap(); + } + Ok(()) +} diff --git a/src/aleo.rs b/src/aleo.rs new file mode 100644 index 000000000..d8e93e8ed --- /dev/null +++ b/src/aleo.rs @@ -0,0 +1,325 @@ +//! Functionality related to [Aleo] blockchain network. +//! +//! Required crate feature: `aleosig` +//! +//! [Aleo]: https://developer.aleo.org/testnet/getting_started/overview#the-network +//! +//! This module provides [sign] and [verify] functions for Aleo signatures +//! using static parameters ([COM_PARAMS], [ENC_PARAMS], [SIG_PARAMS]) +//! and a [JWK-based keypair representation](OKP_CURVE). + +use crate::jwk::{Base64urlUInt, OctetParams, Params, JWK}; +use thiserror::Error; + +use blake2::Blake2s; +use snarkvm_algorithms::{ + commitment::{PedersenCommitmentParameters, PedersenCompressedCommitment}, + encryption::{GroupEncryption, GroupEncryptionParameters}, + signature::{Schnorr, SchnorrParameters, SchnorrSignature}, +}; +use snarkvm_curves::edwards_bls12::{EdwardsAffine, EdwardsProjective}; +use snarkvm_dpc::{ + account::{Address, PrivateKey, ViewKey}, + testnet1::instantiated::Components, +}; +use snarkvm_parameters::{ + global::{ + AccountCommitmentParameters, AccountEncryptionParameters, AccountSignatureParameters, + }, + Parameter, +}; +use snarkvm_utilities::{FromBytes, ToBytes}; +use std::str::FromStr; + +/// An error resulting from attempting to [sign a message using an Aleo private key](sign). +#[derive(Error, Debug)] +pub enum AleoSignError { + #[error("Unable to convert JWK to Aleo private key: {0}")] + JWKToPrivateKey(#[source] ParsePrivateKeyError), + #[error("Unable to convert Aleo private key to view key: {0}")] + ViewKeyFromPrivateKey(#[source] snarkvm_dpc::AccountError), + #[error("Unable to sign with view key: {0}")] + Sign(#[source] snarkvm_dpc::AccountError), + #[error("Unable to write signture as bytes: {0}")] + WriteSignature(#[source] std::io::Error), +} + +/// An error resulting from attempting to [verify a signature from an Aleo account](verify). +#[derive(Error, Debug)] +pub enum AleoVerifyError { + #[error("Invalid signature over message")] + InvalidSignature, + #[error("Unable to verify signature: {0}")] + VerifySignature(#[source] snarkvm_dpc::AccountError), + #[error("Unable to deserialize account address: {0}")] + AddressFromStr(#[source] snarkvm_dpc::AccountError), + #[error("Unable to read signature bytes: {0}")] + ReadSignature(#[source] std::io::Error), +} + +/// An error resulting from attempting to [generate a JWK Aleo private key](generate_private_key_jwk). +#[derive(Error, Debug)] +pub enum AleoGeneratePrivateKeyError { + #[error("Unable to generate new key: {0}")] + NewKey(#[source] snarkvm_dpc::AccountError), + #[error("Unable to base58-decode new key: {0}")] + DecodePrivateKey(#[source] bs58::decode::Error), + #[error("Unable to convert private key to account address: {0}")] + PrivateKeyToAddress(#[source] snarkvm_dpc::AccountError), + #[error("Unable to write account address as bytes: {0}")] + WriteAddress(#[source] std::io::Error), +} + +/// An error resulting from attempting to convert a [JWK] to an Aleo private key. +/// +/// The expected JWK format is described in [OKP_CURVE]. +#[derive(Error, Debug)] +pub enum ParsePrivateKeyError { + #[error("Unexpected JWK OKP curve: {0}")] + UnexpectedCurve(String), + #[error("Unexpected JWK key type. Expected \"OKP\"")] + ExpectedOKP, + #[error("Missing private key (\"d\") OKP JWK parameter")] + MissingPrivateKey, + #[error("Unable to deserialize private key: {0}")] + PrivateKeyFromStr(#[source] snarkvm_dpc::AccountError), + #[error("Unable to convert JWK to account address: {0}")] + JWKToAddress(#[source] ParseAddressError), + #[error("Unable to convert private key to account address: {0}")] + PrivateKeyToAddress(#[source] snarkvm_dpc::AccountError), + #[error("Address mismatch. Computed: {}, expected: {}", .computed, .expected)] + AddressMismatch { + computed: Address, + expected: Address, + }, +} + +/// An error resulting from attempting to convert a [JWK] to an Aleo account address. +/// +/// The expected JWK format is described in [OKP_CURVE]. +#[derive(Error, Debug)] +pub enum ParseAddressError { + #[error("Unexpected JWK OKP curve: {0}")] + UnexpectedCurve(String), + #[error("Unexpected JWK key type. Expected \"OKP\"")] + ExpectedOKP, + #[error("Unable to read address from bytes: {0}")] + ReadAddress(#[source] std::io::Error), +} + +lazy_static! { + /// Aleo account signature parameters + pub static ref SIG_PARAMS: Schnorr = { + SchnorrParameters::read_le(AccountSignatureParameters::load_bytes().unwrap().as_slice()) + .unwrap() + .into() + }; + + /// Aleo account commitment parameters + pub static ref COM_PARAMS: PedersenCompressedCommitment = { + let com_params_bytes = AccountCommitmentParameters::load_bytes().unwrap(); + PedersenCommitmentParameters::read_le(com_params_bytes.as_slice()) + .unwrap() + .into() + }; + + /// Aleo account encryption parameters + pub static ref ENC_PARAMS: GroupEncryption = { + let enc_params_bytes = AccountEncryptionParameters::load_bytes() + .unwrap(); + GroupEncryptionParameters::read_le( + enc_params_bytes + .as_slice(), + ) + .unwrap() + .into() + }; +} + +/// Unregistered JWK OKP curve for Aleo private keys in Aleo Testnet 1 +/// +/// OKP key type is defined in [RFC 8037]. +/// +/// [RFC 8037]: https://datatracker.ietf.org/doc/html/rfc8037 +/// +/// This curve type is intended to be used for Aleo private keys as follows: +/// +/// - key type ("kty"): "OKP" +/// - private key ("d") parameter: base64url-encoded Aleo private key (without Base58 encoding) +/// - public key ("x") parameter: base64url-encoded Aleo account address (without Base58 encoding) +/// +/// An Aleo private key JWK is expected to contain an account address in the public key ("x") +/// parameter that corresponds to the private key ("d") parameter, +/// using [SIG_PARAMS], [COM_PARAMS] and [ENC_PARAMS]. +/// +/// An Aleo public key JWK contains the public key ("x") parameter and MUST not contain a private +/// key ("d") parameter. An Aleo public key JWK is usable for verification of signatures using +/// [ENC_PARAMS]. +pub const OKP_CURVE: &str = "AleoTestnet1Key"; + +/// Generate an Aleo private key in [unofficial JWK format][OKP_CURVE]. **CPU-intensive (slow)**. +/// +/// Uses [SIG_PARAMS], [COM_PARAMS], and [ENC_PARAMS]. +pub fn generate_private_key_jwk() -> Result { + let mut rng = rand::rngs::OsRng {}; + let sig_params = SIG_PARAMS.clone(); + let com_params = COM_PARAMS.clone(); + let enc_params = ENC_PARAMS.clone(); + let private_key = PrivateKey::::new(&sig_params, &com_params, &mut rng) + .map_err(AleoGeneratePrivateKeyError::NewKey)?; + let private_key_bytes = bs58::decode(private_key.to_string()) + .into_vec() + .map_err(AleoGeneratePrivateKeyError::DecodePrivateKey)?; + let address = Address::from_private_key(&sig_params, &com_params, &enc_params, &private_key) + .map_err(AleoGeneratePrivateKeyError::PrivateKeyToAddress)?; + let mut public_key_bytes = Vec::new(); + address + .write_le(&mut public_key_bytes) + .map_err(AleoGeneratePrivateKeyError::WriteAddress)?; + Ok(JWK::from(Params::OKP(OctetParams { + curve: OKP_CURVE.to_string(), + public_key: Base64urlUInt(public_key_bytes), + private_key: Some(Base64urlUInt(private_key_bytes)), + }))) +} + +/// Convert JWK private key to Aleo private key +/// +/// Uses [SIG_PARAMS], [COM_PARAMS], and [ENC_PARAMS] to compute the account address. +fn aleo_jwk_to_private_key(jwk: &JWK) -> Result, ParsePrivateKeyError> { + let params = match &jwk.params { + Params::OKP(ref okp_params) => { + if okp_params.curve != OKP_CURVE { + return Err(ParsePrivateKeyError::UnexpectedCurve( + okp_params.curve.to_string(), + )); + } + okp_params + } + _ => return Err(ParsePrivateKeyError::ExpectedOKP), + }; + let private_key_bytes = params + .private_key + .as_ref() + .ok_or(ParsePrivateKeyError::MissingPrivateKey)?; + let private_key_base58 = bs58::encode(&private_key_bytes.0).into_string(); + let address = aleo_jwk_to_address(jwk).map_err(ParsePrivateKeyError::JWKToAddress)?; + let private_key = PrivateKey::::from_str(&private_key_base58) + .map_err(ParsePrivateKeyError::PrivateKeyFromStr)?; + let address_computed = Address::from_private_key( + &SIG_PARAMS.clone(), + &COM_PARAMS.clone(), + &ENC_PARAMS.clone(), + &private_key, + ) + .map_err(ParsePrivateKeyError::PrivateKeyToAddress)?; + if address_computed != address { + return Err(ParsePrivateKeyError::AddressMismatch { + computed: address_computed, + expected: address, + }); + } + Ok(private_key) +} + +fn aleo_jwk_to_address(jwk: &JWK) -> Result, ParseAddressError> { + let params = match &jwk.params { + Params::OKP(ref okp_params) => { + if okp_params.curve != OKP_CURVE { + return Err(ParseAddressError::UnexpectedCurve( + okp_params.curve.to_string(), + )); + } + okp_params + } + _ => return Err(ParseAddressError::ExpectedOKP), + }; + let public_key_bytes = ¶ms.public_key.0; + let address = Address::::read_le(&**public_key_bytes) + .map_err(ParseAddressError::ReadAddress)?; + Ok(address) +} + +/// Create an Aleo signature. +/// +/// The message is signed using [ENC_PARAMS] and a View Key derived from the given JWK private key with [SIG_PARAMS] and [COM_PARAMS]. +/// +/// The JWK private key `key` is expected to use key type `OKP` with curve according to +/// [OKP_CURVE]. +pub fn sign(msg: &[u8], key: &JWK) -> Result, AleoSignError> { + let private_key = aleo_jwk_to_private_key(key).map_err(AleoSignError::JWKToPrivateKey)?; + let enc_params = ENC_PARAMS.clone(); + let sig_params = SIG_PARAMS.clone(); + let com_params = COM_PARAMS.clone(); + let view_key = ViewKey::::from_private_key(&sig_params, &com_params, &private_key) + .map_err(AleoSignError::ViewKeyFromPrivateKey)?; + let mut rng = rand::rngs::OsRng {}; + let sig = view_key + .sign(&enc_params, msg, &mut rng) + .map_err(AleoSignError::Sign)?; + let mut sig_bytes = Vec::new(); + sig.write_le(&mut sig_bytes) + .map_err(AleoSignError::WriteSignature)?; + Ok(sig_bytes) +} + +/// Verify an Aleo signature by an Aleo address as a string. +/// +/// Verification uses [ENC_PARAMS]. +pub fn verify(msg: &[u8], address: &str, sig: &[u8]) -> Result<(), AleoVerifyError> { + let address = + Address::::from_str(address).map_err(AleoVerifyError::AddressFromStr)?; + let sig = + SchnorrSignature::::read_le(sig).map_err(AleoVerifyError::ReadSignature)?; + let enc_params = ENC_PARAMS.clone(); + let valid = address + .verify_signature(&enc_params, msg, &sig) + .map_err(AleoVerifyError::VerifySignature)?; + if !valid { + return Err(AleoVerifyError::InvalidSignature); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_private_key_jwk() { + let key: JWK = + serde_json::from_str(include_str!("../tests/aleotestnet1-2021-11-22.json")).unwrap(); + let private_key = aleo_jwk_to_private_key(&key).unwrap(); + let private_key_str = private_key.to_string(); + assert_eq!( + private_key_str, + "APrivateKey1w7oJWmo86D26Efs6hBfz8xK7M4ww2jmA5WT3QdmYefVnZdS" + ); + let address = Address::from_private_key( + &SIG_PARAMS.clone(), + &COM_PARAMS.clone(), + &ENC_PARAMS.clone(), + &private_key, + ) + .unwrap(); + assert_eq!( + address.to_string(), + "aleo1al8unplh8vtsuwna0h6u2t6g0hvr7t0tnfkem2we5gj7t70aeuxsd94hsy" + ); + } + + #[test] + fn aleo_jwk_sign_verify() { + let private_key: JWK = + serde_json::from_str(include_str!("../tests/aleotestnet1-2021-11-22.json")).unwrap(); + + let public_key = private_key.to_public(); + let msg1 = b"asdf"; + let msg2 = b"asdfg"; + let sig = sign(msg1, &private_key).unwrap(); + let address = aleo_jwk_to_address(&public_key).unwrap(); + let address_string = format!("{}", &address); + verify(msg1, &address_string, &sig).unwrap(); + verify(msg2, &address_string, &sig).unwrap_err(); + } +} diff --git a/src/bbs.rs b/src/bbs.rs index 04778647b..ac481cc2b 100644 --- a/src/bbs.rs +++ b/src/bbs.rs @@ -69,8 +69,8 @@ use zeroize::Zeroize; /// CurveProjective, /// }; /// fn main() { -/// let g1 = >>::hash_to_curve(PREHASH, DST_G1); -/// let g2 = >>::hash_to_curve(PREHASH, DST_G2); +/// let g1 = >>::hash_to_curve(PREHASH, DST_G1); +/// let g2 = >>::hash_to_curve(PREHASH, DST_G2); /// /// let mut g1_bytes = Vec::new(); /// let mut g2_bytes = Vec::new(); diff --git a/src/caip10.rs b/src/caip10.rs index 32a9dce76..3fb273436 100644 --- a/src/caip10.rs +++ b/src/caip10.rs @@ -72,6 +72,28 @@ fn encode_ed25519(jwk: &JWK) -> Result { Ok(string) } +// convert a JWK to a Aleo account address string if it looks like an Aleo key +#[cfg(feature = "aleosig")] +fn encode_aleo_address(jwk: &JWK, network_id: &str) -> Result { + if network_id != "1" { + return Err("Unexpected Aleo network id"); + } + let params = match jwk.params { + Params::OKP(ref params) if params.curve == crate::aleo::OKP_CURVE => params, + _ => return Err("Invalid public key type for Aleo"), + }; + + use bech32::ToBase32; + let address = bech32::encode( + "aleo", + ¶ms.public_key.0.to_base32(), + bech32::Variant::Bech32m, + ) + .map_err(|_| "Unable to encode Aleo account address")?; + + Ok(address) +} + impl BlockchainAccountId { /// Check that a given public key corresponds to this account id. /// @@ -121,6 +143,9 @@ impl BlockchainAccountId { crate::ripemd::hash_public_key(jwk, 0x1e) .map_err(|e| BlockchainAccountIdVerifyError::HashError(e.to_string())) } + #[cfg(feature = "aleosig")] + ("aleo", network_id) => encode_aleo_address(jwk, network_id) + .map_err(|e| BlockchainAccountIdVerifyError::HashError(e.to_string())), _ => Err(BlockchainAccountIdVerifyError::UnknownChainId( self.chain_id.to_string(), )), diff --git a/src/error.rs b/src/error.rs index fbe50da25..dffb061ee 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,6 @@ //! Error types for `ssi` crate +#[cfg(feature = "aleosig")] +use crate::aleo::{AleoGeneratePrivateKeyError, AleoSignError, AleoVerifyError}; use crate::caip10::BlockchainAccountIdParseError; use crate::caip10::BlockchainAccountIdVerifyError; #[cfg(feature = "keccak-hash")] @@ -335,6 +337,19 @@ pub enum Error { CharTryFrom(CharTryFromError), /// Error converting slice to array TryFromSlice(TryFromSliceError), + /// Aleo signing error + #[cfg(feature = "aleosig")] + AleoSign(AleoSignError), + /// Aleo verification error + #[cfg(feature = "aleosig")] + AleoVerify(AleoVerifyError), + /// Unexpected CAIP-2 namespace + UnexpectedCAIP2Namepace(String, String), + /// Unexpected Aleo namespace + UnexpectedAleoNetwork(String, String), + #[cfg(feature = "aleosig")] + /// Error generating Aleo private key + AleoGeneratePrivateKey(AleoGeneratePrivateKeyError), /// Error parsing CAIP-10 blockchain account id BlockchainAccountIdParse(BlockchainAccountIdParseError), /// Error verifying CAIP-10 blockchain account id against a public key @@ -512,6 +527,8 @@ impl fmt::Display for Error { Error::EncodeTezosSignedMessage(e) => write!(f, "Unable to encode Signed Tezos Message: {}", e), Error::DecodeTezosSignature(e) => write!(f, "Unable to decode Tezos Signature: {}", e), Error::ExpectedOutput(expected, found) => write!(f, "Expected output '{}', but found '{}'", expected, found), + Error::UnexpectedCAIP2Namepace(expected, found) => write!(f, "Expected CAIP-2 namespace '{}' but found '{}'", expected, found), + Error::UnexpectedAleoNetwork(expected, found) => write!(f, "Expected Aleo network '{}' but found '{}'", expected, found), Error::UnknownProcessingMode(mode) => write!(f, "Unknown processing mode '{}'", mode), Error::UnknownRdfDirection(direction) => write!(f, "Unknown RDF direction '{}'", direction), Error::HexString => write!(f, "Expected string beginning with '0x'"), @@ -543,6 +560,12 @@ impl fmt::Display for Error { Error::CharTryFrom(e) => e.fmt(f), Error::BlockchainAccountIdParse(e) => e.fmt(f), Error::BlockchainAccountIdVerify(e) => e.fmt(f), + #[cfg(feature = "aleosig")] + Error::AleoSign(e) => e.fmt(f), + #[cfg(feature = "aleosig")] + Error::AleoVerify(e) => e.fmt(f), + #[cfg(feature = "aleosig")] + Error::AleoGeneratePrivateKey(e) => e.fmt(f), #[cfg(feature = "keccak-hash")] Error::TypedDataConstruction(e) => e.fmt(f), #[cfg(feature = "keccak-hash")] @@ -675,6 +698,20 @@ impl From for Error { } } +#[cfg(feature = "aleosig")] +impl From for Error { + fn from(err: AleoSignError) -> Error { + Error::AleoSign(err) + } +} + +#[cfg(feature = "aleosig")] +impl From for Error { + fn from(err: AleoVerifyError) -> Error { + Error::AleoVerify(err) + } +} + #[cfg(feature = "keccak-hash")] impl From for Error { fn from(err: TypedDataConstructionError) -> Error { diff --git a/src/jwk.rs b/src/jwk.rs index 47fca0133..f1c3f0aca 100644 --- a/src/jwk.rs +++ b/src/jwk.rs @@ -235,6 +235,8 @@ pub enum Algorithm { ESKeccakKR, ESBlake2b, ESBlake2bK, + #[doc(hidden)] + AleoTestnet1Signature, None, } @@ -297,6 +299,11 @@ impl JWK { Ok(JWK::from(Params::EC(ec_params))) } + #[cfg(feature = "aleosig")] + pub fn generate_aleo() -> Result { + crate::aleo::generate_private_key_jwk().map_err(Error::AleoGeneratePrivateKey) + } + pub fn get_algorithm(&self) -> Option { if let Some(algorithm) = self.algorithm { return Some(algorithm); @@ -308,6 +315,10 @@ impl JWK { Params::OKP(okp_params) if okp_params.curve == "Ed25519" => { return Some(Algorithm::EdDSA); } + #[cfg(feature = "aleosig")] + Params::OKP(okp_params) if okp_params.curve == crate::aleo::OKP_CURVE => { + return Some(Algorithm::AleoTestnet1Signature); + } Params::EC(ec_params) => { let curve = match &ec_params.curve { Some(curve) => curve, diff --git a/src/ldp.rs b/src/ldp.rs index 3c5047adb..ae3c18623 100644 --- a/src/ldp.rs +++ b/src/ldp.rs @@ -53,6 +53,10 @@ lazy_static! { let context_str = ssi_contexts::SOLVM; serde_json::from_str(context_str).unwrap() }; + pub static ref ALEOVM_CONTEXT: Value = { + let context_str = ssi_contexts::ALEOVM; + serde_json::from_str(context_str).unwrap() + }; } pub fn get_proof_suite(proof_type: &str) -> Result<&(dyn ProofSuite + Sync), Error> { @@ -89,6 +93,12 @@ pub fn get_proof_suite(proof_type: &str) -> Result<&(dyn ProofSuite + Sync), Err "TezosSignature2021" => &TezosSignature2021, "TezosJcsSignature2021" => &TezosJcsSignature2021, "SolanaSignature2021" => &SolanaSignature2021, + "AleoSignature2021" => { + #[cfg(not(feature = "aleosig"))] + return Err(Error::MissingFeatures("aleosig")); + #[cfg(feature = "aleosig")] + &AleoSignature2021 + } "JsonWebSignature2020" => &JsonWebSignature2020, "EcdsaSecp256r1Signature2019" => &EcdsaSecp256r1Signature2019, _ => return Err(Error::ProofTypeNotImplemented), @@ -103,6 +113,12 @@ fn pick_proof_suite<'a, 'b>( Ok(match algorithm { Algorithm::RS256 => &RsaSignature2018, Algorithm::PS256 => &JsonWebSignature2020, + Algorithm::AleoTestnet1Signature => { + #[cfg(not(feature = "aleosig"))] + return Err(Error::MissingFeatures("aleosig")); + #[cfg(feature = "aleosig")] + &AleoSignature2021 + } Algorithm::EdDSA | Algorithm::EdBlake2b => match verification_method { Some(URI::String(ref vm)) if (vm.starts_with("did:sol:") || vm.starts_with("did:pkh:sol:")) @@ -1958,6 +1974,163 @@ impl ProofSuite for SolanaSignature2021 { } } +#[cfg(feature = "aleosig")] +/// Aleo Signature 2021 +/// +/// Linked data signature suite using [Aleo](crate::aleo). +/// +/// # Suite definition +/// +/// Aleo Signature 2021 is a [Linked Data Proofs][ld-proofs] signature suite consisting of the +/// following algorithms: +/// +/// | Parameter | Value | Specification | +/// |----------------------------|-----------------------------------|----------------------------| +/// |id |https://w3id.org/security#AleoSignature2021|[this document](#) | +/// |[canonicalization algorithm]|https://w3id.org/security#URDNA2015|[RDF Dataset Normalization 1.0][URDNA2015]| +/// |[message digest algorithm] |[SHA-256] |[RFC4634] | +/// |[signature algorithm] |Schnorr signature with [Edwards BLS12] curve|[Aleo Documentation - Accounts][aleo-accounts]| +/// +/// The proof object must contain a [proofValue] property encoding the signature in +/// [Multibase] format. +/// +/// ## Verification method +/// +/// Aleo Signature 2021 may be used with the following verification method types: +/// +/// | Name | IRI | Specification | +/// |----------------------------|-----------------------------------|----------------------------| +/// | AleoMethod2021 |https://w3id.org/security#AleoMethod2021| [this document](#) | +/// |BlockchainVerificationMethod2021|https://w3id.org/security#BlockchainVerificationMethod2021|[Blockchain Vocabulary v1][blockchainvm2021] +/// +/// The verification method object must have a [blockchainAccountId] property, identifying the +/// signer's Aleo +/// account address and network id for verification purposes. The chain id part of the account address +/// identifies an Aleo network as specified in the proposed [CAIP for Aleo Blockchain +/// Reference][caip-aleo-chain-ref]. Signatures use parameters defined per network. Currently only +/// network id "1" (CAIP-2 "aleo:1" / [Aleo Testnet I][testnet1]) is supported. The account +/// address format is documented in [Aleo +/// documentation](https://developer.aleo.org/aleo/concepts/accounts#account-address). +/// +/// [message digest algorithm]: https://w3id.org/security#digestAlgorithm +/// [signature algorithm]: https://w3id.org/security#signatureAlgorithm +/// [canonicalization algorithm]: https://w3id.org/security#canonicalizationAlgorithm +/// [ld-proofs]: https://w3c-ccg.github.io/ld-proofs/ +/// [proofValue]: https://w3id.org/security#proofValue +/// [Multibase]: https://datatracker.ietf.org/doc/html/draft-multiformats-multibase +/// [URDNA2015]: https://json-ld.github.io/rdf-dataset-canonicalization/spec/ +/// [RFC4634]: https://www.rfc-editor.org/rfc/rfc4634 "US Secure Hash Algorithms (SHA and HMAC-SHA)" +/// [SHA-256]: http://www.w3.org/2001/04/xmlenc#sha256 +/// [Edwards BLS12]: https://developer.aleo.org/autogen/advanced/the_aleo_curves/edwards_bls12 +/// [aleo-accounts]: https://developer.aleo.org/aleo/concepts/accounts +/// [blockchainvm2021]: https://w3id.org/security/suites/blockchain-2021#BlockchainVerificationMethod2021 +/// [blockchainAccountId]: https://w3c-ccg.github.io/security-vocab/#blockchainAccountId +/// [caip-aleo-chain-ref]: https://github.com/ChainAgnostic/CAIPs/pull/84 +/// [testnet1]: https://developer.aleo.org/testnet/getting_started/overview/ +pub struct AleoSignature2021; +#[cfg(feature = "aleosig")] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ProofSuite for AleoSignature2021 { + async fn sign( + &self, + document: &(dyn LinkedDataDocument + Sync), + options: &LinkedDataProofOptions, + _resolver: &dyn DIDResolver, + key: &JWK, + extra_proof_properties: Option>, + ) -> Result { + let has_context = document_has_context(document, "TODO:uploadAleoVMContextSomewhere")?; + let mut proof = Proof { + context: if has_context { + Value::Null + } else { + serde_json::json!([ALEOVM_CONTEXT.clone()]) + }, + ..Proof::new("AleoSignature2021") + .with_options(options) + .with_properties(extra_proof_properties) + }; + let message = to_jws_payload(document, &proof).await?; + let sig = crate::aleo::sign(&message, &key)?; + let sig_mb = multibase::encode(multibase::Base::Base58Btc, sig); + proof.proof_value = Some(sig_mb); + Ok(proof) + } + + async fn prepare( + &self, + document: &(dyn LinkedDataDocument + Sync), + options: &LinkedDataProofOptions, + _resolver: &dyn DIDResolver, + _public_key: &JWK, + extra_proof_properties: Option>, + ) -> Result { + let proof = Proof { + context: serde_json::json!([SOLVM_CONTEXT.clone()]), + ..Proof::new("AleoSignature2021") + .with_options(options) + .with_properties(extra_proof_properties) + }; + let message = to_jws_payload(document, &proof).await?; + Ok(ProofPreparation { + proof, + jws_header: None, + signing_input: SigningInput::Bytes(Base64urlUInt(message)), + }) + } + + async fn complete( + &self, + preparation: ProofPreparation, + signature: &str, + ) -> Result { + let mut proof = preparation.proof; + proof.proof_value = Some(signature.to_string()); + Ok(proof) + } + + async fn verify( + &self, + proof: &Proof, + document: &(dyn LinkedDataDocument + Sync), + resolver: &dyn DIDResolver, + ) -> Result { + const NETWORK_ID: &str = "1"; + const NAMESPACE: &str = "aleo"; + let sig_mb = proof + .proof_value + .as_ref() + .ok_or(Error::MissingProofSignature)?; + let (_base, sig) = multibase::decode(&sig_mb)?; + let verification_method = proof + .verification_method + .as_ref() + .ok_or(Error::MissingVerificationMethod)?; + let vm = resolve_vm(verification_method, resolver).await?; + if vm.type_ != "AleoMethod2021" && vm.type_ != "BlockchainVerificationMethod2021" { + return Err(Error::VerificationMethodMismatch); + } + let account_id: BlockchainAccountId = + vm.blockchain_account_id.ok_or(Error::MissingKey)?.parse()?; + if account_id.chain_id.namespace != NAMESPACE { + return Err(Error::UnexpectedCAIP2Namepace( + NAMESPACE.to_string(), + account_id.chain_id.namespace.to_string(), + )); + } + if account_id.chain_id.reference != NETWORK_ID { + return Err(Error::UnexpectedAleoNetwork( + NETWORK_ID.to_string(), + account_id.chain_id.namespace.to_string(), + )); + } + let message = to_jws_payload(document, proof).await?; + crate::aleo::verify(&message, &account_id.account_address, &sig)?; + Ok(Default::default()) + } +} + pub struct EcdsaSecp256r1Signature2019; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -2580,4 +2753,92 @@ mod tests { .await .unwrap(); } + + #[async_std::test] + #[cfg(feature = "aleosig")] + async fn aleosig2021() { + use crate::did::Document; + use crate::did_resolve::{ + DocumentMetadata, ResolutionInputMetadata, ResolutionMetadata, ERROR_NOT_FOUND, + TYPE_DID_LD_JSON, + }; + use crate::vc::Credential; + + struct ExampleResolver; + const EXAMPLE_DID: &str = "did:example:aleovm2021"; + const EXAMPLE_DOC: &'static str = include_str!("../tests/lds-aleo2021-issuer0.jsonld"); + #[async_trait] + impl DIDResolver for ExampleResolver { + async fn resolve( + &self, + did: &str, + _input_metadata: &ResolutionInputMetadata, + ) -> ( + ResolutionMetadata, + Option, + Option, + ) { + if did == EXAMPLE_DID { + let doc = match Document::from_json(EXAMPLE_DOC) { + Ok(doc) => doc, + Err(err) => { + return ( + ResolutionMetadata::from_error(&format!("JSON Error: {:?}", err)), + None, + None, + ); + } + }; + ( + ResolutionMetadata { + content_type: Some(TYPE_DID_LD_JSON.to_string()), + ..Default::default() + }, + Some(doc), + Some(DocumentMetadata::default()), + ) + } else { + (ResolutionMetadata::from_error(ERROR_NOT_FOUND), None, None) + } + } + } + + let private_key: JWK = + serde_json::from_str(include_str!("../tests/aleotestnet1-2021-11-22.json")).unwrap(); + + let vc_str = include_str!("../tests/lds-aleo2021-vc0.jsonld"); + let mut vc = Credential::from_json_unsigned(vc_str).unwrap(); + let resolver = ExampleResolver; + + if vc.proof.iter().flatten().next().is_none() { + // Issue VC / Generate Test Vector + let mut credential = vc.clone(); + let vc_issue_options = LinkedDataProofOptions { + verification_method: Some(URI::String("did:example:aleovm2021#id".to_string())), + proof_purpose: Some(ProofPurpose::AssertionMethod), + ..Default::default() + }; + let proof = AleoSignature2021 + .sign(&vc, &vc_issue_options, &resolver, &private_key, None) + .await + .unwrap(); + credential.add_proof(proof.clone()); + vc = credential; + + use std::fs::File; + use std::io::{BufWriter, Write}; + let outfile = File::create("tests/lds-aleo2021-vc0.jsonld").unwrap(); + let mut output_writer = BufWriter::new(outfile); + serde_json::to_writer_pretty(&mut output_writer, &vc).unwrap(); + output_writer.write(b"\n").unwrap(); + } + + // Verify VC + let proof = vc.proof.iter().flatten().next().unwrap(); + let warnings = AleoSignature2021 + .verify(&proof, &vc, &resolver) + .await + .unwrap(); + assert!(warnings.is_empty()); + } } diff --git a/src/lib.rs b/src/lib.rs index fc06fc17f..16bbe470c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,10 @@ html_logo_url = "https://demo.didkit.dev/2021/10/21/rust-didkit.png", html_favicon_url = "https://demo.didkit.dev/2021/10/21/rust-favicon.ico" )] + +#[cfg(feature = "aleosig")] +pub mod aleo; + pub mod bbs; pub mod blakesig; pub mod caip10; diff --git a/src/vc.rs b/src/vc.rs index 65016c9f6..856171743 100644 --- a/src/vc.rs +++ b/src/vc.rs @@ -2134,6 +2134,7 @@ fn verify_proof_consistency(proof: &Proof, dataset: &DataSet) -> Result<(), Erro ("Eip712Signature2021", "https://w3id.org/security#Eip712Signature2021") => (), ("TezosSignature2021", "https://w3id.org/security#TezosSignature2021") => (), ("TezosJcsSignature2021", "https://w3id.org/security#TezosJcsSignature2021") => (), + ("AleoSignature2021", "https://w3id.org/security#AleoSignature2021") => (), ("SolanaSignature2021", "https://w3id.org/security#SolanaSignature2021") => (), _ => return Err(Error::UnexpectedTriple(type_triple.clone())), }; diff --git a/tests/aleotestnet1-2021-11-22.json b/tests/aleotestnet1-2021-11-22.json new file mode 100644 index 000000000..2025e6eec --- /dev/null +++ b/tests/aleotestnet1-2021-11-22.json @@ -0,0 +1,6 @@ +{ + "kty": "OKP", + "crv": "AleoTestnet1Key", + "x": "78_Jh_c7Fw46fX31xS9Ifdg_LeuabZ2p2aIl5fn9zw0", + "d": "f4a9dNLd0omQcg3SEajVHGqEqwFHDGD9yNc2xpzuiZ3sSJjIf5AnEYXWCQ" +} \ No newline at end of file diff --git a/tests/lds-aleo2021-issuer0.jsonld b/tests/lds-aleo2021-issuer0.jsonld new file mode 100644 index 000000000..7fb87b332 --- /dev/null +++ b/tests/lds-aleo2021-issuer0.jsonld @@ -0,0 +1,24 @@ +{ + "@context": [ + "https://www.w3.org/ns/did/v1", + { + "AleoMethod2021": "https://w3id.org/security#AleoMethod2021", + "blockchainAccountId": "https://w3id.org/security#blockchainAccountId" + } + ], + "id": "did:example:aleovm2021", + "verificationMethod": [ + { + "id": "did:example:aleovm2021#id", + "type": "AleoMethod2021", + "controller": "did:example:aleovm2021", + "blockchainAccountId": "aleo:1:aleo1al8unplh8vtsuwna0h6u2t6g0hvr7t0tnfkem2we5gj7t70aeuxsd94hsy" + } + ], + "assertionMethod": [ + "did:example:aleovm2021#id" + ], + "authentication": [ + "did:example:aleovm2021#id" + ] +} diff --git a/tests/lds-aleo2021-vc0.jsonld b/tests/lds-aleo2021-vc0.jsonld new file mode 100644 index 000000000..74213a11c --- /dev/null +++ b/tests/lds-aleo2021-vc0.jsonld @@ -0,0 +1,98 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiableCredential" + ], + "credentialSubject": {}, + "issuer": "did:example:aleovm2021", + "issuanceDate": "2021-11-23T20:08:36Z", + "proof": [ + { + "@context": [ + { + "AleoMethod2021": { + "@context": { + "@protected": true, + "blockchainAccountId": "https://w3id.org/security#blockchainAccountId", + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "id": "@id", + "type": "@type" + }, + "@id": "https://w3id.org/security#AleoMethod2021" + }, + "AleoSignature2021": { + "@context": { + "@protected": true, + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "id": "@id", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@context": { + "@protected": true, + "assertionMethod": { + "@container": "@set", + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id" + }, + "authentication": { + "@container": "@set", + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id" + }, + "capabilityDelegation": { + "@container": "@set", + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id" + }, + "capabilityInvocation": { + "@container": "@set", + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id" + }, + "id": "@id", + "keyAgreement": { + "@container": "@set", + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id" + }, + "type": "@type" + }, + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab" + }, + "proofValue": { + "@id": "https://w3id.org/security#proofValue", + "@type": "https://w3id.org/security#multibase" + }, + "type": "@type", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + }, + "@id": "https://w3id.org/security#AleoSignature2021" + } + } + ], + "type": "AleoSignature2021", + "proofPurpose": "assertionMethod", + "proofValue": "z5Q7WJwyUL1GbC41wHhDLRcwF3J6M7XBzijj9RranfSzxHvuqwbkbXiS1Azq1sSP9YvBwWS3T5JCPgUsRxJyRXG5D", + "verificationMethod": "did:example:aleovm2021#id", + "created": "2021-11-29T20:03:47.263Z" + } + ] +}