Skip to content

Security: germ-network/swift-mls

SECURITY.md

Security policy

Reporting

Please do not report suspected vulnerabilities through public GitHub issues.

This is a small project: reports are read, but no response timeline is promised, and there is no bug bounty.

Scope

In scope: anything with concrete security impact — key or plaintext disclosure, authentication bypass, acceptance of input the code or spec documents as rejected, memory-safety failures reachable from untrusted input, divergence between spec/ and the code with a security consequence (this repo's rule is that the spec is the contract: where they disagree, the code has the bug).

Out of scope: hardening suggestions without concrete impact. Those are welcome as ordinary issues.

Memory hygiene (zeroization)

The retained secret material — the key schedule's per-epoch fan-out, retained resumption PSKs, HPKE private keys, the per-message secret store (the §9.2 consuming secret tree, its ratchet chains, and the skipped-key cache), and the KDF Extract/Expand seam that derives them — is held in zeroizing storage (swift-crypto's SymmetricKey, via the swift-secret-bytes package), so a derived secret is born and kept in a buffer that scrubs on release rather than an ordinary Data. Caller-supplied seeds and in-flight TreeKEM material are SecretBytes too, not only what is retained: the epoch-secret seed (Group.create, Combiner.HalfCreation), CommitRandomness.firstPathSecret, the path-secret chain carried through beginCommitPath/finishCommitPath/ decapCommitPath/installPathSecrets, the commit_secret fed to KeySchedule.advance, and a Welcome's GroupSecrets.path_secret. This is defense-in-depth with an unprovable ceiling, not a guarantee:

  • It narrows the same-process exposure window (a heap over-read, a core dump, swap, a hibernation image). It does not defend against another process reading our memory — the OS zeroes freed pages before another process sees them, and zeroization adds nothing there.
  • On Apple platforms the scrubbing is CryptoKit's closed-source behavior, which we state but cannot verify; on Linux it rests on a best-effort optimizer barrier. Value-type copies, register spills, and compiler-materialized temporaries are outside what zeroizing a buffer can reach.
  • Coverage stops at the terminal boundaries. The per-message AEAD key/nonce handed to aeadSeal/aeadOpen, the HPKE-seal plaintext, and the plaintext transients at wire decode/encode are ordinary Data — a secret must materialize as Data to cross those provider/wire seams, so custody ends there by construction; several are marked in the source as deliberate custody exits. Four such bridges carry path-secret material specifically: the HPKE-seal plaintext in UpdatePath.swift, the HPKE-open plaintext in DecapPath.swift, and GroupSecrets's wire encode and decode, both in Welcome.swift. GroupSecrets's encode and decode carry joiner_secret too, the same custody bridge. pskSecret (the §8.4 fold), the (PreSharedKeyIdentifier) -> SecretBytes? resolver it is built from, and joiner_secret are all now held in zeroizing storage — the resolver returns SecretBytes? directly, so no PSK secret or joiner secret passes through Data except at the wire boundaries above. Application-supplied signature private keys (SignatureSecretKey) are held in zeroizing storage, like the HPKE private key; the group never retains one.

A provable, testable companion to this ships alongside it: per-epoch key material is dropped as soon as it can no longer be needed, and the resumption-PSK history is bounded to a caller-configured depth — "this key is no longer retained" is a property a test can assert, unlike "these bytes are no longer in memory."

There aren't any published security advisories