Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions G7SensorKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
69AE26EEA16A596AA47DF83A /* G7DexcomAppWarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11ED0ADBD1E5197198A0B215 /* G7DexcomAppWarningView.swift */; };
6F0232BD04ED7FD3626E0D52 /* G7ShareTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA54130B258935826428FB7C /* G7ShareTests.swift */; };
70DD3285A1241531F66439E3 /* G7DexcomCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D1424E75ED6303B966EF89 /* G7DexcomCredentials.swift */; };
FB2B6214AAB554E3EE99A456 /* G7SensorIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DD3001208B28CF3DE59FB6 /* G7SensorIdentity.swift */; };
72A6F9D68F93AE44814C3C3D /* G7StartupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090FCA4FBE539EA494492C16 /* G7StartupView.swift */; };
74F6EF71F734A96B0DB47F52 /* G7PeripheralManager+Handshake.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6180916611D0990A46355025 /* G7PeripheralManager+Handshake.swift */; };
77A93A0B05D5ADEDF217770B /* G7PairingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7CEC5E1884364C2F6DCCF4 /* G7PairingView.swift */; };
Expand Down Expand Up @@ -111,6 +112,7 @@
EE80FBB62E9FF65D5946BBD7 /* G7ChallengeSigner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1A771C4DCFFA924E278E352 /* G7ChallengeSigner.swift */; };
EF41639FF51B727324A04A49 /* G7NotificationPermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3BAC6D6BA20FCF904FA1465 /* G7NotificationPermissionsView.swift */; };
F5F8174E48028D9B111B8C59 /* G7JPAKETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6888359A74F9F636FBDCA8BB /* G7JPAKETests.swift */; };
CE1322361390E1AAD4E94A46 /* G7SensorIdentityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 051E43BB8C8DAC1C57549FF0 /* G7SensorIdentityTests.swift */; };
F998BB980898C160A2440473 /* G7AuthCryptoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A7A4E8FF66BE03B02B2E01 /* G7AuthCryptoTests.swift */; };
FD0DF29C00B05DECD53A4E2C /* G7CalibrationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF238DACCD930310363D3EB5 /* G7CalibrationMessage.swift */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -173,6 +175,7 @@
21F6EDAE3E7F2B5FF3E87922 /* G7PairingPlannerTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7PairingPlannerTests.swift; sourceTree = "<group>"; };
2A301AE5854C83E5BD9C6539 /* G7SensorModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7SensorModel.swift; sourceTree = "<group>"; };
31D1424E75ED6303B966EF89 /* G7DexcomCredentials.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7DexcomCredentials.swift; sourceTree = "<group>"; };
E9DD3001208B28CF3DE59FB6 /* G7SensorIdentity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7SensorIdentity.swift; sourceTree = "<group>"; };
324F8C967DDB24EB158A1A45 /* G7PCert.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7PCert.swift; sourceTree = "<group>"; };
3B0FD2A42D803BF000E5E921 /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; };
43AE2890ABEAE0FB78B276F1 /* G7SensorPackageTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7SensorPackageTests.swift; sourceTree = "<group>"; };
Expand All @@ -185,6 +188,7 @@
64134474341A32F8DFDA266F /* G7P256Tests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7P256Tests.swift; sourceTree = "<group>"; };
641A515C9511A961C9C88318 /* G7ShareUploader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7ShareUploader.swift; sourceTree = "<group>"; };
6888359A74F9F636FBDCA8BB /* G7JPAKETests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7JPAKETests.swift; sourceTree = "<group>"; };
051E43BB8C8DAC1C57549FF0 /* G7SensorIdentityTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7SensorIdentityTests.swift; sourceTree = "<group>"; };
6C50AD3678079467EE110CD1 /* G7AES.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7AES.swift; sourceTree = "<group>"; };
7028829033065F083529D6A8 /* G7PairingPlanner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7PairingPlanner.swift; sourceTree = "<group>"; };
724358361262BEA3A8F375AA /* G7LifecycleAlertTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = G7LifecycleAlertTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -324,6 +328,7 @@
6C50AD3678079467EE110CD1 /* G7AES.swift */,
B1A771C4DCFFA924E278E352 /* G7ChallengeSigner.swift */,
31D1424E75ED6303B966EF89 /* G7DexcomCredentials.swift */,
E9DD3001208B28CF3DE59FB6 /* G7SensorIdentity.swift */,
63E50F6710E2494A0E14A6D8 /* G7JPAKE.swift */,
324F8C967DDB24EB158A1A45 /* G7PCert.swift */,
);
Expand Down Expand Up @@ -423,6 +428,7 @@
64134474341A32F8DFDA266F /* G7P256Tests.swift */,
94A7A4E8FF66BE03B02B2E01 /* G7AuthCryptoTests.swift */,
6888359A74F9F636FBDCA8BB /* G7JPAKETests.swift */,
051E43BB8C8DAC1C57549FF0 /* G7SensorIdentityTests.swift */,
588CB2A449236A6A47F4579B /* G7SessionModeMigrationTests.swift */,
742542EDF25C573BA2E8B09A /* G7AdvertisementTests.swift */,
21F6EDAE3E7F2B5FF3E87922 /* G7PairingPlannerTests.swift */,
Expand Down Expand Up @@ -818,6 +824,7 @@
2ED1C8E18DD717A8FB98CD3C /* G7AES.swift in Sources */,
EE80FBB62E9FF65D5946BBD7 /* G7ChallengeSigner.swift in Sources */,
70DD3285A1241531F66439E3 /* G7DexcomCredentials.swift in Sources */,
FB2B6214AAB554E3EE99A456 /* G7SensorIdentity.swift in Sources */,
8BB9F821830FA00C1939FFE3 /* G7JPAKE.swift in Sources */,
E18DCCF3754672CBF129B139 /* G7PCert.swift in Sources */,
576A7AA8FC9457FE0F540A86 /* G7Authenticator.swift in Sources */,
Expand Down Expand Up @@ -852,6 +859,7 @@
8C80F065A36E33C97219C570 /* G7P256Tests.swift in Sources */,
F998BB980898C160A2440473 /* G7AuthCryptoTests.swift in Sources */,
F5F8174E48028D9B111B8C59 /* G7JPAKETests.swift in Sources */,
CE1322361390E1AAD4E94A46 /* G7SensorIdentityTests.swift in Sources */,
D3DA1DF230DBADA4CB37F401 /* G7SessionModeMigrationTests.swift in Sources */,
D2F6C2D8999636C3D4F31DB1 /* G7AdvertisementTests.swift in Sources */,
97F1FB097E42B4F376FA206F /* G7PairingPlannerTests.swift in Sources */,
Expand Down
115 changes: 115 additions & 0 deletions G7SensorKit/Crypto/G7SensorIdentity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// G7SensorIdentity.swift
// G7SensorKit
//
// Copyright © 2026 LoopKit Authors. All rights reserved.
//

