Skip to content

Commit 0702b5c

Browse files
authored
Merge pull request #1567 from Dstack-TEE/feat/iappauth-v2
feat(kms-auth)!: pass the TEE platform to policies and allow restricting it
2 parents f811fbe + cc1d4b9 commit 0702b5c

22 files changed

Lines changed: 501 additions & 31 deletions

‎CHANGELOG.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121

2222
### Added
2323

24+
- DstackKms, DstackApp: TEE platform allowlists. `AuthorizationRequest` of
25+
`IAppEpochAuth` (the epoch-aware policy interface) carries `teeVariant`, the
26+
platform name the KMS reports (`dstack-tdx`, `dstack-amd-sev-snp`, ...),
27+
passed through unchanged by auth-eth. Each contract allows any platform until
28+
its owner calls `setAllowAnyTeeVariant(false)`; it then allows only the names
29+
set by `setAllowedTeeVariants`, from the platforms `TeeVariantPolicy` knows
30+
(setting an unknown one reverts). The policy covers every epoch, and a
31+
restricted one refuses unknown platforms and the legacy `isAppAllowed` and
32+
`isKmsAllowed`, whose callers report no platform. auth-eth requires
33+
`teeVariant` on `/authorizeKms` and `/authorizeApp`.
2434
- KMS root and application key epochs, epoch-scoped authorization, SealedEnv
2535
HPKE encryption, automatic environment migration, and epoch-aware SDK key
2636
selection.
@@ -76,7 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7686
range; `remove*AtEpochs(0, latest root epoch, value)` in `DstackKms` and
7787
`remove*AtEpochs(0, currentAppEpoch, value)` in `DstackApp` revoke a value
7888
everywhere. `DstackApp` reports its setters as the new ERC-165 interface
79-
`IAppEpochManagement` (`0xc65f31b6`). The `Manage.s.sol` scripts require
89+
`IAppEpochManagement` (`0x8c0524c4`). The `Manage.s.sol` scripts require
8090
`ROOT_EPOCH` or `APP_EPOCH`, or a `FROM_*`/`TO_*` range. `DstackApp`'s
8191
`allowAnyDevice` stays a single switch: `setAllowAnyDevice(bool)` covers every
8292
app epoch, so dropping the device restriction cannot leave an older epoch

‎docs/app-key-epochs.md‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@ before using nonzero epochs. Once the app epoch is nonzero, the legacy
2626
derive epoch-zero keys, so the KMS contract calling the app policy must support
2727
epochs before the app epoch advances. The ERC-165 `IAppEpochAuth` capability combines
2828
`currentAppEpoch()` and `authorizeApp(AuthorizationRequest)` in one interface; the
29-
policy returns the same `AuthorizationResult` struct as `DstackKms`. Legacy policies
29+
policy returns the same `AuthorizationResult` struct as `DstackKms`. The request
30+
also carries `teeVariant`, the platform name the KMS reports, which
31+
`allowAnyTeeVariant` and `allowedTeeVariants` in `DstackApp` and `DstackKms`
32+
restrict to the platforms listed in `TeeVariantPolicy.sol`; listing a new
33+
platform takes a contract upgrade. Like `allowAnyDevice`, the policy covers every
34+
epoch: which platforms may hold an app's keys is a trust decision, not a property
35+
of a key generation. Legacy policies
3036
without this capability remain at zero; failures of an advertised getter propagate.
3137

3238
The Ethereum auth backends query the policy through the KMS contract. The simple
@@ -113,7 +119,7 @@ This permits staged key distribution without changing which epoch the KMS serves
113119

114120
The original bool mappings and `allowAnyDevice` retain their storage locations and
115121
are used directly as row zero. New storage consumes storage-gap slots in each
116-
contract (four in `DstackKms`, three in `DstackApp`); no entries are copied and no reinitializer is required. Existing
122+
contract (seven in `DstackKms`, four in `DstackApp`); no entries are copied and no reinitializer is required. Existing
117123
owners, pending owners, policy flags, root registries and app epoch counters are
118124
preserved. Even if the pre-upgrade counter is nonzero, old global material
119125
approvals grant only row zero after upgrading: explicitly populate the intended

‎docs/onchain-governance.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ The central governance contract that manages OS image whitelisting, app registra
158158
| `addOsImageHashAtEpochs(uint256,uint256,bytes32)` | Whitelist an OS image hash in a range of root epochs |
159159
| `removeOsImageHashAtEpochs(uint256,uint256,bytes32)` | Remove an OS image from a range of root epochs; `0` through the latest revokes it |
160160
| `setGatewayAppId(string)` | Set the trusted Gateway app ID(s) |
161+
| `setAllowAnyTeeVariant(bool)` / `setAllowedTeeVariants(string[])` | Allow KMS instances and apps on any TEE platform, or only the listed ones, at every root epoch |
161162
| `registerApp(address)` | Register an app contract |
162163
| `deployAndRegisterApp(...)` | Deploy and register app in one transaction |
163164
| `isAppAllowed(AppBootInfo)` | Check if an app is allowed to boot |
@@ -176,6 +177,7 @@ to the next epoch. Removing a value from rows `0` through the current epoch revo
176177
| `addDeviceAtEpoch(uint256,bytes32)` / `addDeviceAtEpochs(uint256,uint256,bytes32)` | Whitelist a device ID |
177178
| `removeDeviceAtEpoch(uint256,bytes32)` / `removeDeviceAtEpochs(uint256,uint256,bytes32)` | Remove a device |
178179
| `setAllowAnyDevice(bool)` | Allow any device to run this app, at every app epoch |
180+
| `setAllowAnyTeeVariant(bool)` / `setAllowedTeeVariants(string[])` | Allow any TEE platform, or only the listed ones (e.g. `dstack-tdx`), at every app epoch. Set the list before restricting: a restricted empty list refuses every platform |
179181
| `currentAppEpoch()` | The app epoch new keys are derived for |
180182
| `isAppAllowed(AppBootInfo)` | Check if app can boot with given config (app epoch 0 only) |
181183
| `disableUpgrades()` | Permanently disable contract upgrades |
@@ -189,7 +191,7 @@ app epoch 0 and the epoch-less setters `addComposeHash(bytes32)`,
189191
which authorize them for app epoch 0. Tell the two kinds apart by ERC-165 interface ID:
190192

191193
```bash
192-
cast call <app-id> "supportsInterface(bytes4)(bool)" 0xc65f31b6 --rpc-url $RPC_URL # IAppEpochManagement: epoch setters
194+
cast call <app-id> "supportsInterface(bytes4)(bool)" 0x8c0524c4 --rpc-url $RPC_URL # IAppEpochManagement: epoch setters
193195
cast call <app-id> "supportsInterface(bytes4)(bool)" 0x8fd37527 --rpc-url $RPC_URL # IAppAuthBasicManagement: epoch-less setters
194196
```
195197

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,10 @@ this implementation keeps the API of the removed fastify + ethers server that us
188188
#### dstack-kms 0.6
189189

190190
dstack-kms 0.6 posts `dstack_verifier::PolicyBootInfo`: bytes as unprefixed
191-
hex, plus `teeVariant` and `keyProviderInfo`, which the contract does not take
192-
and this backend drops. the KMS checks `appId`, `composeHash` and `instanceId`
193-
widths itself before calling the webhook. the `dstack-kms 0.6 bootAuth payload`
191+
hex, plus `teeVariant` and `keyProviderInfo`. the legacy `/bootAuth/*` routes
192+
drop both; `/authorizeKms` and `/authorizeApp` require `teeVariant` and pass it
193+
to the contract unchanged, and always drop `keyProviderInfo`. the KMS checks
194+
`appId`, `composeHash` and `instanceId` widths itself before calling the webhook. the `dstack-kms 0.6 bootAuth payload`
194195
tests pin this contract.
195196

196197
#### OpenAPI specification

‎dstack/kms/auth-eth/contracts/DstackApp.sol‎

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pragma solidity ^0.8.24;
99
import "./IAppAuth.sol";
1010
import "./AuthorizationOperation.sol";
1111
import "./IAppEpochAuth.sol";
12+
import "./TeeVariantPolicy.sol";
1213
import "./IAppEpochManagement.sol";
1314
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
1415
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
@@ -48,6 +49,9 @@ contract DstackApp is
4849
mapping(uint256 => mapping(bytes32 => bool)) private _composeHashesByEpoch;
4950
mapping(uint256 => mapping(bytes32 => bool)) private _deviceIdsByEpoch;
5051

52+
// The TEE platforms this app may run on.
53+
TeeVariantPolicy.Policy private _teeVariants;
54+
5155
event EpochPolicyChanged(bytes32 indexed policy, bytes32 indexed value, uint256 indexed epoch, bool enabled);
5256

5357
/// @notice Legacy material events. Epoch 0 only; for other epochs use the *AtEpoch getters and EpochPolicyChanged.
@@ -58,6 +62,8 @@ contract DstackApp is
5862
event UpgradesDisabled();
5963
/// @notice Covers every app epoch; there is no EpochPolicyChanged counterpart.
6064
event AllowAnyDeviceSet(bool allowAny);
65+
event AllowAnyTeeVariantSet(bool allowAny);
66+
event AllowedTeeVariantsSet(string[] teeVariants);
6167
event RequireTcbUpToDateSet(bool requireUpToDate);
6268
/// @notice Additive audit event for reconstructing authorization policy.
6369
event PolicyChanged(address indexed actor, bytes32 indexed policy, bytes32 indexed value, bool enabled);
@@ -231,6 +237,30 @@ contract DstackApp is
231237
_emitPolicy("allow-any-device", bytes32(0), allowed);
232238
}
233239

240+
/// @notice Whether this app may run on any TEE platform, rather than only on
241+
/// {allowedTeeVariants}. Covers every app epoch.
242+
function allowAnyTeeVariant() public view returns (bool) {
243+
return !_teeVariants.restricted;
244+
}
245+
246+
/// @notice The TEE platforms, named as the KMS reports them, this app may run on
247+
/// unless {allowAnyTeeVariant}.
248+
function allowedTeeVariants() external view returns (string[] memory) {
249+
return TeeVariantPolicy.allowedNames(_teeVariants);
250+
}
251+
252+
function setAllowAnyTeeVariant(bool allowed) external onlyOwner {
253+
_teeVariants.restricted = !allowed;
254+
emit AllowAnyTeeVariantSet(allowed);
255+
_emitPolicy("allow-any-tee-variant", bytes32(0), allowed);
256+
}
257+
258+
function setAllowedTeeVariants(string[] calldata teeVariants) external onlyOwner {
259+
uint64 mask = TeeVariantPolicy.setAllowed(_teeVariants, teeVariants);
260+
emit AllowedTeeVariantsSet(teeVariants);
261+
_emitPolicy("allowed-tee-variants", bytes32(uint256(mask)), true);
262+
}
263+
234264
// Set whether TCB status must be UpToDate to boot this app
235265
function setRequireTcbUpToDate(bool _requireUpToDate) external onlyOwner {
236266
requireTcbUpToDate = _requireUpToDate;
@@ -278,7 +308,8 @@ contract DstackApp is
278308
returns (bool isAllowed, string memory reason)
279309
{
280310
if (currentAppEpoch != 0) return (false, "App epoch requires epoch-aware authorization");
281-
return _authorizeApp(bootInfo, 0);
311+
// Legacy callers report no platform, so only an unrestricted policy admits them.
312+
return _authorizeApp(bootInfo, allowAnyTeeVariant(), 0);
282313
}
283314

284315
/// Decide the request at the selected app epoch; the KMS fills gateway and root fields.
@@ -294,11 +325,13 @@ contract DstackApp is
294325
return result;
295326
}
296327
uint256 appEpoch = request.requestedAppEpoch.isSet ? request.requestedAppEpoch.value : currentAppEpoch;
297-
(result.authorized, result.rejectionReason) = _authorizeApp(request.appInfo, appEpoch);
328+
(result.authorized, result.rejectionReason) =
329+
_authorizeApp(request.appInfo, TeeVariantPolicy.admits(_teeVariants, request.teeVariant), appEpoch);
298330
}
299331

300332
function _authorizeApp(
301333
IAppAuth.AppInfo calldata bootInfo,
334+
bool teeVariantAllowed,
302335
uint256 appEpoch
303336
)
304337
private
@@ -324,6 +357,10 @@ contract DstackApp is
324357
return (false, "Device not allowed");
325358
}
326359

360+
if (!teeVariantAllowed) {
361+
return (false, "TEE variant not allowed");
362+
}
363+
327364
return (true, "");
328365
}
329366

@@ -344,5 +381,5 @@ contract DstackApp is
344381
}
345382

346383
// Add storage gap for upgradeable contracts
347-
uint256[46] private __gap;
384+
uint256[45] private __gap;
348385
}

