FreeBSD Keychain — porting / alternatives plan Scoping

What macOS Keychain actually is, what's open vs closed, and the full set of options for giving NextBSD/gershwin a local secret store with Apple's SecItem API — from porting Apple's Security.framework wholesale, to wrapping gnome-keyring, to a clean-room secd-equivalent. Companion to the WiFi management, PAM port, and configd plans.

TL;DR

1. What the macOS keychain actually is

Four layers, only the top two of which are app-visible:

LayerRolelaunchd?
Security.framework (library) The in-process API apps link against: modern SecItem* (SecItemAdd/CopyMatching/Update/Delete) and legacy SecKeychain*. Does no storage itself — marshals every request over Mach IPC to the daemon. No — loaded into each client process
securityd (legacy) / secd (modern data-protection keychain) / trustd The daemon that owns the keychain files, holds unlocked keys in memory, enforces access control, and (on modern Macs) talks to the SEP. trustd handles certificate trust evaluation (a related but separable job). Yes — launchd jobs advertising Mach service names; started on demand when a client does a bootstrap lookup
Keychain files On-disk encrypted databases. Legacy: ~/Library/Keychains/login.keychain-db (per-user, unlocked by login password) and /Library/Keychains/System.keychain (system-wide, e.g. WiFi). Modern: SQLite-backed data-protection keychain. n/a
Secure Enclave (SEP) Hardware coprocessor (T2 / Apple Silicon) that wraps the keychain's master keys and gates them on biometrics/passcode. The actual root of trust on modern hardware. n/a — closed firmware

Critically for us, the daemon half is a textbook launchd-on-demand Mach service, the same pattern as configd / notifyd: a .plist declares a Mach service name, the library does a bootstrap_look_up, launchd starts the daemon on first use. That maps directly onto the infrastructure the launchd and libxpc/DO work already provides.

1.1 The architecture

app / wifid-fbsd / configd | v SecItemCopyMatching(...) +--------------------+ | Security.framework | (library, in-process; no storage) +--------------------+ | v Mach IPC (bootstrap_look_up "com.apple.securityd") +--------------------+ +------------------+ | securityd / secd |<----->| trustd (certs) | | (launchd Mach job) | +------------------+ +--------------------+ | | v v +--------------------+ +-------------------+ | keychain files | | Secure Enclave | | (~/Library/Key...) | | (key wrap, closed)| +--------------------+ +-------------------+

1.2 What a "keychain item" is

Each item has a class (kSecClassGenericPassword, kSecClassInternetPassword, kSecClassCertificate, kSecClassKey, kSecClassIdentity), a set of attributes (account, service, server, protocol, label, …) used as search keys, an encrypted secret payload, and an access-control policy (which apps may read it, whether unlock/biometric is required). WiFi passwords, for example, are kSecClassGenericPassword / kSecClassInternetPassword items in the System keychain. Replicating this data model is the real work — the crypto is the easy part.

2. Open-source status — published, not buildable

ComponentStatusNotes
Security Source published The framework: SecItem / SecKeychain APIs, on-disk format logic, ACL logic. Does not build standalone — Apple's own README notes missing internal headers since ~2016.
securityd Source published The daemon + SecurityTool. Mach/MIG IPC server. Same build caveat.
corecrypto Effectively closed Apple's FIPS-validated crypto core. Source is published but under a restrictive license that bars free reuse — not vendorable. Security.framework's crypto bottoms out here.
Secure Enclave firmware Closed Hardware root of trust. Unportable by definition; FreeBSD has no SEP.
iCloud Keychain (sync/escrow server) Closed The CloudKit sync, SOS circle, and escrow mechanisms. We don't want this anyway.
CommonCrypto, libDER, CoreFoundation Open / available CommonCrypto is a thin API (can sit on libcrypto); libDER is open; CoreFoundation we already have.
Binding constraint: the parts that make Apple's keychain securecorecrypto and the SEP — are exactly the parts you cannot take. So "port Apple's keychain" really means "port Apple's keychain data model and API, and supply your own crypto + (optional) hardware backing." Once that's accepted, a from-scratch daemon stops looking so different from a port.

3. What we actually need (and what we can drop)

FeatureNeed it?Why
Local secret storage + SecItem CRUDYesThe whole point — give apps/daemons a place to keep secrets behind a known API.
Per-user keychain unlocked by login passwordYesMatches user expectation; integrates with the PAM port for unlock-at-login.
System keychain (for WiFi etc.)Yeswifid-fbsd needs a root-owned store for network creds available before login.
Per-app / access-group access controlPartialA simplified uid/path-based policy is enough initially; full code-signing-identity ACLs are a later fidelity bump.
iCloud Keychain syncNoNo Apple-ID infra; out of scope permanently.
Secure Enclave / biometric gatingNoNo SEP hardware. TPM2 is the only analog and it's optional (see §6).
Certificate trust evaluation (trustd)No (separate)Different problem; the base system / OpenSSL already do TLS trust. Don't conflate.
CDSA / legacy SecKeychain full fidelityShim onlyProvide thin legacy shims mapping onto the modern store; don't reimplement CDSA.

