Skip to content

ciris_keyring supersedes the node key it minted seconds earlier when Secure Enclave init fails (-34018) — edge then cannot open the node identity #608

Description

@emooreatx

Summary

On an iOS simulator build without keychain entitlements, ciris_keyring falls back from Secure Enclave to software on every open, and its tier-mismatch rule then supersedes the node key that provision_node_identity minted and verified ~120 ms earlier in the same boot. Edge's use_node_identity open follows and fails with Key not found: ed25519.seed. The engine reports it as a "[NODE-KEY] identity name mismatch", which sends the reader after the wrong cause.

Observed on ciris-server 0.5.204 (ciris_server._native, iOS simulator arm64 slice from ciris-server-v0.5.204-ios.tar.gz), CIRISAgent engine d5bcbdf, macos-15 runner, iOS 26.2 simulator. CIRISClient five-platform gate run 35372833410.

The sequence, from Documents/ciris/logs/ciris-server.log.2026-09-18

17:49:00.076  WARN ciris_keyring::storage: Secure Enclave storage initialization failed, falling back to software alias=ciris-agent-bootstrap error=Key generation failed: SE wrapper key generation failed: (OSStatus error -34018 - failed to add key to keychain ...)
17:49:00.139  WARN ciris_keyring::storage: Secure Enclave storage initialization failed, falling back to software alias=ciris-agent-bootstrap ...
17:49:00.250  WARN ciris_keyring::storage: Secure Enclave storage initialization failed, falling back to software alias=ciris-agent-bootstrap-node ...
17:49:00.337  INFO ciris_server::node_key: node key registered (identity_type=node) node_key_id=ciris-node-bootstrap-cbm6itzsb7
17:49:00.351  INFO ciris_server::node_key: node identity provisioned and VERIFIED (CC 3.4.7.3) — minted, registered and read back ahead of edge init ... node_key_id=ciris-node-bootstrap-cbm6itzsb7 alias=ciris-node-bootstrap
17:49:00.470  WARN ciris_keyring::storage: KEY SUPERSEDED — a higher-tier backend minted a FRESH key for this alias; a prior-install key was found and ARCHIVED ... alias=ciris-node-bootstrap
17:49:00.471  WARN ciris_keyring::storage: KEY SUPERSEDED — ... alias=ciris-node-bootstrap

Then, from the engine (ciris_agent_*.log):

[NODE-KEY] identity name mismatch: we provisioned 'ciris-node-bootstrap-tksnnmy4nf' under alias 'ciris-agent-bootstrap' ... and edge asked for a different name
[FAIL] Initialize Edge Runtime failed: ... use_node_identity=True: could not open the sealed Ed25519 node identity "ciris-node-bootstrap" under <container>/Documents/ciris/identity: Key not found: ed25519.seed. Edge OPENS this identity and never mints it

And the identity directory after the failure:

ciris-agent-bootstrap.ed25519.seed.blob                         60 B
ciris-agent-bootstrap.master.key                                32 B
ciris-node-bootstrap.ed25519.seed.blob.superseded-pre-v8        60 B   ← the key edge needed
ciris-node-bootstrap.master.key.superseded-pre-v8               32 B
ml_dsa_65.seed / node_ml_dsa_65.seed

Same substrate, signed build: works

With Xcode's default ad-hoc simulator signing (entitlements applied — no CODE_SIGNING_ALLOWED=NO), no -34018, no software fallback, no supersede; the identity dir carries ciris-node-bootstrap.ed25519.seed.blob at 113 B (SE-wrapped) and edge opens it. CIRISAgent's own gate builds this way, which is why its nightly never meets this. Full boot to a serving node in 10.1 s (CIRISClient run 35383728911).

What seems wrong

  1. A key minted, registered and read back 120 ms earlier is not a "prior-install key." The supersede rule fires because the second open of the same alias in the same process took a different tier path (software, after the SE attempt failed) and treated the freshly minted software key as lower-tier leftovers. Within one boot — or more precisely, for a key provision_node_identity just verified — supersede should not be reachable.
  2. The fallback is per-open, not per-process. The SE attempt fails identically three times in 200 ms; the keyring tries SE again each time and re-decides the tier. A process-wide "SE unavailable, software it is" decision would also make the supersede question moot.
  3. The engine's diagnostic points at name derivation ("these are derived independently and there is no parameter to reconcile them"); the names agreed here. That message is in CIRISAgent, but the fact it needs — "the key existed and was superseded by this process" — is only in the Rust log, which Python logging never sees. If KEY SUPERSEDED is ever a legitimate outcome, surfacing it through the PyO3 boundary (a warning the engine can log, or a distinct error from use_node_identity open) would make it diagnosable from the engine side.

Footgun scope: any unentitled or dev build — xcodebuild ... CODE_SIGNING_ALLOWED=NO, or a host without keychain access — hits this deterministically, and the failure reads as an engine name-derivation bug.

Repro

CIRISClient .github/workflows/five-platform-live-qa.yml at b59f572^ (the commit before the signing fix) — the macOS job's "iOS simulator" step; the captured ios-logs/ artifact from run 35372833410 has all of the above. Same step at 58c222c passes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Xv1s8qrUoPoFX2MHZUPA15

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions