Skip to content

Normative · Part

Identity, trust and MLS

5.1. SSP remains the identity and policy authority

Section titled “5.1. SSP remains the identity and policy authority”

MLS is not the SOVM trust model.

SSP continues to define:

  • site_id;
  • node root identity;
  • pairing ceremony;
  • human confirmation;
  • vouching policy;
  • node role;
  • table/domain scope;
  • revocation intent.

An MLS membership change MUST be authorized by SSP policy before it is merged.

The node root identity key MUST be an ECDSA key on the NIST P-256 curve (secp256r1), used with SHA-256 — COSE algorithm ES256 (-7).

The node root public key IK_pub MUST be encoded as a SEC 1 compressed point: 33 bytes, leading byte 0x02 or 0x03. Implementations MUST reject an uncompressed (65-byte) or hybrid encoding of a node root public key wherever one is presented, including in a node identity record, a pairing transcript, a COSE_Key, and any kid resolution path.

The reason this is normative rather than a convention is the public-key encoding canonicity rule of the substrate signature profile. The SSP site_id remains the stable node identifier, and it is a hash of these bytes.

Where the platform provides a secure element or equivalent protected keystore, the node root private key MUST be generated inside it and MUST be marked non-extractable at generation. An implementation MUST NOT generate the key in ordinary memory and then import it into a keystore, because an imported key was extractable for the interval in which it existed outside — and because a platform attestation over an imported key attests storage, not origin.

Where the platform provides no such facility, the key is a software root and Section 5.2.3 governs what that means.

5.2.3. key_protection is a recorded attribute

Section titled “5.2.3. key_protection is a recorded attribute”

The node identity record MUST carry a key_protection field with exactly one of these values:

Level Meaning
hardware_attested Generated in and non-extractable from a secure element, and accompanied by attestation evidence that a verifier has checked per Section 5.2.4
hardware_unattested Generated in and non-extractable from hardware, with no platform attestation over the key available
software Key material exists in ordinary process memory at some point

The record MUST additionally carry an attestation_kind field with exactly one of platform_key_attestation, app_attestation, or none.

attestation_kind exists because hardware_unattested otherwise averages together two materially different assurances. An iOS root backed by the Secure Enclave, where App Attest attests the application but no API attests the key, and a node that simply asserts hardware backing with no evidence of any kind, are both hardware_unattested — and a verifier that cannot tell them apart cannot write a policy that distinguishes them. app_attestation is not evidence about the key and MUST NOT be treated as such; it is evidence that the claim came from an unmodified application build, which is strictly more than nothing and strictly less than key attestation.

5.2.4. What hardware_attested requires a verifier to check

Section titled “5.2.4. What hardware_attested requires a verifier to check”

A node MUST NOT be recorded as hardware_attested on the strength of its own assertion. hardware_attested is a verified conclusion, not a claim, and the following is what verifying it means.

A node presenting key_protection = hardware_attested MUST present attestation evidence: a platform-issued certificate chain over the node root public key, produced by the keystore that generated it.

A verifier MUST check all of:

  1. the chain terminates in a root the verifier holds in a pinned trust store configured out of band, and not in a root learned from the presenter;
  2. every certificate in the chain is within its validity period and is not revoked according to the platform’s published revocation data;
  3. the attested public key is bit-identical, under the Section 5.2.1 encoding, to the IK_pub the identity record binds;
  4. the attestation records that the key is non-extractable and was generated inside the secure element;
  5. the attested security level meets or exceeds the level required by policy for the role being claimed.

If the evidence is absent, malformed, expired, revoked, unchainable to a pinned root, or attests a different key, the verifier MUST treat the node as software — not as hardware_unattested, and not as the level it claimed. Downgrading a failed attestation to hardware_unattested would make presenting broken evidence strictly better than presenting none, which is the wrong gradient.

This is the fail-closed rule for this section, consistent with the substrate signature profile’s existing posture on unsupported algorithms, and it is what stops a software node from asserting its way into a role reserved for hardware roots.

5.2.5. The attestation root is a third-party trust dependency

Section titled “5.2.5. The attestation root is a third-party trust dependency”

The verification in Section 5.2.4 places a platform vendor’s attestation root inside SOVM/1’s trust boundary. That is a real dependency and this specification does not pretend otherwise.