4. Alternatives

Seven options, from most-Apple to least-effort. Verdicts summarized in §4.8.

4.A — Port Apple's Security.framework + securityd wholesale

Vendor the published source, reconstruct the missing build glue, swap corecrypto for libcrypto, stub the SEP.

Rejected Highest fidelity, worst effort/maintenance. The closed crypto + SEP mean you can't actually ship Apple's security guarantees regardless.

4.B — Clean-room native secd-equivalent recommended

Write a new launchd-managed Mach/DO daemon (com.apple.securityd) exposing the public SecItem API, backed by our own encrypted store and base crypto. Implement the published API contract, not Apple's implementation.

Chosen Best balance. Detail in §5.

4.C — Wrap gnome-keyring / libsecret (freedesktop Secret Service)

Run gnome-keyring-daemon as the storage backend; put a SecItem→Secret-Service translation shim in our Security-shim library.

Fallback only Reasonable if a working store is needed fast and D-Bus is already present; otherwise the dependency cost outweighs the saved daemon work.

4.D — Wrap KWallet

Rejected Strictly worse fit than C for this project.

4.E — pass / gpg-agent backend

Rejected Wrong shape — can't present SecItem semantics.

4.F — PKCS#11 (p11-kit + SoftHSM / OpenSC)

Complementary Not a standalone answer; a possible plug-in backend for cert/key items later.

4.G — Status quo: plaintext Network.plist / flat files

Interim The honest current state until a consumer justifies B.

4.8 Verdict matrix

OptionApple APILicense fitIPC fitEffortVerdict
A. Port Security.frameworkExactcorecrypto blockedMach6–12+ moRejected
B. Clean-room secdPublic APIBSDMach/DO2–3 moChosen
C. gnome-keyring/libsecretShimmedLGPLD-Bus3–5 wkFallback
D. KWalletShimmedLGPLD-Bus/Qt4–6 wkRejected
E. pass / gpg-agentNoneGPL toolCLIdaysRejected
F. PKCS#11Keys onlyBSD/LGPLlibvariesBackend
G. plaintext plistNone0Interim

5. Recommended design — com.apple.securityd

Shape: a launchd-managed Mach/DO daemon presenting the public SecItem keychain API, backed by an encrypted per-user (and per-system) file store, with crypto from base libcrypto/libsodium and an optional TPM2 key-wrapping backend. The Security-shim library does a bootstrap_look_up and forwards calls — identical client/daemon pattern to configd and notifyd.

  1. Daemon. Launchd job labelled com.apple.securityd (file com.apple.securityd.plist) — the same string for the job Label and the advertised Mach service (legacy alias com.apple.SecurityServer); started on demand. Per-user instances in the user's launchd domain (keychains are per-user); one system instance (root) for the System keychain. Mirrors the per-user/per-session domain handling the launchd port already supports.
  2. Library. A Security-compatible shim exporting SecItemAdd/CopyMatching/Update/Delete (+ legacy SecKeychain* shims) that marshal CFDictionary queries over DO/Mach to secd. CoreFoundation types are already available.
  3. Store. One encrypted SQLite (or flat) DB per keychain: ~/Library/Keychains/login.keychain-db and /Library/Keychains/System.keychain, matching Apple paths so muscle memory / tooling line up. Items carry class + attributes (searchable, plaintext) + encrypted secret blob + access policy.
  4. Key hierarchy. A per-keychain master key derived from the unlock secret (login password) via Argon2/scrypt; the master key wraps per-item content-encryption keys (AES-256-GCM). Locking drops the master key from memory; unlocking re-derives it. (See §6.)
  5. Unlock. A PAM module (pam_secd, analog of pam_keychain/pam_gnome_keyring) unlocks the login keychain at login using the same password — direct tie-in to the just-completed PAM port. Manual lock/unlock via a CLI (security(1)-equivalent) and an Agent callback for GUI prompts when an app needs an item from a locked keychain.
  6. Access control. v1: uid + executable-path policy ("which program asked"). v2: stronger identity (code-signing/Mach audit-token-based, if/when code signing exists). Never weaker than gnome-keyring's model.

5.1 Naming — follow the shipped system's convention

This is settled by what the running image actually does, not by what earlier draft plans guessed. A live launchctl list on a NextBSD box shows the system daemons keeping Apple's exact labels — the job Label, the .plist filename, and the advertised Mach service are all the one com.apple.* string:

launchd% list
PID   Status  Label
 54   -       com.apple.syslogd
 53   -       com.apple.notifyd
 52   -       com.apple.mDNSResponder
 51   -       com.apple.kextd
 50   -       com.apple.hostnamed
 48   -       com.apple.configd
 47   -       com.apple.aslmanager
 46   -       com.apple.IPConfiguration
 45   -       com.apple.DiskArbitration
 55   -       com.openssh.sshd          # third-party → upstream's own reverse-DNS

So a daemon that models or replaces an Apple service takes Apple's own name outright. For the keychain that is com.apple.securityd (modern data-protection alt: com.apple.secd; legacy bootstrap alias: com.apple.SecurityServer). There is no separate "our identity" label — that was a wrong inference; the shipped daemons don't do it. A clean-room Security.framework has bootstrap_look_up("com.apple.securityd") baked in, and using Apple's label for the job too keeps everything aligned with the rest of the running stack.

The project's namespace layering (read off the shipped image):
LayerNamespaceExamples
Userland daemon modeling/replacing an Apple servicecom.apple.* (Apple's exact label; job = service)com.apple.configd, com.apple.notifyd, com.apple.IPConfiguration, com.apple.DiskArbitration, com.apple.securityd
Third-party daemonupstream's own reverse-DNScom.openssh.sshd
Kernel kexts (loaded by com.apple.kextd)org.nextbsd.*org.nextbsd.driver.*, org.nextbsd.kpi.*, org.nextbsd.filesystems.*
Desktop / GUI layerorg.gershwin.*org.gershwin.loginwindow, org.gershwin.dshelper
Not org.nextbsd.secd (that namespace is kernel-space only) and not org.freebsd.secd (the org.freebsd.* labels in older plan drafts don't match the shipped image, which uses com.apple.* for these daemons — see the audit note in the WiFi/Bluetooth companions).

6. Crypto & storage — the honest threat model

7. API surface to implement (v1)

/* Modern data-protection API — the priority surface */
OSStatus SecItemAdd(CFDictionaryRef attributes, CFTypeRef *result);
OSStatus SecItemCopyMatching(CFDictionaryRef query, CFTypeRef *result);
OSStatus SecItemUpdate(CFDictionaryRef query, CFDictionaryRef attributesToUpdate);
OSStatus SecItemDelete(CFDictionaryRef query);

/* Classes to support first */
kSecClassGenericPassword     // app/daemon secrets, WiFi PSKs
kSecClassInternetPassword    // server creds (has protocol/server/port attrs)
kSecClassKey / kSecClassCertificate / kSecClassIdentity   // later; PKCS#11 backend option

/* Common attributes (search keys) */
kSecAttrAccount  kSecAttrService  kSecAttrServer  kSecAttrProtocol
kSecAttrLabel    kSecAttrAccessGroup  kSecAttrAccessible

/* Legacy shims (thin, mapped onto the modern store) */
SecKeychainAddGenericPassword(...);
SecKeychainFindGenericPassword(...);
SecKeychainAddInternetPassword(...);

Getting GenericPassword + InternetPassword CRUD correct covers WiFi creds, network shares, and the vast majority of app needs. Key/cert/identity classes and a possible PKCS#11 (§4.F) backend come later.

8. Open questions

Q1. First consumer? The plan only starts when something needs it. Likely candidates: wifid-fbsd network creds, a future mail/browser, or network-share mounts. Until then, plaintext Network.plist stands (WiFi plan Q2).
Q2. Per-user vs system scope on day one? The System keychain (root, pre-login) is what wifid-fbsd would need first; the per-user login keychain is what desktop apps need. Probably ship the system instance first since WiFi is the nearer consumer.
Q3. TPM2 from the start or as a follow-up? Software-only is simpler and works everywhere; TPM2 sealing is the meaningful hardening but adds a conditional dependency and PCR-policy complexity. Lean: ship software-only, add TPM2 as opt-in phase 2.
Q4. How faithful must access control be? Full Apple ACLs are tied to code-signing identities we don't have. Start with uid+path; revisit if/when a code-signing story exists. Document the gap so nobody assumes Apple-grade app isolation.
Q5. Reuse the on-disk format or invent our own? Apple's .keychain-db format is undocumented/SEP-entangled in modern form. Inventing a clean format (encrypted SQLite) is simpler and we control it; Apple-path filenames give familiarity without binary-format compatibility (which buys us nothing without macOS interop).

9. References