aere-contracts/contracts/compliance/AereTrustRegistry.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

344 lines
18 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import "@openzeppelin/contracts/access/Ownable.sol";
/// @dev The subset of the LIVE, deployed AerePQCKeyRegistry (chain 2800, 0x1eCa...3691) this registry
/// uses to verify an issuer's POST-QUANTUM attestation. verifyWithKey routes to the live native
/// precompile for the key's scheme (0x0AE1 Falcon-512) and verifies the signature IN FULL
/// on-chain, fail-closed.
interface IAerePQCKeyRegistryTrust {
function verifyWithKey(uint256 keyId, bytes32 message, bytes calldata signature) external view returns (bool);
function statusOf(uint256 keyId) external view returns (uint8);
function schemeOf(uint256 keyId) external view returns (uint8);
}
/**
* @title AereTrustRegistry, a federated registry of ACCREDITED credential issuers for Aere Network
*
* @notice The trust root for Aere's institution-grade compliance passport. Compliance trust is NOT
* a single Foundation key: it is a FEDERATED SET of accredited issuers, each authorized to
* issue a defined set of credential types. This mirrors, in spirit, the eIDAS 2.0 / EUDI
* "trusted list" model, in which a supervisory body maintains a machine-readable list of
* qualified trust service providers and the services they are accredited for. Here the Aere
* Foundation is the trusted-list operator (the registry owner); each entry is an accredited
* issuer with a DID, an accreditation SCOPE (the credential types it may issue), a lifecycle
* STATUS, and an OPTIONAL post-quantum (Falcon-512) key reference so the issuer's attestations
* can be PQC-signed and verified on-chain via the live precompile 0x0AE1.
*
* WHY POST-QUANTUM. Every accreditation stack in production today authenticates an issuer's
* attestation with a classical ECDSA / RSA signature, which a cryptographically relevant
* quantum computer forges. An Aere issuer additionally binds a Falcon-512 key here, so its
* attestations are verified with a NIST post-quantum signature scheme in full on-chain. That
* is the differentiator this registry exists to carry.
*
* DESIGN PRINCIPLES.
* - FAIL-CLOSED. An unknown issuer, a suspended or revoked issuer, or a credential type
* outside an issuer's scope all resolve to NOT accredited. isAccredited never returns a
* false positive.
* - APPEND-ONLY HISTORY, NO SILENT REWRITE. Every status transition and every scope change
* is pushed to an append-only history. The current status is a pointer; the record of how
* it got there is immutable and fully auditable.
* - TERMINAL REVOCATION. Accreditation revocation is terminal (a revoked issuer can never
* be reinstated). Suspension is reversible.
*
* @dev HONEST SCOPE. This is application-layer compliance tooling. It changes NOTHING about Aere
* consensus, which stays classical secp256k1 ECDSA QBFT. What is post-quantum here is the
* AUTHENTICITY of an issuer attestation, verified by the live precompile. This contract holds
* no funds. Whether any REAL institution is accredited, and whether such accreditation carries
* legal weight under eIDAS 2.0, is a partnership / regulatory matter that is external to this
* code and is NOT asserted by deploying it.
*
* @dev No PII on chain. An issuer entry stores a DID (a public identifier, not personal data) and
* opaque bytes32 credential-type tags. Reason codes on status transitions are opaque bytes32.
*/
contract AereTrustRegistry is Ownable {
/// @notice Falcon-512 scheme id in AerePQCKeyRegistry (live precompile 0x0AE1).
uint8 public constant SCHEME_FALCON512 = 1;
/// @notice AerePQCKeyRegistry.STATUS_ACTIVE (only an ACTIVE key is a current key).
uint8 internal constant KEY_STATUS_ACTIVE = 1;
/// @notice Sentinel for "no post-quantum key linked to this issuer".
uint256 public constant NO_KEY = type(uint256).max;
/// @notice Issuer accreditation lifecycle. None=never accredited; Active=may issue in scope;
/// Suspended=temporarily halted (reversible); Revoked=terminal.
enum Status {
None,
Active,
Suspended,
Revoked
}
/// @notice The LIVE AerePQCKeyRegistry used to verify an issuer's Falcon-512 attestations.
IAerePQCKeyRegistryTrust public immutable KEY_REGISTRY;
struct Issuer {
address controller; // the issuer id / controlling address
Status status; // current lifecycle status (history is append-only, see below)
uint256 pqcKeyId; // Falcon-512 keyId in KEY_REGISTRY, or NO_KEY
uint64 accreditedAt; // block timestamp of first accreditation
string did; // the issuer's did:aere DID (public identifier, not PII)
}
/// @notice One append-only record per status transition. No entry is ever rewritten.
struct StatusEvent {
Status status;
uint64 blockNumber;
uint64 timestamp;
bytes32 reason; // opaque reason code (e.g. keccak256("audit-lapsed")); never PII
}
/// @notice One append-only record per scope change.
struct ScopeEvent {
bytes32 credentialType;
bool granted; // true = granted, false = removed
uint64 timestamp;
}
// issuerId is the index into _issuers. Index 0 is a reserved placeholder so issuerIdByAddress's
// default 0 unambiguously means "no such issuer"; real issuers have issuerId >= 1.
Issuer[] private _issuers;
mapping(address => uint256) public issuerIdByAddress; // controller => issuerId (0 = none)
mapping(uint256 => mapping(bytes32 => bool)) private _inScope; // issuerId => credentialType => granted
mapping(uint256 => bytes32[]) private _scopeList; // issuerId => append-only granted-type list (may repeat across grant/revoke cycles)
mapping(uint256 => StatusEvent[]) private _statusHistory; // issuerId => append-only status log
mapping(uint256 => ScopeEvent[]) private _scopeHistory; // issuerId => append-only scope log
event IssuerAccredited(uint256 indexed issuerId, address indexed controller, string did, uint256 pqcKeyId);
event IssuerStatusChanged(uint256 indexed issuerId, Status indexed oldStatus, Status indexed newStatus, bytes32 reason);
event IssuerScopeChanged(uint256 indexed issuerId, bytes32 indexed credentialType, bool granted);
event IssuerPqcKeySet(uint256 indexed issuerId, uint256 oldKeyId, uint256 newKeyId);
error ZeroAddress();
error EmptyDid();
error AlreadyAccredited(address controller);
error UnknownIssuer(uint256 issuerId);
error NotActive(uint256 issuerId);
error NotSuspended(uint256 issuerId);
error AlreadyRevoked(uint256 issuerId);
error PqcKeyNotActiveFalcon(uint256 keyId);
error ScopeAlreadyGranted(uint256 issuerId, bytes32 credentialType);
error ScopeNotGranted(uint256 issuerId, bytes32 credentialType);
constructor(address keyRegistry_) {
if (keyRegistry_ == address(0)) revert ZeroAddress();
KEY_REGISTRY = IAerePQCKeyRegistryTrust(keyRegistry_);
// Reserve index 0 as an inert placeholder so issuerId 0 is never a real issuer.
_issuers.push(Issuer({controller: address(0), status: Status.None, pqcKeyId: NO_KEY, accreditedAt: 0, did: ""}));
}
// ==========================================================================
// Accreditation (owner)
// ==========================================================================
/**
* @notice Accredit a new issuer with an initial scope and an optional Falcon-512 key reference.
* Owner-only (the Foundation as trusted-list operator). The controller address is the
* issuer id and the party that controls the linked credential-issuing keys.
* @param controller the issuer's controlling address (also its issuer id / address).
* @param did the issuer's did:aere DID (public identifier; not validated for full DID
* syntax here, that is an off-chain concern, see [VERIFY] in the docs).
* @param scopes the credential types this issuer may issue (opaque bytes32 tags).
* @param pqcKeyId a Falcon-512 keyId in KEY_REGISTRY, or NO_KEY for no post-quantum key.
* @return issuerId the assigned issuer id (>= 1).
*/
function accreditIssuer(address controller, string calldata did, bytes32[] calldata scopes, uint256 pqcKeyId)
external
onlyOwner
returns (uint256 issuerId)
{
if (controller == address(0)) revert ZeroAddress();
if (bytes(did).length == 0) revert EmptyDid();
if (issuerIdByAddress[controller] != 0) revert AlreadyAccredited(controller);
if (pqcKeyId != NO_KEY) _requireActiveFalconKey(pqcKeyId);
issuerId = _issuers.length;
_issuers.push(
Issuer({
controller: controller,
status: Status.Active,
pqcKeyId: pqcKeyId,
accreditedAt: uint64(block.timestamp),
did: did
})
);
issuerIdByAddress[controller] = issuerId;
for (uint256 i = 0; i < scopes.length; i++) {
_grantScope(issuerId, scopes[i]);
}
_statusHistory[issuerId].push(
StatusEvent({status: Status.Active, blockNumber: uint64(block.number), timestamp: uint64(block.timestamp), reason: bytes32(0)})
);
emit IssuerAccredited(issuerId, controller, did, pqcKeyId);
}
/// @notice Grant an additional credential type to an issuer's accreditation scope (owner-only).
function grantScope(uint256 issuerId, bytes32 credentialType) external onlyOwner {
_requireIssuer(issuerId);
_grantScope(issuerId, credentialType);
}
/// @notice Remove a credential type from an issuer's scope (owner-only). Append-only history keeps
/// the record of the grant and the removal; the current scope simply no longer includes it.
function revokeScope(uint256 issuerId, bytes32 credentialType) external onlyOwner {
_requireIssuer(issuerId);
if (!_inScope[issuerId][credentialType]) revert ScopeNotGranted(issuerId, credentialType);
_inScope[issuerId][credentialType] = false;
_scopeHistory[issuerId].push(ScopeEvent({credentialType: credentialType, granted: false, timestamp: uint64(block.timestamp)}));
emit IssuerScopeChanged(issuerId, credentialType, false);
}
/// @notice Suspend an active issuer (reversible). All its credentials fail verification until
/// reinstated (fail-closed). Owner-only.
function suspendIssuer(uint256 issuerId, bytes32 reason) external onlyOwner {
_requireIssuer(issuerId);
if (_issuers[issuerId].status != Status.Active) revert NotActive(issuerId);
_transition(issuerId, Status.Suspended, reason);
}
/// @notice Reinstate a suspended issuer to Active (owner-only).
function reinstateIssuer(uint256 issuerId, bytes32 reason) external onlyOwner {
_requireIssuer(issuerId);
if (_issuers[issuerId].status != Status.Suspended) revert NotSuspended(issuerId);
_transition(issuerId, Status.Active, reason);
}
/// @notice Permanently revoke an issuer's accreditation (terminal, not reversible). Owner-only.
function revokeIssuer(uint256 issuerId, bytes32 reason) external onlyOwner {
_requireIssuer(issuerId);
if (_issuers[issuerId].status == Status.Revoked) revert AlreadyRevoked(issuerId);
_transition(issuerId, Status.Revoked, reason);
}
/// @notice Set or clear an issuer's Falcon-512 key reference (owner-only). Pass NO_KEY to clear.
function setIssuerPqcKey(uint256 issuerId, uint256 pqcKeyId) external onlyOwner {
_requireIssuer(issuerId);
if (pqcKeyId != NO_KEY) _requireActiveFalconKey(pqcKeyId);
uint256 old = _issuers[issuerId].pqcKeyId;
_issuers[issuerId].pqcKeyId = pqcKeyId;
emit IssuerPqcKeySet(issuerId, old, pqcKeyId);
}
// ==========================================================================
// Accreditation + PQC verification (views)
// ==========================================================================
/// @notice True iff `issuerId` is ACTIVE and `credentialType` is currently in its scope.
/// Fail-closed for unknown / suspended / revoked issuers and out-of-scope types.
function isAccredited(uint256 issuerId, bytes32 credentialType) external view returns (bool) {
if (issuerId == 0 || issuerId >= _issuers.length) return false;
if (_issuers[issuerId].status != Status.Active) return false;
return _inScope[issuerId][credentialType];
}
/**
* @notice Verify a POST-QUANTUM (Falcon-512) attestation by an issuer over a 32-byte message,
* via the live precompile 0x0AE1 through the AerePQCKeyRegistry. Fail-closed: returns
* false (never reverts) if the issuer is not Active, has no linked Falcon key, the key is
* not an ACTIVE Falcon-512 key, or the signature is invalid / tampered.
* @param issuerId the accredited issuer.
* @param message the 32-byte message the issuer's Falcon key signed.
* @param signature the Falcon-512 signature envelope (nonce(40) || esig).
*/
function verifyIssuerSignature(uint256 issuerId, bytes32 message, bytes calldata signature)
external
view
returns (bool)
{
if (issuerId == 0 || issuerId >= _issuers.length) return false;
Issuer storage iss = _issuers[issuerId];
if (iss.status != Status.Active) return false;
uint256 keyId = iss.pqcKeyId;
if (keyId == NO_KEY) return false;
if (KEY_REGISTRY.statusOf(keyId) != KEY_STATUS_ACTIVE) return false;
if (KEY_REGISTRY.schemeOf(keyId) != SCHEME_FALCON512) return false;
return KEY_REGISTRY.verifyWithKey(keyId, message, signature);
}
// ==========================================================================
// Reads
// ==========================================================================
/// @notice Number of issuer ids ever assigned (issuerId runs 1..issuerCount, id 0 is reserved).
function issuerCount() external view returns (uint256) {
return _issuers.length - 1;
}
/// @notice The current lifecycle status of an issuer (Status.None for an unknown id).
function issuerStatus(uint256 issuerId) external view returns (Status) {
if (issuerId == 0 || issuerId >= _issuers.length) return Status.None;
return _issuers[issuerId].status;
}
/// @notice Read an issuer entry. Reverts on an unknown id.
function getIssuer(uint256 issuerId)
external
view
returns (address controller, Status status, uint256 pqcKeyId, uint64 accreditedAt, string memory did)
{
_requireIssuer(issuerId);
Issuer storage iss = _issuers[issuerId];
return (iss.controller, iss.status, iss.pqcKeyId, iss.accreditedAt, iss.did);
}
/// @notice The Falcon-512 keyId linked to an issuer (NO_KEY if none / unknown issuer).
function pqcKeyOf(uint256 issuerId) external view returns (uint256) {
if (issuerId == 0 || issuerId >= _issuers.length) return NO_KEY;
return _issuers[issuerId].pqcKeyId;
}
/// @notice The append-only status-history length for an issuer.
function statusHistoryLength(uint256 issuerId) external view returns (uint256) {
return _statusHistory[issuerId].length;
}
/// @notice Read one append-only status-history entry.
function statusHistoryAt(uint256 issuerId, uint256 index) external view returns (StatusEvent memory) {
return _statusHistory[issuerId][index];
}
/// @notice The append-only scope-history length for an issuer.
function scopeHistoryLength(uint256 issuerId) external view returns (uint256) {
return _scopeHistory[issuerId].length;
}
/// @notice Read one append-only scope-history entry.
function scopeHistoryAt(uint256 issuerId, uint256 index) external view returns (ScopeEvent memory) {
return _scopeHistory[issuerId][index];
}
// ==========================================================================
// Internal
// ==========================================================================
function _requireIssuer(uint256 issuerId) internal view {
if (issuerId == 0 || issuerId >= _issuers.length) revert UnknownIssuer(issuerId);
}
function _requireActiveFalconKey(uint256 keyId) internal view {
if (KEY_REGISTRY.statusOf(keyId) != KEY_STATUS_ACTIVE || KEY_REGISTRY.schemeOf(keyId) != SCHEME_FALCON512) {
revert PqcKeyNotActiveFalcon(keyId);
}
}
function _grantScope(uint256 issuerId, bytes32 credentialType) internal {
if (_inScope[issuerId][credentialType]) revert ScopeAlreadyGranted(issuerId, credentialType);
_inScope[issuerId][credentialType] = true;
_scopeList[issuerId].push(credentialType);
_scopeHistory[issuerId].push(ScopeEvent({credentialType: credentialType, granted: true, timestamp: uint64(block.timestamp)}));
emit IssuerScopeChanged(issuerId, credentialType, true);
}
function _transition(uint256 issuerId, Status newStatus, bytes32 reason) internal {
Status old = _issuers[issuerId].status;
_issuers[issuerId].status = newStatus;
_statusHistory[issuerId].push(
StatusEvent({status: newStatus, blockNumber: uint64(block.number), timestamp: uint64(block.timestamp), reason: reason})
);
emit IssuerStatusChanged(issuerId, old, newStatus, reason);
}
}