Sui.

帖子

分享您的知识。

DollyStaff.
Sep 27, 2025
专家问答

Cross-Domain Identity Portability

How can I design a cross-domain identity system in Sui that remains portable across other Move-based blockchains without breaking capability security?

  • Sui
  • Architecture
  • Transaction Processing
  • Security Protocols
  • NFT Ecosystem
0
2
分享
评论
.

答案

2
draSUIla.
Sep 28 2025, 23:27

I build cross-domain identity systems for Move ecosystems in production, and the right design balances interoperability, auditable authority, and capability safety. Below I give a concrete architecture (why each piece exists), Move patterns you can drop into Sui and other Move chains, cross-chain portability techniques, capability binding (so capabilities can’t be escalated across chains), revocation & selective disclosure approaches, and a testing/audit checklist. I write in first person and include runnable-looking Move sketches + recommended client flows.


Big picture (the contract I enforce)

  1. Canonical DID documents (content-addressed): store a small on-chain anchor (hash/CID) instead of large JSON. This makes DID documents portable across chains.
  2. Attestations signed by issuers (off-chain signed, on-chain anchored): attestation = issuer signature over (subject DID, claim, epoch, chain-domain). Any Move chain can verify it.
  3. Capabilities bound to DID and proof material: capabilities are has key objects, and include cryptographic binding to a DID (signature or bound controller address) so they cannot be transferred/used by others.
  4. Revocation via Accumulators / Merkle revocation lists: support compact revocation checks on any chain.
  5. Cross-chain portability via canonical encodings + relayer/anchor pattern: publish attestations roots (or aggregated signatures) on the target chain, or use a light-client/zk bridge where available.
  6. Selective disclosure / privacy options: support signed commitments, Merkle-ledgers of attributes, and optional zero-knowledge proofs for privacy-sensitive claims.

Threat model I design against

  • Forged attestations — prevented by signature verification of issuer keys.
  • Capability theft / transfer — prevented by binding caps cryptographically to DID/private key/controller.
  • Replay across chains / epoch confusion — prevented by domain separation (chain_id, epoch) in all signed messages.
  • Revocation latency — minimized by using on-chain revocation roots and reasonably short finality windows for cross-chain anchors.
  • Cross-chain equivocation — mitigated with aggregated signatures + dispute windows for published roots.

Canonical data model (guiding rules)

  • Use content addressing: large DID docs live off-chain (IPFS/Arweave), store cid (hash) on-chain.
  • Use canonical serialization (CBOR / deterministic binary) for messages that will be signed/verified across chains.
  • Always include a domain separator in signatures: e.g., SUI_DID_V1|chain_id|package_id|action|payload_hash|epoch. That prevents cross-chain replay and binds an attestation to a chain/contract context.

Move architecture (three modules)

I sketch three modules:

  1. did::registry — register DID anchors (CID), controller keys, verification methods, and rotation.
  2. did::attest — accept publisher of signed attestations (store minimal anchor) and revocation roots.
  3. did::caps — capability objects bound to a DID (and optionally bound to a specific chain/tx) with runtime check_cap that verifies cryptographic binding.

I include the most important entrypoints and checks; adapt native crypto verification (ed25519/bls) to your runtime.


1) did::registry (anchor DID docs + controllers)

address 0xDIDPKG {
module did::registry {
    use sui::object::{UID};
    use sui::tx_context::{self, TxContext};
    use sui::event;
    use std::vector;

    // Minimal DID anchor object (content-addressed)
    struct DidAnchor has key {
        id: UID,
        did: vector<u8>,      // canonical DID string "did:move:..." as bytes
        cid: vector<u8>,      // content hash of DID document (e.g., SHA-256 or IPFS CID bytes)
        controller: address,  // primary controller address on this chain (optional helper)
        created_at: u64,
        seq: u64,             // increment on rotation
    }

    struct DidRotatedEvent has store { did: vector<u8>, cid: vector<u8>, seq: u64, by: address, ts: u64 }

    /// Anchor a DID doc (store hash only). The controller is the on-chain actor anchoring this doc.
    public(entry) fun anchor_did(did: vector<u8>, cid: vector<u8>, controller: address, ctx: &mut TxContext): DidAnchor {
        let id = object::new(ctx);
        let anchor = DidAnchor {
            id,
            did,
            cid,
            controller,
            created_at: tx_context::epoch(ctx),
            seq: 1u64,
        };
        event::emit(DidRotatedEvent { did: anchor.did, cid: anchor.cid, seq: anchor.seq, by: tx_context::sender(ctx), ts: anchor.created_at });
        anchor
    }

    /// Rotate/change DID document: new CID and optionally new controller
    public(entry) fun rotate_did(anchor: &mut DidAnchor, new_cid: vector<u8>, new_controller: option::Option<address>) {
        // access control: only current controller can rotate in this simple model
        assert!(tx_context::sender(ctx) == anchor.controller, 1);
        anchor.cid = new_cid;
        if (option::is_some(&new_controller)) {
            anchor.controller = option::borrow(&new_controller).copy();
        }
        anchor.seq = anchor.seq + 1;
        event::emit(DidRotatedEvent { did: anchor.did, cid: anchor.cid, seq: anchor.seq, by: tx_context::sender(ctx), ts: tx_context::epoch(ctx) });
    }
}
}

Notes:

  • I store cid (hash) rather than the whole DID doc so clients can retrieve the doc off-chain but verify the anchor on-chain.
  • For multi-chain portability, did strings are globally unique (e.g., did:move:<namespace>:<unique>). Use a registry namespace consistent across Move chains.

2) did::attest (signed claims / attestations + revocation roots)

address 0xDIDPKG {
module did::attest {
    use sui::object::{UID};
    use sui::tx_context::{self, TxContext};
    use sui::event;
    use std::vector;

    // Attestation anchor; we store the claim hash and issuer & signature metadata.
    struct Attestation has key {
        id: UID,
        subject_did: vector<u8>, // DID bytes
        claim_hash: vector<u8>,  // hash of canonical claim payload
        issuer: vector<u8>,      // issuer public key or key id (enc)
        sig: vector<u8>,         // signature bytes (ed25519/BLS) over domain-separated payload
        chain_domain: vector<u8>,// domain: chain_id|package_id to prevent replay
        epoch: u64,
        revoked: bool,
    }

    struct RevocationRoot has key {
        id: UID,
        epoch: u64,
        root: vector<u8>, // Merkle root of revoked attestation IDs or claim hashes
    }

    struct AttestationCreated has store { subject: vector<u8>, claim_hash: vector<u8>, issuer: vector<u8>, epoch: u64 }
    struct AttestationRevoked has store { id: UID, epoch: u64 }

    // NOTE: native verify function should be provided in runtime for ed25519/bls
    native public fun verify_sig(pubkey: vector<u8>, sig: vector<u8>, message: vector<u8>): bool;

    // Anchor an attestation: off-chain issued claim + signature included here for on-chain verification
    public(entry) fun publish_attestation(subject_did: vector<u8>, claim_hash: vector<u8>, issuer_pubkey: vector<u8>, sig: vector<u8>, chain_domain: vector<u8>, ctx: &mut TxContext): Attestation {
        // verify signature off-chain preferred; we still do an on-chain check if native available
        let msg = canonical_msg(subject_did, claim_hash, chain_domain, tx_context::epoch(ctx));
        assert!(verify_sig(issuer_pubkey, sig, msg), 100);

        let id = object::new(ctx);
        let a = Attestation { id, subject_did, claim_hash, issuer: issuer_pubkey, sig, chain_domain, epoch: tx_context::epoch(ctx), revoked: false };
        event::emit(AttestationCreated { subject: a.subject_did, claim_hash: a.claim_hash, issuer: a.issuer, epoch: a.epoch });
        a
    }

    // Revoke attestation by setting revoked flag or by publishing a revocation root that includes its id/hash
    public(entry) fun revoke_attestation(a: &mut Attestation) {
        // Only issuer or authorized revoker - enforced off-chain or via issuer keycheck if bound
        a.revoked = true;
        event::emit(AttestationRevoked { id: a.id, epoch: tx_context::epoch(ctx) });
    }

    // Helper: canonical msg construction (deterministic bytes for signature verification)
    public(fun) fun canonical_msg(subject: vector<u8>, claim_hash: vector<u8>, chain_domain: vector<u8>, epoch: u64): vector<u8> {
        // simple concat — replace with CBOR or fixed-width encoding in prod
        // return subject || "|" || claim_hash || "|" || chain_domain || "|" || epoch ascii
        // Pseudocode:
        vector::concat(subject, vector::concat(claim_hash, vector::concat(chain_domain, u64_to_vec(epoch))))
    }
}
}

