Normative · Part
Identity, trust and MLS
5. Identity, Trust, and MLS
Section titled “5. 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.
5.2. Node root identity
Section titled “5.2. Node root identity”5.2.1. Suite and encoding
Section titled “5.2.1. Suite and encoding”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.
5.2.2. Non-extractability
Section titled “5.2.2. Non-extractability”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:
- 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;
- every certificate in the chain is within its validity period and is not revoked according to the platform’s published revocation data;
- the attested public key is bit-identical, under the
Section 5.2.1 encoding, to the
IK_pubthe identity record binds; - the attestation records that the key is non-extractable and was generated inside the secure element;
- 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_attestedfor 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.
5.2.6. Policy, and the default
Section titled “5.2.6. Policy, and the default”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.
5.3. MLS credential binding
Section titled “5.3. MLS credential binding”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.
5.3.1. Generation acceptance
Section titled “5.3.1. Generation acceptance”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.
5.4. Hardware signing integration
Section titled “5.4. Hardware signing integration”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.
5.5. Admission
Section titled “5.5. Admission”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]
5.6. Removal
Section titled “5.6. Removal”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.
5.7. Membership authorization artifacts
Section titled “5.7. Membership authorization artifacts”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:
operationisaddorremove, 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, asentitlement_classlives on its own instantiation page. They are distinct from the identically namedoperationenumeration of the SSP/1 change event (SSP/1 section 53,upsertanddelete), which names a row mutation rather than a membership change; the content type disambiguates the two on the wire.opaque_domain_idnames 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_idis the node being added or removed, andoperation_idbinds the record to the admission or removal ceremony that produced it.mesh_idis not in the payload. It is theexternal_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_generationis dense, the genesis record carries generation0with a null predecessor, andprevious_record_hashis 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.