diff --git a/Cargo.lock b/Cargo.lock index 79d928b..5a2361e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ dependencies = [ "serde", "serde-aux", "serde_json", - "sha2 0.10.6", + "sha2 0.10.8", "thiserror", "tokio", "url", @@ -191,6 +191,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -373,6 +379,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + [[package]] name = "core-foundation" version = "0.9.3" @@ -423,6 +435,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "crypto-bigint" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28f85c3514d2a6e64160359b45a3918c3b4178bcbf4ae5d03ab2d02e521c479a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -499,9 +523,20 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" dependencies = [ - "const-oid", - "crypto-bigint", - "pem-rfc7468", + "const-oid 0.7.1", + "crypto-bigint 0.3.2", + "pem-rfc7468 0.3.1", +] + +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid 0.9.5", + "pem-rfc7468 0.7.0", + "zeroize", ] [[package]] @@ -555,12 +590,14 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid 0.9.5", "crypto-common", + "subtle", ] [[package]] @@ -572,6 +609,40 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.8", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki 0.7.2", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9775b22bc152ad86a0cf23f0f348b884b26add12bf741e7ffc4d4ab2ab4d205" +dependencies = [ + "base16ct", + "crypto-bigint 0.5.4", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encoding_rs" version = "0.8.33" @@ -633,6 +704,16 @@ dependencies = [ "instant", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fnv" version = "1.0.7" @@ -760,6 +841,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -784,6 +866,17 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.18" @@ -892,6 +985,15 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hmac-drbg" version = "0.3.0" @@ -900,7 +1002,7 @@ checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" dependencies = [ "digest 0.9.0", "generic-array", - "hmac", + "hmac 0.8.1", ] [[package]] @@ -1143,6 +1245,7 @@ dependencies = [ "config", "diesel", "diesel_migrations", + "ecdsa", "env_logger", "fake", "hex", @@ -1156,10 +1259,14 @@ dependencies = [ "lazy_static", "libsecp256k1", "log", + "p256", "rand 0.8.5", "serde", "serde_json", + "sha2 0.10.8", "sha3", + "strum", + "strum_macros", "thiserror", "tokio", "url", @@ -1579,6 +1686,18 @@ dependencies = [ "hashbrown 0.9.1", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", +] + [[package]] name = "paris" version = "1.5.15" @@ -1623,6 +1742,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.2.0" @@ -1670,7 +1798,7 @@ checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" dependencies = [ "once_cell", "pest", - "sha2 0.10.6", + "sha2 0.10.8", ] [[package]] @@ -1711,8 +1839,8 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" dependencies = [ - "der", - "pkcs8", + "der 0.5.1", + "pkcs8 0.8.0", "zeroize", ] @@ -1722,11 +1850,21 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" dependencies = [ - "der", - "spki", + "der 0.5.1", + "spki 0.5.4", "zeroize", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.8", + "spki 0.7.2", +] + [[package]] name = "pkg-config" version = "0.3.26" @@ -1791,6 +1929,15 @@ dependencies = [ "reqwest", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-crate" version = "0.1.5" @@ -2008,6 +2155,16 @@ dependencies = [ "winreg", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "rle-decode-fast" version = "1.0.3" @@ -2032,13 +2189,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" dependencies = [ "byteorder", - "digest 0.10.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-iter", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.8.0", "rand_core 0.6.4", "smallvec", "subtle", @@ -2105,6 +2262,20 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.8", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.8.2" @@ -2197,13 +2368,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.6", + "digest 0.10.7", ] [[package]] @@ -2212,7 +2383,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54c2bb1a323307527314a36bfb73f24febb08ce2b8a554bf4ffd6f51ad15198c" dependencies = [ - "digest 0.10.6", + "digest 0.10.7", "keccak", ] @@ -2225,6 +2396,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + [[package]] name = "slab" version = "0.4.8" @@ -2263,7 +2444,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" dependencies = [ "base64ct", - "der", + "der 0.5.1", +] + +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der 0.7.8", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7932b74..4dc2269 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,8 @@ hyper-tls = "*" tokio = { version = "1", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +strum = "*" +strum_macros = "*" json-patch = "*" # diesel `uuidv07` feature is bound tight with `uuid` v0.7.x @@ -45,6 +47,10 @@ sha3 = "0.10" # Keccak256 base64 = "0.13" hex = "0.4" hex-literal = "0.3" +ecdsa = "*" +p256 = "*" +sha2 = "0.10.8" + # arweave arweave-rs = "0.1.2" diff --git a/docs/api.apib b/docs/api.apib index f21e3c5..1d9dbcd 100644 --- a/docs/api.apib +++ b/docs/api.apib @@ -2,6 +2,7 @@ FORMAT: 1A # Changelog + - <2023-11-01 Wed> :: Add subkey mechanism - <2022-02-28 Mon> :: init # General @@ -22,6 +23,52 @@ For example, for JavaScript, [json-patch](https://github.com/idubrov/json-patch) for Rust. +## About subkey signing + +From , you can upload data with signature +generated by a subkey which is already binded to an avatar on +ProofService. + +Notice: for subkey with `algorithm: es256` and `algorithm: rs256`, you +should `sha256` the `sign_payload` given by `POST /v1/kv/payload` +first, sign the sha256 digest, then submit to `POST /v1/kv`. + +### About subkey public key format + +- for `algorighm: "rs256"` : TODO +- for `algorighm: "es256"` : `0xX_CONCAT_Y_64BYTES_HEXSTRING` +- for `algorighm: "secp256k1"` : `0xCOMPRESSED_PUBKEY_HEXSTRING` or `0xUNCOMPRESSED_PUBKEY_HEXSTRING` + +### Format of `signature` field: + +- for `algorighm: "rs256"` : TODO +- for `algorighm: "es256"` : `0xR_CONCAT_S` +- for `algorighm: "secp256k1"` : `0xR_CONCAT_S_CONCAT_V` + +### Extra: CBOR encoded signature struct + +Response after calling `await navigator.credentials.get()` should be like: + +```js +{ + + authenticatorData: Uint8Array, + // Literal string of clientData, see below + clientDataJSON: Uint8Array, + signature: Uint8Array, +} + +// Struct of clientDataJSON: +// After JSON.parse(response.clientDataJSON.toString()) +{ + "type": "webauthn.get", + // base64.encode(sha256(sign_payload)) + "challenge": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "origin": "http://localhost:3000", + "crossOrigin": false +} +``` + # Group KV ## Get current KV of a persona [GET /v1/kv] @@ -30,7 +77,6 @@ for Rust. + Parameters - - persona (string, required) - Deprecated. Use `avatar` instead. - avatar (string, required) - Persona public key (hexstring started with `0x`). + Example @@ -41,7 +87,6 @@ for Rust. + Attributes (object) - + persona (string, required) - Deprecated. Use `avatar` instead. + avatar (string, required) - Avatar public key (uncompressed hexstring started with `0x`). + proofs (array[object], required) - All proofs belong to this persona + platform (string, required) - Platform (incl. `nextid`, which means public key itself). @@ -119,14 +164,19 @@ Avatar not found (no KV was ever created). > Make sure to save order-aware struct in `[]` value. +> Notice: following field groups should and only appear once. +> - For avatar sign flow: `avatar`, `platform`, `identity` +> - For subkey sign flow: `algorithm`, `public_key` + + Request (application/json) + Attributes (object) - + persona (string, required) - Deprecated. Use `avatar` instead. - + avatar (string, required) - Avatar public key (both comressed / uncompressed and with/without `0x` are OK). - + platform (string, required) - Platform (incl. `nextid`, which means public key itself). - + identity (string, required) - Identity. + + avatar (string, optional) - For Avatar sign flow: Avatar public key (both comressed / uncompressed and with/without `0x` are OK). + + algorithm (string, optional) - For subkey sign flow: Algorithm of this subkey (now supporting `es256` and `secp256k1`) + + public_key (string, optional) - For subkey sign flow: Public key of this subkey. See description above for format. + + platform (string, optional) - Platform (incl. `nextid`, which means public key itself). + + identity (string, optional) - Identity. + patch (object, required) - Patch to current data + Body @@ -163,16 +213,17 @@ Avatar not found (no KV was ever created). "sign_payload": "{\"action\":\"kv\",\"created_at\":1646983606,\"patch\":{\"a\":\"sample\",\"key_to_delete\":null,\"structure\":[\"it\",\"could\",\"be\",\"anything\"],\"this\":\"is\"},\"prev\":null,\"uuid\":\"40c13c92-31e5-40d1-aebb-143d8e5b9c5e\"}" } -## Update a full set of key-value pairs [POST /v1/kv] +## Update a set of key-value pairs [POST /v1/kv] + Request (application/json) + Attributes (object) - + persona (string, required) - Deprecated. Use `avatar` instead. - + avatar (string, required) - Avatar public key. - + platform (string, required) - Platform (incl. `nextid`, which means public key itself). - + identity (string, required) - Identity. + + avatar (string, optional) - for Avatar sign flow: Avatar public key. + + algorithm (string, optional) - For subkey sign flow: Algorithm of this subkey (now supporting `es256` and `secp256k1`) + + public_key (string, optional) - For subkey sign flow: Public key of this subkey. See description above for format. + + platform (string, optional) - Platform (incl. `nextid`, which means public key itself). + + identity (string, optional) - Identity. + uuid (string, required) - UUID generated by server in `POST /v1/kv/payload`. + created_at (number, required) - Creation timestamp generated by server in `POST /v1/kv/payload`. + signature (string, required) - Signature of this request. Base64-ed. diff --git a/flake.lock b/flake.lock index 63b1374..f79a842 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1697379843, - "narHash": "sha256-RcnGuJgC2K/UpTy+d32piEoBXq2M+nVFzM3ah/ZdJzg=", + "lastModified": 1700538105, + "narHash": "sha256-uZhOCmwv8VupEmPZm3erbr9XXmyg7K67Ul3+Rx2XMe0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "12bdeb01ff9e2d3917e6a44037ed7df6e6c3df9d", + "rev": "51a01a7e5515b469886c120e38db325c96694c2f", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 10d0e41..9ad5ca2 100644 --- a/flake.nix +++ b/flake.nix @@ -15,7 +15,18 @@ { # defaultPackage = naersk-lib.buildPackage ./.; devShells.default = pkgs.mkShell { - buildInputs = with pkgs; [ cargo rustc rustfmt rust-analyzer pre-commit rustPackages.clippy pkg-config openssl postgresql ]; + buildInputs = with pkgs; [ + cargo + rustc + rustfmt + rust-analyzer + pre-commit + rustPackages.clippy + pkg-config + openssl + postgresql + diesel-cli + ]; RUST_SRC_PATH = pkgs.rustPlatform.rustLibSrc; }; }); diff --git a/src/controller/payload.rs b/src/controller/payload.rs index 8e17f51..918d9dd 100644 --- a/src/controller/payload.rs +++ b/src/controller/payload.rs @@ -3,15 +3,18 @@ use crate::{ crypto::secp256k1::Secp256k1KeyPair, error::Error, model::{establish_connection, kv_chains::NewKVChain}, - proof_client::can_set_kv, + proof_client::{can_set_kv, self}, }; use http::StatusCode; use serde::{Deserialize, Serialize}; +use super::error_response; + #[derive(Debug, Clone, Serialize, Deserialize)] struct PayloadRequest { - pub persona: Option, pub avatar: Option, + pub algorithm: Option, + pub public_key: Option, pub platform: String, pub identity: String, pub patch: serde_json::Value, @@ -26,20 +29,50 @@ struct PayloadResponse { pub async fn controller(req: Request) -> Result { let params: PayloadRequest = json_parse_body(&req)?; + if params.avatar.is_some() { + sign_payload_with_avatar(¶ms).await + } else if params.algorithm.is_some() && params.public_key.is_some() { + sign_payload_with_subkey(¶ms).await + } else { + Ok(error_response( + Error::ParamError("(avatar) or (algorithm, public_key) is not provided".into()) + )) + } +} - let keypair = Secp256k1KeyPair::from_pubkey_hex( - ¶ms - .avatar - .or(params.persona) - .ok_or_else(|| Error::ParamError("avatar not found".into()))?, - )?; +async fn sign_payload_with_avatar(params: &PayloadRequest) -> Result { + let keypair = Secp256k1KeyPair::from_pubkey_hex(params.avatar.as_ref().unwrap())?; can_set_kv(&keypair.public_key, ¶ms.platform, ¶ms.identity).await?; let mut conn = establish_connection(); let mut new_kvchain = NewKVChain::for_persona(&mut conn, &keypair.public_key)?; - new_kvchain.platform = params.platform; - new_kvchain.identity = params.identity; - new_kvchain.patch = params.patch; + new_kvchain.platform = params.platform.clone(); + new_kvchain.identity = params.identity.clone(); + new_kvchain.patch = params.patch.clone(); + let sign_payload = new_kvchain.generate_signature_payload()?; + + Ok(json_response( + StatusCode::OK, + &PayloadResponse { + sign_payload: serde_json::to_string(&sign_payload)?, + uuid: sign_payload.uuid.to_string(), + created_at: sign_payload.created_at, + }, + )?) +} + +async fn sign_payload_with_subkey(params: &PayloadRequest) -> Result { + let algorithm = params.algorithm.as_ref().unwrap(); + let public_key = params.public_key.as_ref().unwrap(); + let subkey = proof_client::find_subkey(&algorithm, &public_key).await?; + let avatar = Secp256k1KeyPair::from_pubkey_hex(&subkey.avatar)?; + can_set_kv(&avatar.public_key, ¶ms.platform, ¶ms.identity).await?; + + let mut conn = establish_connection(); + let mut new_kvchain = NewKVChain::for_persona(&mut conn, &avatar.public_key)?; + new_kvchain.platform = params.platform.clone(); + new_kvchain.identity = params.identity.clone(); + new_kvchain.patch = params.patch.clone(); let sign_payload = new_kvchain.generate_signature_payload()?; Ok(json_response( @@ -99,8 +132,9 @@ mod tests { } = Secp256k1KeyPair::generate(); let req_body = PayloadRequest { - persona: Some(compress_public_key(&public_key)), - avatar: None, + avatar: Some(compress_public_key(&public_key)), + algorithm: None, + public_key: None, platform: "facebook".into(), identity: Faker.fake(), patch: json!({"test":"abc"}), @@ -131,8 +165,9 @@ mod tests { let old_kv_chain = generate_data(&mut conn, &public_key).unwrap(); let req_body = PayloadRequest { - persona: None, avatar: Some(compress_public_key(&public_key)), + algorithm: None, + public_key: None, platform: "facebook".into(), identity: Faker.fake(), patch: json!({"test":"abc"}), diff --git a/src/controller/upload.rs b/src/controller/upload.rs index e6074e4..7a9d86c 100644 --- a/src/controller/upload.rs +++ b/src/controller/upload.rs @@ -1,19 +1,22 @@ -use super::{json_response, query::query_response}; +use super::{json_response, query::query_response, error_response}; use crate::{ controller::{json_parse_body, Request, Response}, crypto::secp256k1::Secp256k1KeyPair, error::Error, - model::{self, kv_chains::NewKVChain, arweave::KVChainArweaveDocument}, - proof_client::can_set_kv, - util::{base64_to_vec, timestamp_to_naive}, + model::{self, kv_chains::{NewKVChain, KVChain}, arweave::KVChainArweaveDocument}, + proof_client::{can_set_kv, find_subkey}, + util::{base64_to_vec, timestamp_to_naive, vec_to_hex}, }; +use diesel::PgConnection; use http::StatusCode; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Deserialize, Serialize)] struct UploadRequest { - pub persona: Option, pub avatar: Option, + pub algorithm: Option, + pub public_key: Option, + pub platform: String, pub identity: String, pub signature: String, @@ -24,18 +27,27 @@ struct UploadRequest { pub async fn controller(request: Request) -> Result { let req: UploadRequest = json_parse_body(&request)?; + + if req.avatar.is_some() { + create_with_avatar(req).await + } else if req.algorithm.is_some() && req.public_key.is_some() { + create_with_subkey(req).await + } else { + Ok(error_response( + Error::ParamError("(avatar) or (algorithm, public_key) is not provided".into()) + )) + } +} + +async fn create_with_avatar(req: UploadRequest) -> Result { let sig = base64_to_vec(&req.signature)?; - let avatar = req.avatar.clone(); - let persona = Secp256k1KeyPair::from_pubkey_hex( - &req.avatar - .or(req.persona) - .ok_or_else(|| Error::ParamError("avatar not found".into()))?, - )?; let uuid = uuid::Uuid::parse_str(&req.uuid)?; - can_set_kv(&persona.public_key, &req.platform, &req.identity).await?; + + let avatar = Secp256k1KeyPair::from_pubkey_hex(&req.avatar.unwrap())?; + can_set_kv(&avatar.public_key, &req.platform, &req.identity).await?; let mut conn = model::establish_connection(); - let mut new_kv = NewKVChain::for_persona(&mut conn, &persona.public_key)?; + let mut new_kv = NewKVChain::for_persona(&mut conn, &avatar.public_key)?; new_kv.platform = req.platform; new_kv.identity = req.identity; new_kv.signature = sig; @@ -45,43 +57,84 @@ pub async fn controller(request: Request) -> Result { new_kv.signature_payload = serde_json::to_string(&new_kv.generate_signature_payload()?).unwrap(); - // Validate signature new_kv.validate()?; + let kv_link = new_kv.finalize(&mut conn)?; + // Apply patch + kv_link.perform_patch(&mut conn)?; + + upload_to_arweave(&new_kv, &kv_link, &mut conn).await?; - let previous_arweave_id = new_kv.clone().find_last_chain_arweave(&mut conn)?; + // All done. Build response. + let response = query_response(&mut conn, &avatar.public_key)?; + + json_response(StatusCode::CREATED, &response) +} + +async fn create_with_subkey(req: UploadRequest) -> Result { + let sig = base64_to_vec(&req.signature)?; + let uuid = uuid::Uuid::parse_str(&req.uuid)?; + let algo = req.algorithm.unwrap(); + let pk = req.public_key.unwrap(); + let subkey = find_subkey(&algo, &pk).await?; + + let mut conn = model::establish_connection(); + let avatar = Secp256k1KeyPair::from_pubkey_hex(&subkey.avatar)?; + let mut new_kv = NewKVChain::for_persona(&mut conn, &avatar.public_key)?; + new_kv.platform = req.platform; + new_kv.identity = req.identity; + new_kv.signature = sig; + new_kv.patch = req.patch.clone(); + new_kv.uuid = uuid; + new_kv.created_at = timestamp_to_naive(req.created_at); + new_kv.signature_payload = + serde_json::to_string(&new_kv.generate_signature_payload()?).unwrap(); + + algo.verify(&pk, &new_kv.signature_payload, &req.signature)?; + let kv_link = new_kv.finalize(&mut conn)?; + // Apply patch + kv_link.perform_patch(&mut conn)?; + + upload_to_arweave(&new_kv, &kv_link, &mut conn).await?; + + // All done. Build response. + let response = query_response(&mut conn, &avatar.public_key)?; + + json_response(StatusCode::CREATED, &response) +} + +async fn upload_to_arweave(new_kv: &NewKVChain, kv: &KVChain, conn: &mut PgConnection) -> Result<(), Error> { + // Arweave configuration is not set. Return empty string + if crate::config::C.arweave.is_none() { + log::info!("Arweave is not configured. Skipped uploading."); + return Ok(()) + } + + let previous_arweave_id = new_kv.find_last_chain_arweave(conn)?; // Try take the kvchain data upload to the arweave. let arweave_document = KVChainArweaveDocument{ - avatar: avatar.unwrap_or("".into()), - uuid, + avatar: vec_to_hex(&kv.persona), + uuid: kv.uuid, persona: vec![], - platform: new_kv.platform.clone(), - identity: new_kv.identity.clone(), - patch: new_kv.patch.clone(), - signature: new_kv.signature.clone(), - created_at: new_kv.created_at, - signature_payload: new_kv.signature_payload.clone(), - previous_id: new_kv.previous_id.clone(), + platform: kv.platform.clone(), + identity: kv.identity.clone(), + patch: kv.patch.clone(), + signature: kv.signature.clone(), + created_at: kv.created_at, + signature_payload: kv.signature_payload.clone(), + previous_id: kv.previous_id.clone(), previous_arweave_id: previous_arweave_id.clone(), }; - // Valid. Insert it. - let kv_link = new_kv.finalize(&mut conn)?; - - // Apply patch - kv_link.perform_patch(&mut conn)?; - // Upload to arweave // TODO: should make it as a background job let result = arweave_document.upload_to_arweave().await.ok(); - let _ = kv_link.insert_arweave_id(&mut conn, result); - - // All done. Build response. - let response = query_response(&mut conn, &persona.public_key)?; + let _ = kv.insert_arweave_id(conn, result); - json_response(StatusCode::CREATED, &response) + Ok(()) } + #[cfg(test)] mod tests { use super::*; @@ -100,8 +153,9 @@ mod tests { /// And then return the response body. async fn create_req_and_send(new_kv_chain: NewKVChain, public_key: PublicKey) -> QueryResponse { let req_body = UploadRequest { - persona: Some(compress_public_key(&public_key)), avatar: Some(compress_public_key(&public_key)), + algorithm: None, + public_key: None, platform: new_kv_chain.platform.clone(), identity: new_kv_chain.identity, signature: vec_to_base64(&new_kv_chain.signature), diff --git a/src/crypto/es256.rs b/src/crypto/es256.rs new file mode 100644 index 0000000..1e10575 --- /dev/null +++ b/src/crypto/es256.rs @@ -0,0 +1,31 @@ +use ecdsa::RecoveryId; +use p256::{PublicKey, ecdsa::Signature}; + + +use crate::{error::Error, crypto::util::hash_sha256}; + +pub fn public_key_from_hex(pubkey_hex: &str) -> Result { + let pubkey_bytes = hex::decode(pubkey_hex)?; + if pubkey_bytes.len() != 64 { + return Err(Error::ParamError(format!("public key of es256 mismatch: expect 64B, got {}B", pubkey_bytes.len()))); + }; + + PublicKey::from_sec1_bytes(pubkey_bytes.as_slice()).map_err(|err| err.into()) +} + +pub fn validate_sig(pubkey_hex: &str, sign_payload: &str, signature: Vec) -> Result { + let pubkey = public_key_from_hex(pubkey_hex)?; + let signature = Signature::from_slice(signature.as_ref())?; + let payload_hash = hash_sha256(sign_payload); + + let _recovery_id = RecoveryId::trial_recovery_from_prehash(&pubkey.into(), payload_hash.as_slice(), &signature)?; + Ok(pubkey) +} + +#[cfg(test)] +mod tests { + #[test] + fn test_name() { + + } +} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 3ee603b..897c418 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -1,2 +1,3 @@ pub mod secp256k1; +pub mod es256; pub mod util; diff --git a/src/crypto/util.rs b/src/crypto/util.rs index ba458ab..f071019 100644 --- a/src/crypto/util.rs +++ b/src/crypto/util.rs @@ -19,7 +19,7 @@ pub fn hex_public_key(pk: &PublicKey) -> String { /// # use kv_server::crypto::util::hash_keccak256; /// # use hex_literal::hex; /// # -/// let result = hash_keccak256(&"Test123"); +/// let result = hash_keccak256("Test123"); /// let expected: [u8; 32] = hex!("504AF7475B7341893F803C8EBABFBAEA60EAE7B6A42CB006960C3FDB14DCF8AD"); /// assert_eq!(result, expected); /// ``` @@ -28,3 +28,21 @@ pub fn hash_keccak256(message: &str) -> [u8; 32] { hasher.update(message); hasher.finalize().into() } + +/// SHA256(message) +/// # Example +/// +/// ```rust +/// # use kv_server::crypto::util::hash_sha256; +/// # use hex_literal::hex; +/// let result = hash_sha256("Test123"); +/// let expected: [u8; 32] = hex!("d9b5f58f0b38198293971865a14074f59eba3e82595becbe86ae51f1d9f1f65e"); +/// assert_eq!(result, expected); +/// ``` +pub fn hash_sha256(message: &str) -> [u8; 32] { + let mut context = sha2::Sha256::new(); + context.update(message.as_bytes()); + let mut result: [u8; 32] = [0; 32]; + result.copy_from_slice(context.finalize().as_ref()); + result +} diff --git a/src/error/mod.rs b/src/error/mod.rs index 2f281f6..ac07c92 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -37,6 +37,10 @@ pub enum Error { UrlParseError(#[from] url::ParseError), #[error("arweave error: {0}")] ArweaveError(#[from] arweave_rs::error::Error), + #[error("P256 curve error: {0}")] + P256CurveError(#[from] p256::elliptic_curve::Error), + #[error("P256 ECDSA error: {0}")] + P256ECDSAError(#[from] p256::ecdsa::Error), } impl Error { @@ -58,6 +62,9 @@ impl Error { Error::UuidParseError(_) => StatusCode::BAD_REQUEST, Error::UrlParseError(_) => StatusCode::BAD_REQUEST, Error::ArweaveError(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::P256CurveError(_) => StatusCode::BAD_REQUEST, + Error::P256ECDSAError(_) => StatusCode::BAD_REQUEST, + } } } diff --git a/src/lib.rs b/src/lib.rs index 172dfbd..4f122ee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ #[macro_use] extern crate lazy_static; -#[macro_use] extern crate diesel; pub mod config; @@ -10,5 +9,6 @@ pub mod crypto; pub mod error; pub mod model; pub mod proof_client; +pub mod types; mod schema; pub mod util; diff --git a/src/model/arweave/mod.rs b/src/model/arweave/mod.rs index 7ce255b..2c88e68 100644 --- a/src/model/arweave/mod.rs +++ b/src/model/arweave/mod.rs @@ -30,10 +30,6 @@ pub struct KVChainArweaveDocument { impl KVChainArweaveDocument { // If arweave configuration is missing, returns Ok("".to_string()) pub async fn upload_to_arweave(self) -> Result { - // Arweave configuration is not set. Return empty string - if C.arweave.is_none() { - return Ok("".into()) - } let arweave_config = C.arweave.clone().unwrap(); // create the signer diff --git a/src/model/kv_chains/mod.rs b/src/model/kv_chains/mod.rs index 5214b26..6c4436e 100644 --- a/src/model/kv_chains/mod.rs +++ b/src/model/kv_chains/mod.rs @@ -44,7 +44,7 @@ pub struct NewKVChain { pub signature: Vec, pub signature_payload: String, pub created_at: NaiveDateTime, - pub arweave_id: Option, + pub arweave_id: Option, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -116,6 +116,11 @@ impl NewKVChain { }) } + /// Generate a signature body which should be signed with Subkey. + pub fn generate_subkey_signature_payload(&self) -> Result{ + todo!() + } + /// Generate a signature using given keypair. /// For development and test only. pub fn sign(&self, keypair: &Secp256k1KeyPair) -> Result, Error> { @@ -151,7 +156,7 @@ impl NewKVChain { /// Find last chain arweave id. pub fn find_last_chain_arweave( - self, + &self, conn: &mut PgConnection, ) -> Result, Error> { @@ -200,15 +205,15 @@ impl KVChain { let (kv_record, _is_new) = kv::find_or_create(conn, &self.platform, &self.identity, &public_key)?; kv_record.patch(conn, &self.patch)?; - + Ok(kv_record) } /// Insert arweave id into kv and kv_chains. pub fn insert_arweave_id(&self, conn: &mut PgConnection, new_arweave: Option) -> Result<(), Error> { - + use crate::model::kv; - + // insert arweave id into table kv let Secp256k1KeyPair { public_key, @@ -218,7 +223,7 @@ impl KVChain { let (kv_record, _is_new) = kv::find_or_create(conn, &self.platform, &self.identity, &public_key)?; kv_record.update_arweave(conn, new_arweave.clone())?; - + // insert arweave id into table kv_chains diesel::update(self) .set(arweave_id.eq(new_arweave)) @@ -244,8 +249,8 @@ pub fn find_kv_chain_by_id( // Found if found.is_some() { return Ok((found.unwrap(), true)); - } - + } + // Not found Ok((found.unwrap(), false)) } @@ -263,4 +268,4 @@ pub fn find_all_by_identity( .get_results(conn)?; Ok(result) -} \ No newline at end of file +} diff --git a/src/proof_client/mod.rs b/src/proof_client/mod.rs index 8c3d8d7..943bef0 100644 --- a/src/proof_client/mod.rs +++ b/src/proof_client/mod.rs @@ -1,6 +1,6 @@ mod tests; -use crate::{crypto::secp256k1::Secp256k1KeyPair, error::Error}; +use crate::{crypto::secp256k1::Secp256k1KeyPair, error::Error, types::subkey::Algorithm}; use http::{Response, StatusCode}; use hyper::{body::HttpBody as _, client::HttpConnector, Body, Client}; use hyper_tls::HttpsConnector; @@ -38,12 +38,28 @@ pub struct ProofQueryResponsePagination { pub next: u32, } +#[derive(Deserialize, Debug)] +pub struct SubkeyQueryResponse { + pub subkeys: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct SubkeyQueryResponseSingle { + pub avatar: String, + pub algorithm: Algorithm, + pub public_key: String, + pub name: String, + #[serde(rename = "RP_ID")] + pub rp_id: String, + pub created_at: u32, +} + #[derive(Deserialize, Debug)] pub struct ErrorResponse { pub message: String, } -pub fn make_client() -> Client> { +fn make_client() -> Client> { let https = HttpsConnector::new(); let client = Client::builder().build::<_, hyper::Body>(https); client @@ -64,7 +80,7 @@ where } /// Persona should be 33-bytes hexstring (`0x[0-9a-f]{66}`) -pub async fn query(base: &str, persona: &str) -> Result { +async fn query_avatar(base: &str, persona: &str) -> Result { let client = make_client(); let uri = format!("{}/v1/proof?platform=nextid&identity={}", base, persona) .parse() @@ -81,22 +97,72 @@ pub async fn query(base: &str, persona: &str) -> Result Result { + let client = make_client(); + let uri = format!( + "{}/v1/subkey?algorithm={}&public_key={}", + base, + algorithm.to_string(), + public_key + ) + .parse() + .unwrap(); + let mut resp = client.get(uri).await?; + if !resp.status().is_success() { + let body: ErrorResponse = parse_body(&mut resp).await?; + return Err(Error::General( + format!("ProofService error: {}", body.message), + resp.status(), + )); + } + + parse_body(&mut resp).await.map_err(|e| e.into()) +} + +/// Determine if given subkey exists on ProofService. Returns the binded avatar public key. +pub async fn find_subkey( + algorithm: &Algorithm, + public_key: &str, +) -> Result { + let query_result = + query_subkey(&crate::config::C.proof_service.url, &algorithm, public_key).await?; + if query_result.subkeys.len() == 0 { + return Err(Error::General( + "Subkey not found on ProofService".into(), + StatusCode::BAD_REQUEST, + )); + }; + let subkey_found = query_result + .subkeys + .iter() + .find(|&sk| sk.public_key == public_key && sk.algorithm == *algorithm) + .ok_or(Error::General( + "Subkey not found on ProofService".into(), + StatusCode::BAD_REQUEST, + ))?; + Ok(subkey_found.clone()) +} + /// Determine if persona-platform-identity pair can set a KV. pub async fn can_set_kv( persona_pubkey: &PublicKey, - platform: &String, - identity: &String, + platform: &str, + identity: &str, ) -> Result<(), Error> { // FIXME: super stupid test stub if cfg!(test) { return Ok(()); } // KV of NextID: validate if identity == persona. - if *platform == "nextid".to_string() { + if platform == "nextid" { let Secp256k1KeyPair { public_key: identity_pubkey, secret_key: _, - } = Secp256k1KeyPair::from_pubkey_hex(&identity)?; + } = Secp256k1KeyPair::from_pubkey_hex(identity)?; if identity_pubkey == *persona_pubkey { return Ok(()); } else { @@ -110,7 +176,7 @@ pub async fn can_set_kv( let persona_compressed_hex = format!("0x{}", hex::encode(persona_pubkey.serialize_compressed())); let query_response = - query(&crate::config::C.proof_service.url, &persona_compressed_hex).await?; + query_avatar(&crate::config::C.proof_service.url, &persona_compressed_hex).await?; if query_response.ids.len() == 0 { return Err(Error::General( format!( diff --git a/src/proof_client/tests.rs b/src/proof_client/tests.rs index 3da61fb..6f4ba98 100644 --- a/src/proof_client/tests.rs +++ b/src/proof_client/tests.rs @@ -1,11 +1,11 @@ #[cfg(test)] mod tests { - use crate::{error::Error, proof_client::query}; + use crate::{error::Error, proof_client::query_avatar}; const PROOF_SERVICE_URL: &str = "https://proof-service.nextnext.id"; // Staging #[tokio::test] async fn test_smoke() -> Result<(), Error> { - let result = query( + let result = query_avatar( PROOF_SERVICE_URL, "0x000000000000000000000000000000000000000000000000000000000000000000", ) diff --git a/src/schema.rs b/src/schema.rs index 2a787a0..2b92ad4 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,4 +1,6 @@ -table! { +// @generated automatically by Diesel CLI. + +diesel::table! { kv (id) { id -> Int4, uuid -> Nullable, @@ -12,7 +14,7 @@ table! { } } -table! { +diesel::table! { kv_chains (id) { id -> Int4, uuid -> Uuid, @@ -29,7 +31,7 @@ table! { } } -allow_tables_to_appear_in_same_query!( +diesel::allow_tables_to_appear_in_same_query!( kv, kv_chains, ); diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..573d43f --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1 @@ +pub mod subkey; diff --git a/src/types/subkey.rs b/src/types/subkey.rs new file mode 100644 index 0000000..e2bc84e --- /dev/null +++ b/src/types/subkey.rs @@ -0,0 +1,53 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::{Display, EnumString}; + +use crate::{error::Error, util, crypto}; + +/// Algorithm supported by subkey +#[derive(Display, Debug, Copy, Clone, Serialize, Deserialize, EnumString, PartialEq, Eq)] +pub enum Algorithm { + /// Secp256k1 curve, signing after Keccak256 hash (personal_sign) using ECDSA + #[strum(serialize = "secp256k1")] + #[serde(rename = "secp256k1")] + Secp256k1, + /// P-256 (aka Secp256r1) curve, signing after SHA256 hash using ECDSA + #[strum(serialize = "es256")] + #[serde(rename = "es256")] + ES256, +} + +impl Algorithm { + pub fn verify( + &self, + public_key: &str, + sign_payload: &str, + signature: &str, + ) -> Result<(), Error> { + let signature_bytes = parse_signature(signature)?; + match self { + Algorithm::Secp256k1 => { + let public_key = crypto::secp256k1::Secp256k1KeyPair::from_pubkey_hex(public_key)?.public_key; + let recovered = crypto::secp256k1::Secp256k1KeyPair::recover_from_personal_signature(&signature_bytes, sign_payload)?; + if public_key == recovered { + Ok(()) + } else { + Err(Error::SignatureValidationError("Signature not match".into())) + } + }, + Algorithm::ES256 => { + crypto::es256::validate_sig(public_key, sign_payload, signature_bytes).map(|_| ()) + }, + } + } +} + +fn parse_signature(signature: &str) -> Result, Error> { + let result = if signature.starts_with("0x") { + hex::decode(signature.strip_prefix("0x").unwrap())? + }else { + // Base64 + util::base64_to_vec(signature)? + }; + + Ok(result) +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 916586c..44e6fed 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -9,6 +9,11 @@ pub fn vec_to_base64(bytes_vec: &Vec) -> String { base64::encode(bytes_vec) } +/// With `0x` prefix +pub fn vec_to_hex(bytes_vec: &Vec) -> String { + format!("0x{}", hex::encode(bytes_vec)) +} + /// Returns current UNIX timestamp (unit: second). pub fn timestamp() -> i64 { naive_now().timestamp()