Signing owner ops
A device key speaks for its account everywhere in governance — so, before core rc.83, a stolen device could promote an accomplice, transfer a group to them and remove the owner. Core now refuses the ops that can do that unless they carry a proof signed by the account’s root key, which a device does not hold:
| Op | Who | Route |
|---|---|---|
transferOwnership |
owner | POST /admin-api/groups/:id/transfer-ownership |
ownerDeleteGroup |
owner | POST /admin-api/groups/:id/owner-delete |
changeNamespaceAdmin |
owner | POST /admin-api/namespaces/:id/admin |
setTeeAdmissionPolicy |
admin’s own root | PUT …/settings/tee-admission-policy |
setTeeAuthoringPolicy / disableTeeAuthoringPolicy |
admin’s own root | PUT/DELETE …/settings/tee-authoring-policy |
A node that holds the account root (merod init provisions one) signs the
proof itself: call these with no proof. For a nodeless account, or a root kept
on the phone, MeroKit signs it.
Let the SDK sign
Section titled “Let the SDK sign”Pass the root (see Account roots) and the SDK does
the rest: it reads namespaceId and ownerOpCounter with getGroupInfo,
builds the op exactly as the node will, signs, and sends it as rootProof.
let admin = await mero.adminlet root = RootSource.signer(accountRoot.signer) // or .secret(hex)
try await admin.transferOwnership(groupId, newOwner: newOwnerAccount, root: root)try await admin.ownerDeleteGroup(groupId, root: root)try await admin.changeNamespaceAdmin(namespaceId, newAdmin: account, root: root)try await admin.setTeeAuthoringPolicy(namespaceId, allowedMrtd: mrtds, root: root)try await admin.disableTeeAuthoringPolicy(namespaceId, root: root)try await admin.setTeeAdmissionPolicy( namespaceId, request: .signedRelease(SignedReleaseTeePolicy(allowedProfiles: ["locked-read-only"]), allowedTcbStatuses: ["UpToDate"]), root: root)Each proof is single-use: it names the group’s guarded-op counter, which
the node advances once the op applies. A 409 means the counter moved between
the read and the send — call again. 403 means a proof was needed (or named
another account), 400 a malformed one. A root that has been handed off signs
at a later keyEpoch, passing the chain of RootKeyHandoffs (hex) that
reaches it.
Sign it yourself
Section titled “Sign it yourself”The raw-rootProof forms stay for a proof signed elsewhere — offline, by a
cold root:
let info = try await admin.getGroupInfo(groupId)let proof = try await signOwnerOpProof(OwnerOpProofInput( root: .secret(rootSecret), namespaceId: info.namespaceId!, groupId: groupId, // the namespace root for adminChanged / TEE op: try transferOwnershipOp(newOwner), counter: info.ownerOpCounter!))try await admin.transferOwnership(groupId, request: TransferOwnershipRequest(newOwner: newOwner, rootProof: proof))The encoders are transferOwnershipOp, groupDeleteOp, adminChangedOp (a
root-plane op), teeAuthoringPolicyOp, teeAdmissionPolicyOp and
teeReleaseAdmissionPolicyOp; SetTeeAdmissionPolicyRequest.ownerOp() builds
the one the node builds from a request (its minReleaseVersion normalised as
core normalises it). ownerOpDigest, ownerOpSigningPayload and
rootGuardedOpBytes expose each stage. The proof is the hex borsh of core’s
AccountProof<OwnerOpAuthorization>:
AccountGenesis { version u8 = 2, root_pk [32] }chain u32 count, then 132-byte RootKeyHandoffsOwnerOpAuthorization { account [32], namespace_id [32], group_id [32], kind u8, op_digest [32], counter u64, key_epoch u32, signature [64] }OwnerOpKind’s raw value is the byte signed: transferOwnership 0,
adminChanged 1, groupDelete 2, teeAdmissionPolicy 3,
teeAuthoringPolicy 4, teeReleaseAdmissionPolicy 5.
Scoping a device
Section titled “Scoping a device”The device certificate says this key is a device of the account; a scope says which applications it may speak for. A namespace binds a device only when both arrive together.
let link = try await admin.linkAccountDevice( namespaceId, credential: deviceCertificateHex, scope: DeviceScopeInput(signer: accountRoot.signer, device: deviceId, applications: [applicationId])) // [] = every applicationsignDeviceScope(_:) returns the hex AccountProof<DeviceScope> on its own,
and deviceScopePayload the 32 bytes it signs. A higher scopeEpoch
supersedes an earlier scope.