diff --git a/Cargo.toml b/Cargo.toml index e371d29..cfa13fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,8 @@ members = [ "contracts/governance", "contracts/insurance", "contracts/teachlink", + "contracts/identity_registry", + "contracts/credential_registry", ] [workspace.package] diff --git a/contracts/IDENTITY_CREDENTIAL.md b/contracts/IDENTITY_CREDENTIAL.md new file mode 100644 index 0000000..117f0fa --- /dev/null +++ b/contracts/IDENTITY_CREDENTIAL.md @@ -0,0 +1,23 @@ +**Identity & Credential Contracts (Soroban)** + +Overview +- `identity-registry`: on-chain DID registry. Stores DID -> controller, auth methods, recovery address. +- `credential-registry`: on-chain credential index. Stores credential hash -> (issuer DID, subject DID, metadata pointer, expires_at, status). + +Key on-chain guarantees +- Deterministic verification can check presence and status of a credential on-chain. +- Full VC JSON and ZK proofs remain off-chain; only hashes/roots and status bits stored on-chain. + +Next steps / integration notes +- Wire `credential-registry` to call `identity-registry` for authoritative issuer controller checks. +- Add Merkle/bitmap-based revocation root support for efficient revocation proofs. +- Implement cross-contract calls and auth to allow DID controllers (not raw addresses) to issue/revoke. +- Add off-chain ZK proof verifier support: store verification circuits' commitment roots on-chain and provide helper APIs for verifiers. +- Marketplace, federation, selective-disclosure circuits, and biometric-binding are implemented off-chain; contracts store anchors/roots. + +Files added +- `contracts/identity_registry` — Cargo + src/lib.rs +- `contracts/credential_registry` — Cargo + src/lib.rs + +Testing & build +- Use the workspace's soroban toolchain and existing patterns (see other `contracts/*` crates) to build and test. diff --git a/contracts/credential_registry/Cargo.toml b/contracts/credential_registry/Cargo.toml new file mode 100644 index 0000000..bce73ef --- /dev/null +++ b/contracts/credential_registry/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "credential-registry" +version = "0.1.0" +edition.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +soroban-sdk.workspace = true + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } + +[features] +testutils = ["soroban-sdk/testutils"] + +[lints] +workspace = true + diff --git a/contracts/credential_registry/src/lib.rs b/contracts/credential_registry/src/lib.rs new file mode 100644 index 0000000..8b69510 --- /dev/null +++ b/contracts/credential_registry/src/lib.rs @@ -0,0 +1,104 @@ +#![no_std] + +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Bytes, BytesN, Env}; + +#[contract] +pub struct CredentialRegistryContract; + +#[derive(Clone)] +pub enum CredentialStatus { + Active, + Revoked, + Expired, +} + +#[contractimpl] +impl CredentialRegistryContract { + // Issue a credential by storing its hash and metadata pointer. + // `credential_hash` should be a deterministic hash (e.g., SHA-256) of the full VC JSON. + pub fn issue_credential( + env: &Env, + credential_hash: BytesN<32>, + issuer: Address, + issuer_did: Bytes, + subject_did: Bytes, + metadata_ptr: Bytes, + expires_at: i128, + ) { + issuer.require_auth(); + let key = (symbol_short!("cred"), credential_hash.clone()); + assert!( + !env.storage().persistent().has(&key), + "credential already exists" + ); + let record: (Bytes, Bytes, Bytes, i128, i32) = ( + issuer_did.clone(), + subject_did.clone(), + metadata_ptr.clone(), + expires_at, + 0i32, + ); + env.storage().persistent().set(&key, &record); + env.events().publish( + (symbol_short!("crediss"),), + ( + credential_hash, + issuer_did, + subject_did, + metadata_ptr, + expires_at, + ), + ); + } + + // Revoke a credential. Caller must be issuer (signed address) + pub fn revoke_credential(env: &Env, credential_hash: BytesN<32>, issuer: Address) { + issuer.require_auth(); + let key = (symbol_short!("cred"), credential_hash.clone()); + let opt: Option<(Bytes, Bytes, Bytes, i128, i32)> = env.storage().persistent().get(&key); + match opt { + Some((issuer_did, subject_did, metadata_ptr, expires_at, _status)) => { + let record: (Bytes, Bytes, Bytes, i128, i32) = ( + issuer_did.clone(), + subject_did.clone(), + metadata_ptr.clone(), + expires_at, + 1i32, + ); + env.storage().persistent().set(&key, &record); + env.events().publish( + (symbol_short!("credrev"),), + (credential_hash, issuer_did, subject_did), + ); + } + None => panic!("credential not found"), + } + } + + // Get credential record: returns (issuer_did, subject_did, metadata_ptr, expires_at, status) + pub fn get_credential( + env: &Env, + credential_hash: BytesN<32>, + ) -> Option<(Bytes, Bytes, Bytes, i128, i32)> { + let key = (symbol_short!("cred"), credential_hash.clone()); + env.storage().persistent().get(&key) + } + + // Check if credential is active (not revoked and not expired) + pub fn is_active(env: &Env, credential_hash: BytesN<32>, now_ts: i128) -> bool { + match Self::get_credential(env, credential_hash.clone()) { + Some((_issuer, _subject, _meta, expires_at, status)) => { + if status == 1 { + return false; + } + if expires_at > 0 && now_ts > expires_at { + return false; + } + true + } + None => false, + } + } +} + +fn main() {} diff --git a/contracts/identity_registry/Cargo.toml b/contracts/identity_registry/Cargo.toml new file mode 100644 index 0000000..e90a9b5 --- /dev/null +++ b/contracts/identity_registry/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "identity-registry" +version = "0.1.0" +edition.workspace = true +repository.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +soroban-sdk.workspace = true + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } + +[features] +testutils = ["soroban-sdk/testutils"] + +[lints] +workspace = true + diff --git a/contracts/identity_registry/src/lib.rs b/contracts/identity_registry/src/lib.rs new file mode 100644 index 0000000..9fd5934 --- /dev/null +++ b/contracts/identity_registry/src/lib.rs @@ -0,0 +1,145 @@ +#![no_std] + +use soroban_sdk::{contract, contractimpl, symbol_short, Address, Bytes, BytesN, Env}; + +#[contract] +pub struct IdentityRegistryContract; + +#[contractimpl] +impl IdentityRegistryContract { + // Create a new DID mapping to a controller address. + pub fn create_did(env: &Env, identity_id: BytesN<32>, controller: Address) { + controller.require_auth(); + let key = (symbol_short!("didctl"), identity_id.clone()); + assert!(!env.storage().persistent().has(&key), "DID already exists"); + env.storage().persistent().set(&key, &controller); + env.events() + .publish((symbol_short!("didcrt"),), (identity_id, controller)); + } + + // Get controller for a DID + pub fn get_controller(env: &Env, identity_id: BytesN<32>) -> Option
{ + let key = (symbol_short!("didctl"), identity_id.clone()); + env.storage().persistent().get(&key) + } + + // Update controller. Caller must pass the current controller and sign the call. + pub fn set_controller( + env: &Env, + identity_id: BytesN<32>, + current_controller: Address, + new_controller: Address, + ) { + current_controller.require_auth(); + let key = (symbol_short!("did_ctrl"), identity_id.clone()); + let opt: Option = env.storage().persistent().get(&key); + match opt { + Some(stored) => { + assert!( + stored == current_controller, + "only controller can change controller" + ); + env.storage().persistent().set(&key, &new_controller); + env.events() + .publish((symbol_short!("didchg"),), (identity_id, new_controller)); + } + None => panic!("DID not found"), + } + } + + // Add or update an authentication method (e.g., key, service) for a DID + pub fn set_auth_method( + env: &Env, + identity_id: BytesN<32>, + controller: Address, + method_id: Bytes, + public_key: Bytes, + ) { + controller.require_auth(); + let ctrl_key = (symbol_short!("didctl"), identity_id.clone()); + let current: Option = env.storage().persistent().get(&ctrl_key); + assert!(current.is_some(), "DID not found"); + assert!( + current.unwrap() == controller, + "only controller can set auth methods" + ); + let key = ( + symbol_short!("auth"), + identity_id.clone(), + method_id.clone(), + ); + env.storage().persistent().set(&key, &public_key); + env.events().publish( + (symbol_short!("authset"),), + (identity_id, method_id, public_key), + ); + } + + // Remove an auth method + pub fn remove_auth_method( + env: &Env, + identity_id: BytesN<32>, + controller: Address, + method_id: Bytes, + ) { + controller.require_auth(); + let ctrl_key = (symbol_short!("did_ctrl"), identity_id.clone()); + let current: Option = env.storage().persistent().get(&ctrl_key); + assert!(current.is_some(), "DID not found"); + assert!( + current.unwrap() == controller, + "only controller can remove auth methods" + ); + let key = ( + symbol_short!("auth"), + identity_id.clone(), + method_id.clone(), + ); + env.storage().persistent().remove(&key); + env.events() + .publish((symbol_short!("authrem"),), (identity_id, method_id)); + } + + // Set a recovery address that may be used to recover control of the DID + pub fn set_recovery( + env: &Env, + identity_id: BytesN<32>, + controller: Address, + recovery: Address, + ) { + controller.require_auth(); + let ctrl_key = (symbol_short!("did_ctrl"), identity_id.clone()); + let current: Option = env.storage().persistent().get(&ctrl_key); + assert!(current.is_some(), "DID not found"); + assert!( + current.unwrap() == controller, + "only controller can set recovery" + ); + let key = (symbol_short!("recovery"), identity_id.clone()); + env.storage().persistent().set(&key, &recovery); + env.events() + .publish((symbol_short!("recovery"),), (identity_id, recovery)); + } + + // Recover controller using the configured recovery address + pub fn recover(env: &Env, identity_id: BytesN<32>, recovery: Address, new_controller: Address) { + recovery.require_auth(); + let rec_key = (symbol_short!("recovery"), identity_id.clone()); + let rec_opt: Option = env.storage().persistent().get(&rec_key); + match rec_opt { + Some(recovery_addr) => { + assert!( + recovery_addr == recovery, + "only recovery address can perform recovery" + ); + let ctrl_key = (symbol_short!("did_ctrl"), identity_id.clone()); + env.storage().persistent().set(&ctrl_key, &new_controller); + env.events() + .publish((symbol_short!("didrec"),), (identity_id, new_controller)); + } + None => panic!("no recovery configured for DID"), + } + } +} + +fn main() {}