Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ members = [
"contracts/governance",
"contracts/insurance",
"contracts/teachlink",
"contracts/identity_registry",
"contracts/credential_registry",
]

[workspace.package]
Expand Down
23 changes: 23 additions & 0 deletions contracts/IDENTITY_CREDENTIAL.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions contracts/credential_registry/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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

104 changes: 104 additions & 0 deletions contracts/credential_registry/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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() {}
22 changes: 22 additions & 0 deletions contracts/identity_registry/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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

145 changes: 145 additions & 0 deletions contracts/identity_registry/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Address> {
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<Address> = 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<Address> = 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<Address> = 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<Address> = 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<Address> = 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() {}
Loading