import CryptoKit
import Foundation
import Security

/// Whether the sensor on the other end of a direct connection is a genuine
/// Dexcom sensor, the g7sim development emulator, or could not be verified.
///
/// A real G7 proves itself during pairing: it streams an X.509 P-256
/// certificate chain whose leaf is signed by Dexcom's sensor intermediate
/// `DEX01PG2`, and signs the display's key-challenge nonce with that leaf key
/// (ECDSA-P256 over SHA256(nonce)). The g7sim emulator mirrors this with its
/// own published CA, so the two are distinguishable. This is a safety marker —
/// it never blocks a connection, and `.unverified` is treated like a real
/// sensor (older sensors, or a capture we cannot parse, simply are not
/// flagged). The emulator's key is public, so `.simulated` only ever errs
/// toward "this is simulated data", never the reverse for a real sensor.
public enum G7SensorAuthenticity: String, Equatable {
case genuine
case simulated
case unverified
}

enum G7SensorIdentity {

/// Dexcom's sensor intermediate (`CN=DEX01PG2`), captured from a real G7 on
/// 2026-10-10. A genuine sensor's leaf is signed by this.
static let dexcomSensorAnchor = Data(hexadecimalString: [
"308201e93082018fa00302010202145bb833870d8ff2cb8bfb33c39c1ba24f96",
"c8b9cd300a06082a8648ce3d04030230133111300f06035504030c0844455830",
"30504731301e170d3233303931333137323830355a170d333830393039313732",
"3830355a30133111300f06035504030c0844455830315047323059301306072a",
"8648ce3d020106082a8648ce3d0301070342000432959b9acf8602090669f3be",
"8834790b9635ccf48bcc92e072c0bad63ba823cd5a84d4e9ea415d3ed64efabd",
"b3a04f9c4ba723af4a751d020ce745700ad6c4a8a381c03081bd300f0603551d",
"130101ff040530030101ff301f0603551d230418301680149e0f1e36f3f276a7",
"01fe8e883a6e26a635bd6afc305a0603551d1f04533051304fa034a032863068",
"7474703a2f2f63726c2e64702e736161732e7072696d656b65792e636f6d2f63",
"726c2f44455830305047312e63726ca217a41530133111300f06035504030c08",
"4445583030504731301d0603551d0e04160414d0d56feb7d94f4535aa0cadd41",
"283536cb3aa2bf300e0603551d0f0101ff040403020186300a06082a8648ce3d",
"0403020348003045022100d023f2223574705f40ab3d04a658a367392167c35c",
"c22be4e7788c1de0c370350220532ef909c220944665bd9ac13cf0f4980d7b1f",
"5bea1f2ca0de1c63be876ae566"
].joined())!

/// The g7sim emulator's CA. Its signing key is public (a development
/// marker), so this proves "simulated", not authenticity.
static let emulatorAnchor = Data(hexadecimalString: [
"308201573081fda003020102020101300a06082a8648ce3d0403023013311130",
"0f06035504031308473753494d2d4341301e170d323630313031303030303030",
"5a170d3436303130313030303030305a30133111300f06035504031308473753",
"494d2d43413059301306072a8648ce3d020106082a8648ce3d03010703420004",
"4ebb9d03cece8ddd5d96d6a350615ea4b6e4ca6817bee3bac0938fb9045da5a9",
"cfda605269304703d2ebc1658d791be44831a3691dc27b3e83af202ed94ac673",
"a3423040300e0603551d0f0101ff040403020284300f0603551d130101ff0405",
"30030101ff301d0603551d0e0416041461e808e54c75334680ca4d6a2642a4c1",
"3ed34aed300a06082a8648ce3d0403020349003046022100d432d52df4713636",
"ecff04699d54cdce48946a0ed30dec1ab65dea4e22317b2c022100aecf28201b",
"e53e58221a1623de554be8a2b5c71e9497a6177fb8c3edf4871027"
].joined())!

/// Classifies a sensor from its leaf certificate, its 64-byte key-challenge
/// signature (raw r‖s), and the nonce the display sent in the `0C` request.
/// Never throws; anything it cannot verify is `.unverified`.
static func authenticity(leafCertificate: Data, challengeSignature: Data, nonce: Data) -> G7SensorAuthenticity {
guard challengeSignature.count == 64,
let leaf = SecCertificateCreateWithData(nil, leafCertificate as CFData),
let leafKey = SecCertificateCopyKey(leaf),
let leafPublicKey = SecKeyCopyExternalRepresentation(leafKey, nil) as Data?,
let publicKey = try? P256.Signing.PublicKey(x963Representation: leafPublicKey),
let signature = try? P256.Signing.ECDSASignature(rawRepresentation: challengeSignature)
else {
return .unverified
}

// The sensor must have signed our nonce with the leaf key: proof it
// holds the private key of the certificate it presented.
guard publicKey.isValidSignature(signature, for: nonce) else {
return .unverified
}

if chains(leaf, toAnchor: dexcomSensorAnchor) {
return .genuine
}
if chains(leaf, toAnchor: emulatorAnchor) {
return .simulated
}
return .unverified
}

/// Whether `leaf` is signed by the single pinned `anchor` (used as the only
/// trust anchor, so neither system roots nor the sensor's own streamed
/// intermediate can substitute for it).
private static func chains(_ leaf: SecCertificate, toAnchor anchorDER: Data) -> Bool {
guard let anchor = SecCertificateCreateWithData(nil, anchorDER as CFData) else {
return false
}
var trust: SecTrust?
guard SecTrustCreateWithCertificates([leaf, anchor] as CFArray, SecPolicyCreateBasicX509(), &trust) == errSecSuccess,
let trust = trust
else {
return false
}
SecTrustSetAnchorCertificates(trust, [anchor] as CFArray)
SecTrustSetAnchorCertificatesOnly(trust, true)
return SecTrustEvaluateWithError(trust, nil)
}
}
46 changes: 39 additions & 7 deletions G7SensorKit/G7CGMManager/G7Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ final class G7Authenticator {
/// Whether this run performed the EC-JPAKE exchange (as opposed to
/// reusing a stored key).
let didExchangeKeys: Bool
/// Whether the sensor proved itself genuine Dexcom, the g7sim emulator,
/// or could not be verified. Only meaningful after a fresh pairing (a
/// reconnect does not re-exchange certificates); `.unverified` then.
let authenticity: G7SensorAuthenticity
}

