Skip to content

Commit 952ddb8

Browse files
committed
test(auth-eth): add Halmos symbolic tests for DstackKms root epochs
Prove the root registry step invariant (append-only entries, chain hash recursion, hash-bound activation and pop, kmsInfo following the current epoch), canonical key encodings, authorization epoch bounds, bootstrap-only authorization before registration, KMS and app authorization matching epoch policy, epoch isolation of KMS policy writes, and legacy entry points following the current epochs. Mutation testing confirms each property catches a targeted bug. Document that wrong-kind rejections report root epoch (0, 0x0), unlike the README and the auth-simple and auth-mock backends. Signed-off-by: Kevin Wang <wy721@qq.com>
1 parent 9576277 commit 952ddb8

3 files changed

Lines changed: 515 additions & 0 deletions

File tree

‎dstack/kms/auth-eth/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ slither .
150150
halmos --contract DstackAppSymbolicTest
151151
halmos --contract DstackAppEpochSymbolicTest
152152
halmos --contract DstackKmsSymbolicTest
153+
halmos --contract DstackKmsEpochSymbolicTest
153154
```
154155

155156
Forge 1.8 enables `dynamic_test_linking` by default, which compiles `new` in

‎dstack/kms/auth-eth/docs/formal-verification.md‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,55 @@ counterexample. Dropping the inactive-epoch check in `_authorizeApp` does
151151
not: policy for an inactive epoch cannot be written, so that check is
152152
defense in depth.
153153

154+
`test/DstackKmsEpoch.symbolic.t.sol` (root epochs; setUp registers epochs
155+
0-2, activates 1 so that 2 is pending, admits one KMS identity and OS image
156+
per epoch, and deploys an app at app epoch 1):
157+
- [x] `check_RootRegistry_StepInvariant` — after any single call to any
158+
enumerated mutating function, by any caller with symbolic args: the
159+
current epoch stays registered and moves only by one, through the
160+
owner's activation naming its chain hash; the registry changes by at
161+
most one entry at its end, and only the owner's pop of a pending
162+
epoch naming its chain hash removes one; existing entries keep their
163+
keys and hashes; every chain hash commits to its keys and the previous
164+
hash; `kmsInfo` holds the current keys without legacy evidence.
165+
- [x] `check_RegisterRootEpoch_AcceptsOnlyCanonicalEncodings` — registration
166+
succeeds exactly for a 91-byte uncompressed P-256 SPKI and a 33-byte
167+
compressed secp256k1 key.
168+
- [x] `check_Authorize_EnforcesEpochBounds` — a wrong-kind operation, an app
169+
root epoch above the current one, a KMS root epoch above the latest
170+
registered one, or an app epoch above the app's current one is never
171+
authorized; right-kind results report the current root epoch and hash.
172+
- [x] `check_Authorize_OnlyBootstrapBeforeRegistration` — with no root epoch
173+
registered, only `kms_bootstrap` can be authorized.
174+
- [x] `check_AuthorizeKms_MatchesEpochPolicy` — after any one owner write,
175+
KMS authorization allows exactly when the selected root epoch is
176+
registered, the TCB is up to date, and that epoch's row admits the OS
177+
image, aggregated MR and device.
178+
- [x] `check_AuthorizeApp_MatchesKmsAndAppPolicy` — app authorization allows
179+
exactly when both epochs are active, the selected root epoch admits
180+
the OS image, and the app's own `authorizeApp` admits it.
181+
- [x] `check_KmsPolicyWrite_PreservesOtherEpochDecisions` — a KMS policy
182+
write at one root epoch, through the explicit or the latest-epoch
183+
setters, never changes a KMS decision at another epoch.
184+
- [x] `check_LegacyEntryPoints_FollowCurrentEpochs` — `isKmsAllowed`
185+
decides as `kms_serve` at the current root epoch, and `isAppAllowed`
186+
denies an app that has left app epoch 0.
187+
188+
Mutation-tested: a chain hash that ignores the previous epoch, activation
189+
that leaves `kmsInfo` stale, a pop that ignores the chain hash, app requests
190+
selecting a pending root epoch, KMS requests limited to the current epoch,
191+
`isKmsAllowed` checking the latest epoch, accepting an `0x04` k256 tag, an
192+
OS image row aliasing epoch 1 onto the legacy row, an app check skipping the
193+
OS image, and non-bootstrap operations before registration each produce a
194+
counterexample.
195+
196+
Finding: a wrong-kind operation is rejected before the result is filled with
197+
the current root epoch and chain hash, so it reports `(0, 0x0)`, while the KMS
198+
README states that rejections carry the current epochs and auth-simple and
199+
auth-mock do return them. The KMS then reports "Root epoch not registered"
200+
instead of the rejection reason. A KMS never sends a wrong-kind operation, so
201+
this only affects error reporting.
202+
154203
Single-call (`test/DstackKms.symbolic.t.sol`):
155204
- [x] `check_RegisterApp_AnyCallerCanRegisterNonZeroAddress` + `_RejectsZeroAddress`
156205
— codifies the permissionless-by-design behavior; see "Findings"

0 commit comments

Comments
 (0)