An implementation MUST therefore:

  • Pin explicitly. The trust store is configuration, updated by a deliberate act. A verifier MUST NOT accept an attestation root delivered alongside the attestation it is being used to validate.
  • Fail closed on unavailable revocation data. If revocation data for a chain cannot be obtained or is stale beyond a locally configured bound, the verifier MUST treat the attestation as unverified and apply Section 5.2.4’s downgrade, rather than accepting the chain on the strength of it not being known-bad.
  • Support model-class exclusion. Where a hardware model or firmware version is found to have an extractability or attestation defect, policy MUST be able to refuse hardware_attested for the identifying attributes that appear in the attestation, without a specification change and without waiting for the vendor to revoke.

The residual exposure — that a vendor root is compromised, mis-issues, or is retired — is not eliminated by any of the above. It is bounded: a node whose attestation cannot be validated becomes software, remains usable in roles that permit software, and is refused in roles that do not. Loss of the vendor root degrades assurance rather than availability.

SSP policy MUST be able to require a minimum key_protection level for a given role or scope, ordered software < hardware_unattested < hardware_attested. A node that cannot meet the required level MUST be refused rather than admitted at a lower level.

An implementation MUST apply a default minimum where policy states none, and that default MUST be:

Capability Default minimum
Minting Section 5.7 membership authorization records hardware_unattested
Any other role software

Specifying the default is the point. A specification that requires a minimum to be expressible and says nothing about its value gets one behaviour per implementation at exactly the boundary where they must agree, and the disagreement surfaces as a node admitted by one peer and refused by another.

The authorization-record default is set where it is because Section 5.7 records are deliberately portable and self-contained: a member validates the chain without waiting for its local policy replica to converge, so a stolen root mints authorizations every other node accepts by design. hardware_attested is not the default there only because it is unreachable on iOS (Section 5.2.3), and a default no iOS node can satisfy is a default that gets configured away.

An MLS leaf credential and signature key MUST be cryptographically bound to the corresponding SSP node identity.

OpenMLS currently supports BasicCredential, for which the identity-to-key meaning is application-defined. SOVM MUST therefore define and verify that binding rather than assuming OpenMLS provides it.

A preferred design is:

flowchart LR
  accTitle: What the SSP root identity key anchors
  accDescr: The SSP identity public key both derives the stable site identifier and signs the MLS credential binding.
  ik["SSP IK_pub"] --> site[site_id]
  ik --> binding[signs MLS credential binding]

The binding transcript is deterministic CBOR signed with COSE_Sign1 under the SSP root identity key, under the domain-separation label sovm-mls-credential-binding-v1:

{
binding_version,
protocol_label = "sovm-mls-credential-binding-v1",
mesh_id,
site_id,
mls_credential_identity,
mls_signature_pubkey,
mls_ciphersuite,
credential_generation,
key_protection,
attestation_kind,
attestation_evidence_hash,
previous_binding_hash
}

attestation_evidence_hash is BLAKE3 over the exact bytes of the attestation evidence, or a zero-length string where attestation_kind is none.

previous_binding_hash is BLAKE3 over the exact CBOR-encoded bytes of the predecessor COSE_Sign1 structure — the transcript this one supersedes. At generation 0 the binding has no predecessor; previous_binding_hash is a zero-length bstr at genesis and MUST NOT be omitted.

Binding the protection level into the transcript is what makes it signed rather than asserted out of band: a peer cannot claim one level in an identity record and a different one to a policy check, because the level a verifier acts on is inside the structure whose signature it already verifies.

The evidence hash rather than the evidence is bound because attestation chains are large, are revalidated against revocation data that changes after signing, and would otherwise force a transcript re-sign on every certificate rotation. The hash pins which evidence was presented at binding time; the verifier obtains the evidence itself alongside the identity record and checks the hash matches.

The label is sovm-mls-credential-binding-v1. A verifier that does not recognize the label fails closed on the transcript, which is the intended behaviour and the reason the label is inside the signature.

Supersession and revocation use the monotonic credential_generation, not wall-clock validity windows, consistent with Section 9.5’s rejection of untrusted timestamps. The ciphersuite is bound to prevent cross-suite substitution. The signed binding is embedded in the BasicCredential identity field so KeyPackages are self-verifying.