‎dstack/kms/auth-eth/contracts/DstackKms.sol‎

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pragma solidity ^0.8.24;
99
import "./IAppAuth.sol";
1010
import "./AuthorizationOperation.sol";
1111
import "./IAppEpochAuth.sol";
12+
import "./TeeVariantPolicy.sol";
1213
import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol";
1314
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
1415
import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
@@ -89,6 +90,9 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
8990
mapping(uint256 => mapping(bytes32 => bool)) private _kmsDeviceIdsByRoot;
9091
mapping(uint256 => mapping(bytes32 => bool)) private _osImagesByRoot;
9192

93+
// The TEE platforms KMS instances and apps may run on.
94+
TeeVariantPolicy.Policy private _teeVariants;
95+
9296
event EpochPolicyChanged(bytes32 indexed policy, bytes32 indexed value, uint256 indexed epoch, bool enabled);
9397

9498
// Events
@@ -104,6 +108,8 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
104108
event OsImageHashAdded(bytes32 osImageHash);
105109
event OsImageHashRemoved(bytes32 osImageHash);
106110
event GatewayAppIdSet(string gatewayAppId);
111+
event AllowAnyTeeVariantSet(bool allowAny);
112+
event AllowedTeeVariantsSet(string[] teeVariants);
107113
// slither-disable-next-line unindexed-event-address
108114
event AppImplementationSet(address implementation);
109115
event RootEpochRegistered(
@@ -267,6 +273,30 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
267273
return (keys.caPubkey, keys.k256Pubkey, keys.evidenceUrl, keys.chainHash);
268274
}
269275

276+
/// @notice Whether KMS instances and apps may run on any TEE platform, rather than only on
277+
/// {allowedTeeVariants}. Covers every root epoch.
278+
function allowAnyTeeVariant() public view returns (bool) {
279+
return !_teeVariants.restricted;
280+
}
281+
282+
/// @notice The TEE platforms, named as the KMS reports them, KMS instances and apps may run on
283+
/// unless {allowAnyTeeVariant}.
284+
function allowedTeeVariants() external view returns (string[] memory) {
285+
return TeeVariantPolicy.allowedNames(_teeVariants);
286+
}
287+
288+
function setAllowAnyTeeVariant(bool allowed) external onlyOwner {
289+
_teeVariants.restricted = !allowed;
290+
emit AllowAnyTeeVariantSet(allowed);
291+
_emitPolicy("allow-any-tee-variant", bytes32(0), allowed);
292+
}
293+
294+
function setAllowedTeeVariants(string[] calldata teeVariants) external onlyOwner {
295+
uint64 mask = TeeVariantPolicy.setAllowed(_teeVariants, teeVariants);
296+
emit AllowedTeeVariantsSet(teeVariants);
297+
_emitPolicy("allowed-tee-variants", bytes32(uint256(mask)), true);
298+
}
299+
270300
// Function to set trusted Gateway App ID
271301
function setGatewayAppId(string memory appId) external onlyOwner {
272302
gatewayAppId = appId;
@@ -550,11 +580,14 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
550580
}
551581
uint256 selectedRootEpoch =
552582
request.requestedRootEpoch.isSet ? request.requestedRootEpoch.value : _currentRootEpoch;
553-
return _checkAppDeployment(request.appInfo, selectedRootEpoch);
583+
return _checkAppDeployment(
584+
request.appInfo, TeeVariantPolicy.admits(_teeVariants, request.teeVariant), selectedRootEpoch
585+
);
554586
}
555587

556588
function _checkAppDeployment(
557589
AppInfo calldata bootInfo,
590+
bool teeVariantAllowed,
558591
uint256 selectedRootEpoch
559592
)
560593
private
@@ -564,6 +597,7 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
564597
if (!registeredApps[bootInfo.appId]) return "App not registered";
565598
if (!allowedOsImagesAtEpoch(selectedRootEpoch, bootInfo.osImageHash)) return "OS image is not allowed";
566599
if (!isContract(bootInfo.appId)) return "App not deployed or invalid address";
600+
if (!teeVariantAllowed) return "TEE variant not allowed";
567601
return "";
568602
}
569603

@@ -600,7 +634,8 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
600634
}
601635
uint256 selectedRootEpoch =
602636
request.requestedRootEpoch.isSet ? request.requestedRootEpoch.value : _currentRootEpoch;
603-
(result.authorized, result.rejectionReason) = _kmsPolicy(request.appInfo, selectedRootEpoch);
637+
(result.authorized, result.rejectionReason) =
638+
_kmsPolicy(request.appInfo, TeeVariantPolicy.admits(_teeVariants, request.teeVariant), selectedRootEpoch);
604639
}
605640