/// Per-step response deadline for a reconnect. Short: the link is up and
Expand All @@ -134,6 +138,13 @@ final class G7Authenticator {
/// Which of the sensor's display slots this client takes.
private let displayType: G7DisplayType

/// Captured during a fresh pairing to classify the sensor afterwards: its
/// leaf certificate, the nonce we sent in the key challenge, and the
/// signature it returned.
private var sensorLeafCertificate: Data?
private var keyChallengeNonce: Data?
private var keyChallengeSignature: Data?

/// Receives a short description of each step as it happens, for the
/// in-app device communication log. A tester reporting "it got stuck"
/// is only diagnosable if these are visible outside Xcode. Never carries
Expand Down Expand Up @@ -226,7 +237,7 @@ final class G7Authenticator {
// pairing; so do we.
if !didExchangeKeys, status.isAuthenticated, status.isBonded {
report("Already authenticated and bonded")
return Result(sharedKey: sharedKey, deviceName: peripheral.peripheral.name, didExchangeKeys: didExchangeKeys)
return Result(sharedKey: sharedKey, deviceName: peripheral.peripheral.name, didExchangeKeys: didExchangeKeys, authenticity: .unverified)
}

if !didExchangeKeys {
Expand All @@ -244,13 +255,27 @@ final class G7Authenticator {
try answerKeyChallenge(peripheral, authentication: authentication)
try finalizeAndBond(peripheral, authentication: authentication)

let authenticity = evaluateAuthenticity()
report("Sensor authenticity: \(authenticity.rawValue)")
return Result(
sharedKey: sharedKey,
deviceName: peripheral.peripheral.name,
didExchangeKeys: didExchangeKeys
didExchangeKeys: didExchangeKeys,
authenticity: authenticity
)
}

/// Classifies the sensor from what it presented during this fresh pairing.
private func evaluateAuthenticity() -> G7SensorAuthenticity {
guard let leaf = sensorLeafCertificate,
let nonce = keyChallengeNonce,
let signature = keyChallengeSignature
else {
return .unverified
}
return G7SensorIdentity.authenticity(leafCertificate: leaf, challengeSignature: signature, nonce: nonce)
}

// MARK: - EC-JPAKE

private func exchangeKeys(_ peripheral: G7PeripheralManager) throws -> Data {
Expand Down Expand Up @@ -389,6 +414,11 @@ final class G7Authenticator {
}

let theirs = try waitForCertificateBytes(peripheral, buffer: buffer, count: expectedLength, step: step)
// Index 1 is the sensor's leaf certificate (index 0 is the issuing
// intermediate), which classifies the sensor after the handshake.
if index == 1 {
sensorLeafCertificate = theirs
}
report("\(step): received \(theirs.count) bytes, sending ours (\(certificate.count) bytes)")
try peripheral.writeCertificateBytes(certificate)
}
Expand All @@ -400,8 +430,10 @@ final class G7Authenticator {
report("\(step): sending a nonce")

let buffer = installCertificateBuffer(peripheral)
let nonce = G7JPAKE.secureRandomBytes(16)
keyChallengeNonce = nonce
try peripheral.writeValue(
Data([0x0C]) + G7JPAKE.secureRandomBytes(16),
Data([0x0C]) + nonce,
for: .authentication,
type: .withResponse
)
Expand All @@ -412,10 +444,10 @@ final class G7Authenticator {
throw G7AuthenticatorError.unexpectedResponse(step: step, response: acknowledgement)
}

// The sensor's own 64-byte block arrives alongside. We do not verify
// it: the sensor is checking us, not the other way round, and the
// signing key it would be checked against is the one we just sent.
_ = try waitForCertificateBytes(peripheral, buffer: buffer, count: 64, step: step)
// The sensor's own 64-byte block arrives alongside: an ECDSA-P256
// signature over SHA256(our nonce) with its leaf key, which classifies
// the sensor (G7SensorIdentity). It is not needed to complete pairing.
keyChallengeSignature = try waitForCertificateBytes(peripheral, buffer: buffer, count: 64, step: step)

let signature = try G7ChallengeSigner.sign(challengeAcknowledgement: acknowledgement)
report("\(step): sending our signature")
Expand Down
Loading