Notes:

  • I verify signatures on publish using a native verifier (verify_sig). If the runtime lacks that native, one can do on-chain verification via precompiled modules or accept unverified anchors with off-chain verification enforced by clients (less secure).
  • chain_domain is critical: it binds the attestation to a chain/package/version so the attestation cannot be trivially replayed to another chain without explicit anchoring.

3) did::caps (capability objects bound to DID + proof binding)

address 0xDIDPKG {
module did::caps {
    use sui::object::{UID};
    use sui::tx_context::{self, TxContext};
    use sui::event;
    use std::vector;
    use did::registry;
    use did::attest;

    struct Cap has key {
        id: UID,
        namespace: vector<u8>,
        action: vector<u8>,
        bound_did: vector<u8>,     // DID this capability is bound to
        bound_proof: vector<u8>,   // signature proving consent (signed by DID controller key)
        issued_by: address,
        expires_at: u64,
        delegation_bounds: u8,
    }

    struct CapUsed has store { cap_id: UID, by: vector<u8>, action: vector<u8>, epoch: u64 }

    // native verify signature
    native public fun verify_sig(pubkey: vector<u8>, sig: vector<u8>, message: vector<u8>): bool;

    /// Mint Cap anchored to a DID: caller must supply a `bound_proof` which is a signature
    /// by the DID controller over the cap_id (or canonical cap payload). This prevents transfer-use.
    public(entry) fun mint_bound_cap(
        namespace: vector<u8>,
        action: vector<u8>,
        bound_did: vector<u8>,
        bound_proof: vector<u8>,
        controller_pubkey: vector<u8>,
        expires_at: u64,
        delegation_bounds: u8,
        ctx: &mut TxContext
    ): Cap {
        // Verify proof: controller signed domain separated message "CAP_BIND|<namespace>|<action>|<cap_nonce>|<chain_domain>"
        let msg = make_cap_bind_msg(namespace, action, bound_did, tx_context::epoch(ctx));
        assert!(verify_sig(controller_pubkey, bound_proof, msg), 100);

        let id = object::new(ctx);
        Cap { id, namespace, action, bound_did, bound_proof, issued_by: tx_context::sender(ctx), expires_at, delegation_bounds }
    }

    // check_cap must be called by protected modules before executing action.
    public(fun) check_cap(cap: &Cap, expected_ns: &vector<u8>, expected_action: &vector<u8>, ctx: &TxContext) {
        assert!(*expected_ns == cap.namespace, 200);
        assert!(*expected_action == cap.action, 201);
        let now = tx_context::epoch(ctx);
        if (cap.expires_at != 0) { assert!(now <= cap.expires_at, 202); }

        // Ensure DID controller still anchors to same controller or attestation still valid:
        // 1) load DID anchor and ensure sequence hasn't rotated beyond what was signed (out-of-band proof)
        // 2) OR require a non-expired attestation proving binding for simplicity (omitted for brevity)
    }

    public(entry) fun use_cap(cap: Cap) {
        // protected action uses the cap; we check binding properties in check_cap
        event::emit(CapUsed { cap_id: cap.id, by: cap.bound_did, action: cap.action, epoch: tx_context::epoch(ctx) });
        // cap consumed or retained based on design
    }

    public(fun) make_cap_bind_msg(ns: vector<u8>, act: vector<u8>, did: vector[u8>, epoch: u64): vector<u8> {
        // canonical message used for DID controller signature
        // return "CAP_BIND|"||ns||"|"||act||"|"||did||"|"||epoch
    }
}
}

Key idea: I require the DID controller (the key listed in the DID doc) to sign a small binding message proving that this DID consents to hold/use this capability. That signature is stored as bound_proof. On check_cap, the module verifies that binding has been honored (optionally re-verifying signature or checking controller rotation sequence).


Cross-chain portability patterns I use

  1. Content addressing: off-chain DID docs and attestations referenced by their hash/CID allow any chain to fetch/verify the full doc.
  2. Domain separation in signed messages: every attestation or cap bind message includes the chain_id and package_id so signatures do not trivially replay across chains. If a claim should be portable, the issuer signs a portable variant (no chain_id) explicitly — but then you must accept greater risk.
  3. Anchor roots on target chains: when you need an attestation to be trusted on chain B, publish an aggregated commitment (Merkle root or aggregated signature) of attestations on chain B. A light relayer or bridge posts that root in did::attest on chain B. Consumers verify inclusion using standard Merkle proof.
  4. Use threshold/aggregated signatures for fast cross-chain settlement: aggregator signs a root with an aggregated signature which is cheaper to verify on the target chain.
  5. Light-client or zk proofs (advanced): verify a snapshot of chain A on chain B using a succinct proof rather than repeated relayer publishing. Implement if you need strong finality guarantees.

Selective disclosure & privacy

  • I avoid storing PII on-chain. Claims are hashed; the full claim lives off-chain.
  • For selective disclosure, I publish a commitment (Merkle root) of attributes; the subject can present a Merkle proof for a single attribute to a verifier without revealing other attributes.
  • For higher privacy, use ZK proofs: attestor issues a ZK credential that proves an attribute (e.g., over-21) without revealing the value; store just the proof anchor on chain.

Client flows I design (practical sequences)

A — Create DID & anchor doc (portable)

  1. Off-chain: create canonical DID doc (keys, verification methods), store on IPFS → get cid.
  2. On-chain (any Move chain): call did::registry::anchor_did(did, cid, controller_address) → store anchor.
  3. Repeat on other chains by calling anchor with same did string; or rely on relayer to replicate anchors.

B — Issue attestation (issuer)

  1. Issuer prepares canonical claim payload, computes claim_hash, includes chain_domain if binding to a chain.
  2. Issuer signs the canonical message with their key.
  3. Issuer (or subject) publishes attestation anchor on chain via did::attest::publish_attestation(...). Or issuer keeps signed attestation off-chain and lets subject present it to verifiers; publishing is optional but enables on-chain discovery & revocation.

C — Mint bound capability (subject consents)

  1. Subject (DID controller) signs a CAP_BIND message with their controller key.
  2. Some authority calls did::caps::mint_bound_cap(..., bound_proof = signature ...) to mint a Cap bound to DID. Because signature includes domain separation, the cap cannot be used on other chains unless the signature allowed portability.

D — Verify capability in module

  1. Protected module calls did::caps::check_cap(cap, expected_ns, expected_action) and also verifies binding (e.g., re-verifying signature / checking DID rotation seq is compatible).
  2. If valid, execute the privileged action.

E — Cross-chain validation

  1. Relayer publishes a Merkle root of attestations from chain A to chain B (or posts aggregated signature).
  2. Verifier on chain B calls did::attest::verify_inclusion(root_obj, leaf_proof) or uses native aggregated sig verify.

Revocation & rotation patterns I use

  • Short-lived attestations: encourage issuers to use epochs and re-issue periodically so stale claims auto-expire.
  • Revocation roots: issuers publish Merkle roots of revoked claim hashes; verifiers check inclusion against the latest revocation root anchored on their chain.
  • DID rotation: the DID anchor includes sequence number seq; bound proofs MUST include the seq they were signed for, and on verification you check that the DID’s current seq is still compatible (i.e., subject didn’t rotate away from the signing key). If rotation occurs, prior bound_proof can be considered invalid unless re-signed.

Why this design keeps capability security intact

  • Cryptographic binding of caps to DID controller signature makes theft or transfer worthless (the verifier checks that the holder matches the DID controller).
  • Domain separation in signatures prevents cross-chain replay and forces explicit cross-chain anchoring when portability is intended.
  • Revocation roots / epoching give timely ways to invalidate compromised keys or attestations.
  • On-chain anchors + off-chain docs give verifiers everything they need to check provenance without storing PII on chain.

Test & audit checklist (what I run)

  • Signature verification tests for every key type (ed25519, secp256k1, BLS if used).
  • Replay tests: attempt to reuse attestation/cap bind across different chain_domain values.
  • Rotation tests: rotate DID controller and confirm old bound_proofs reject.
  • Revocation tests: include attestation in revocation root and verify rejection on consumer check.
  • Cross-chain anchoring tests: relayer posts root to target chain; consumer verifies Merkle proof inclusion.
  • Edge cases: missing DID anchor, missing controller key, expired epoch, malformed canonical serialization.

Practical recommendations I follow in production

  • Use deterministic CBOR or fixed binary encoding for anything that will be signed and verified cross-chain. Document it and pin versions.
  • Keep DID doc sizes off-chain; store small cid anchors on every chain you want to support.
  • Log all anchor/attestation events so indexers can build cross-chain mapping and relayers can find anchors quickly.
  • Make cross-chain publishes and revocations auditable and give a short but nonzero dispute window to allow fraud proofs.
  • Provide a reference client library that builds canonical messages and proofs so different chains/clients produce the same bytes to be signed.

Final checklist — what I would implement first

  1. did::registry anchor + rotation + events (Sui).
  2. did::attest publish & verify with native signature verification.
  3. did::caps that require bound_proof signed by DID controller.
  4. Off-chain relayer service to anchor attestations/roots to other Move chains (with aggregated sig option).
  5. Client libs (TS/Rust) that canonicalize messages and build/verify Merkle proofs.
0
评论
.
lite.vue.
Sep 28 2025, 23:44

This time I’ll speak as a security-focused systems architect who prioritizes minimizing trust assumptions, making attestation verification auditable, and ensuring capabilities can’t be replayed or escalated across chains. I’ll give an alternative architecture, a clear set of rules I enforce, concrete Move-style patterns (runnable-looking), and practical migration / interoperability strategies.


Short thesis (my high-level take)

I treat identity as portable attestations + minimal on-chain anchors, and I treat capabilities as cryptographically-authorized actions rather than transferable tokens. The main differences in my approach are:

  • I avoid on-chain mutable authority where possible; authority is validated via signatures + short-lived on-chain anchors. This reduces long-term attack surface and simplifies cross-chain portability.
  • I make capabilities verifiable without owning an on-chain object in the general case — the holder proves entitlement by providing a signed token tied to the DID and the transaction.
  • I separate attestations (who you are) from cap grants (what you may do) so that revocation and portability are independent and auditable.

Core principles I use in every design

  1. Signature-first, anchor-second. Attestations and capability bindings are primarily represented by cryptographic signatures (verifiable anywhere). On-chain anchors (DID anchor, revocation roots) are convenience/trust enforcers, not the only source of truth.
  2. Domain separation everywhere. Every signed statement includes an explicit domain field (chain_id|package_id|purpose|epoch) so nothing can be replayed across chains.
  3. Short-lived proofs with refreshability. Rather than relying on forever-valid objects, I prefer short-lived signed proofs and a cheap re-anchoring flow for long-term grants.
  4. Minimal on-chain state. Store only the small bits you absolutely need on-chain: anchor hashes, revocation roots, and policy objects. Heavy data and logs live off-chain (indexers), referenced by content-addressed hashes.
  5. Make capability checks cheap for consumers. Consumers either verify a compact signature (cheap native) or verify a Merkle proof against an anchored root (logarithmic cost).

Alternative architecture (components and why)

  1. Portable Credential (signed JWT/Cred format)

    • Format: { sub: DID, claims: {...}, iat, exp, domain } signed by issuer.
    • Portability: any Move chain or off-chain verifier that accepts the issuer’s public key can validate it.
    • Why: simple, widely understood, and minimal on-chain footprint.
  2. On-chain Minimal Anchors

    • Anchor types: DidAnchor(cid), RevocationRoot(epoch, root), PolicyObject.
    • Why: anchors let verifiers discover that an issuer’s key was valid at a point in time and that a claim hasn’t been globally revoked.
  3. Capability Authorization Tokens (CATs) — my twist on capabilities

    • A CAT is an off-chain signed authorization from an authority (governance, DAO, issuer) that includes: cap_id, target_did, allowed_action, nonce, expiration, domain, and is signed by the authority’s private key.
    • The actor (subject) presents the CAT along with proof that they control the DID (e.g., a signature from the DID controller key) when calling a protected entrypoint.
    • On-chain modules validate both signatures and domain fields; they consult a tiny RevocationRegistry only for high-risk operations.
    • Why: CATs are cheap (one or two signature verifies), portable, and do not require minting home-chain objects to grant privileges.
  4. Selective On-Chain Caps (for ultra-sensitive ops)

    • For the highest-risk actions (protocol upgrades, treasury moves), I do mint an on-chain BoundCap (object) but make it short-lived and require both: (a) the on-chain object and (b) a fresh off-chain CAT signed by the DID controller.
    • Why: this combines fast off-chain auth and a redeemable on-chain token for the most sensitive flows.

How I prevent privilege escalation and cross-chain replay

  • Dual signatures for critical actions: require both an authority signature and a DID-controller signature in the same transaction for any cap usage. That prevents anyone with authority-signature copy from acting without the subject’s consent.
  • Nonces + epoch: every CAT has a nonce and an epoch; modules check nonce monotonicity (or require short life) to prevent replay. Nonce checks are optionally anchored in a small per-DID NonceAnchor if you need global single-use guarantees.
  • Domain binding: domain = chain_id || package_id || action_name. If you want the same cap to be portable, the authority must sign a portable CAT where domain is deliberately generic — but that is a conscious, explicit operation and can carry higher risk.
  • Revocation as small root + fraud proofs: I use Merkle roots of revoked CAT IDs anchored on-chain. Consumers verifying a CAT check inclusion against the latest anchored root (or rely on an indexer). This keeps revocation cheap while allowing offline verification.

Concrete Move-style patterns (alternative to earlier modules)

I’ll sketch three primitives: registry::anchor_did, auth::verify_cat (verifies CAT and DID-control), and caps::mint_bound_cap (optional on-chain cap).

1) DID anchor (minimal)

