Apply code-review fixes: identity/pagination, load races, TLS pin, leaks
Addresses a multi-agent code review (26 confirmed findings + a URLSession leak) across correctness, concurrency, security, and cleanup: - MongoDocument.id: deterministic fallback (was UUID() on every access) to stop SwiftUI List/ForEach identity churn; getAll pagination drives its cursor off the real _id via documentId, so a missing _id stops paging instead of looping on a fabricated cursor. - View models: per-load generation guard (+ captured-tab guard on ONU detail) so a cancelled or overlapping load can't commit stale results or land data on the wrong tab. - JSONValue.intValue: guard Int(d) against NaN/inf/out-of-range (crash). - ServerTrustEvaluator: hash the real SubjectPublicKeyInfo (prepend the algorithm-specific ASN.1 SPKI header) so a standard openssl-captured public-key pin actually matches; unknown key types fail closed. - AppEnvironment.configure: clear stale cookies + Keychain creds on host change. - FirmwareFile.isCompatible: empty metadata no longer passes as compatible-for-all. - APIClient: invalidate URLSession on deinit (per-configure/probe leak). - Dashboard best-effort sections keep last-good on transient failure; unified optical thresholds; LoginView prefill-once; populatedBuckets unique ForEach id; Format rounding rollover; dead-code/docstring/dedup cleanups. Verification pending on the Mac (no Swift toolchain on the Linux dev box). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8de962eeb8
commit
203465913c
21 changed files with 303 additions and 103 deletions
|
|
@ -84,16 +84,69 @@ final class ServerTrustEvaluator: NSObject, URLSessionDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
/// Base64 of SHA-256 over the DER-encoded SubjectPublicKeyInfo's key bits.
|
||||
/// (Capture once with `openssl` and store as a pin; this returns the raw
|
||||
/// public-key hash for comparison.)
|
||||
/// Base64 of SHA-256 over the DER-encoded **SubjectPublicKeyInfo**, i.e. the
|
||||
/// standard public-key pin. `SecKeyCopyExternalRepresentation` returns only the
|
||||
/// raw key (PKCS#1 `RSAPublicKey` for RSA, the `04||X||Y` point for EC) — NOT
|
||||
/// the SPKI — so we prepend the algorithm-specific ASN.1 SPKI header before
|
||||
/// hashing. The result matches the canonical capture:
|
||||
///
|
||||
/// openssl s_client -connect host:443 </dev/null 2>/dev/null \
|
||||
/// | openssl x509 -pubkey -noout \
|
||||
/// | openssl pkey -pubin -outform der \
|
||||
/// | openssl dgst -sha256 -binary | base64
|
||||
///
|
||||
/// Returns nil for key types/sizes without a known header (caller fails closed).
|
||||
static func publicKeySHA256Base64(for certificate: SecCertificate) -> String? {
|
||||
guard let key = SecCertificateCopyKey(certificate),
|
||||
let data = SecKeyCopyExternalRepresentation(key, nil) as Data? else {
|
||||
return nil
|
||||
}
|
||||
return CryptoSHA256.base64(of: data)
|
||||
let raw = SecKeyCopyExternalRepresentation(key, nil) as Data?,
|
||||
let attrs = SecKeyCopyAttributes(key) as? [String: Any],
|
||||
let keyType = attrs[kSecAttrKeyType as String] as? String,
|
||||
let keySize = attrs[kSecAttrKeySizeInBits as String] as? Int,
|
||||
let header = spkiHeader(keyType: keyType, keySizeInBits: keySize)
|
||||
else { return nil }
|
||||
var spki = Data(header)
|
||||
spki.append(raw)
|
||||
return CryptoSHA256.base64(of: spki)
|
||||
}
|
||||
|
||||
/// ASN.1 SubjectPublicKeyInfo prefix for the raw key returned by
|
||||
/// `SecKeyCopyExternalRepresentation`, keyed by (algorithm, key size). These
|
||||
/// are the fixed, well-known DER headers; the raw key bits follow.
|
||||
private static func spkiHeader(keyType: String, keySizeInBits: Int) -> [UInt8]? {
|
||||
if keyType == (kSecAttrKeyTypeRSA as String) {
|
||||
switch keySizeInBits {
|
||||
case 2048: return rsa2048SPKIHeader
|
||||
case 3072: return rsa3072SPKIHeader
|
||||
case 4096: return rsa4096SPKIHeader
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
if keyType == (kSecAttrKeyTypeECSECPrimeRandom as String) {
|
||||
switch keySizeInBits {
|
||||
case 256: return ecP256SPKIHeader
|
||||
case 384: return ecP384SPKIHeader
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static let rsa2048SPKIHeader: [UInt8] = [
|
||||
0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
|
||||
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00]
|
||||
private static let rsa3072SPKIHeader: [UInt8] = [
|
||||
0x30, 0x82, 0x01, 0xa2, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
|
||||
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x8f, 0x00]
|
||||
private static let rsa4096SPKIHeader: [UInt8] = [
|
||||
0x30, 0x82, 0x02, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
|
||||
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x02, 0x0f, 0x00]
|
||||
private static let ecP256SPKIHeader: [UInt8] = [
|
||||
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
|
||||
0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
|
||||
0x42, 0x00]
|
||||
private static let ecP384SPKIHeader: [UInt8] = [
|
||||
0x30, 0x76, 0x30, 0x10, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
|
||||
0x01, 0x06, 0x05, 0x2b, 0x81, 0x04, 0x00, 0x22, 0x03, 0x62, 0x00]
|
||||
}
|
||||
|
||||
/// Minimal SHA-256 wrapper using CryptoKit.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue