Repository navigation
Conversation
An Omnipod 5 shows Insulet's pod intermediate in SPS2.1, its own certificate issued by that intermediate in SPS2, and signs the channel-binding transcript with the key inside it. Until now nothing looked at who issued any of it: the pod's signature was checked against whatever key its certificate carried, which proves the pod holds that key and nothing about where the key came from. O5PodIdentity answers the one remaining question — who signed this certificate — and records the answer on the pod so settings can show it. It pins two things: the public key of Insulet's INS01PG1, taken from a real pod, and the certificate authority the LoopKit/pod simulator publishes for exactly this purpose. A pod signed by the first is genuine, by the second simulated, and anything else unverified. This is a marker, not a gate. It never blocks a pairing, and an unverified pod is treated like a genuine one, so an older pod, a different intermediate, or a certificate we cannot parse is never falsely flagged. The simulator's key is public, so "simulated" errs only toward saying something is simulated, never the other way for a real pod. Settings shows a row for the simulated case alone, as the G7 does. It deliberately does not use SecTrust, which G7SensorIdentity does. SecTrust evaluates validity dates, and a real pod was found pairing with a certificate nineteen months past its notAfter — nothing in the pairing had ever looked, so nothing had noticed. Honouring expiry here would have reported pods in the field as unverified. SignedCertificate splits the DER instead and checks the signature against the pinned key, with no notion of time. Two things are unproven. The genuine path has never run against a real pod: the certificate that would make a fixture was truncated in the log it came from, so only the simulated path is covered by a test. And one pod has been seen, whose own certificate names it pod_test_cert, so whether INS01PG1 issues every pod in the field is unknown.
It sat just above the Configuration section, which on an iPhone meant scrolling past the pod image and its delivery figures to find out the pod is not real. A warning nobody scrolls to is not a warning. It goes at the top of the settings list now, above the pod itself.
A simulated pod isn't a danger to anyone, so it doesn't need a warning at the top of the pod's settings. Pod Details now has a Certificate row, "Simulated pod", shown only for a pod whose certificate comes from the pod simulator. Warning about a pod that isn't genuine belongs in pairing, if anywhere.
A pod whose certificate is not Insulet's may be a simulator, or someone intercepting the pairing. Stop the key exchange once the pod has shown its certificate, before the LTK is agreed, hang up, and ask. Continue Pairing pairs again from scratch and lets that attempt through; Cancel leaves the pod untouched so the user can move to a new area and try again. A certificate now only counts as genuine or simulated when the pod also signed the exchange with its key, since a real pod's certificate can be replayed.
CommentIs this really a valid concern where we need to check a certificate is truly from Insulet? As someone who uses the rPi all the time for testing, do we really want to add another question to answer while testing. Unless we really need to add this, I'd prefer we not complicate matters. But if we need to go forward, there were some issues Claude found when reviewing. Claude ReviewI asked Claude (Opus 5.5) to review and here is the response. Review of #202I read the five commits this PR adds on top of The approach holds up. Classification only counts when the pod's transcript signature verifies, and the transcript covers both ephemeral public keys and both nonces. That means a replayed real-pod certificate can't come out as "genuine". Stopping before the LTK is agreed is the right place for the check. Two issues are worth fixing before merge; the rest are smaller. Should fix1. The "accept" flag can carry over to a later, unrelated pairing
2. A single pinned intermediate key, taken from one pod, risks false alarms on real pods
There's also a cost question. After an abort at SPS2 and Suggestions:
Smaller points
Looks good
|
|
Is this needed? I believe it's a real vulnerability with an easy fix, that doesn't impact the normal user in every day use. If it it becomes too much to click through the dialog on pairing as a tester, we could add a "don't show this again" button, or something like that. |
…enuine pod showed - The user's choice to pair with a non-Insulet certificate is now passed down with the pairing attempt (pairAndPrime → blePairAndSetupPod → pairPod) instead of being stored on BlePodComms. A stored flag survived any attempt that failed before the certificate check, such as a failed discovery, and would have let a later pairing skip the prompt. - Insulet anchors are a list, so a new pod intermediate can be added. - When a pod's certificate isn't genuine, log the SPS2.1 intermediate's key and whether the pod's certificate is signed by it, at default level, so a new Insulet intermediate can be identified from a user's log. - Save the result for every O5 pairing. Pod Details shows "Not from Insulet" for a pod the user chose to pair with; pods paired before this change, and DASH pods, show nothing. - Add podAuthenticity to PodState's debug description. - Test that every anchor parses as a P-256 key. - Correct comments that still described an unverified pod as treated like a genuine one, and the bad-signature comment that said pairing continues.
|
Thanks Marion. I checked each point against the code. 56d340b addresses them: Should fix
Smaller points
OmniTests pass (197, 3 skipped) on this branch, which has the latest next-dev merged in. |
|
Tested on a real Omnipod 5 pod. Both builds were next-dev with OmnipodKit at 56d340b, installed on an iPhone 16 Pro. 1. Abort, then pair again with the same pod. I used a local-only debug build that runs the certificate check and then treats every pod as not from Insulet. The "Not from Insulet" alert appeared and I tapped Cancel. The pod kept advertising its unpaired ID ( 2. Genuine path, with the PR exactly as pushed. I installed this build over the debug one and paired the same pod. There was no alert. It paired, primed and finished setup, so the pinned INS01PG1 key matches this pod. Both real-pod checks this PR was waiting on now pass for this pod. |
Test✅ successful test ConfigurationUse a Trio instance because my two test phones are busy with other Loop tests. Trio, dev, commit 685a0d829 Prepare a patch from OmnipodKit PR 202 and apply that to the Trio OmnipodKit submodule Test Detailsbuild onto SE 3rd gen iOS 27.2 phone with Nightscout as CGM and OmnipodKit as the pump manager
Pair an Omnipod 5 rPi Pod with this build:
Repeat this for DASH rPi Pod
Note the Device Name change:There may be code somewhere that expects the device name to be what it used to be. Check LoopFollow code: |
Test Eros Pod✅ successful pair, prime, insert of Eros pod Configuration: Trio / ErosSame Trio build as previous comment ✅ successful pair, prime, insert of Eros pod Pod Details:
Configuration: Loop / Omnipod 5The SE 3rd gen Loop instance has the test fix for dexcom-share-client-swift and the test PR for additional days of carbohydrates display for LoopWorkspace next-dev commit, 1321af1 It is connected to an actual Omnipod 5 pod and is using Dexcom Share as a CGM Evidently the Certificate is not reported if the Omnipod 5 Pod was not paired with this code. |
marionbarker
left a comment
There was a problem hiding this comment.
Approve with caveat that it's not clear why we are changing the old " :: Fake POD ::" name. I think that comes from the pod code, not here anyway.
Tested with an Eros pod - not issues.
Final Claude reviewVerdict: approve. Both blocking issues from the first review are fixed. Most of the smaller points are addressed too. What remains is one real-pod test that should happen before release, plus two minor follow-ups. Providing the suggested tests, but not sure these need to be done and may have already been tested. Before release (not blocking merge)
|
|
I think that the text for pairing popup should be simplified to leave out the details about someone may be intercepting communication as I still believe this is just a theoretical possibility. Also, if we are going to be doing certificate checking, won’t be also be checking its validity period? I’d suggest changing the bold pop up text to a more general “Invalid Pod Certificate” and have a single explanatory text sentence of “The pod certificate is not a valid certificate issued by Insulet.” which would also cover the case of an expired certificate w/o having to go into details, would be fully expected (and not overly obnoxious) when pairing with the O5 pod sim, and wouldn’t be unnecessarily alarming. I would really want to avoid having different mechanisms in OmnipodKit for dealing with pod simulators for different pod types. I have tried to minimize the differences between pod types in OmnipodKit whenever possible, but this PR introduces some unnecessary differences just the O5 pod sim that I’d rather not include. The Dash method for pod sims is to avoid using any special code in OmnipodKit and just have the pod sim itself just use alternate human identifiable strings that show up in Pod Details display in |
An Omnipod 5 shows Insulet's pod intermediate in SPS2.1, its own certificate issued by
that intermediate in SPS2, and signs the channel-binding transcript with the key inside
it. Until now nothing looked at who issued any of it:
o5validatePodSps2checks thepod's signature against whatever key its certificate carried, which proves the pod holds
that key and nothing about where the key came from.
O5PodIdentityanswers the one remaining question — who signed this certificate — andpairing now asks the user before going ahead with a pod whose certificate is not Insulet's.
INS01PG1(issued byINS00PG1), whose public keyis pinned here, captured from a real Omnipod 5 on 2026-10-27, and the pod signed this
exchange with its key.
publishes for this purpose (
go run ./cmd/o5keypair -ca).exchange. A real pod's certificate can be replayed, so the signature is what makes it
the pod's.
Asking at pairing
Pairing is when a faked pod connection matters. If the pod's certificate is not
Insulet's, pairing stops right after the pod shows it, before the LTK is agreed, hangs
up, and asks:
The prompt doesn't say "simulated" even when the certificate is the simulator's. That
key is public, so an attacker could use it too; all we can tell is that it isn't Insulet's.
the certificate. The simulator issues a new pod certificate on every pairing, so the
acceptance can't be tied to one certificate.
pairing screen shows "Pod certificate is not from Insulet" with Retry, and no advice,
because a simulator user expects this.
Once paired, the authenticity is recorded on
PodState. Pod Details shows aCertificate: Simulated pod row for the simulated case.
This follows
G7SensorIdentityclosely — Insulet and Dexcom use the same PKI vendor andthe same naming scheme (
INS01PG1/INS00PG1againstDEX01PG2/DEX00PG1).One deliberate divergence from the G7
G7SensorIdentity.chains(_:toAnchor:)usesSecTrustEvaluateWithError, which evaluatesvalidity dates. A real pod was found pairing with a certificate nineteen months past its
notAfter — nothing in the pairing had ever looked, so nothing had noticed. Using
SecTrust here would have reported pods in the field as unverified.
SignedCertificatetherefore splits the DER and checks the signature against the pinnedkey directly, with no notion of time. The comment says so, to stop it being "fixed" back
into a bug.
What is not proven
The prompt makes these matter more than before. If either fails on a real pod, that pod
lands on
.unverifiedand every user pairing one sees the prompt:a fixture was truncated by
os_log(286 of 799 bytes), so only the simulated path iscovered by a test. The pinned key is complete and the mechanism is proven by the
simulated case, but a real pairing is what would confirm it — one line in the log:
Pod certificate is genuine. The pod's SPS2 signature must also verify on a real pod(
Pod SPS2 signature verification PASSED), which until now was only logged.pod_test_certwithOU=AuthUsers, so it may be a sample rather than retail, and Dexcom'sPG2suffixsuggests these intermediates have generations. Whether
INS01PG1issues every pod inthe field is unknown.
A real Omnipod 5 pairing should confirm both before this ships.
Tests
OmniTests/O5/O5PodIdentityTests— 6 tests, passing: a real simulator-issued certificateclassifies as
.simulatedand not as genuine; tampering with the signed body drops it to.unverified; empty, malformed, truncated and random input are.unverifiedand do notcrash; the DER split finds the signed body and the signature; both pinned anchors are the
shapes they claim to be. All 197 OmniTests pass.
In the iOS Simulator against the pod simulator (with LoopKit/pod#27, which keeps an
unpaired Omnipod 5 pairable after an abandoned exchange):
The branch also merges current
next-dev(#206, the LoopKitimport), which it needs to build against current LoopKit.