module registry::did {
    use sui::object::{UID};
    use sui::tx_context::{self, TxContext};
    use sui::event;

    struct DidAnchor has key {
        id: UID,
        did: vector<u8>,
        cid: vector<u8>, // content hash
        controller_pubkey: vector<u8>, // hex/bytes of pubkey for convenience
        seq: u64,
        created_at: u64,
    }

    public(entry) fun anchor_did(did: vector<u8>, cid: vector<u8>, controller_pubkey: vector<u8>, ctx: &mut TxContext): DidAnchor {
        let id = object::new(ctx);
        let a = DidAnchor { id, did, cid, controller_pubkey, seq: 1u64, created_at: tx_context::epoch(ctx) };
        // emit event for indexing
        a
    }
}

Notes: the anchor records the controller pubkey at the time of anchoring. For portability you can anchor the same did on multiple chains.

2) Off-chain CAT format & verify helper (on-chain verification of signatures and revocation)

Off-chain CAT (JSON) example:

{
  "cap_id":"cap:treasury:withdraw",
  "target_did":"did:move:alice",
  "action":"withdraw",
  "exp": 1700000000,
  "nonce": 42,
  "domain": "sui:package:0xABC::treasury:withdraw",
  "issuer": "0xAuthorityPubKey",
  "sig": "base64sig"
}

