-
Notifications
You must be signed in to change notification settings - Fork 443
Tamper-resistant block timing: SCBlockClock + trusted-time verification #930
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9f9113d
777e750
35436d0
838d9de
c00028d
b78bb12
483030f
af07da8
7cc8895
de1d3cb
14b1112
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| #import <Foundation/Foundation.h> | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| /// Tamper-resistant block-elapsed-time accounting. | ||
| /// Combines mach_continuous_time() with a periodic on-disk checkpoint so that | ||
| /// changing the system clock with `sudo date` cannot end a block early. | ||
| /// | ||
| /// All mutating class methods (recordBlockStart, tickCheckpoint) must be invoked | ||
| /// serialized — e.g. from the daemon's main runloop. SCSettings @synchronized | ||
| /// protects each get/set leg, but not the combined read-modify-write pattern. | ||
| @interface SCBlockClock : NSObject | ||
|
|
||
| /// Called once at block start. Writes the BlockTimekeeping dictionary into SCSettings. | ||
| + (void)recordBlockStartWithDuration:(NSTimeInterval)durationSeconds; | ||
|
|
||
| /// As above, but also persists enough block-config to rebuild the block if SCSettings | ||
| /// is tampered with (e.g. the stock SelfControl Killer running resetAllSettingsToDefaults). | ||
| /// BlockTimekeeping is intentionally NOT in defaultSettingsDict, so it survives that reset. | ||
| + (void)recordBlockStartWithDuration:(NSTimeInterval)durationSeconds | ||
| blocklist:(nullable NSArray<NSString*>*)blocklist | ||
| isAllowlist:(BOOL)isAllowlist | ||
| endDate:(nullable NSDate*)endDate; | ||
|
|
||
| /// Block-config previously stashed by recordBlockStart, for the daemon's tampering- | ||
| /// recovery path. Returns nil if no block is recorded or the metadata was never stored. | ||
| + (nullable NSArray<NSString*>*)savedActiveBlocklist; | ||
| + (BOOL)savedActiveBlockAsWhitelist; | ||
| + (nullable NSDate*)savedBlockEndDate; | ||
|
|
||
| /// Erase all block-tracking state. Call after a legitimate block end so a future | ||
| /// checkupBlock does not misread stale data as evidence of tampering. | ||
| + (void)clearAllBlockState; | ||
|
|
||
| /// Called every ~30 s by the daemon. Updates elapsedSecondsAccumulated and the | ||
| /// last-checkpoint values, using the smaller of the wall-clock delta and the | ||
| /// monotonic delta. A negative wall-clock delta credits zero. | ||
| + (void)tickCheckpoint; | ||
|
|
||
| /// Total real seconds elapsed since the block started. Returns 0 if no block. | ||
| + (NSTimeInterval)elapsedSecondsForCurrentBlock; | ||
|
|
||
| /// YES iff elapsedSecondsForCurrentBlock >= the recorded duration. | ||
| + (BOOL)blockDurationHasElapsed; | ||
|
|
||
| /// Recorded total block duration in seconds. Returns 0 if no block is recorded. | ||
| + (NSTimeInterval)blockDurationSeconds; | ||
|
|
||
| /// Seconds left until elapsedSecondsForCurrentBlock reaches blockDurationSeconds. | ||
| /// Clamped at 0; returns 0 if no block is recorded. | ||
| + (NSTimeInterval)remainingSecondsForCurrentBlock; | ||
|
|
||
| #ifdef DEBUG | ||
| /// Test-only override. Pass nil to clear. | ||
| + (void)setBootSessionUUIDOverrideForTesting:(nullable NSString*)uuid; | ||
| #endif | ||
|
|
||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| #import "SCBlockClock.h" | ||
| #import "SCSettings.h" | ||
| #import <mach/mach_time.h> | ||
| #import <sys/sysctl.h> | ||
|
|
||
| static NSString* const kBlockTimekeepingKey = @"BlockTimekeeping"; | ||
| static NSString* const kBlockStartWallClockKey = @"blockStartWallClock"; | ||
| static NSString* const kBlockStartContinuousKey = @"blockStartContinuousTime"; | ||
| static NSString* const kBootSessionUUIDKey = @"bootSessionUUID"; | ||
| static NSString* const kBlockDurationSecondsKey = @"blockDurationSeconds"; | ||
| static NSString* const kElapsedAccumulatedKey = @"elapsedSecondsAccumulated"; | ||
| static NSString* const kLastCheckpointWallKey = @"lastCheckpointWallClock"; | ||
| static NSString* const kLastCheckpointContKey = @"lastCheckpointContinuous"; | ||
|
|
||
| // Tamper-recovery fields. Mirrored from SCSettings at block start so the daemon | ||
| // can reconstitute the block if the user wipes SCSettings (e.g. via the stock | ||
| // SelfControl Killer). These keys live inside BlockTimekeeping, which is not in | ||
| // defaultSettingsDict and therefore survives resetAllSettingsToDefaults. | ||
| static NSString* const kSavedBlocklistKey = @"savedActiveBlocklist"; | ||
| static NSString* const kSavedIsAllowlistKey = @"savedActiveBlockAsWhitelist"; | ||
| static NSString* const kSavedEndDateKey = @"savedBlockEndDate"; | ||
|
|
||
| static NSString* sBootUUIDOverride = nil; | ||
|
|
||
| @implementation SCBlockClock | ||
|
|
||
| + (NSString*)currentBootSessionUUID { | ||
| if (sBootUUIDOverride != nil) return sBootUUIDOverride; | ||
| struct timeval boottime; | ||
| size_t size = sizeof(boottime); | ||
| if (sysctlbyname("kern.boottime", &boottime, &size, NULL, 0) != 0) { | ||
| return @"unknown"; | ||
| } | ||
| return [NSString stringWithFormat: @"%ld.%d", (long)boottime.tv_sec, boottime.tv_usec]; | ||
| } | ||
|
|
||
| #ifdef DEBUG | ||
| + (void)setBootSessionUUIDOverrideForTesting:(NSString*)uuid { | ||
| sBootUUIDOverride = [uuid copy]; | ||
| } | ||
| #endif | ||
|
|
||
| + (uint64_t)continuousNanos { | ||
| static mach_timebase_info_data_t tb; | ||
| static dispatch_once_t once; | ||
| dispatch_once(&once, ^{ mach_timebase_info(&tb); }); | ||
| // mach_continuous_time() returns mach ticks; convert to nanoseconds via the | ||
| // platform timebase (numer/denom). On Apple Silicon ticks != nanoseconds. | ||
| return mach_continuous_time() * tb.numer / tb.denom; | ||
| } | ||
|
|
||
| + (void)recordBlockStartWithDuration:(NSTimeInterval)durationSeconds { | ||
| [self recordBlockStartWithDuration: durationSeconds | ||
| blocklist: nil | ||
| isAllowlist: NO | ||
| endDate: nil]; | ||
| } | ||
|
|
||
| + (void)recordBlockStartWithDuration:(NSTimeInterval)durationSeconds | ||
| blocklist:(NSArray<NSString*>*)blocklist | ||
| isAllowlist:(BOOL)isAllowlist | ||
| endDate:(NSDate*)endDate { | ||
| NSDate* now = [NSDate date]; | ||
| uint64_t cont = [self continuousNanos]; | ||
| NSMutableDictionary* tk = [@{ | ||
| kBlockStartWallClockKey: now, | ||
| kBlockStartContinuousKey: @(cont), | ||
| kBootSessionUUIDKey: [self currentBootSessionUUID], | ||
| kBlockDurationSecondsKey: @(durationSeconds), | ||
| kElapsedAccumulatedKey: @(0.0), | ||
| kLastCheckpointWallKey: now, | ||
| kLastCheckpointContKey: @(cont), | ||
| kSavedIsAllowlistKey: @(isAllowlist), | ||
| } mutableCopy]; | ||
| if (blocklist != nil) tk[kSavedBlocklistKey] = blocklist; | ||
| if (endDate != nil) tk[kSavedEndDateKey] = endDate; | ||
| [[SCSettings sharedSettings] setValue: tk forKey: kBlockTimekeepingKey]; | ||
| } | ||
|
|
||
| + (NSArray<NSString*>*)savedActiveBlocklist { | ||
| NSDictionary* tk = [self readTK]; | ||
| NSArray* list = tk[kSavedBlocklistKey]; | ||
| return [list isKindOfClass: [NSArray class]] ? list : nil; | ||
| } | ||
|
|
||
| + (BOOL)savedActiveBlockAsWhitelist { | ||
| NSDictionary* tk = [self readTK]; | ||
| return [tk[kSavedIsAllowlistKey] boolValue]; | ||
| } | ||
|
|
||
| + (NSDate*)savedBlockEndDate { | ||
| NSDictionary* tk = [self readTK]; | ||
| NSDate* d = tk[kSavedEndDateKey]; | ||
| return [d isKindOfClass: [NSDate class]] ? d : nil; | ||
| } | ||
|
|
||
| + (void)clearAllBlockState { | ||
| [[SCSettings sharedSettings] setValue: nil forKey: kBlockTimekeepingKey]; | ||
| } | ||
|
|
||
| + (NSDictionary*)readTK { | ||
| return [[SCSettings sharedSettings] valueForKey: kBlockTimekeepingKey]; | ||
| } | ||
|
|
||
| + (void)writeTK:(NSDictionary*)tk { | ||
| [[SCSettings sharedSettings] setValue: tk forKey: kBlockTimekeepingKey]; | ||
| } | ||
|
|
||
| + (NSTimeInterval)inFlightDeltaFromTK:(NSDictionary*)tk | ||
| now:(NSDate*)now | ||
| continuousNanos:(uint64_t)cont { | ||
| NSDate* lastWall = tk[kLastCheckpointWallKey]; | ||
| uint64_t lastCont = [tk[kLastCheckpointContKey] unsignedLongLongValue]; | ||
|
|
||
| NSTimeInterval deltaWall = [now timeIntervalSinceDate: lastWall]; | ||
| NSTimeInterval deltaCont = ((double)(cont - lastCont)) / 1e9; | ||
|
|
||
| NSTimeInterval trustedDelta = MIN(deltaCont, MAX(0.0, deltaWall)); | ||
| if (trustedDelta < 0) trustedDelta = 0; // defense vs corrupt persisted lastCont | ||
| return trustedDelta; | ||
| } | ||
|
|
||
| + (void)tickCheckpoint { | ||
| NSDictionary* tk = [self readTK]; | ||
| if (tk == nil) return; | ||
|
|
||
| NSDate* now = [NSDate date]; | ||
| uint64_t cont = [self continuousNanos]; | ||
| NSString* currentBoot = [self currentBootSessionUUID]; | ||
|
|
||
| if (![tk[kBootSessionUUIDKey] isEqualToString: currentBoot]) { | ||
| // Cross-boot: monotonic counter has reset. Credit max(0, wall-clock gap). | ||
| NSTimeInterval gapWall = [now timeIntervalSinceDate: tk[kLastCheckpointWallKey]]; | ||
| NSTimeInterval credit = MAX(0.0, gapWall); | ||
| NSTimeInterval newAccum = [tk[kElapsedAccumulatedKey] doubleValue] + credit; | ||
|
|
||
| NSMutableDictionary* updated = [tk mutableCopy]; | ||
| updated[kElapsedAccumulatedKey] = @(newAccum); | ||
| updated[kLastCheckpointWallKey] = now; | ||
| updated[kLastCheckpointContKey] = @(cont); | ||
| updated[kBootSessionUUIDKey] = currentBoot; | ||
| [self writeTK: updated]; | ||
| return; | ||
| } | ||
|
|
||
| NSTimeInterval trustedDelta = [self inFlightDeltaFromTK: tk now: now continuousNanos: cont]; | ||
| NSTimeInterval newAccum = [tk[kElapsedAccumulatedKey] doubleValue] + trustedDelta; | ||
|
|
||
| NSMutableDictionary* updated = [tk mutableCopy]; | ||
| updated[kElapsedAccumulatedKey] = @(newAccum); | ||
| updated[kLastCheckpointWallKey] = now; | ||
| updated[kLastCheckpointContKey] = @(cont); | ||
| [self writeTK: updated]; | ||
| } | ||
|
|
||
| + (NSTimeInterval)elapsedSecondsForCurrentBlock { | ||
| NSDictionary* tk = [self readTK]; | ||
| if (tk == nil) return 0.0; | ||
| if (![tk[kBootSessionUUIDKey] isEqualToString: [self currentBootSessionUUID]]) { | ||
| return [tk[kElapsedAccumulatedKey] doubleValue]; | ||
| } | ||
| return [tk[kElapsedAccumulatedKey] doubleValue] | ||
| + [self inFlightDeltaFromTK: tk now: [NSDate date] continuousNanos: [self continuousNanos]]; | ||
| } | ||
|
|
||
| + (BOOL)blockDurationHasElapsed { | ||
| NSDictionary* tk = [self readTK]; | ||
| if (tk == nil) return NO; | ||
| return [self elapsedSecondsForCurrentBlock] >= [tk[kBlockDurationSecondsKey] doubleValue]; | ||
| } | ||
|
|
||
| + (NSTimeInterval)blockDurationSeconds { | ||
| NSDictionary* tk = [self readTK]; | ||
| if (tk == nil) return 0.0; | ||
| return [tk[kBlockDurationSecondsKey] doubleValue]; | ||
| } | ||
|
|
||
| + (NSTimeInterval)remainingSecondsForCurrentBlock { | ||
| NSDictionary* tk = [self readTK]; | ||
| if (tk == nil) return 0.0; | ||
| NSTimeInterval duration = [tk[kBlockDurationSecondsKey] doubleValue]; | ||
| NSTimeInterval elapsed = [self elapsedSecondsForCurrentBlock]; | ||
| NSTimeInterval remaining = duration - elapsed; | ||
| return remaining > 0 ? remaining : 0.0; | ||
| } | ||
|
|
||
| @end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
| #import "SCBlockUtilities.h" | ||
| #import "HostFileBlocker.h" | ||
| #import "PacketFilter.h" | ||
| #import "SCBlockClock.h" | ||
|
|
||
| @implementation SCBlockUtilities | ||
|
|
||
|
|
@@ -61,6 +62,11 @@ + (BOOL)currentBlockIsExpired { | |
| } | ||
| } | ||
|
|
||
| + (BOOL)currentBlockIsTrulyExpired { | ||
| if (![self currentBlockIsExpired]) return NO; | ||
| return [SCBlockClock blockDurationHasElapsed]; | ||
| } | ||
|
Comment on lines
+65
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pre-upgrade / missing-checkpoint blocks may never be "truly expired".
Consider either:
The right answer depends on threat-model intent for the upgrade path, but the current behavior is failure-closed in a way that hurts honest users. 🤖 Prompt for AI Agents |
||
|
|
||
| + (BOOL)blockRulesFoundOnSystem { | ||
| return [PacketFilter blockFoundInPF] || [HostFileBlocker blockFoundInHostsFile]; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| #import <Foundation/Foundation.h> | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| /// Baked-in pin list for the trusted-time HTTPS gate. Each entry is | ||
| /// @{ @"host": @"<hostname>", @"spki": @"<base64 SHA-256 of leaf SPKI>" }. | ||
| /// The quorum check requires 2 of 3 to succeed, so a single rotation | ||
| /// failure does not break the gate. | ||
| @interface SCPinnedHosts : NSObject | ||
|
|
||
| + (NSArray<NSDictionary<NSString*, NSString*>*>*)hosts; | ||
|
|
||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| #import "SCPinnedHosts.h" | ||
|
|
||
| @implementation SCPinnedHosts | ||
|
|
||
| + (NSArray<NSDictionary<NSString*, NSString*>*>*)hosts { | ||
| static NSArray* hosts = nil; | ||
| static dispatch_once_t once; | ||
| dispatch_once(&once, ^{ | ||
| // Pins are SHA-256 (base64) of SecKeyCopyExternalRepresentation() output | ||
| // for the leaf certificate's public key — i.e., the raw public-key bytes | ||
| // (RSA modulus+exponent, or EC point), NOT the full ASN.1 SPKI. | ||
| // To recompute, see the SCTrustedTime pinning delegate in SCTrustedTime.m. | ||
| hosts = @[ | ||
| @{ @"host": @"www.apple.com", @"spki": @"vGYr5lNKlPJ0wSZMROTsiP+dd8iCbBOe7YZMVoy4cK0=" }, | ||
| @{ @"host": @"www.google.com", @"spki": @"u6g6X+7KXDBCQ91pQEWODwB8+hbZW43/7pbjYIAzpNU=" }, | ||
| @{ @"host": @"www.cloudflare.com", @"spki": @"b1g23yk9nBZr6VXWRfLBYuSS/kzvrmf565y9Cs+3sNw=" }, | ||
| ]; | ||
| }); | ||
| return hosts; | ||
| } | ||
|
Comment on lines
+5
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❓ Verification inconclusiveScript executed: #!/bin/bash
# Recompute SHA-256(base64) of SecKeyCopyExternalRepresentation()-equivalent
# raw public-key bytes for each pinned host and compare to the baked values.
set -euo pipefail
compute_pin() {
local host="$1"
# Get leaf cert
local pem
pem=$(echo | openssl s_client -servername "$host" -connect "${host}:443" -showcerts 2>/dev/null \
| awk '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/{print; if(/-END/) exit}')
# Extract leaf public key in DER (this gives ASN.1 SPKI, NOT what the code hashes).
# The code hashes SecKeyCopyExternalRepresentation output:
# RSA -> PKCS#1 RSAPublicKey (modulus + exponent)
# EC -> X9.63 uncompressed point (0x04 || X || Y)
local key_alg
key_alg=$(echo "$pem" | openssl x509 -noout -pubkey | openssl pkey -pubin -text -noout 2>/dev/null | head -1)
echo "host=$host"
echo "key_alg=$key_alg"
if echo "$key_alg" | grep -qi 'RSA'; then
# PKCS#1 RSAPublicKey DER:
echo "$pem" | openssl x509 -noout -pubkey \
| openssl rsa -pubin -RSAPublicKey_out -outform DER 2>/dev/null \
| openssl dgst -sha256 -binary | base64
else
# EC X9.63 uncompressed point: extract the BIT STRING from SPKI minus header.
# Approximation: use openssl ec -pubin -outform DER then strip the SPKI prefix.
echo "(EC pin recomputation requires X9.63 raw point — recompute on a Mac with SecKeyCopyExternalRepresentation)"
fi
echo "---"
}
compute_pin www.apple.com
compute_pin www.google.com
compute_pin www.cloudflare.comRepository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol Script executed: # Find references to SCPinnedHosts and understand the unlock/gate logic
rg -l "SCPinnedHosts" --type objc --type swiftRepository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol stdout: Script executed: # Search for usage of the hosts method and understand the verification flow
rg "hosts\]" --type objc -A 3 -B 3 | head -100Repository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol stdout: Script executed: # Look for quorum logic or multiple host verification
rg -i "quorum|verify|pin" --type objc --type swift | head -50Repository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol stdout: Script executed: # Find where the unlock gate logic is implemented
fd -name "*.m" -o -name "*.swift" | xargs rg -l "unlock|gate" | head -20Repository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol stdout: stderr: Script executed: cat -n Common/Utility/SCTrustedTime.m | head -150Repository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol stdout: Script executed: cat -n Common/Utility/SCPinnedHosts.hRepository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol stdout: Script executed: # Look at where unlock/gate logic calls verifyTimeIsAfter
rg -A 10 "verifyTimeIsAfter" Daemon/SCDaemonBlockMethods.mRepository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol stdout: Script executed: rg -i "rotate|fallback|backup|next.*pin|alternate" --type objc --type swift | grep -v "node_modules" | head -30Repository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol stdout: Script executed: # Check the test to see if it actually validates the pins work
cat -n SelfControlTests/SCTrustedTimeTests.mRepository: SelfControlApp/selfcontrol Repository: SelfControlApp/selfcontrol stdout: Hardcoded leaf-cert pins without rotation strategy will lock users on routine certificate rotation. These three hosts rotate leaf certificates regularly. The quorum check (2 of 3) tolerates a single host failure, but simultaneous rotation on two hosts — or one rotation coinciding with any other outage — will hard-lock legitimate users with no recovery path:
Verify each pin is current against the live leaf certificates before merge. The code hashes 🤖 Prompt for AI Agents |
||
|
|
||
| @end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| #import <Foundation/Foundation.h> | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| /// Trusted-internet time verification for the block unlock gate. | ||
| /// All callers must be prepared for any method to return nil/NO on failure. | ||
| @interface SCTrustedTime : NSObject | ||
|
|
||
| /// Parses an HTTP IMF-fixdate (RFC 7231 §7.1.1.1). Returns nil on any error. | ||
| + (nullable NSDate*)parseHTTPDateHeader:(nullable NSString*)header; | ||
|
|
||
| typedef void (^SCTrustedTimeCompletion)(NSDate* _Nullable verifiedTime, NSError* _Nullable error); | ||
|
|
||
| /// Fetches https://<host>/ HEAD and returns the time from the Date: header IFF | ||
| /// the server's leaf SubjectPublicKeyInfo SHA-256 (base64) matches expectedSPKI. | ||
| + (void)fetchTimeFromHost:(NSString*)host | ||
| expectedSPKI:(NSString*)spki | ||
| timeout:(NSTimeInterval)timeoutSeconds | ||
| completion:(SCTrustedTimeCompletion)completion; | ||
|
Comment on lines
+14
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Misleading "SPKI" wording in the public API – this is NOT a SubjectPublicKeyInfo hash. The implementation hashes the output of 📝 Suggested wording fix-/// Fetches https://<host>/ HEAD and returns the time from the Date: header IFF
-/// the server's leaf SubjectPublicKeyInfo SHA-256 (base64) matches expectedSPKI.
+/// Fetches https://<host>/ HEAD and returns the time from the Date: header IFF
+/// SHA-256 (base64) of `SecKeyCopyExternalRepresentation()` for some certificate
+/// in the server's chain matches `expectedPublicKeyHash`. NOTE: this is the raw
+/// public-key bytes (RSA modulus+exponent or EC point), NOT the ASN.1 SPKI hash
+/// produced by typical `openssl ... -pubkey` recipes.
+ (void)fetchTimeFromHost:(NSString*)host
- expectedSPKI:(NSString*)spki
+ expectedPublicKeyHash:(NSString*)keyHash
timeout:(NSTimeInterval)timeoutSeconds
completion:(SCTrustedTimeCompletion)completion;🤖 Prompt for AI Agents |
||
|
|
||
| typedef void (^SCTrustedTimeQuorumCompletion)(BOOL verified, NSDate* _Nullable medianTime, NSError* _Nullable error); | ||
|
|
||
| /// Returns verified=YES iff at least 2 pinned hosts respond, all returned times | ||
| /// are within 5 minutes of each other, and the median response is >= threshold. | ||
| + (void)verifyTimeIsAfter:(NSDate*)threshold | ||
| completion:(SCTrustedTimeQuorumCompletion)completion; | ||
|
|
||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Returning
"unknown"onsysctlbynamefailure can cause a spurious cross-boot credit.If
sysctlbynamefails atrecordBlockStartWithDuration:(persistingbootSessionUUID = "unknown") but later succeeds attickCheckpointtime (returning a real UUID), the mismatch on Line 89 will execute the cross-boot branch and creditMAX(0, wall-clock gap)toward elapsed time. That credit is fully attacker-controlled viasudo datebetween the failure and the next checkpoint. Readingkern.boottimeis extremely unlikely to fail in practice, but the failure mode bypasses exactly the threat model this clock is built to resist.Consider one of: (a) refusing to record a block start when the boot UUID is unavailable, (b) re-reading the UUID at recording time and asserting/logging on failure, or (c) treating
"unknown"specially intickCheckpointso it doesn't trigger the cross-boot path.🤖 Prompt for AI Agents