Three obligations govern credential_generation. They are independent and all three MUST be implemented; G2a alone is insufficient against G2b’s threat.

G2a — verifier obligation. A verifier MUST maintain, per site_id, the highest credential_generation it has accepted in a valid binding transcript, and MUST persist that value across restart. It MUST reject a transcript whose credential_generation is strictly below that mark. Acceptance raises the mark; rejection never lowers it.

For a peer with no keyring-recorded credential_generation — which is the steady state for vouched joins and the initial state for any peer — a transcript claiming key_protection better than software MUST be rejected. A peer needing a higher recorded level MUST establish it by presenting attestation evidence directly to the recording node, per SSP/1 §19.4.

G2b — minter obligation. A node MUST increment credential_generation on any change to a bound field. A change to key_protection, attestation_kind, or attestation_evidence_hash MUST always produce a strictly higher credential_generation than the superseded transcript. Without this obligation G2a does not prevent re-enrolment rollback: a node that rebinds at software without incrementing leaves the earlier hardware_attested transcript permanently below the high-water mark — and therefore permanently acceptable.

G2c — predecessor hash. previous_binding_hash chains each transcript to its predecessor. A verifier that has accepted a transcript at generation N and receives a new transcript at generation N MUST check that the new transcript’s previous_binding_hash equals the hash of the accepted one; a mismatch MUST be surfaced as a fork. Because a single IK_priv cannot produce two valid transcripts at the same generation with the same predecessor hash, a fork is evidence of key compromise. Two records that claim the same predecessor share a generation, so a verifier needs no separate predecessor index — the generation high-water mark and the stored predecessor hash together cover the check.

Credential strategy is likewise decided: a node uses one SSP-bound credential reused across all SOVM groups. Per-group leaf keypairs remain fresh as provided by MLS, which preserves the blast-radius property that matters.

The reuse is sound because a mesh sits under one trust authority: every node in it is already known to that authority, and the delivery service already links node to group through routing and KeyPackage delivery. Per-group pseudonymous credentials would add binding-record and KeyPackage overhead without a corresponding privacy gain against an observer who can see that routing.

That reasoning is scoped to what a mesh is, and it has a limit worth stating. Where several parties operate nodes within one authority and their activity should not be correlatable to each other, one credential across all groups makes each node’s group membership linkable to any party that can observe more than one group. Pseudonymous per-group credentials are the mechanism that answers it, and this revision does not specify them. Nothing else in this document depends on the choice.

The OpenMLS integration MUST implement or adapt the library signing interface such that long-lived SSP identity signatures can be produced by Secure Enclave, Android Keystore/StrongBox, TPM, or an equivalent protected keystore where available.

A software-only production fallback MUST be an explicit policy decision, MUST be recorded as key_protection = software (Section 5.2.3), and MUST NOT occur silently.

A hardware signer MUST verify each signature it produces, inside the element, against the public key, before returning it, and MUST abort on failure. This is not redundancy against a buggy signer. ECDSA signing is a multi-step computation over the private key, and a fault induced during it — voltage glitch, clock glitch, electromagnetic injection — can yield a signature that verifies incorrectly or leaks the private key to an attacker who collects a faulty and a correct signature over the same message. Verify-after-sign detects the faulty output before it is released and is the standard countermeasure.

This obligation binds a hardware signer only where the element exposes an atomic sign-verify-or-abort primitive — one operation, in which the signature does not cross the element boundary at all unless the in-element verification against the public key has already passed. Atomicity is the requirement, not merely that both computations run on element hardware. Two separate commands do not satisfy it. A construction that signs, returns the signature, and then asks the element to verify the value handed back to it — TPM 2.0’s TPM2_Sign followed by TPM2_VerifySignature, or Windows PCP’s NCryptSignHash followed by NCryptVerifySignature, which is that pair under CNG — has already released the faulty output before anything checks it. This is the reasoning that rejects a host-side check, one level down: the adversary who can induce the fault can read the signature off the bus in the gap between the two calls, and can arrange that the second call’s abort branch never runs, because the sequencing belongs to the host and not to the element.