On-chain verification helper (pseudocode Move):

module auth::verify {
    native fun verify_signature(pubkey: vector<u8>, message: vector<u8>, sig: vector<u8>): bool;

    // Verify a CAT and that the caller controls the DID (caller signs a proof)
    public(entry) fun verify_cat_and_holder(cat_serialized: vector<u8>, cat_sig: vector<u8>, issuer_pubkey: vector<u8>, holder_proof_sig: vector<u8>, holder_pubkey: vector<u8>, ctx: &mut TxContext) {
        // 1) verify cat signature by issuer
        assert!(verify_signature(issuer_pubkey, cat_serialized, cat_sig), 100);

        // 2) parse cat_serialized, extract target_did and domain and expiry, nonce etc.
        // 3) verify holder_proof_sig: holder_pubkey signed the transaction digest or a domain separator proving control (subject must sign)
        assert!(verify_signature(holder_pubkey, tx_context::tx_digest(ctx), holder_proof_sig), 101);

        // 4) optional: check revocation root inclusion via small on-chain registry
        // 5) check domain binding matches this package/action
    }
}

Why this is good: only two signature verifications, no need to mint objects for the common case, portable across chains (signatures verify anywhere).

3) Optional on-chain short-lived BoundCap (for upgrades)

When you need a hard on-chain object:

module caps::bound {
    use sui::object::{UID};
    use sui::tx_context::{self, TxContext};

    struct BoundCap has key {
        id: UID,
        cap_id: vector<u8>,
        bound_did: vector<u8>,
        expires_at: u64,
    }

    // Mint a bound cap — minted only if a valid CAT + holder proof is presented (both signatures verified)
    public(entry) fun mint_bound_cap(cat_serialized: vector<u8>, cat_sig: vector<u8>, issuer_pubkey: vector<u8>, holder_proof_sig: vector<u8>, holder_pubkey: vector<u8>, expires_at: u64, ctx: &mut TxContext): BoundCap {
        auth::verify::verify_cat_and_holder(cat_serialized, cat_sig, issuer_pubkey, holder_proof_sig, holder_pubkey, ctx);
        // after verification mint the on-chain cap
        BoundCap { id: object::new(ctx), cap_id: /* from parsed cat */, bound_did: /* from parsed cat */, expires_at }
    }
}

This cap is intentionally short-lived (e.g., valid only for N blocks/epochs). It’s useful for operations requiring an on-chain primitive (e.g., to be passed to other Move modules expecting object-cap semantics).


Cross-chain portability patterns (practical)

  • Publish CAT roots for offline indexing: batch CATs into a Merkle tree off-chain and publish the root on target chains when portability is desired. Consumers use Merkle proof + CAT signature to verify.
  • Canonicalization and client libraries: provide a single canonical serializing library (TS/Rust) so CAT bytes are identical across chains/clients.
  • Dual-signed portable CATs: if a capability must be globally portable, require both issuer signature and an explicit cross-chain attestation by the DID or a cross-chain authority that signs a portable domain (e.g., domain = "global:cap:treasury:withdraw").
  • Relayer anchored attestations: relayers carry an indexer that publishes aggregated snapshots of attestations or revocations to other chains; these anchors are time-stamped and can be challenged within a dispute window.

