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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ A decentralized health credential platform built on the Stellar Soroban Smart Co
#### Health Authority Contract
- Manages authorized health authority issuers
- Validates credential issuance from certified authorities
- Stores health authority public keys and signatures
- Enables credential authority verification
- Stores authority Ed25519 public keys and active/suspended status
- Verifies authority signatures against the registered key

#### Upgrade Mechanism
- **Proxy Pattern**: Seamless contract upgrades without data loss
Expand Down
4 changes: 4 additions & 0 deletions contracts/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ pub enum Error {
IssuerNotAuthorized = 18,
CredentialRevoked = 19,
CredentialNotFound = 20,
// Health authority errors
AuthorityNotFound = 21,
AuthorityAlreadyRegistered = 22,
AuthorityInactive = 23,
}
203 changes: 203 additions & 0 deletions contracts/src/health_authority.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
//! Registry for the public keys of health-credential issuers.
//!
//! An authority owns its entry: registration, key rotation, and status updates
//! all require authorization from the authority address. Credential consumers
//! can use [`HealthAuthority::validate_authority_signature`] to verify an
//! Ed25519 signature against the currently registered key.

use soroban_sdk::{contract, contractimpl, contracttype, Address, Bytes, BytesN, Env};

use crate::errors::Error;

/// Public, on-chain metadata for a health authority.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Authority {
/// The account that controls this authority's registry entry.
pub address: Address,
/// The authority's Ed25519 verification key.
pub public_key: BytesN<32>,
/// Whether signatures from this authority may currently be trusted.
pub is_active: bool,
pub registered_at: u64,
pub updated_at: u64,
}

#[contracttype]
pub enum DataKey {
Authority(Address),
}

// Persistent authority entries are refreshed to roughly one year whenever
// they are read or written. This avoids an active issuer being archived while
// keeping the registry scalable: every operation touches only one entry.
const PERSISTENT_TTL_THRESHOLD: u32 = 1_051_200; // ~61 days
const PERSISTENT_TTL_EXTEND: u32 = 6_307_200; // ~365 days

#[contract]
pub struct HealthAuthority;

#[contractimpl]
impl HealthAuthority {
/// Register an authority and its Ed25519 public key.
///
/// The authority must authorise registration, preventing a third party
/// from registering a key for an address it does not control.
pub fn register_authority(
env: &Env,
authority: Address,
public_key: BytesN<32>,
) -> Result<(), Error> {
authority.require_auth();

let key = DataKey::Authority(authority.clone());
if env.storage().persistent().has(&key) {
return Err(Error::AuthorityAlreadyRegistered);
}

let now = env.ledger().timestamp();
let record = Authority {
address: authority,
public_key,
is_active: true,
registered_at: now,
updated_at: now,
};

write_authority(env, &key, &record);
Ok(())
}

/// Return an authority's public registry record.
pub fn get_authority(env: &Env, authority: Address) -> Result<Authority, Error> {
read_authority(env, authority)
}

/// Return an authority's current Ed25519 public key.
pub fn get_authority_public_key(env: &Env, authority: Address) -> Result<BytesN<32>, Error> {
Ok(read_authority(env, authority)?.public_key)
}

/// Rotate an authority's signing key.
pub fn update_authority_public_key(
env: &Env,
authority: Address,
public_key: BytesN<32>,
) -> Result<(), Error> {
authority.require_auth();

let key = DataKey::Authority(authority.clone());
let mut record = read_authority_by_key(env, &key)?;
record.public_key = public_key;
record.updated_at = env.ledger().timestamp();

write_authority(env, &key, &record);
Ok(())
}

/// Activate or suspend an authority. Only the authority can change its
/// own status.
pub fn set_authority_status(
env: &Env,
authority: Address,
is_active: bool,
) -> Result<(), Error> {
authority.require_auth();

let key = DataKey::Authority(authority.clone());
let mut record = read_authority_by_key(env, &key)?;
record.is_active = is_active;
record.updated_at = env.ledger().timestamp();

write_authority(env, &key, &record);
Ok(())
}

/// Convenience operation for suspending an authority.
pub fn deactivate_authority(env: &Env, authority: Address) -> Result<(), Error> {
Self::set_authority_status(env, authority, false)
}

/// Convenience operation for restoring a suspended authority.
pub fn activate_authority(env: &Env, authority: Address) -> Result<(), Error> {
Self::set_authority_status(env, authority, true)
}

/// Returns `true` only when an authority is registered and active.
///
/// This deliberately returns `false` for unknown addresses so consumers
/// can use it as a safe, non-panicking trust check.
pub fn verify_authority(env: &Env, authority: Address) -> bool {
let key = DataKey::Authority(authority);
match env.storage().persistent().get::<_, Authority>(&key) {
Some(record) => {
bump_persistent(env, &key);
record.is_active
}
None => false,
}
}

/// Alias for callers that want to check status without fetching metadata.
pub fn is_authority_active(env: &Env, authority: Address) -> bool {
Self::verify_authority(env, authority)
}

/// Validate an Ed25519 signature made by an active authority.
///
/// Soroban's crypto host function aborts the invocation when the signature
/// is malformed or does not match the supplied message/key. A successful
/// return therefore means the signature is valid. Unknown and inactive
/// authorities instead return a typed contract error.
pub fn validate_authority_signature(
env: &Env,
authority: Address,
message: Bytes,
signature: BytesN<64>,
) -> Result<bool, Error> {
let record = read_authority(env, authority)?;
if !record.is_active {
return Err(Error::AuthorityInactive);
}

env.crypto()
.ed25519_verify(&record.public_key, &message, &signature);
Ok(true)
}

/// Compatibility-friendly name for signature validation.
pub fn verify_authority_signature(
env: &Env,
authority: Address,
message: Bytes,
signature: BytesN<64>,
) -> Result<bool, Error> {
Self::validate_authority_signature(env, authority, message, signature)
}
}

fn read_authority(env: &Env, authority: Address) -> Result<Authority, Error> {
let key = DataKey::Authority(authority);
read_authority_by_key(env, &key)
}

fn read_authority_by_key(env: &Env, key: &DataKey) -> Result<Authority, Error> {
let record = env
.storage()
.persistent()
.get(key)
.ok_or(Error::AuthorityNotFound)?;
bump_persistent(env, key);
Ok(record)
}

fn write_authority(env: &Env, key: &DataKey, authority: &Authority) {
env.storage().persistent().set(key, authority);
bump_persistent(env, key);
}

fn bump_persistent(env: &Env, key: &DataKey) {
env.storage()
.persistent()
.extend_ttl(key, PERSISTENT_TTL_THRESHOLD, PERSISTENT_TTL_EXTEND);
}
89 changes: 89 additions & 0 deletions contracts/src/health_authority_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
extern crate std;

use soroban_sdk::{testutils::Address as _, Address, Bytes, BytesN, Env};

use crate::health_authority::{HealthAuthority, HealthAuthorityClient};

fn setup() -> (Env, HealthAuthorityClient<'static>, Address) {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register_contract(None, HealthAuthority {});
let client = HealthAuthorityClient::new(&env, &contract_id);
let authority = Address::generate(&env);

(env, client, authority)
}

fn rfc8032_public_key(env: &Env) -> BytesN<32> {
BytesN::from_array(
env,
&[
0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7, 0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64,
0x07, 0x3a, 0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25, 0xaf, 0x02, 0x1a, 0x68,
0xf7, 0x07, 0x51, 0x1a,
],
)
}

fn rfc8032_empty_message_signature(env: &Env) -> BytesN<64> {
BytesN::from_array(
env,
&[
0xe5, 0x56, 0x43, 0x00, 0xc3, 0x60, 0xac, 0x72, 0x90, 0x86, 0xe2, 0xcc, 0x80, 0x6e,
0x82, 0x8a, 0x84, 0x87, 0x7f, 0x1e, 0xb8, 0xe5, 0xd9, 0x74, 0xd8, 0x73, 0xe0, 0x65,
0x22, 0x49, 0x01, 0x55, 0x5f, 0xb8, 0x82, 0x15, 0x90, 0xa3, 0x3b, 0xac, 0xc6, 0x1e,
0x39, 0x70, 0x1c, 0xf9, 0xb4, 0x6b, 0xd2, 0x5b, 0xf5, 0xf0, 0x59, 0x5b, 0xbe, 0x24,
0x65, 0x51, 0x41, 0x43, 0x8e, 0x7a, 0x10, 0x0b,
],
)
}

#[test]
fn registration_stores_the_authority_key() {
let (env, client, authority) = setup();
let public_key = rfc8032_public_key(&env);

client.register_authority(&authority, &public_key);

let record = client.get_authority(&authority);
assert_eq!(record.address, authority);
assert_eq!(record.public_key, public_key);
assert!(record.is_active);
assert_eq!(client.get_authority_public_key(&authority), public_key);
assert!(client.verify_authority(&authority));
}

#[test]
fn authority_status_can_be_managed() {
let (env, client, authority) = setup();
client.register_authority(&authority, &rfc8032_public_key(&env));

client.deactivate_authority(&authority);
assert!(!client.is_authority_active(&authority));

client.activate_authority(&authority);
assert!(client.is_authority_active(&authority));
}

#[test]
fn valid_authority_signature_is_accepted() {
let (env, client, authority) = setup();
client.register_authority(&authority, &rfc8032_public_key(&env));

let message = Bytes::new(&env);
let signature = rfc8032_empty_message_signature(&env);
assert!(client.validate_authority_signature(&authority, &message, &signature));
}

#[test]
fn rotating_an_authority_key_replaces_the_stored_key() {
let (env, client, authority) = setup();
let original_key = rfc8032_public_key(&env);
let replacement_key = BytesN::from_array(&env, &[7; 32]);
client.register_authority(&authority, &original_key);

client.update_authority_public_key(&authority, &replacement_key);

assert_eq!(client.get_authority_public_key(&authority), replacement_key);
}
5 changes: 5 additions & 0 deletions contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod auditing;
pub mod data_sharing;
pub mod errors;
pub mod events;
pub mod health_authority;
pub mod identity_registry;
pub mod storage;
pub mod types;
Expand All @@ -20,11 +21,15 @@ mod integration_tests;
#[cfg(test)]
mod benchmarks;

#[cfg(test)]
mod health_authority_tests;

#[cfg(test)]
mod test;

pub use access_control::AccessControl;
pub use data_sharing::DataSharing;
pub use errors::Error;
pub use health_authority::HealthAuthority;
pub use identity_registry::IdentityRegistry;
pub use verification::Verification;
2 changes: 0 additions & 2 deletions contracts/src/test.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![cfg(test)]

use crate::auditing::{Auditing, AuditingClient};
use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String, Symbol};

Expand Down
Loading