Where the platform’s only signing entry point releases the signature before any in-element check, and offers no attribute to request one, the element cannot discharge this clause, and no implementation choice available to the caller changes that. A requirement no implementation can meet is not a requirement, and this paragraph states the boundary rather than leaving the clause aspirational where it cannot be met. No platform named at the head of this section is currently known to expose a conforming primitive over its public API. The clause is stated as a condition rather than repealed, because it binds the moment one does.

An implementation MUST declare, as a local fact that MUST NOT be transmitted to a peer or recorded in a node identity record, whether the secure element backing its root identity key exposes such a primitive. Absence of the declaration MUST be treated as does not expose one: a fail-closed default, not an omission a caller may rely on silently. An implementation that cannot establish atomicity from the platform’s documented contract MUST declare that it has none — an element may well check its own work, but a control that cannot be established cannot be asserted. The declaration exists so that the residual is nameable in the implementation’s own per-platform conformance verdict (G2), not so that it can drive admission — there is no peer-side policy it is an input to, which is why it does not travel.

That declaration is orthogonal to key_protection (Section 5.2.3). An element that cannot discharge this clause may still generate its key in-element and hold it non-extractably, and MUST continue to be recorded at whatever key_protection level Sections 5.2.3 and 5.2.4 otherwise assign it. The two clauses defend against different adversaries — extraction of the key at rest, against a fault induced during one signing operation — and are independently satisfiable. A verifier never sees this declaration and MUST NOT infer it: in particular, a verifier that can deduce the peer’s platform by other means MUST NOT read that deduction as a downgrade of key_protection.

Where the obligation does bind, it is discharged inside the element. It does not extend to a software signer, and the reason is worth stating so that it is not re-opened: in software the private key is already in process memory, so an adversary able to induce faults in that process can read the key directly, and verify-after-sign buys nothing. Verification performed outside the element is not a conforming discharge of this clause either — the adversary the clause is written for has physical access to the device and therefore controls whatever would run the external check, and simply does not run it. What an external verify defends against is accidental fault only.

Accordingly, a change event signed under an event-signing subkey is not subject to this clause, because such a key is a software signer by construction.

The substrate signature profile’s deterministic-nonce rule does not cover this. RFC 6979 removes the entropy failure mode and is scoped to software signers; fault injection is a physical attack on the signing device and is exactly the setting where the key is in hardware. The two clauses address disjoint failures and both are required.

A node joins cryptographic groups only after SSP admission succeeds.

Conceptually:

flowchart TD
  accTitle: Node admission sequence
  accDescr: Pairing, vouching policy and human confirmation together produce an SSP admission approval, which is what permits an MLS Add and Welcome.
  pairing[pairing / QR / SAS] --> approved[SSP admission approved]
  vouch[vouching policy] --> approved
  human[human confirmation] --> approved
  approved --> add[MLS Add / Welcome]

A node removal is:

flowchart TD
  accTitle: Node removal sequence
  accDescr: A policy decision removes the node from the trusted keyring, which drives an MLS Remove and Commit, a new MLS epoch, and finally a new application domain key generation the removed node never receives.
  decision[user / SSP policy] --> keyring[remove from trusted SSP keyring]
  keyring --> commit[MLS Remove + Commit]
  commit --> epoch[new MLS epoch]
  epoch --> kek[new application domain KEK generation]

The removed member MUST NOT receive secrets for the new epoch.

Membership Commits are authorized by portable signed artifacts, not by local SSP-policy freshness.

Every Add or Remove Commit MUST reference a COSE-signed SSP authorization record produced by the node that executed the corresponding admission or removal ceremony, chained to the trusted SSP keyring, and carrying a dense authorization generation (substrate section 11).

Authorization records are hash-chained: a generation alone permits two authorized nodes to independently mint conflicting next-generation records, with only the winning MLS Commit distinguishing them. Each record therefore binds its predecessor explicitly.

The record is registered as sovm/sop-authorization-v1 and takes the SSP/1 COSE profile unchanged. It seals nothing and carries no key material, so it needs none of the object plane’s richer per-record external_aad:

MembershipAuthorization = COSE_Sign1(
protected = {
alg : ES256,
content type : "sovm/sop-authorization-v1",
kid : issuer site_id (16 bytes)
},
payload = deterministic CBOR {
operation : tstr, ; "add" or "remove"
target_site_id : bstr 16,
opaque_domain_id : bstr,
operation_id : bstr 16,
vouch_evidence : [* bstr 32],
authorization_generation : unsigned,
previous_record_hash : bstr 32 / null
},
external_aad = mesh_id (16 raw bytes)
)

Rules:

  • operation is add or remove, lowercase. A receiver MUST reject a record carrying any other value: an unrecognized operation is not silently ignored, exactly as the extension policy requires of an unknown value in a known record. These two values are enumeration values of one record field and not registry identifiers, so they live here rather than in the registry, as entitlement_class lives on its own instantiation page. They are distinct from the identically named operation enumeration of the SSP/1 change event (SSP/1 section 53, upsert and delete), which names a row mutation rather than a membership change; the content type disambiguates the two on the wire.
  • opaque_domain_id names the cryptographic domain whose MLS group membership the Commit changes, and it is load-bearing rather than descriptive. Bound inside the signed payload, it is what stops an authorization being replayed against a different group: without it, “add node Y” is a bearer token good against every domain, and the domain taxonomy is precisely a confidentiality boundary — section 6.4 puts the governing question as “which node must NOT read this”. A future editor MUST NOT drop the field as redundant.
  • target_site_id is the node being added or removed, and operation_id binds the record to the admission or removal ceremony that produced it.
  • mesh_id is not in the payload. It is the external_aad, which binds the record to one mesh without transmitting it. A payload copy would be a second value free to disagree with the binding, and a verifier that checked only the copy would accept a record bound to another mesh.
  • Records chain per mesh: authorization_generation is dense, the genesis record carries generation 0 with a null predecessor, and previous_record_hash is the BLAKE3 hash of the exact bytes of the record it supersedes (one hash) — substrate section 11 unchanged and unnarrowed.

There is one chain per mesh, and its authority is not its signer. The chain is not per subject and not per domain: one membership-authorization chain exists, whose authority is the mesh’s admission authority, while kid names the authorized node that minted the link. Substrate section 12 requires an instantiation to state its signer rule separately wherever authority and signer differ, and here they differ. A change of minting node is an ordinary link, not a new chain.

vouch_evidence is an audit trail, not an input to validity. It carries BLAKE3 hashes of the sovm/ssp-vouch-v1 certificates the operation rested on, and MAY be empty for an operation that rested on none. A verifier MUST NOT reject a record because it cannot resolve those hashes, and MUST surface a hash it can resolve that does not verify. Requiring resolution would make validation depend on the verifier having already replicated the certificates, reintroducing the ordering dependency this section exists to remove.

A forked chain resolves to neither branch and to no third value. A node observing two records claiming the same predecessor MUST surface the conflict. Because generations are dense, two such records necessarily share an authorization_generation, so this is exactly the fork section 11 already requires be surfaced; this instantiation adds no same-predecessor index of its own. Both branches are retained as permanent evidence. What resolves the fork is the sequenced Commit: a Commit is validated against the branch it names, and the chain makes a losing or conflicting record independently visible and the SSP authorization history auditable on its own, rather than only through whichever MLS Commit happened to win.

Neither sibling chain’s fork rule is taken here. A forked role chain resolves as absent, which is deny-safe there because policy resolution fails closed on a missing layer; absent here would mean no membership change may be authorized at all until an equivocation is cleared, and an equivocating authority has no incentive to clear it — turning a detectable integrity fault into an indefinite availability outage that an attacker can trigger at will. A forked capability-requirement chain resolves as the union of its branches, because absent there would read as “require nothing”; union here would hold a conflicting add and remove both authorized, the most permissive reading available.

This chained-record schema is shared with sequencing receipts (Section 22.5) in the SOVM COSE profile.

Validation of a Commit is therefore self-contained: a member verifies the signature chain and generation density without requiring that its local SSP policy replica has already converged. This removes the ordering dependency in which a node that has not yet replicated a policy update would fail-closed reject a legitimate Commit and create an availability failure indistinguishable from an attack.

Local SSP policy remains authoritative for detecting and resolving conflicts after the fact; a node that later observes contradictory policy state MUST surface the conflict rather than silently reconcile it.