Concrete data formats for every key and address type in the CB-MPC application
All public keys in CB-MPC use compressed SEC1 encoding. The first byte signals the parity of the Y coordinate, followed by the 32-byte X coordinate.
The canonical public key format throughout the app. Produced by cbmpc_ecdsa2p_key_pubkey() and stored in ManagedKey.publicKey as a hex string.
| Curve | secp256k1 (OpenSSL NID 714) |
| Size | 33 bytes raw / 66 hex chars |
| Prefix | 02 (even Y) or 03 (odd Y) |
| Encoding | Hex string, lowercase, no 0x prefix in storage |
| Recognizable By | Starts with 02 or 03, exactly 66 hex chars |
In the app UI, public keys are truncated to two formats:
publicKeyDisplay: first 16 hex chars + "..." → 02a1b2c3d4e5f6a7...
shortAddress: "0x" + first 4 + "..." + last 4 → 0x02a1...a1b2
Identical SEC1 format as secp256k1 but on a different curve. Supported by the C API but not the primary curve for blockchain use.
| Curve | prime256v1 / P-256 (OpenSSL NID 415) |
| Size | 33 bytes raw / 66 hex chars |
| Recognizable By | Same format as secp256k1 -- requires metadata to distinguish curves |
Compressed public keys for secp256k1 and prime256v1 are visually identical (both 33 bytes, same prefix bytes). The app stores curveCode (714 or 415) as metadata in ManagedKey to disambiguate. Without this metadata, the curve cannot be determined from the key alone.
Used for multi-party EdDSA signing via cbmpc_eddsamp_sign(). Raw 32-byte compressed point on Curve25519.
| Curve | Ed25519 (Curve25519, twisted Edwards) |
| Size | 32 bytes raw / 64 hex chars |
| Recognizable By | 64 hex chars (no 02/03 prefix) -- 2 chars shorter than secp256k1 |
| Used For | Multi-party threshold signing (N-of-M via cbmpc_eckey_mp_t) |
Derived from the secp256k1 public key: decompress to 64-byte (X,Y), Keccak-256 hash, take the last 20 bytes. The app currently stores the last 40 hex chars of the compressed public key as a display address.
| Derivation | Keccak-256(04 || X || Y)[12:32] |
| Size | 20 bytes / 40 hex chars + "0x" prefix |
| Checksum | EIP-55 mixed-case (not currently enforced in app) |
| Normalization | AddressBook auto-prepends "0x" if missing |
| Display | 0x71C7...976F (first 4 + last 4 hex chars) |
An Ethereum address is deterministically derived from a public key. Since the public key is static (unchanged by MPC share refresh), the Ethereum address is also static. Refreshing key shares does not change the address.
Key shares are the secret half of MPC -- each party holds a share that is never combined. Shares are regenerated during key refresh ceremonies, making them inherently mutable.
Created by cbmpc_ecdsa2p_dkg(). Each device holds one share of a 2-party ECDSA key. Serialized as an opaque binary blob via cbmpc_ecdsa2p_key_serialize().
| Threshold | 2-of-2 -- both shares required to sign |
| Parties | Party 0 (device) + Party 1 (device or server) |
| Format | Opaque binary -- not human-readable |
| Mutability | Regenerated on cbmpc_ecdsa2p_refresh() -- new share, same public key |
| Storage | Device Keychain (kSecAttrAccessibleWhenUnlockedThisDeviceOnly) |
| Keychain ID | cb-mpc.share.{keyId}.{partyId} |
After a key refresh ceremony, the binary representation of both shares changes completely. The public key remains identical -- only the internal share randomness is rotated. This is why share data is marked italic/mutable while the public key stays bold/static.
When both shares are stored together (e.g., device-to-device key), they use a length-prefixed packing:
Layout: [4-byte k0 length][k0 bytes][k1 bytes] -- k1 length is inferred from total size minus header minus k0 length.
Created by threshold DKG with access control structures. Supports arbitrary (t, n) configurations. Serialized as a 5-field structure.
| Threshold | t-of-n -- configurable via access structure |
| Fields | x_share (scalar), Q (joint public key), Qis (array of per-party public keys), curve (NID), party_name |
| Conversion | Can convert to additive shares via cbmpc_eckey_mp_to_additive() |
| Signing | cbmpc_eddsamp_sign() for Ed25519 threshold signatures |
Unlike 2-of-2 keys where the threshold is implicit, multi-party shares must carry metadata (party count, threshold, access structure) to be usable. The party_name field identifies which party this share belongs to. When transferring shares (QR, Bluetooth), both the share data and the party metadata must travel together.
Hierarchical deterministic keys use BIP-44 paths to derive child keys from a master. The path is a string of indices; hardened indices (marked with ') use the top bit of a 32-bit integer.
| Preset | Path Pattern | Varies | Example Sequence |
|---|---|---|---|
| MetaMask | m/44'/60'/0'/0/X |
address_index | .../0/0, .../0/1, .../0/2 |
| MetaMask (imported) | m/44'/60'/0'/0 |
none (root only) | m/44'/60'/0'/0 |
| Ledger Live | m/44'/60'/X'/0/0 |
account | .../0'/0/0, .../1'/0/0, .../2'/0/0 |
| Custom | m/44'/60'/* |
any | user-defined |
Parsed by KeyStore.parseBIP44Path() into an array of UInt32 values:
| 44' | 0x8000002C (44 | 0x80000000) |
| 60' | 0x8000003C (60 | 0x80000000) |
| 0' | 0x80000000 (0 | 0x80000000) |
| 0 (non-hardened) | 0x00000000 |
| Path | m (root) |
| Display Tag | HD-MASTER |
| Can Derive | Yes -- produces HD-CHILD keys |
| Shares | MPC shares of master key (mutable) |
| Public Key | Root compressed SEC1 (static) |
| Path | m/44'/60'/0'/0/0 |
| Display Tag | HD-CHILD |
| Links to | parentKeyId (UUID of master) |
| Public Key | Deterministic from master + path (static) |
| Address | Unique Ethereum address per path |
| Input | 32-byte SHA-256 hash |
| Encoding | DER (ASN.1 SEQUENCE of two INTEGERs) |
| Size | ~70-72 bytes (variable) |
| Produced By | cbmpc_ecdsa2p_sign() |
| Input | Raw message bytes (not pre-hashed) |
| Encoding | Raw concatenation (R || S) |
| Size | 64 bytes (fixed) |
| Produced By | cbmpc_eddsamp_sign() |
The app implements EIP-712 typed data signing with a pure-Swift Keccak-256 (Ethereum variant, NOT NIST SHA-3).
| Hash Function | Keccak-256 (Ethereum variant, 32-byte output) |
| Domain Fields | chainId, verifyingContract, name, version |
| Type Encoding | "TypeName(type1 name1,type2 name2,...)" |
| Output | 32-byte hash signed by ECDSA |
Key shares exported via QR use base64-encoded binary frames. Each frame contains a magic header, part index, and payload chunk. Maximum 450 bytes per binary frame (~600 chars base64).
| Max Frame | 450 bytes binary / ~600 chars base64 |
| QR Version | 9-10 (reliable phone-to-phone scanning) |
| Splitting | Data split evenly across parts (not fill-first) |
| Integer Reads | readUInt16BE / readUInt32BE (manual byte reads, no load(as:)) |
| Scanner | AVCaptureMetadataOutput.stringValue → Data(base64Encoded:) |
Every key in the app wraps raw cryptographic material with structured metadata stored in Core Data. This metadata is what allows the UI to distinguish key types and display meaningful information.
| Field | Type | Example | Notes |
|---|---|---|---|
| id | UUID | A1B2C3D4-E5F6-7890-ABCD-EF1234567890 |
Primary key, links to Keychain entries |
| publicKey | String (hex) | 02a1b2c3d4...f0a1b2 |
66-char compressed SEC1 |
| keyType | Enum | simple | hdMaster | hdChild |
Display: ECDSA, HD-MASTER, HD-CHILD |
| curveCode | Int32 | 714 or 415 |
OpenSSL NID -- disambiguates curves |
| derivationPath | String? | m/44'/60'/0'/0/0 |
nil for simple keys, "m" for HD master |
| parentKeyId | UUID? | ... |
HD-CHILD → its HD-MASTER |
| storageLocation | Enum | secureEnclave | keychain |
Display: Device Keychain, iCloud Keychain |
| ceremonyType | String | device_device | device_server |
In KeyShare metadata (Keychain) |
A compressed secp256k1 public key and a compressed P-256 public key are byte-for-byte indistinguishable without the curveCode field. Similarly, a 2-of-2 key share and an N-of-M key share are both opaque binary -- only the keyType and ceremonyType metadata tells the app (and the user) what threshold scheme applies. Always transfer metadata alongside key material.
| Data Type | Raw Size | Encoding | Display | Persistence |
|---|---|---|---|---|
| secp256k1 pubkey | 33 bytes | Compressed SEC1 hex | Bold (static) | Core Data |
| P-256 pubkey | 33 bytes | Compressed SEC1 hex | Bold (static) | Core Data |
| Ed25519 pubkey | 32 bytes | Raw hex | Bold (static) | Core Data |
| Ethereum address | 20 bytes | 0x + hex | Bold (static) | Derived on display |
| 2-of-2 ECDSA share | variable | Opaque binary | Italic (mutable) | Device Keychain |
| N-of-M threshold share | variable | 5-field struct | Italic (mutable) | Device Keychain |
| HD master keyset | variable | Opaque binary | Italic (mutable shares) | Device Keychain |
| BIP-44 path | 5 x UInt32 | String m/44'/60'/... |
Bold (static) | Core Data |
| ECDSA signature | ~70-72 bytes | DER | Italic (per-message) | Signing records |
| EdDSA signature | 64 bytes | Raw R||S | Italic (per-message) | Signing records |
| QR frame | ≤450 bytes | Base64 text | Italic (ephemeral) | Not persisted |