606641
// Legacy entry point for pre-epoch KMS binaries: reads epoch 0, and refuses
@@ -609,10 +644,19 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
609644
if (_currentRootEpoch != 0) {
610645
return (false, "Root epoch requires epoch-aware authorization");
611646
}
612-
return _kmsPolicy(bootInfo, 0);
647+
// Legacy callers report no platform, so only an unrestricted policy admits them.
648+
return _kmsPolicy(bootInfo, allowAnyTeeVariant(), 0);
613649
}
614650

615-
function _kmsPolicy(AppInfo memory bootInfo, uint256 selectedRootEpoch) private view returns (bool, string memory) {
651+
function _kmsPolicy(
652+
AppInfo memory bootInfo,
653+
bool teeVariantAllowed,
654+
uint256 selectedRootEpoch
655+
)
656+
private
657+
view
658+
returns (bool, string memory)
659+
{
616660
// Check if the TCB status is up to date
617661
if (keccak256(abi.encodePacked(bootInfo.tcbStatus)) != keccak256(abi.encodePacked("UpToDate"))) {
618662
return (false, "TCB status is not up to date");
@@ -633,6 +677,10 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
633677
return (false, "KMS is not allowed to boot on this device");
634678
}
635679

680+
if (!teeVariantAllowed) {
681+
return (false, "TEE variant not allowed");
682+
}
683+
636684
return (true, "");
637685
}
638686

@@ -650,7 +698,8 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
650698
if (_appEpoch(bootInfo.appId) != 0) {
651699
return (false, "App epoch requires epoch-aware authorization");
652700
}
653-
string memory rejection = _checkAppDeployment(bootInfo, 0);
701+
// Legacy callers report no platform, so only an unrestricted policy admits them.
702+
string memory rejection = _checkAppDeployment(bootInfo, allowAnyTeeVariant(), 0);
654703
if (bytes(rejection).length != 0) return (false, rejection);
655704
// Call the app's isAppAllowed function. The tuple is forwarded as the
656705
// function's return value; the slither detector trips on the named
@@ -660,7 +709,7 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
660709
}
661710

662711
// Add storage gap for upgradeable contracts
663-
uint256[44] private __gap;
712+
uint256[43] private __gap;
664713
}
665714

666715
function isContract(address addr) view returns (bool) {

‎dstack/kms/auth-eth/contracts/IAppEpochAuth.sol‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ struct AuthorizationRequest {
1919
bytes32 operation;
2020
OptionalUint256 requestedAppEpoch;
2121
OptionalUint256 requestedRootEpoch;
22+
/// The TEE platform the KMS attested, e.g. "dstack-tdx" or "dstack-amd-sev-snp".
23+
/// Measurements, device IDs and TCB status are only meaningful per platform,
24+
/// and one OS image may boot on several.
25+
string teeVariant;
2226
}
2327

2428
struct AuthorizationResult {

‎dstack/kms/auth-eth/contracts/IAppEpochManagement.sol‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ pragma solidity ^0.8.24;
88

99
/// Allowlist management. Each allowlist setter names the app epoch rows it
1010
/// writes: one epoch, or `fromEpoch` through `toEpoch`, at most the next epoch.
11-
/// `setAllowAnyDevice` covers every app epoch.
11+
/// `setAllowAnyDevice`, `setAllowAnyTeeVariant` and `setAllowedTeeVariants` cover
12+
/// every app epoch.
1213
/// Replaces the epoch-less IAppAuthBasicManagement (0x8fd37527), which policies
1314
/// predating app epochs still report; tools tell the two apart by interface ID.
1415
interface IAppEpochManagement {
@@ -21,4 +22,6 @@ interface IAppEpochManagement {
2122
function removeDeviceAtEpoch(uint256 epoch, bytes32 deviceId) external;
2223
function removeDeviceAtEpochs(uint256 fromEpoch, uint256 toEpoch, bytes32 deviceId) external;
2324
function setAllowAnyDevice(bool allowed) external;
25+
function setAllowAnyTeeVariant(bool allowed) external;
26+
function setAllowedTeeVariants(string[] calldata teeVariants) external;
2427
}

0 commit comments

Comments
 (0)