Sign a TLK share's extra record fields when verifying it - #4
Merged
gregakespret merged 7 commits intoSep 13, 2026
Merged
Conversation
-[CKKSTLKShare dataForSigning:] signs the seven positional fields and then every record field it doesn't know (bar server_*), sorted by key. We signed only the positional part, so a share carrying any extra field failed its check, and the ? in fetch_shares_for aborted the keychain join on the first one. A beta account fails this way on every attempt, in a different zone each time. Extras are serialized as Apple does: strings as UTF-8, bytes raw, dates as ISO 8601 whole seconds UTC counted from CloudKit's 2001 epoch, numbers as eight little-endian bytes of unsignedLongLongValue. References, lists, assets and locations are skipped. A failed check still aborts the join; it now first logs the record's field names (never values) so the next mismatch can be pinned from the logs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S8kaPMUXu4LSU1K91q6FDN
NSNumber's unsignedLongLongValue only saturates some negative doubles: a whole double below 2^55 reads back as two's complement, and anything else keeps the low 64 bits of CFNumber's 128-bit conversion. Dates are rounded to the nearest millisecond before the fraction is dropped, as CFDateFormatter does. Both were checked against Foundation on arm64. The signature-failure warning now includes the error itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1DuWnYYwPDETmxswCcT8m
One share we couldn't verify threw away every other verified TLK and failed the keychain join. CKKS leaves such a share untrusted and carries on, as we already did for a share from an unknown sender. The share is still never used. If no share verifies at all, the first signature error is returned rather than an empty list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1DuWnYYwPDETmxswCcT8m
authenticated_data_v2 had its own copy of the extra-field encoding, with the bugs the TLK share signature had: dates read as Unix time instead of counted from 2001, negative doubles mapped to 0, and a panic on an out-of-range date. CKKSItem.m uses the same rules as dataForSigning, so both now go through one helper, ckks_extra_field_bytes. An item carrying a date or double field no longer fails decryption. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1DuWnYYwPDETmxswCcT8m
…ger::new login_apple_delegates now takes the AppleAccount, which carries the pet, spd and anisette client itself. PasswordManager::new gained a data_updated callback; the test binary doesn't need one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1DuWnYYwPDETmxswCcT8m
Skipping a share that fails verification could lose a zone's key for good: sync_keychain only fetched shares while the key store was empty, and it saved the zone's change tag even when items in it failed to decrypt, so those items were never downloaded again. sync_keychain now fetches the shares again whenever a requested zone has no key. A failed refetch is only a warning if other keys are already held. A zone keeps its old change tag while any item in it is missing its key, so the item comes back once the key arrives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U1DuWnYYwPDETmxswCcT8m
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fetch_shares_fornow verifies a TLK share over the same bytes Apple signs.-[CKKSTLKShare dataForSigning:](apple-oss-distributions/Security,keychain/ckks/CKKSTLKShare.m) signs the seven positional fields, then every record field it doesn't know, exceptserver_*, sorted by key.data_for_signingtakes the share's record fields and appends those extras the way Apple serializes them:unsignedLongLongValue, including Foundation's handling of negative and very large doubles (checked against Foundation on arm64)Without the extras, a share carrying any extra field fails its check, and the
?aborts the keychain join on the first one. A beta account hits this on every attempt, in a different zone each time (Entering on key Health, thenBackstop), after its bottle has already decrypted.A share that fails verification is now skipped, not fatal. CKKS leaves such a share untrusted and carries on, as we already did for a share from an unknown sender. The share is never used. The
warn!names the error and the record's field names (never values), so the next mismatch shows which field caused it. If no share verifies at all, the first signature error is returned instead of an empty list. So that a skipped share can't cost a zone its key for good,sync_keychainfetches the shares again whenever a requested zone has no key, and a zone keeps its old change tag while any item in it is waiting on a missing key.Item decryption uses the same serializer.
CuttlefishEncItem::authenticated_data_v2had its own copy of the extra-field encoding, with the same bugs: dates read as Unix time, negative doubles mapped to 0, and a panic on an out-of-range date.CKKSItem.muses the same rules asdataForSigning, so both now go throughckks_extra_field_bytes.Also:
src/test.rsbuilds again. Its calls tologin_apple_delegatesandPasswordManager::newwere out of date.Tests: nine unit tests in
keychain.rs. The expected payloads come from Apple's source or were measured on Foundation, not from this code:server_*is left outRun them with
cargo test --lib(10 pass). CI runs tests only once #3 lands.Known gaps: dates before 1582, in year 0 or earlier, or after 9999 still format differently from ICU. It's also unclear how Apple treats an encrypted extra field. Either one only makes a share fail verification; neither can make a forged share pass.
Related: OpenBubbles#34 fixes the same payload and also skips shares that fail.
🤖 Generated with Claude Code
https://claude.ai/code/session_01U1DuWnYYwPDETmxswCcT8m