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.
326 lines
16 KiB
Solidity
326 lines
16 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
/// @dev The AereTrustRegistry surface this contract relies on: accreditation lookups and the
|
|
/// POST-QUANTUM issuer signature verification (which routes to the live precompile 0x0AE1).
|
|
interface IAereTrustRegistryVC {
|
|
function isAccredited(uint256 issuerId, bytes32 credentialType) external view returns (bool);
|
|
function issuerStatus(uint256 issuerId) external view returns (uint8); // AereTrustRegistry.Status
|
|
function verifyIssuerSignature(uint256 issuerId, bytes32 message, bytes calldata signature)
|
|
external
|
|
view
|
|
returns (bool);
|
|
}
|
|
|
|
/// @dev The AereBitstringStatusList surface used for the not-revoked check.
|
|
interface IAereStatusListVC {
|
|
function statusOf(uint256 listId, uint256 index) external view returns (bool);
|
|
}
|
|
|
|
/**
|
|
* @title AereVerifiableCredential, a W3C Verifiable Credentials 2.0 style on-chain anchor + verifier
|
|
*
|
|
* @notice Anchors and verifies W3C Verifiable Credentials (VC 2.0 data model) on Aere Network with a
|
|
* POST-QUANTUM issuer signature path. A credential here binds:
|
|
* - an ISSUER, an accredited issuer id in the AereTrustRegistry;
|
|
* - a SUBJECT, identified by a did:aere DID (bound by its hash; the DID string itself is a
|
|
* public identifier carried off-chain, no PII on chain);
|
|
* - a CREDENTIAL TYPE (an opaque bytes32 tag, e.g. keccak256("KYCCredential"));
|
|
* - a VALIDITY window (validFrom / validUntil, W3C validFrom / validUntil);
|
|
* - a STATUS-LIST ENTRY (a W3C BitstringStatusListEntry: a list id + a status index);
|
|
* - a CLAIMS HASH, the keccak256 of the off-chain VC JSON claims (the credentialSubject),
|
|
* so the on-chain anchor commits to the credential content without publishing it.
|
|
*
|
|
* VERIFICATION (verifyPresented / isValid) checks, in order and fail-closed:
|
|
* 1. the issuer is ACCREDITED for this credential type in the AereTrustRegistry;
|
|
* 2. the issuer's POST-QUANTUM (Falcon-512) signature over the credential digest is valid,
|
|
* verified in full on-chain via the live precompile 0x0AE1 (through the Trust Registry);
|
|
* 3. the credential is WITHIN its validity window (not-yet-valid and expired both fail);
|
|
* 4. the credential is NOT REVOKED per its Bitstring Status List entry.
|
|
*
|
|
* DID METHOD. Subjects (and issuers) use the `did:aere` method, consistent with Aere's
|
|
* AereIdentityRegistry8004 (did:aere:<chainId>:<id>). Full DID Core syntax validation is an
|
|
* off-chain concern; anchorCredentialWithDid enforces the `did:aere:` prefix as a minimal
|
|
* on-chain guard. [VERIFY] against the finalized did:aere method registration.
|
|
*
|
|
* ANCHORING. anchorCredential verifies the FULL path (accreditation + PQC signature + validity)
|
|
* fail-closed and records the credential once. Authorship is the POST-QUANTUM signature alone,
|
|
* so anchoring may be relayed by any msg.sender (only the issuer's Falcon key can produce a
|
|
* valid signature over the contract-bound digest). A recorded anchor is then re-checked LIVE
|
|
* by isValid, which fails closed the instant the issuer is suspended / revoked or the
|
|
* credential is revoked on its status list, without needing the signature again.
|
|
*
|
|
* @dev HONEST SCOPE. Application-layer compliance tooling. Holds no funds; changes nothing about
|
|
* Aere consensus (classical secp256k1 ECDSA QBFT). What is post-quantum is the AUTHENTICITY of
|
|
* the issuer's attestation, verified by the live precompile. Whether any real institution
|
|
* issues credentials here is a partnership matter external to this code and is NOT asserted.
|
|
*
|
|
* @dev [VERIFY] against the finalized W3C VC 2.0 serialization: this contract anchors a COMMITMENT
|
|
* to a VC (its digest + claims hash), not the JSON-LD document itself. The mapping of these
|
|
* fields to the VC JSON members (context, type, issuer, validFrom, credentialStatus) is
|
|
* defined in docs/AERE-PQ-SCREEN.md; reconcile exact JSON member names before claiming wire
|
|
* conformance.
|
|
*/
|
|
contract AereVerifiableCredential {
|
|
/// @notice AereTrustRegistry.Status.Active (an issuer must be Active to issue / stay valid).
|
|
uint8 internal constant ISSUER_STATUS_ACTIVE = 1;
|
|
|
|
/// @notice Domain separator binding a credential digest to this contract and version.
|
|
bytes32 public constant VC_DOMAIN = keccak256("AereVerifiableCredential.v2.credential");
|
|
|
|
/// @notice The DID method these credentials use for subjects and issuers.
|
|
string public constant DID_METHOD = "did:aere";
|
|
|
|
/// @notice The federated accredited-issuer trust registry.
|
|
IAereTrustRegistryVC public immutable TRUST_REGISTRY;
|
|
|
|
/// @notice The W3C Bitstring Status List used for revocation / suspension.
|
|
IAereStatusListVC public immutable STATUS_LIST;
|
|
|
|
/// @notice A W3C VC 2.0 credential binding. `subjectDidHash` is keccak256(bytes(subjectDid)); the
|
|
/// DID string is carried off-chain to keep the hot path free of variable-length data and
|
|
/// to keep identifiers off the chain. `claimsHash` is keccak256 of the off-chain VC claims.
|
|
struct Credential {
|
|
uint256 issuerId; // accredited issuer id in the AereTrustRegistry
|
|
bytes32 subjectDidHash; // keccak256(bytes(subjectDid)), subjectDid is a did:aere DID
|
|
bytes32 credentialType; // opaque type tag, e.g. keccak256("KYCCredential")
|
|
uint64 validFrom; // W3C validFrom (unix seconds, inclusive)
|
|
uint64 validUntil; // W3C validUntil (unix seconds, inclusive); 0 = no expiry
|
|
uint256 statusListId; // W3C BitstringStatusListEntry: the list
|
|
uint256 statusIndex; // W3C BitstringStatusListEntry: the index within the list
|
|
bytes32 claimsHash; // keccak256 of the off-chain credentialSubject claims (no PII on chain)
|
|
}
|
|
|
|
struct Anchor {
|
|
bool exists;
|
|
uint256 issuerId;
|
|
bytes32 credentialType;
|
|
bytes32 subjectDidHash;
|
|
uint64 validFrom;
|
|
uint64 validUntil;
|
|
uint256 statusListId;
|
|
uint256 statusIndex;
|
|
uint64 anchoredAt;
|
|
}
|
|
|
|
mapping(bytes32 => Anchor) private _anchors; // credentialId => anchor
|
|
bytes32[] private _anchorIds; // append-only list of anchored credential ids
|
|
|
|
event CredentialAnchored(
|
|
bytes32 indexed credentialId,
|
|
uint256 indexed issuerId,
|
|
bytes32 indexed credentialType,
|
|
bytes32 subjectDidHash,
|
|
uint256 statusListId,
|
|
uint256 statusIndex
|
|
);
|
|
event CredentialAnchoredWithDid(bytes32 indexed credentialId, string subjectDid);
|
|
|
|
error ZeroAddress();
|
|
error NotAccredited(uint256 issuerId, bytes32 credentialType);
|
|
error SignatureInvalid();
|
|
error NotYetValid(uint64 validFrom);
|
|
error Expired(uint64 validUntil);
|
|
error AlreadyAnchored(bytes32 credentialId);
|
|
error UnknownCredential(bytes32 credentialId);
|
|
error DidMismatch();
|
|
error NotAereDid();
|
|
|
|
constructor(address trustRegistry_, address statusList_) {
|
|
if (trustRegistry_ == address(0) || statusList_ == address(0)) revert ZeroAddress();
|
|
TRUST_REGISTRY = IAereTrustRegistryVC(trustRegistry_);
|
|
STATUS_LIST = IAereStatusListVC(statusList_);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Digest (what the issuer signs)
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice The canonical 32-byte digest an issuer's Falcon-512 key signs to attest a credential.
|
|
* Binds chainId + this contract + every credential field, so a signature cannot be
|
|
* replayed against another contract, chain, subject, type, validity, or status entry.
|
|
* This value is also the credentialId used to anchor and look the credential up.
|
|
*/
|
|
function credentialDigest(Credential calldata c) public view returns (bytes32) {
|
|
return keccak256(
|
|
abi.encode(
|
|
VC_DOMAIN,
|
|
block.chainid,
|
|
address(this),
|
|
c.issuerId,
|
|
c.subjectDidHash,
|
|
c.credentialType,
|
|
c.validFrom,
|
|
c.validUntil,
|
|
c.statusListId,
|
|
c.statusIndex,
|
|
c.claimsHash
|
|
)
|
|
);
|
|
}
|
|
|
|
/// @notice Alias of {credentialDigest}: the id a credential is anchored and referenced under.
|
|
function credentialId(Credential calldata c) external view returns (bytes32) {
|
|
return credentialDigest(c);
|
|
}
|
|
|
|
/// @notice Helper: the subjectDidHash for a DID string (keccak256 of its UTF-8 bytes).
|
|
function hashDid(string calldata did) external pure returns (bytes32) {
|
|
return keccak256(bytes(did));
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Verification (views)
|
|
// ==========================================================================
|
|
|
|
/// @notice True iff the issuer is accredited for the type AND its Falcon-512 signature over the
|
|
/// credential digest verifies on-chain via 0x0AE1. Does NOT check validity or revocation.
|
|
/// Fail-closed (returns false, never reverts).
|
|
function verifyCredentialSignature(Credential calldata c, bytes calldata issuerSig) public view returns (bool) {
|
|
if (!TRUST_REGISTRY.isAccredited(c.issuerId, c.credentialType)) return false;
|
|
return TRUST_REGISTRY.verifyIssuerSignature(c.issuerId, credentialDigest(c), issuerSig);
|
|
}
|
|
|
|
/// @notice True iff `c` is within its validity window at the current block time.
|
|
function isWithinValidity(Credential calldata c) public view returns (bool) {
|
|
if (block.timestamp < c.validFrom) return false;
|
|
if (c.validUntil != 0 && block.timestamp > c.validUntil) return false;
|
|
return true;
|
|
}
|
|
|
|
/// @notice True iff `c` is marked revoked/suspended on its Bitstring Status List entry.
|
|
function isRevoked(Credential calldata c) public view returns (bool) {
|
|
return STATUS_LIST.statusOf(c.statusListId, c.statusIndex);
|
|
}
|
|
|
|
/**
|
|
* @notice Full W3C verification of a PRESENTED credential in one call, WITHOUT requiring it to be
|
|
* anchored: accreditation + post-quantum signature + within-validity + not-revoked. Returns
|
|
* true only if all four hold. Fail-closed (never reverts), so any verifier can call it via
|
|
* eth_call to check a credential a holder presents off-chain.
|
|
*/
|
|
function verifyPresented(Credential calldata c, bytes calldata issuerSig) external view returns (bool) {
|
|
if (!verifyCredentialSignature(c, issuerSig)) return false;
|
|
if (!isWithinValidity(c)) return false;
|
|
if (isRevoked(c)) return false;
|
|
return true;
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Anchor
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice Anchor a credential on-chain after verifying the FULL path fail-closed: the issuer must
|
|
* be accredited for the type, the credential must be within its validity window, and the
|
|
* issuer's Falcon-512 signature over the credential digest must verify via 0x0AE1. Records
|
|
* the credential once and emits CredentialAnchored. Reverts (records nothing) on any
|
|
* failure. Relayable by anyone: authorship is the post-quantum signature.
|
|
* @return id the credentialId (== credentialDigest) the credential was anchored under.
|
|
*/
|
|
function anchorCredential(Credential calldata c, bytes calldata issuerSig) public returns (bytes32 id) {
|
|
id = credentialDigest(c);
|
|
if (_anchors[id].exists) revert AlreadyAnchored(id);
|
|
|
|
if (!TRUST_REGISTRY.isAccredited(c.issuerId, c.credentialType)) {
|
|
revert NotAccredited(c.issuerId, c.credentialType);
|
|
}
|
|
if (block.timestamp < c.validFrom) revert NotYetValid(c.validFrom);
|
|
if (c.validUntil != 0 && block.timestamp > c.validUntil) revert Expired(c.validUntil);
|
|
|
|
// Full on-chain post-quantum verification of the issuer's attestation (fail-closed).
|
|
if (!TRUST_REGISTRY.verifyIssuerSignature(c.issuerId, id, issuerSig)) revert SignatureInvalid();
|
|
|
|
_anchors[id] = Anchor({
|
|
exists: true,
|
|
issuerId: c.issuerId,
|
|
credentialType: c.credentialType,
|
|
subjectDidHash: c.subjectDidHash,
|
|
validFrom: c.validFrom,
|
|
validUntil: c.validUntil,
|
|
statusListId: c.statusListId,
|
|
statusIndex: c.statusIndex,
|
|
anchoredAt: uint64(block.timestamp)
|
|
});
|
|
_anchorIds.push(id);
|
|
|
|
emit CredentialAnchored(id, c.issuerId, c.credentialType, c.subjectDidHash, c.statusListId, c.statusIndex);
|
|
}
|
|
|
|
/**
|
|
* @notice As {anchorCredential}, but additionally binds and emits the subject's did:aere DID
|
|
* string (enforcing keccak256(subjectDid) == c.subjectDidHash and the `did:aere:` prefix),
|
|
* so resolvers can recover the subject DID from events without any PII on chain.
|
|
*/
|
|
function anchorCredentialWithDid(Credential calldata c, string calldata subjectDid, bytes calldata issuerSig)
|
|
external
|
|
returns (bytes32 id)
|
|
{
|
|
if (keccak256(bytes(subjectDid)) != c.subjectDidHash) revert DidMismatch();
|
|
if (!_isAereDid(subjectDid)) revert NotAereDid();
|
|
id = anchorCredential(c, issuerSig);
|
|
emit CredentialAnchoredWithDid(id, subjectDid);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Anchored-credential reads
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice Live validity of an ANCHORED credential: the issuer must STILL be accredited for the
|
|
* type, the credential must be within its validity window, and it must not be revoked on
|
|
* its status list. Fails closed the instant the issuer is suspended / revoked (which flips
|
|
* isAccredited to false) or the credential's status bit is set. The post-quantum signature
|
|
* was verified at anchor time and cannot silently become valid again after revocation,
|
|
* because issuer suspension / key revocation both fail this check.
|
|
*/
|
|
function isValid(bytes32 id) external view returns (bool) {
|
|
Anchor storage a = _anchors[id];
|
|
if (!a.exists) return false;
|
|
if (!TRUST_REGISTRY.isAccredited(a.issuerId, a.credentialType)) return false;
|
|
if (block.timestamp < a.validFrom) return false;
|
|
if (a.validUntil != 0 && block.timestamp > a.validUntil) return false;
|
|
if (STATUS_LIST.statusOf(a.statusListId, a.statusIndex)) return false;
|
|
return true;
|
|
}
|
|
|
|
/// @notice True iff a credential id has been anchored (regardless of current validity).
|
|
function isAnchored(bytes32 id) external view returns (bool) {
|
|
return _anchors[id].exists;
|
|
}
|
|
|
|
/// @notice Read an anchored credential record. Reverts on an unknown id.
|
|
function getAnchor(bytes32 id) external view returns (Anchor memory) {
|
|
Anchor storage a = _anchors[id];
|
|
if (!a.exists) revert UnknownCredential(id);
|
|
return a;
|
|
}
|
|
|
|
/// @notice Total number of anchored credentials.
|
|
function anchorCount() external view returns (uint256) {
|
|
return _anchorIds.length;
|
|
}
|
|
|
|
/// @notice The credential id at an index in the append-only anchor list.
|
|
function anchorIdAt(uint256 index) external view returns (bytes32) {
|
|
return _anchorIds[index];
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Internal
|
|
// ==========================================================================
|
|
|
|
/// @dev Minimal on-chain guard that a DID string begins with "did:aere:". Full DID Core syntax
|
|
/// validation is an off-chain concern. [VERIFY] against the finalized did:aere method.
|
|
function _isAereDid(string calldata did) internal pure returns (bool) {
|
|
bytes calldata b = bytes(did);
|
|
bytes memory prefix = bytes("did:aere:");
|
|
if (b.length < prefix.length) return false;
|
|
for (uint256 i = 0; i < prefix.length; i++) {
|
|
if (b[i] != prefix[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|