Trade-offs & when to use which pattern

  • CAT-only (no on-chain cap): best for high-throughput, low-risk actions (user-level permissions, single-use auth). Extremely portable and cheap. Use when you trust issuer keys and revocation latency tolerances are acceptable.
  • BoundCap on-chain (short-lived): use for high-risk actions requiring a tangible on-chain object (e.g., multisig-like flows). Slightly more expensive and less portable unless re-minted on target chain by relayer.
  • Anchor-heavy (store attestation on-chain): use for auditability or legal/regulatory needs — keep minimal data on-chain (hashes) to limit gas.

Practical operational controls I enforce

  • Issuer key lifecycle: rotate keys on a schedule and publish rotation anchors (so old signatures can be validated against historical anchors).
  • Indexers & watchtowers: run indexers to track CAT issuance and revocation; watchtowers alert for suspicious mass issuance.
  • Dispute & slashing: for issued CATs tied to economic actions, require issuers to post a bond that can be slashed if fraud is proven.
  • Auditable logs: emit events for every CAT verification, bound cap mint, and DID rotation so auditors can rebuild timelines.

Example attacker scenarios and mitigations (I always test these)

  1. Stolen issuer key: attacker signs CATs — mitigation: quick revocation anchors + slashing of issuer bond + rotation of keys; consumers use short-lived CATs to limit exposure.
  2. Replay across chains: attacker replays CAT on another chain — mitigation: strict domain binding and relayer anchors required for cross-chain use.
  3. Cap transfer and misuse: someone transfers a bound-cap object — mitigated by requiring holder proof signature at use-time (dual-signatures) or binding caps to DID with holder checks.
  4. Relayer equivocation: relayer publishes different roots on different chains — mitigate with aggregated signatures, multi-relayer consensus, or on-chain dispute.

How I validate the system

  • Unit tests for canonical serialization and all signature verification paths.
  • Cross-chain replay tests where I attempt to use CATs on the wrong chain and ensure verification fails.
  • Fuzz tests: random CAT payloads, domain fields, expired nonces, malformed proofs.
  • Red-team exercises: stolen key scenarios, relayer equivocation, and mass issuance stress.

Final summary — what I would deploy first

  1. Implement CAT format, canonical serializer, and client libs (TS + Rust).
  2. Implement Move auth::verify with native signature checks and a small RevocationRegistry.
  3. Start with CAT-only flows for most operations (fast, portable).
  4. Add BoundCap only for critical on-chain flows and make them short-lived.
  5. Provide relayer tools for cross-chain anchoring and Merkle roots for batch portability.
0
评论
.

你知道答案吗?

请登录并分享。