From 3108ac0e3a3fece4080d05d4f0b7533f061a0fa1 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 12 Nov 2026 02:53:12 -0800 Subject: [PATCH] feat(kms-auth)!: pass the TEE platform to policies and allow restricting it One OS image hash can boot on several TEE platforms (a release ships both TDX and SEV-SNP firmware), and app keys derive from the app ID alone, so an app approved on bare-metal TDX could be served on any platform the KMS enables. The KMS already sends teeVariant; auth-eth dropped it. - Rename the unreleased IAppEpochAuth to IAppAuthV2. AuthorizationRequest carries AppInfoV2: the legacy AppInfo plus the teeVariant name the KMS reports, which auth-eth passes through unchanged, so the KMS defines the names and each policy picks its own representation. DstackKms still forwards AppInfo to policies without IAppAuthV2. - DstackApp and DstackKms allow any platform until the owner calls setAllowAnyTeeVariant(false), then only the names set by setAllowedTeeVariants. TeeVariantPolicy keeps them as one packed slot: a restricted flag and a bitmask over the platform names it hardcodes, so a check costs one read; listing an unknown name reverts, and a restricted policy refuses one. The policy covers every epoch: which platforms may hold keys is a trust decision, not a property of a key generation. A restricted policy refuses the legacy isAppAllowed/isKmsAllowed, whose callers report no platform; the 0.6.x KMS also serves SEV-SNP and Nitro, so they cannot be taken as TDX. - auth-eth requires teeVariant on /authorizeKms and /authorizeApp. - Manage.s.sol SetKmsTeeVariants/SetTeeVariants take TEE_VARIANTS=any or a list of names; Query.s.sol prints the policies. BREAKING CHANGE: unreleased ABI only: authorizeKms/authorizeApp, and the IAppAuthV2 and IAppEpochManagement (now 0x8c0524c4) interface IDs. Signed-off-by: Kevin Wang --- CHANGELOG.md | 12 +- docs/app-key-epochs.md | 16 ++- docs/onchain-governance.md | 4 +- dstack/kms/auth-eth/README.md | 5 +- dstack/kms/auth-eth/contracts/DstackApp.sol | 67 ++++++++-- dstack/kms/auth-eth/contracts/DstackKms.sol | 120 +++++++++++++++--- .../{IAppEpochAuth.sol => IAppAuthV2.sol} | 24 +++- .../contracts/IAppEpochManagement.sol | 5 +- .../auth-eth/contracts/TeeVariantPolicy.sol | 64 ++++++++++ .../kms/auth-eth/docs/formal-verification.md | 4 +- dstack/kms/auth-eth/index.test.ts | 6 +- dstack/kms/auth-eth/index.ts | 15 ++- dstack/kms/auth-eth/openapi.json | 7 + dstack/kms/auth-eth/script/Manage.s.sol | 36 ++++++ dstack/kms/auth-eth/script/Query.s.sol | 9 ++ dstack/kms/auth-eth/script/README.md | 18 +++ dstack/kms/auth-eth/test/AppInfoV2.sol | 23 ++++ dstack/kms/auth-eth/test/DstackApp.t.sol | 67 +++++++++- .../test/DstackAppEpoch.symbolic.t.sol | 3 +- dstack/kms/auth-eth/test/DstackKms.t.sol | 103 +++++++++++---- .../test/DstackKmsEpoch.symbolic.t.sol | 9 +- .../kms/auth-eth/test/LegacyAppCompat.t.sol | 7 +- dstack/kms/auth-eth/test/ManageScript.t.sol | 20 +++ 23 files changed, 552 insertions(+), 92 deletions(-) rename dstack/kms/auth-eth/contracts/{IAppEpochAuth.sol => IAppAuthV2.sol} (62%) create mode 100644 dstack/kms/auth-eth/contracts/TeeVariantPolicy.sol create mode 100644 dstack/kms/auth-eth/test/AppInfoV2.sol diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a360311..094271d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- DstackKms, DstackApp: TEE platform allowlists. `IAppAuthV2` (the epoch-aware + policy interface) receives `AppInfoV2`, the legacy `AppInfo` plus the + `teeVariant` platform name the KMS reports (`dstack-tdx`, `dstack-amd-sev-snp`, + ...), passed through unchanged by auth-eth. Each contract allows any platform + until its owner calls `setAllowAnyTeeVariant(false)`; it then allows only the + names set by `setAllowedTeeVariants`, from the platforms `TeeVariantPolicy` + knows (setting an unknown one reverts). The policy covers every epoch, and a + restricted one refuses unknown platforms and the legacy `isAppAllowed` and + `isKmsAllowed`, whose callers report no platform. auth-eth requires `teeVariant` on + `/authorizeKms` and `/authorizeApp`. - KMS root and application key epochs, epoch-scoped authorization, SealedEnv HPKE encryption, automatic environment migration, and epoch-aware SDK key selection. @@ -76,7 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 range; `remove*AtEpochs(0, latest root epoch, value)` in `DstackKms` and `remove*AtEpochs(0, currentAppEpoch, value)` in `DstackApp` revoke a value everywhere. `DstackApp` reports its setters as the new ERC-165 interface - `IAppEpochManagement` (`0xc65f31b6`). The `Manage.s.sol` scripts require + `IAppEpochManagement` (`0x8c0524c4`). The `Manage.s.sol` scripts require `ROOT_EPOCH` or `APP_EPOCH`, or a `FROM_*`/`TO_*` range. `DstackApp`'s `allowAnyDevice` stays a single switch: `setAllowAnyDevice(bool)` covers every app epoch, so dropping the device restriction cannot leave an older epoch diff --git a/docs/app-key-epochs.md b/docs/app-key-epochs.md index f89f13c64..9c2bc8dd9 100644 --- a/docs/app-key-epochs.md +++ b/docs/app-key-epochs.md @@ -24,9 +24,15 @@ implementation before advancing, and upgrade the KMS/auth backend and guests before using nonzero epochs. Once the app epoch is nonzero, the legacy `isAppAllowed` of both `DstackApp` and `DstackKms` refuses, since its callers derive epoch-zero keys, so the KMS contract calling the app policy must support -epochs before the app epoch advances. The ERC-165 `IAppEpochAuth` capability combines +epochs before the app epoch advances. The ERC-165 `IAppAuthV2` capability combines `currentAppEpoch()` and `authorizeApp(AuthorizationRequest)` in one interface; the -policy returns the same `AuthorizationResult` struct as `DstackKms`. Legacy policies +policy returns the same `AuthorizationResult` struct as `DstackKms`. The request +carries `AppInfoV2`, the legacy `AppInfo` plus the `teeVariant` platform name the +KMS reports, which `allowAnyTeeVariant` and `allowedTeeVariants` in `DstackApp` and +`DstackKms` restrict to the platforms listed in `TeeVariantPolicy.sol`; listing a +new platform takes a contract upgrade. Like `allowAnyDevice`, the policy covers +every epoch: which platforms may hold an app's keys is a trust decision, not a +property of a key generation. Legacy policies without this capability remain at zero; failures of an advertised getter propagate. The Ethereum auth backends query the policy through the KMS contract. The simple @@ -73,7 +79,7 @@ writes app epochs up to the next one. and fail without them. App policies deployed before app epochs (v0.5.x, v0.6.0) keep working with an -upgraded `DstackKms` and KMS: they report no `IAppEpochAuth`, so `authorizeApp` +upgraded `DstackKms` and KMS: they report no `IAppAuthV2`, so `authorizeApp` asks their `isAppAllowed` and only for app epoch zero, and `currentAppEpoch` reports zero for them. Their owners keep the epoch-less setters until they upgrade, which `Manage.s.sol` detects and uses with no epoch or `APP_EPOCH=0`. @@ -113,7 +119,7 @@ This permits staged key distribution without changing which epoch the KMS serves The original bool mappings and `allowAnyDevice` retain their storage locations and are used directly as row zero. New storage consumes storage-gap slots in each -contract (four in `DstackKms`, three in `DstackApp`); no entries are copied and no reinitializer is required. Existing +contract (seven in `DstackKms`, four in `DstackApp`); no entries are copied and no reinitializer is required. Existing owners, pending owners, policy flags, root registries and app epoch counters are preserved. Even if the pre-upgrade counter is nonzero, old global material approvals grant only row zero after upgrading: explicitly populate the intended @@ -121,7 +127,7 @@ nonzero rows. `allowAnyDevice` keeps covering every app epoch. Upgrade the KMS policy before app policies, then approve the intended rows before restarting guests or activating a new root. `DstackApp` advertises the optional -ERC-165 `IAppEpochAuth` capability. `DstackKms.authorizeApp` first applies the +ERC-165 `IAppAuthV2` capability. `DstackKms.authorizeApp` first applies the checks it owns (operation scope, registered root epoch, requested root epoch not beyond the current one, app registration, OS image at the selected root epoch, deployed contract), then forwards the request unchanged to the policy's diff --git a/docs/onchain-governance.md b/docs/onchain-governance.md index cb4555244..1a1546681 100644 --- a/docs/onchain-governance.md +++ b/docs/onchain-governance.md @@ -158,6 +158,7 @@ The central governance contract that manages OS image whitelisting, app registra | `addOsImageHashAtEpochs(uint256,uint256,bytes32)` | Whitelist an OS image hash in a range of root epochs | | `removeOsImageHashAtEpochs(uint256,uint256,bytes32)` | Remove an OS image from a range of root epochs; `0` through the latest revokes it | | `setGatewayAppId(string)` | Set the trusted Gateway app ID(s) | +| `setAllowAnyTeeVariant(bool)` / `setAllowedTeeVariants(string[])` | Allow KMS instances and apps on any TEE platform, or only the listed ones, at every root epoch | | `registerApp(address)` | Register an app contract | | `deployAndRegisterApp(...)` | Deploy and register app in one transaction | | `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 | `addDeviceAtEpoch(uint256,bytes32)` / `addDeviceAtEpochs(uint256,uint256,bytes32)` | Whitelist a device ID | | `removeDeviceAtEpoch(uint256,bytes32)` / `removeDeviceAtEpochs(uint256,uint256,bytes32)` | Remove a device | | `setAllowAnyDevice(bool)` | Allow any device to run this app, at every app epoch | +| `setAllowAnyTeeVariant(bool)` / `setAllowedTeeVariants(string[])` | Allow any TEE platform, or only the listed ones (e.g. `dstack-tdx`), at every app epoch | | `currentAppEpoch()` | The app epoch new keys are derived for | | `isAppAllowed(AppBootInfo)` | Check if app can boot with given config (app epoch 0 only) | | `disableUpgrades()` | Permanently disable contract upgrades | @@ -189,7 +191,7 @@ app epoch 0 and the epoch-less setters `addComposeHash(bytes32)`, which authorize them for app epoch 0. Tell the two kinds apart by ERC-165 interface ID: ```bash -cast call "supportsInterface(bytes4)(bool)" 0xc65f31b6 --rpc-url $RPC_URL # IAppEpochManagement: epoch setters +cast call "supportsInterface(bytes4)(bool)" 0x8c0524c4 --rpc-url $RPC_URL # IAppEpochManagement: epoch setters cast call "supportsInterface(bytes4)(bool)" 0x8fd37527 --rpc-url $RPC_URL # IAppAuthBasicManagement: epoch-less setters ``` diff --git a/dstack/kms/auth-eth/README.md b/dstack/kms/auth-eth/README.md index 5a905ceb2..fae7a0c22 100644 --- a/dstack/kms/auth-eth/README.md +++ b/dstack/kms/auth-eth/README.md @@ -188,8 +188,9 @@ this implementation keeps the API of the removed fastify + ethers server that us #### dstack-kms 0.6 dstack-kms 0.6 posts `dstack_verifier::PolicyBootInfo`: bytes as unprefixed -hex, plus `teeVariant` and `keyProviderInfo`, which the contract does not take -and this backend drops. the KMS checks `appId`, `composeHash` and `instanceId` +hex, plus `teeVariant` and `keyProviderInfo`. The legacy `/bootAuth/*` routes +drop both. `/authorizeKms` and `/authorizeApp` require `teeVariant` and pass it +to the contract unchanged in `AppInfoV2`; `keyProviderInfo` is always dropped. the KMS checks `appId`, `composeHash` and `instanceId` widths itself before calling the webhook. the `dstack-kms 0.6 bootAuth payload` tests pin this contract. diff --git a/dstack/kms/auth-eth/contracts/DstackApp.sol b/dstack/kms/auth-eth/contracts/DstackApp.sol index 96b974c14..55bb68410 100644 --- a/dstack/kms/auth-eth/contracts/DstackApp.sol +++ b/dstack/kms/auth-eth/contracts/DstackApp.sol @@ -8,7 +8,8 @@ pragma solidity ^0.8.24; import "./IAppAuth.sol"; import "./AuthorizationOperation.sol"; -import "./IAppEpochAuth.sol"; +import "./IAppAuthV2.sol"; +import "./TeeVariantPolicy.sol"; import "./IAppEpochManagement.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; @@ -21,7 +22,7 @@ contract DstackApp is UUPSUpgradeable, ERC165Upgradeable, IAppAuth, - IAppEpochAuth, + IAppAuthV2, IAppEpochManagement { /// @notice Allowed compose hashes for this app. Epoch 0 only; for other epochs use the *AtEpoch getters and @@ -48,6 +49,8 @@ contract DstackApp is mapping(uint256 => mapping(bytes32 => bool)) private _composeHashesByEpoch; mapping(uint256 => mapping(bytes32 => bool)) private _deviceIdsByEpoch; + TeeVariantPolicy.Policy private _teeVariants; + event EpochPolicyChanged(bytes32 indexed policy, bytes32 indexed value, uint256 indexed epoch, bool enabled); /// @notice Legacy material events. Epoch 0 only; for other epochs use the *AtEpoch getters and EpochPolicyChanged. @@ -58,6 +61,8 @@ contract DstackApp is event UpgradesDisabled(); /// @notice Covers every app epoch; there is no EpochPolicyChanged counterpart. event AllowAnyDeviceSet(bool allowAny); + event AllowAnyTeeVariantSet(bool allowAny); + event AllowedTeeVariantsSet(string[] teeVariants); event RequireTcbUpToDateSet(bool requireUpToDate); /// @notice Additive audit event for reconstructing authorization policy. event PolicyChanged(address indexed actor, bytes32 indexed policy, bytes32 indexed value, bool enabled); @@ -147,7 +152,7 @@ contract DstackApp is override(ERC165Upgradeable, IERC165) returns (bool) { - return interfaceId == type(IAppEpochAuth).interfaceId || interfaceId == type(IAppEpochManagement).interfaceId + return interfaceId == type(IAppAuthV2).interfaceId || interfaceId == type(IAppEpochManagement).interfaceId || interfaceId == 0x1e079198 // IAppAuth || super.supportsInterface(interfaceId); } @@ -231,6 +236,30 @@ contract DstackApp is _emitPolicy("allow-any-device", bytes32(0), allowed); } + /// @notice Whether this app may run on any TEE platform, rather than only on + /// {allowedTeeVariants}. Covers every app epoch. + function allowAnyTeeVariant() public view returns (bool) { + return !_teeVariants.restricted; + } + + /// @notice The TEE platforms, named as the KMS reports them, this app may run on + /// unless {allowAnyTeeVariant}. + function allowedTeeVariants() external view returns (string[] memory) { + return TeeVariantPolicy.allowedNames(_teeVariants); + } + + function setAllowAnyTeeVariant(bool allowed) external onlyOwner { + _teeVariants.restricted = !allowed; + emit AllowAnyTeeVariantSet(allowed); + _emitPolicy("allow-any-tee-variant", bytes32(0), allowed); + } + + function setAllowedTeeVariants(string[] calldata teeVariants) external onlyOwner { + TeeVariantPolicy.setAllowed(_teeVariants, teeVariants); + emit AllowedTeeVariantsSet(teeVariants); + _emitPolicy("allowed-tee-variants", keccak256(abi.encode(teeVariants)), true); + } + // Set whether TCB status must be UpToDate to boot this app function setRequireTcbUpToDate(bool _requireUpToDate) external onlyOwner { requireTcbUpToDate = _requireUpToDate; @@ -278,7 +307,8 @@ contract DstackApp is returns (bool isAllowed, string memory reason) { if (currentAppEpoch != 0) return (false, "App epoch requires epoch-aware authorization"); - return _authorizeApp(bootInfo, 0); + // Legacy callers report no platform, so only an unrestricted policy admits them. + return _authorizeApp(bootInfo.tcbStatus, bootInfo.composeHash, bootInfo.deviceId, allowAnyTeeVariant(), 0); } /// Decide the request at the selected app epoch; the KMS fills gateway and root fields. @@ -294,11 +324,21 @@ contract DstackApp is return result; } uint256 appEpoch = request.requestedAppEpoch.isSet ? request.requestedAppEpoch.value : currentAppEpoch; - (result.authorized, result.rejectionReason) = _authorizeApp(request.appInfo, appEpoch); + AppInfoV2 calldata bootInfo = request.appInfo; + (result.authorized, result.rejectionReason) = _authorizeApp( + bootInfo.tcbStatus, + bootInfo.composeHash, + bootInfo.deviceId, + TeeVariantPolicy.admits(_teeVariants, bootInfo.teeVariant), + appEpoch + ); } function _authorizeApp( - IAppAuth.AppInfo calldata bootInfo, + string calldata tcbStatus, + bytes32 composeHash, + bytes32 deviceId, + bool teeVariantAllowed, uint256 appEpoch ) private @@ -307,23 +347,24 @@ contract DstackApp is { if (appEpoch > currentAppEpoch) return (false, "Requested app epoch is not active"); // Optionally require TCB status to be up to date - if ( - requireTcbUpToDate - && keccak256(abi.encodePacked(bootInfo.tcbStatus)) != keccak256(abi.encodePacked("UpToDate")) - ) { + if (requireTcbUpToDate && keccak256(abi.encodePacked(tcbStatus)) != keccak256(abi.encodePacked("UpToDate"))) { return (false, "TCB status is not up to date"); } // Check if compose hash is allowed - if (!allowedComposeHashesAtEpoch(appEpoch, bootInfo.composeHash)) { + if (!allowedComposeHashesAtEpoch(appEpoch, composeHash)) { return (false, "Compose hash not allowed"); } // Check if device is allowed (when device restriction is enabled) - if (!allowAnyDevice && !allowedDeviceIdsAtEpoch(appEpoch, bootInfo.deviceId)) { + if (!allowAnyDevice && !allowedDeviceIdsAtEpoch(appEpoch, deviceId)) { return (false, "Device not allowed"); } + if (!teeVariantAllowed) { + return (false, "TEE variant not allowed"); + } + return (true, ""); } @@ -344,5 +385,5 @@ contract DstackApp is } // Add storage gap for upgradeable contracts - uint256[46] private __gap; + uint256[45] private __gap; } diff --git a/dstack/kms/auth-eth/contracts/DstackKms.sol b/dstack/kms/auth-eth/contracts/DstackKms.sol index 91651a39a..448b6450b 100644 --- a/dstack/kms/auth-eth/contracts/DstackKms.sol +++ b/dstack/kms/auth-eth/contracts/DstackKms.sol @@ -8,7 +8,8 @@ pragma solidity ^0.8.24; import "./IAppAuth.sol"; import "./AuthorizationOperation.sol"; -import "./IAppEpochAuth.sol"; +import "./IAppAuthV2.sol"; +import "./TeeVariantPolicy.sol"; import "@openzeppelin/contracts/utils/introspection/ERC165Checker.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; @@ -89,6 +90,8 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E mapping(uint256 => mapping(bytes32 => bool)) private _kmsDeviceIdsByRoot; mapping(uint256 => mapping(bytes32 => bool)) private _osImagesByRoot; + TeeVariantPolicy.Policy private _teeVariants; + event EpochPolicyChanged(bytes32 indexed policy, bytes32 indexed value, uint256 indexed epoch, bool enabled); // Events @@ -104,6 +107,8 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E event OsImageHashAdded(bytes32 osImageHash); event OsImageHashRemoved(bytes32 osImageHash); event GatewayAppIdSet(string gatewayAppId); + event AllowAnyTeeVariantSet(bool allowAny); + event AllowedTeeVariantsSet(string[] teeVariants); // slither-disable-next-line unindexed-event-address event AppImplementationSet(address implementation); event RootEpochRegistered( @@ -267,6 +272,30 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E return (keys.caPubkey, keys.k256Pubkey, keys.evidenceUrl, keys.chainHash); } + /// @notice Whether KMS instances and apps may run on any TEE platform, rather than only on + /// {allowedTeeVariants}. Covers every root epoch. + function allowAnyTeeVariant() public view returns (bool) { + return !_teeVariants.restricted; + } + + /// @notice The TEE platforms, named as the KMS reports them, KMS instances and apps may run on + /// unless {allowAnyTeeVariant}. + function allowedTeeVariants() external view returns (string[] memory) { + return TeeVariantPolicy.allowedNames(_teeVariants); + } + + function setAllowAnyTeeVariant(bool allowed) external onlyOwner { + _teeVariants.restricted = !allowed; + emit AllowAnyTeeVariantSet(allowed); + _emitPolicy("allow-any-tee-variant", bytes32(0), allowed); + } + + function setAllowedTeeVariants(string[] calldata teeVariants) external onlyOwner { + TeeVariantPolicy.setAllowed(_teeVariants, teeVariants); + emit AllowedTeeVariantsSet(teeVariants); + _emitPolicy("allowed-tee-variants", keccak256(abi.encode(teeVariants)), true); + } + // Function to set trusted Gateway App ID function setGatewayAppId(string memory appId) external onlyOwner { gatewayAppId = appId; @@ -494,8 +523,8 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E /// Zero for an app authorization rejects anyway, so the decision reports the rejection. function _appEpoch(address appId) private view returns (uint256) { if (!registeredApps[appId] || appId.code.length == 0) return 0; - if (!ERC165Checker.supportsInterface(appId, type(IAppEpochAuth).interfaceId)) return 0; - return IAppEpochAuth(appId).currentAppEpoch(); + if (!ERC165Checker.supportsInterface(appId, type(IAppAuthV2).interfaceId)) return 0; + return IAppAuthV2(appId).currentAppEpoch(); } /// Authorize an attested identity and return the root epoch from the same state. @@ -514,15 +543,15 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E address appId = request.appInfo.appId; string memory rejection = _checkAppRequest(request); if (bytes(rejection).length == 0) { - if (ERC165Checker.supportsInterface(appId, type(IAppEpochAuth).interfaceId)) { + if (ERC165Checker.supportsInterface(appId, type(IAppAuthV2).interfaceId)) { // The policy owns the operation scope and the app epoch, so its // decision and epoch come from one call. - result = IAppEpochAuth(appId).authorizeApp(request); + result = IAppAuthV2(appId).authorizeApp(request); } else if (request.requestedAppEpoch.isSet && request.requestedAppEpoch.value != 0) { result.rejectionReason = "Requested app epoch is not active"; } else { // Legacy policies only know app epoch zero. - (result.authorized, result.rejectionReason) = IAppAuth(appId).isAppAllowed(request.appInfo); + (result.authorized, result.rejectionReason) = IAppAuth(appId).isAppAllowed(_appInfo(request.appInfo)); } } else { result.rejectionReason = rejection; @@ -550,20 +579,29 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E } uint256 selectedRootEpoch = request.requestedRootEpoch.isSet ? request.requestedRootEpoch.value : _currentRootEpoch; - return _checkAppDeployment(request.appInfo, selectedRootEpoch); + AppInfoV2 calldata bootInfo = request.appInfo; + return _checkAppDeployment( + bootInfo.appId, + bootInfo.osImageHash, + TeeVariantPolicy.admits(_teeVariants, bootInfo.teeVariant), + selectedRootEpoch + ); } function _checkAppDeployment( - AppInfo calldata bootInfo, + address appId, + bytes32 osImageHash, + bool teeVariantAllowed, uint256 selectedRootEpoch ) private view returns (string memory) { - if (!registeredApps[bootInfo.appId]) return "App not registered"; - if (!allowedOsImagesAtEpoch(selectedRootEpoch, bootInfo.osImageHash)) return "OS image is not allowed"; - if (!isContract(bootInfo.appId)) return "App not deployed or invalid address"; + if (!registeredApps[appId]) return "App not registered"; + if (!allowedOsImagesAtEpoch(selectedRootEpoch, osImageHash)) return "OS image is not allowed"; + if (!isContract(appId)) return "App not deployed or invalid address"; + if (!teeVariantAllowed) return "TEE variant not allowed"; return ""; } @@ -600,7 +638,15 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E } uint256 selectedRootEpoch = request.requestedRootEpoch.isSet ? request.requestedRootEpoch.value : _currentRootEpoch; - (result.authorized, result.rejectionReason) = _kmsPolicy(request.appInfo, selectedRootEpoch); + AppInfoV2 calldata bootInfo = request.appInfo; + (result.authorized, result.rejectionReason) = _kmsPolicy( + bootInfo.tcbStatus, + bootInfo.osImageHash, + bootInfo.mrAggregated, + bootInfo.deviceId, + TeeVariantPolicy.admits(_teeVariants, bootInfo.teeVariant), + selectedRootEpoch + ); } // Legacy entry point for pre-epoch KMS binaries: reads epoch 0, and refuses @@ -609,30 +655,48 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E if (_currentRootEpoch != 0) { return (false, "Root epoch requires epoch-aware authorization"); } - return _kmsPolicy(bootInfo, 0); + // Legacy callers report no platform, so only an unrestricted policy admits them. + return _kmsPolicy( + bootInfo.tcbStatus, bootInfo.osImageHash, bootInfo.mrAggregated, bootInfo.deviceId, allowAnyTeeVariant(), 0 + ); } - function _kmsPolicy(AppInfo memory bootInfo, uint256 selectedRootEpoch) private view returns (bool, string memory) { + function _kmsPolicy( + string calldata tcbStatus, + bytes32 osImageHash, + bytes32 mrAggregated, + bytes32 deviceId, + bool teeVariantAllowed, + uint256 selectedRootEpoch + ) + private + view + returns (bool, string memory) + { // Check if the TCB status is up to date - if (keccak256(abi.encodePacked(bootInfo.tcbStatus)) != keccak256(abi.encodePacked("UpToDate"))) { + if (keccak256(abi.encodePacked(tcbStatus)) != keccak256(abi.encodePacked("UpToDate"))) { return (false, "TCB status is not up to date"); } // Check if the OS image is allowed - if (!allowedOsImagesAtEpoch(selectedRootEpoch, bootInfo.osImageHash)) { + if (!allowedOsImagesAtEpoch(selectedRootEpoch, osImageHash)) { return (false, "OS image is not allowed"); } // Check if the aggregated MR is allowed - if (!kmsAllowedAggregatedMrsAtEpoch(selectedRootEpoch, bootInfo.mrAggregated)) { + if (!kmsAllowedAggregatedMrsAtEpoch(selectedRootEpoch, mrAggregated)) { return (false, "Aggregated MR not allowed"); } // Check if the KMS device ID is allowed - if (!kmsAllowedDeviceIdsAtEpoch(selectedRootEpoch, bootInfo.deviceId)) { + if (!kmsAllowedDeviceIdsAtEpoch(selectedRootEpoch, deviceId)) { return (false, "KMS is not allowed to boot on this device"); } + if (!teeVariantAllowed) { + return (false, "TEE variant not allowed"); + } + return (true, ""); } @@ -650,7 +714,8 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E if (_appEpoch(bootInfo.appId) != 0) { return (false, "App epoch requires epoch-aware authorization"); } - string memory rejection = _checkAppDeployment(bootInfo, 0); + // Legacy callers report no platform, so only an unrestricted policy admits them. + string memory rejection = _checkAppDeployment(bootInfo.appId, bootInfo.osImageHash, allowAnyTeeVariant(), 0); if (bytes(rejection).length != 0) return (false, rejection); // Call the app's isAppAllowed function. The tuple is forwarded as the // function's return value; the slither detector trips on the named @@ -659,8 +724,23 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E return IAppAuth(bootInfo.appId).isAppAllowed(bootInfo); } + /// The legacy view of a boot identity, for policies without `IAppAuthV2`. + function _appInfo(AppInfoV2 calldata bootInfo) private pure returns (AppInfo memory) { + return AppInfo({ + appId: bootInfo.appId, + composeHash: bootInfo.composeHash, + instanceId: bootInfo.instanceId, + deviceId: bootInfo.deviceId, + mrAggregated: bootInfo.mrAggregated, + mrSystem: bootInfo.mrSystem, + osImageHash: bootInfo.osImageHash, + tcbStatus: bootInfo.tcbStatus, + advisoryIds: bootInfo.advisoryIds + }); + } + // Add storage gap for upgradeable contracts - uint256[44] private __gap; + uint256[43] private __gap; } function isContract(address addr) view returns (bool) { diff --git a/dstack/kms/auth-eth/contracts/IAppEpochAuth.sol b/dstack/kms/auth-eth/contracts/IAppAuthV2.sol similarity index 62% rename from dstack/kms/auth-eth/contracts/IAppEpochAuth.sol rename to dstack/kms/auth-eth/contracts/IAppAuthV2.sol index be720f49c..97bf07127 100644 --- a/dstack/kms/auth-eth/contracts/IAppEpochAuth.sol +++ b/dstack/kms/auth-eth/contracts/IAppAuthV2.sol @@ -6,7 +6,22 @@ pragma solidity ^0.8.24; -import "./IAppAuth.sol"; +/// The attested boot identity: `IAppAuth.AppInfo` plus the TEE platform, since +/// measurements, device IDs and TCB status are only meaningful per platform and +/// one OS image may boot on several. +struct AppInfoV2 { + address appId; + bytes32 composeHash; + address instanceId; + bytes32 deviceId; + bytes32 mrAggregated; + bytes32 mrSystem; + bytes32 osImageHash; + string tcbStatus; + string[] advisoryIds; + /// The TEE platform the KMS attested, e.g. "dstack-tdx" or "dstack-amd-sev-snp". + string teeVariant; +} /// An explicitly selected value; `isSet == false` selects the current one. struct OptionalUint256 { @@ -15,7 +30,7 @@ struct OptionalUint256 { } struct AuthorizationRequest { - IAppAuth.AppInfo appInfo; + AppInfoV2 appInfo; bytes32 operation; OptionalUint256 requestedAppEpoch; OptionalUint256 requestedRootEpoch; @@ -33,8 +48,9 @@ struct AuthorizationResult { bytes32 latestRootEpochHash; } -/// Optional epoch- and operation-aware policy capability. Legacy policies only authorize app epoch zero. -interface IAppEpochAuth { +/// Optional epoch-, operation- and platform-aware policy capability. Legacy +/// `IAppAuth` policies only authorize app epoch zero. +interface IAppAuthV2 { function currentAppEpoch() external view returns (uint256); /// Receives the KMS request unchanged. The policy validates the operation and app diff --git a/dstack/kms/auth-eth/contracts/IAppEpochManagement.sol b/dstack/kms/auth-eth/contracts/IAppEpochManagement.sol index 8af0f5b85..28ed4e2d7 100644 --- a/dstack/kms/auth-eth/contracts/IAppEpochManagement.sol +++ b/dstack/kms/auth-eth/contracts/IAppEpochManagement.sol @@ -8,7 +8,8 @@ pragma solidity ^0.8.24; /// Allowlist management. Each allowlist setter names the app epoch rows it /// writes: one epoch, or `fromEpoch` through `toEpoch`, at most the next epoch. -/// `setAllowAnyDevice` covers every app epoch. +/// `setAllowAnyDevice`, `setAllowAnyTeeVariant` and `setAllowedTeeVariants` cover +/// every app epoch. /// Replaces the epoch-less IAppAuthBasicManagement (0x8fd37527), which policies /// predating app epochs still report; tools tell the two apart by interface ID. interface IAppEpochManagement { @@ -21,4 +22,6 @@ interface IAppEpochManagement { function removeDeviceAtEpoch(uint256 epoch, bytes32 deviceId) external; function removeDeviceAtEpochs(uint256 fromEpoch, uint256 toEpoch, bytes32 deviceId) external; function setAllowAnyDevice(bool allowed) external; + function setAllowAnyTeeVariant(bool allowed) external; + function setAllowedTeeVariants(string[] calldata teeVariants) external; } diff --git a/dstack/kms/auth-eth/contracts/TeeVariantPolicy.sol b/dstack/kms/auth-eth/contracts/TeeVariantPolicy.sol new file mode 100644 index 000000000..e3ea1badb --- /dev/null +++ b/dstack/kms/auth-eth/contracts/TeeVariantPolicy.sol @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: © 2026 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.24; + +/// The TEE platforms a policy admits, named as the KMS reports them in +/// `AppInfoV2.teeVariant`. It covers every epoch: which platforms may hold keys +/// is a trust decision, not a property of a key generation. +library TeeVariantPolicy { + /// Zero-initialized as unrestricted, so proxies upgraded from before platform + /// policies keep their decisions. Bit `n` of `allowed` admits `names()[n]`. + struct Policy { + bool restricted; + uint64 allowed; + } + + /// The platforms a policy can list, in the order of dstack's `TeeVariant`. + /// Append new ones; a restricted policy refuses names it does not know. + function names() internal pure returns (string[5] memory) { + return ["dstack-tdx", "dstack-gcp-tdx", "dstack-nitro-enclave", "dstack-amd-sev-snp", "dstack-aws-nitro-tpm"]; + } + + function admits(Policy storage policy, string calldata teeVariant) internal view returns (bool) { + if (!policy.restricted) return true; + (bool known, uint8 index) = _indexOf(teeVariant); + return known && (policy.allowed >> index) & 1 == 1; + } + + function setAllowed(Policy storage policy, string[] calldata teeVariants) internal { + uint64 allowed = 0; + for (uint256 i = 0; i < teeVariants.length; ++i) { + (bool known, uint8 index) = _indexOf(teeVariants[i]); + require(known, "unknown TEE variant"); + allowed |= uint64(1) << index; + } + policy.allowed = allowed; + } + + function allowedNames(Policy storage policy) internal view returns (string[] memory list) { + string[5] memory all = names(); + uint64 allowed = policy.allowed; + list = new string[](_popcount(allowed)); + uint256 n = 0; + for (uint256 i = 0; i < all.length; ++i) { + if ((allowed >> i) & 1 == 1) list[n++] = all[i]; + } + } + + function _indexOf(string calldata teeVariant) private pure returns (bool, uint8) { + bytes32 key = keccak256(bytes(teeVariant)); + string[5] memory all = names(); + for (uint8 i = 0; i < all.length; ++i) { + if (keccak256(bytes(all[i])) == key) return (true, i); + } + return (false, 0); + } + + function _popcount(uint64 bits) private pure returns (uint256 count) { + for (; bits != 0; bits &= bits - 1) { + ++count; + } + } +} diff --git a/dstack/kms/auth-eth/docs/formal-verification.md b/dstack/kms/auth-eth/docs/formal-verification.md index 409330c5e..856e7b59f 100644 --- a/dstack/kms/auth-eth/docs/formal-verification.md +++ b/dstack/kms/auth-eth/docs/formal-verification.md @@ -21,7 +21,7 @@ correctness matters more than gas. - `contracts/DstackApp.sol` (209 LOC) — per-app boot authorization - `contracts/DstackKms.sol` (276 LOC) — KMS whitelist + factory -- `contracts/IAppAuth.sol`, `contracts/IAppEpochAuth.sol`, `contracts/IAppEpochManagement.sol` — interfaces +- `contracts/IAppAuth.sol`, `contracts/IAppAuthV2.sol`, `contracts/IAppEpochManagement.sol` — interfaces ## Layer 0 — Slither (static analysis) @@ -202,7 +202,7 @@ per epoch, and deploys an app at app epoch 1): - [x] `check_EpochZeroRows_AreTheLegacyMappings` — after any one owner write, the v0.5.x getters and the epoch 0 getters agree. - [x] `check_AuthorizeApp_LegacyPolicyOnlyAtAppEpochZero` — an app policy - without `IAppEpochAuth`, like v0.5.x and v0.6.0 `DstackApp`, is + without `IAppAuthV2`, like v0.5.x and v0.6.0 `DstackApp`, is authorized exactly when the KMS checks pass at the selected root epoch, no nonzero app epoch is requested, and its `isAppAllowed` admits it; its app epoch is reported as 0. diff --git a/dstack/kms/auth-eth/index.test.ts b/dstack/kms/auth-eth/index.test.ts index 6eff517e3..056d3a9a7 100644 --- a/dstack/kms/auth-eth/index.test.ts +++ b/dstack/kms/auth-eth/index.test.ts @@ -879,7 +879,7 @@ describe('GET /app/:appId', () => { describe('POST /authorizeApp', () => { const appInfo = { - appId: '0x' + '11'.repeat(20), composeHash: '0x' + '22'.repeat(32), + teeVariant: 'dstack-amd-sev-snp', appId: '0x' + '11'.repeat(20), composeHash: '0x' + '22'.repeat(32), instanceId: '', deviceId: '0x' + '33'.repeat(32), mrAggregated: '0x' + '44'.repeat(32), mrSystem: '0x' + '55'.repeat(32), osImageHash: '0x' + '66'.repeat(32), tcbStatus: 'UpToDate', advisoryIds: [], @@ -901,7 +901,7 @@ describe('POST /authorizeApp', () => { expect(mockReadContract).toHaveBeenCalledTimes(1); expect(mockReadContract).toHaveBeenCalledWith(expect.objectContaining({ functionName: 'authorizeApp', blockNumber: 98n, - args: [expect.objectContaining({ appInfo: expect.objectContaining({ appId: appInfo.appId }), requestedAppEpoch: { isSet: false, value: 0n }, requestedRootEpoch: { isSet: false, value: 0n } })], + args: [expect.objectContaining({ appInfo: expect.objectContaining({ teeVariant: appInfo.teeVariant, appId: appInfo.appId }), requestedAppEpoch: { isSet: false, value: 0n }, requestedRootEpoch: { isSet: false, value: 0n } })], })); }); @@ -954,6 +954,8 @@ describe('POST /authorizeApp', () => { const requests = [ { status: 400, body: JSON.stringify({ ...appInfo, operation: 'unknown' }) }, { status: 400, body: JSON.stringify({ ...appInfo, operation: 'app_get_key', mrAggregated: 'not-hex' }) }, + { status: 400, body: JSON.stringify({ ...appInfo, operation: 'app_get_key', teeVariant: undefined }) }, + { status: 400, body: JSON.stringify({ ...appInfo, operation: 'app_get_key', teeVariant: '' }) }, { status: 400, body: '{"appId":' }, { status: 413, body: JSON.stringify({ ...appInfo, operation: 'app_get_key', padding: 'a'.repeat(256 * 1024) }) }, ]; diff --git a/dstack/kms/auth-eth/index.ts b/dstack/kms/auth-eth/index.ts index 380f88805..ce1879c1c 100644 --- a/dstack/kms/auth-eth/index.ts +++ b/dstack/kms/auth-eth/index.ts @@ -38,6 +38,7 @@ const BootInfoSchema = z.object({ }); const AuthorizationRequestSchema = BootInfoSchema.omit({ rootEpoch: true, rootEpochHash: true }).extend({ + teeVariant: z.string().min(1).max(64), operation: z.enum(['kms_bootstrap', 'kms_serve', 'kms_export_keys', 'kms_import_keys', 'app_get_key', 'app_get_cert']), requestedAppEpoch: z.number().int().min(0).max(0xffffffff).optional(), requestedRootEpoch: z.number().int().min(0).max(0xffffffff).optional(), @@ -91,11 +92,13 @@ const APP_INFO_COMPONENTS = [ { name: 'advisoryIds', type: 'string[]' } ] as const; +const APP_INFO_V2_COMPONENTS = [...APP_INFO_COMPONENTS, { name: 'teeVariant', type: 'string' }] as const; + const AUTHORIZATION_ABI = { type: 'function', stateMutability: 'view', inputs: [{ name: 'request', type: 'tuple', components: [ - { name: 'appInfo', type: 'tuple', components: APP_INFO_COMPONENTS }, + { name: 'appInfo', type: 'tuple', components: APP_INFO_V2_COMPONENTS }, { name: 'operation', type: 'bytes32' }, { name: 'requestedAppEpoch', type: 'tuple', components: [ { name: 'isSet', type: 'bool' }, { name: 'value', type: 'uint256' }, @@ -277,17 +280,17 @@ export class EthereumBackend { } - async checkAuthorization(appInfo: AuthorizationRequest, method: 'authorizeKms' | 'authorizeApp') { + async checkAuthorization(request: AuthorizationRequest, method: 'authorizeKms' | 'authorizeApp') { const blockNumber = await this.resolveBlock(); const result = await this.client.readContract({ address: this.kmsContractAddr, abi: DSTACK_KMS_ABI, functionName: method, args: [{ - appInfo: this.contractAppInfo(appInfo), - operation: keccak256(toBytes(appInfo.operation)), - requestedAppEpoch: { isSet: appInfo.requestedAppEpoch !== undefined, value: BigInt(appInfo.requestedAppEpoch ?? 0) }, - requestedRootEpoch: { isSet: appInfo.requestedRootEpoch !== undefined, value: BigInt(appInfo.requestedRootEpoch ?? 0) }, + appInfo: { ...this.contractAppInfo(request), teeVariant: request.teeVariant }, + operation: keccak256(toBytes(request.operation)), + requestedAppEpoch: { isSet: request.requestedAppEpoch !== undefined, value: BigInt(request.requestedAppEpoch ?? 0) }, + requestedRootEpoch: { isSet: request.requestedRootEpoch !== undefined, value: BigInt(request.requestedRootEpoch ?? 0) }, }], blockNumber, }); diff --git a/dstack/kms/auth-eth/openapi.json b/dstack/kms/auth-eth/openapi.json index c0af9430d..b1cb2b5c2 100644 --- a/dstack/kms/auth-eth/openapi.json +++ b/dstack/kms/auth-eth/openapi.json @@ -478,6 +478,12 @@ "maximum": 4294967295, "description": "Omit to use the current application epoch. Explicit zero selects epoch 0; future epochs are denied." }, + "teeVariant": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "description": "TEE platform of the attested identity, as the KMS names it, e.g. dstack-tdx; passed to the contract unchanged." + }, "operation": { "type": "string", "enum": [ @@ -491,6 +497,7 @@ } }, "required": [ + "teeVariant", "operation" ] } diff --git a/dstack/kms/auth-eth/script/Manage.s.sol b/dstack/kms/auth-eth/script/Manage.s.sol index ff5c6bfd7..34bb02768 100644 --- a/dstack/kms/auth-eth/script/Manage.s.sol +++ b/dstack/kms/auth-eth/script/Manage.s.sol @@ -56,6 +56,13 @@ abstract contract BaseScript is Script { return (vm.envUint(from), vm.envUint(to)); } + // TEE_VARIANTS: `any`, or the comma-separated platform names, as the KMS + // reports them, that alone are allowed. + function teeVariants() internal view returns (bool any, string[] memory names) { + names = vm.envString("TEE_VARIANTS", ","); + any = names.length == 1 && keccak256(bytes(names[0])) == keccak256("any"); + } + function rootEpochs() internal view returns (uint256, uint256) { return epochRange("ROOT_EPOCH", "FROM_ROOT_EPOCH", "TO_ROOT_EPOCH"); } @@ -132,6 +139,20 @@ contract AddKmsDevice is BaseScript { } } +contract SetKmsTeeVariants is BaseScript { + function run() external { + (bool any, string[] memory names) = teeVariants(); + + vm.startBroadcast(vm.envUint("PRIVATE_KEY")); + // Write the list before restricting, so the restriction never applies to a stale one. + if (!any) kms.setAllowedTeeVariants(names); + kms.setAllowAnyTeeVariant(any); + vm.stopBroadcast(); + + console.log("Set KMS TEE variants to:", vm.envString("TEE_VARIANTS")); + } +} + contract SetGatewayAppId is BaseScript { function run() external { string memory gatewayId = vm.envString("GATEWAY_APP_ID"); @@ -251,6 +272,21 @@ contract SetAllowAnyDevice is BaseScript { } } +contract SetTeeVariants is BaseScript { + function run() external { + (bool any, string[] memory names) = teeVariants(); + require(address(legacyApp()) == address(0), "this app predates TEE variant policies"); + + vm.startBroadcast(vm.envUint("PRIVATE_KEY")); + // Write the list before restricting, so the restriction never applies to a stale one. + if (!any) app.setAllowedTeeVariants(names); + app.setAllowAnyTeeVariant(any); + vm.stopBroadcast(); + + console.log("Set TEE variants to:", vm.envString("TEE_VARIANTS")); + } +} + // Factory Deployment contract DeployApp is BaseScript { function run() external returns (address) { diff --git a/dstack/kms/auth-eth/script/Query.s.sol b/dstack/kms/auth-eth/script/Query.s.sol index 534b2de46..5c9c59b36 100644 --- a/dstack/kms/auth-eth/script/Query.s.sol +++ b/dstack/kms/auth-eth/script/Query.s.sol @@ -21,6 +21,13 @@ abstract contract BaseQueryScript is Script { DstackKms public kms; DstackApp public app; + function logTeeVariants(bool any, string[] memory names) internal pure { + console.log("Allow Any TEE Variant:", any); + for (uint256 i = 0; i < names.length; ++i) { + console.log(" Allowed TEE variant:", names[i]); + } + } + function setUp() public virtual { address kmsAddr = vm.envOr("KMS_CONTRACT_ADDR", address(0)); if (kmsAddr != address(0)) { @@ -202,6 +209,7 @@ contract GetKmsSettings is BaseQueryScript { console.log("Owner:", kms.owner()); console.log("Gateway App ID:", kms.gatewayAppId()); console.log("App Implementation:", kms.appImplementation()); + logTeeVariants(kms.allowAnyTeeVariant(), kms.allowedTeeVariants()); // Get KMS info (bytes memory k256Pubkey, bytes memory caPubkey, bytes memory quote, bytes memory eventlog) = kms.kmsInfo(); @@ -243,6 +251,7 @@ contract GetAppSettings is BaseQueryScript { console.log("Contract Address:", address(app)); console.log("Owner:", app.owner()); console.log("Allow Any Device:", app.allowAnyDevice()); + logTeeVariants(app.allowAnyTeeVariant(), app.allowedTeeVariants()); // Check implementation via storage bytes32 implSlot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; diff --git a/dstack/kms/auth-eth/script/README.md b/dstack/kms/auth-eth/script/README.md index cb322fc19..a6ebc4b22 100644 --- a/dstack/kms/auth-eth/script/README.md +++ b/dstack/kms/auth-eth/script/README.md @@ -51,6 +51,17 @@ KMS_CONTRACT_ADDR=0x... ROOT_EPOCH=0 DEVICE_ID=0x1234... \ forge script script/Manage.s.sol:AddKmsDevice --broadcast --rpc-url $RPC_URL ``` +#### Set KMS TEE Variants +`TEE_VARIANTS` is `any`, or the comma-separated platform names, as the KMS reports +them, that alone are allowed: `dstack-tdx`, `dstack-gcp-tdx`, `dstack-nitro-enclave`, +`dstack-amd-sev-snp` or `dstack-aws-nitro-tpm` (see `contracts/TeeVariantPolicy.sol`). It applies to KMS instances and every app at every root epoch. A +restricted policy refuses the legacy `isKmsAllowed`/`isAppAllowed`, whose callers +report no platform. +```bash +KMS_CONTRACT_ADDR=0x... TEE_VARIANTS=dstack-tdx \ +forge script script/Manage.s.sol:SetKmsTeeVariants --broadcast --rpc-url $RPC_URL +``` + #### Set Gateway App ID ```bash KMS_CONTRACT_ADDR=0x... GATEWAY_APP_ID="my-gateway" \ @@ -170,6 +181,13 @@ KMS_CONTRACT_ADDR=0x... APP_CONTRACT_ADDR=0x... ALLOW_ANY_DEVICE=true \ forge script script/Manage.s.sol:SetAllowAnyDevice --broadcast --rpc-url $RPC_URL ``` +#### Set TEE Variants +Same `TEE_VARIANTS` values as for the KMS; applies to every app epoch. +```bash +KMS_CONTRACT_ADDR=0x... APP_CONTRACT_ADDR=0x... TEE_VARIANTS=dstack-tdx,dstack-gcp-tdx \ +forge script script/Manage.s.sol:SetTeeVariants --broadcast --rpc-url $RPC_URL +``` + #### Remove Compose Hash ```bash KMS_CONTRACT_ADDR=0x... APP_CONTRACT_ADDR=0x... APP_EPOCH=0 COMPOSE_HASH=0x1234... \ diff --git a/dstack/kms/auth-eth/test/AppInfoV2.sol b/dstack/kms/auth-eth/test/AppInfoV2.sol new file mode 100644 index 000000000..392a40373 --- /dev/null +++ b/dstack/kms/auth-eth/test/AppInfoV2.sol @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: © 2026 Phala Network +// +// SPDX-License-Identifier: Apache-2.0 + +pragma solidity ^0.8.24; + +import "../contracts/IAppAuth.sol"; +import "../contracts/IAppAuthV2.sol"; + +function appInfoV2(IAppAuth.AppInfo memory info) pure returns (AppInfoV2 memory) { + return AppInfoV2({ + appId: info.appId, + composeHash: info.composeHash, + instanceId: info.instanceId, + deviceId: info.deviceId, + mrAggregated: info.mrAggregated, + mrSystem: info.mrSystem, + osImageHash: info.osImageHash, + tcbStatus: info.tcbStatus, + advisoryIds: info.advisoryIds, + teeVariant: "dstack-tdx" + }); +} diff --git a/dstack/kms/auth-eth/test/DstackApp.t.sol b/dstack/kms/auth-eth/test/DstackApp.t.sol index 0a7e0acce..9a65b3d54 100644 --- a/dstack/kms/auth-eth/test/DstackApp.t.sol +++ b/dstack/kms/auth-eth/test/DstackApp.t.sol @@ -10,6 +10,7 @@ import "forge-std/Test.sol"; import "openzeppelin-foundry-upgrades/Upgrades.sol"; import "../contracts/DstackApp.sol"; import "../contracts/IAppAuth.sol"; +import "./AppInfoV2.sol"; contract DstackAppTest is Test { DstackApp public app; @@ -426,7 +427,7 @@ contract DstackAppTest is Test { pure returns (AuthorizationRequest memory) { - return AuthorizationRequest(info, operation, appEpoch, OptionalUint256(false, 0)); + return AuthorizationRequest(appInfoV2(info), operation, appEpoch, OptionalUint256(false, 0)); } function authorizeAt( @@ -442,6 +443,62 @@ contract DstackAppTest is Test { return (result.authorized, result.rejectionReason); } + function test_TeeVariantsAreUnrestrictedUntilRestrictedAndCoverEveryEpoch() public { + IAppAuth.AppInfo memory info = epochInfo(); + vm.startPrank(owner); + app.addComposeHashAtEpochs(0, 1, info.composeHash); + app.addDeviceAtEpochs(0, 1, info.deviceId); + vm.stopPrank(); + AuthorizationRequest memory request = appRequest(info, OptionalUint256(true, 0), keccak256("app_get_key")); + request.appInfo.teeVariant = "dstack-amd-sev-snp"; + assertTrue(app.allowAnyTeeVariant()); + assertTrue(app.authorizeApp(request).authorized); + + string[] memory tdx = new string[](1); + tdx[0] = "dstack-tdx"; + vm.startPrank(owner); + app.setAllowedTeeVariants(tdx); + // The list applies only once the policy is restricted. + assertTrue(app.authorizeApp(request).authorized); + app.setAllowAnyTeeVariant(false); + vm.stopPrank(); + assertEq(app.allowedTeeVariants()[0], "dstack-tdx"); + assertEq(app.authorizeApp(request).rejectionReason, "TEE variant not allowed"); + (bool allowed, string memory reason) = app.isAppAllowed(info); + assertFalse(allowed); + assertEq(reason, "TEE variant not allowed"); + request.appInfo.teeVariant = "dstack-tdx"; + assertTrue(app.authorizeApp(request).authorized); + + // The policy covers every app epoch. + vm.prank(owner); + app.advanceAppEpoch(0); + request.requestedAppEpoch = OptionalUint256(true, 1); + assertTrue(app.authorizeApp(request).authorized); + request.appInfo.teeVariant = "dstack-amd-sev-snp"; + assertEq(app.authorizeApp(request).rejectionReason, "TEE variant not allowed"); + + // Only known platforms can be listed, and an empty list admits none. + string[] memory unknown = new string[](1); + unknown[0] = "dstack-unknown"; + vm.startPrank(owner); + vm.expectRevert("unknown TEE variant"); + app.setAllowedTeeVariants(unknown); + app.setAllowedTeeVariants(new string[](0)); + vm.stopPrank(); + assertEq(app.allowedTeeVariants().length, 0); + request.appInfo.teeVariant = "dstack-tdx"; + assertEq(app.authorizeApp(request).rejectionReason, "TEE variant not allowed"); + + vm.startPrank(user); + bytes memory unauthorized = abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, user); + vm.expectRevert(unauthorized); + app.setAllowAnyTeeVariant(true); + vm.expectRevert(unauthorized); + app.setAllowedTeeVariants(tdx); + vm.stopPrank(); + } + function test_AuthorizeAppOwnsOperationScopeAndAppEpoch() public { IAppAuth.AppInfo memory info = epochInfo(); vm.startPrank(owner); @@ -691,11 +748,11 @@ contract DstackAppTest is Test { } function test_epoch_auth_interface_combines_discovery_and_authorization() public view { - bytes4 combined = IAppEpochAuth.currentAppEpoch.selector ^ IAppEpochAuth.authorizeApp.selector; - assertEq(bytes32(type(IAppEpochAuth).interfaceId), bytes32(combined)); + bytes4 combined = IAppAuthV2.currentAppEpoch.selector ^ IAppAuthV2.authorizeApp.selector; + assertEq(bytes32(type(IAppAuthV2).interfaceId), bytes32(combined)); assertTrue(app.supportsInterface(combined)); - assertFalse(app.supportsInterface(IAppEpochAuth.currentAppEpoch.selector)); - assertFalse(app.supportsInterface(IAppEpochAuth.authorizeApp.selector)); + assertFalse(app.supportsInterface(IAppAuthV2.currentAppEpoch.selector)); + assertFalse(app.supportsInterface(IAppAuthV2.authorizeApp.selector)); } function testFuzz_AppAuthorizationMatchesOnlyTheSelectedRow(uint8 approved, uint8 requested) public { diff --git a/dstack/kms/auth-eth/test/DstackAppEpoch.symbolic.t.sol b/dstack/kms/auth-eth/test/DstackAppEpoch.symbolic.t.sol index e9c644dfe..d96473324 100644 --- a/dstack/kms/auth-eth/test/DstackAppEpoch.symbolic.t.sol +++ b/dstack/kms/auth-eth/test/DstackAppEpoch.symbolic.t.sol @@ -10,6 +10,7 @@ import "forge-std/Test.sol"; import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../contracts/AuthorizationOperation.sol"; import "../contracts/DstackApp.sol"; +import "./AppInfoV2.sol"; function _appAuth( DstackApp target, @@ -21,7 +22,7 @@ function _appAuth( returns (bool, string memory) { AuthorizationResult memory result = target.authorizeApp( - AuthorizationRequest(info, operation, OptionalUint256(true, epoch), OptionalUint256(false, 0)) + AuthorizationRequest(appInfoV2(info), operation, OptionalUint256(true, epoch), OptionalUint256(false, 0)) ); return (result.authorized, result.rejectionReason); } diff --git a/dstack/kms/auth-eth/test/DstackKms.t.sol b/dstack/kms/auth-eth/test/DstackKms.t.sol index 0281634a8..783008996 100644 --- a/dstack/kms/auth-eth/test/DstackKms.t.sol +++ b/dstack/kms/auth-eth/test/DstackKms.t.sol @@ -12,6 +12,7 @@ import "../contracts/DstackKms.sol"; import "../contracts/DstackApp.sol"; import "../contracts/IAppAuth.sol"; import { DstackKms as DstackKmsV059 } from "../contracts/test-utils/reference/v0.5.9/DstackKmsV059.sol"; +import "./AppInfoV2.sol"; contract DstackKmsTest is Test { DstackKms public kms; @@ -494,7 +495,7 @@ contract DstackKmsTest is Test { function authorize(IAppAuth.AppInfo memory info, bool isKms) internal view returns (AuthorizationResult memory) { AuthorizationRequest memory request = AuthorizationRequest( - info, + appInfoV2(info), isKms ? keccak256("kms_serve") : keccak256("app_get_key"), OptionalUint256(false, 0), OptionalUint256(false, 0) @@ -502,6 +503,60 @@ contract DstackKmsTest is Test { return isKms ? kms.authorizeKms(request) : kms.authorizeApp(request); } + function test_TeeVariantsRestrictKmsAndApps() public { + IAppAuth.AppInfo memory info = authorizationInfo(address(kms)); + assertTrue(kms.allowAnyTeeVariant()); + string[] memory tdx = new string[](2); + tdx[0] = "dstack-tdx"; + tdx[1] = "dstack-gcp-tdx"; + vm.startPrank(owner); + kms.registerRootEpoch(ca(0xa0), k256(0xb0), "https://example.com/epoch-0"); + kms.addOsImageHashAtEpoch(0, info.osImageHash); + kms.addKmsDeviceAtEpoch(0, info.deviceId); + kms.addKmsAggregatedMrAtEpoch(0, info.mrAggregated); + address appId = kms.deployAndRegisterApp(user, false, true, info.deviceId, info.composeHash); + kms.setAllowedTeeVariants(tdx); + kms.setAllowAnyTeeVariant(false); + vm.stopPrank(); + + assertTrue(authorize(info, true).authorized); + (bool allowed, string memory reason) = kms.isKmsAllowed(info); + assertFalse(allowed); + assertEq(reason, "TEE variant not allowed"); + AuthorizationRequest memory request = AuthorizationRequest( + appInfoV2(info), keccak256("kms_serve"), OptionalUint256(false, 0), OptionalUint256(false, 0) + ); + request.appInfo.teeVariant = "dstack-gcp-tdx"; + assertTrue(kms.authorizeKms(request).authorized); + request.appInfo.teeVariant = "dstack-amd-sev-snp"; + assertEq(kms.authorizeKms(request).rejectionReason, "TEE variant not allowed"); + // A platform the contract does not know is refused once restricted. + request.appInfo.teeVariant = "dstack-new-platform"; + assertEq(kms.authorizeKms(request).rejectionReason, "TEE variant not allowed"); + + info.appId = appId; + assertTrue(authorize(info, false).authorized); + (allowed, reason) = kms.isAppAllowed(info); + assertFalse(allowed); + assertEq(reason, "TEE variant not allowed"); + request = AuthorizationRequest( + appInfoV2(info), keccak256("app_get_key"), OptionalUint256(false, 0), OptionalUint256(false, 0) + ); + request.appInfo.teeVariant = "dstack-amd-sev-snp"; + assertEq(kms.authorizeApp(request).rejectionReason, "TEE variant not allowed"); + + // Allowing any platform lifts the restriction, including for legacy callers. + vm.prank(owner); + kms.setAllowAnyTeeVariant(true); + assertTrue(kms.authorizeApp(request).authorized); + (allowed,) = kms.isAppAllowed(info); + assertTrue(allowed); + + vm.prank(user); + vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, user)); + kms.setAllowAnyTeeVariant(false); + } + function test_AuthorizationOperationsFailClosedAndBootstrapBeforeRegistration() public { IAppAuth.AppInfo memory info = authorizationInfo(address(kms)); vm.startPrank(owner); @@ -510,7 +565,7 @@ contract DstackKmsTest is Test { kms.addKmsAggregatedMrAtEpoch(0, info.mrAggregated); vm.stopPrank(); AuthorizationRequest memory request = AuthorizationRequest( - info, keccak256("kms_bootstrap"), OptionalUint256(false, 0), OptionalUint256(false, 0) + appInfoV2(info), keccak256("kms_bootstrap"), OptionalUint256(false, 0), OptionalUint256(false, 0) ); assertTrue(kms.authorizeKms(request).authorized); assertEq(kms.authorizeKms(request).currentRootEpochHash, bytes32(0)); @@ -592,8 +647,9 @@ contract DstackKmsTest is Test { result = authorize(info, true); assertFalse(result.authorized); assertEq(result.rejectionReason, "Aggregated MR not allowed"); - AuthorizationRequest memory appRequest = - AuthorizationRequest(info, keccak256("app_get_key"), OptionalUint256(true, 1), OptionalUint256(false, 0)); + AuthorizationRequest memory appRequest = AuthorizationRequest( + appInfoV2(info), keccak256("app_get_key"), OptionalUint256(true, 1), OptionalUint256(false, 0) + ); result = kms.authorizeApp(appRequest); assertFalse(result.authorized); assertEq(result.rejectionReason, "Requested app epoch is not active"); @@ -610,23 +666,21 @@ contract DstackKmsTest is Test { function test_AppEpochCapabilityIsOptionalButGetterFailuresPropagate() public { address policy = address(new DstackApp()); kms.registerApp(policy); - vm.mockCall(policy, abi.encodeCall(IAppEpochAuth.currentAppEpoch, ()), abi.encode(uint256(7))); + vm.mockCall(policy, abi.encodeCall(IAppAuthV2.currentAppEpoch, ()), abi.encode(uint256(7))); assertEq(kms.currentAppEpoch(policy), 7); vm.mockCall( - policy, abi.encodeCall(IERC165.supportsInterface, (type(IAppEpochAuth).interfaceId)), abi.encode(false) + policy, abi.encodeCall(IERC165.supportsInterface, (type(IAppAuthV2).interfaceId)), abi.encode(false) ); // Advertising either old single-method interface does not opt into the combined capability. vm.mockCall( - policy, - abi.encodeCall(IERC165.supportsInterface, (IAppEpochAuth.currentAppEpoch.selector)), - abi.encode(true) + policy, abi.encodeCall(IERC165.supportsInterface, (IAppAuthV2.currentAppEpoch.selector)), abi.encode(true) ); vm.mockCall( - policy, abi.encodeCall(IERC165.supportsInterface, (IAppEpochAuth.authorizeApp.selector)), abi.encode(true) + policy, abi.encodeCall(IERC165.supportsInterface, (IAppAuthV2.authorizeApp.selector)), abi.encode(true) ); assertEq(kms.currentAppEpoch(policy), 0); vm.clearMockedCalls(); - vm.mockCallRevert(policy, abi.encodeCall(IAppEpochAuth.currentAppEpoch, ()), "epoch unavailable"); + vm.mockCallRevert(policy, abi.encodeCall(IAppAuthV2.currentAppEpoch, ()), "epoch unavailable"); vm.expectRevert("epoch unavailable"); kms.currentAppEpoch(policy); } @@ -642,7 +696,7 @@ contract DstackKmsTest is Test { // A registered legacy policy without the capability is epoch zero. kms.registerApp(policy); vm.mockCall( - policy, abi.encodeCall(IERC165.supportsInterface, (type(IAppEpochAuth).interfaceId)), abi.encode(false) + policy, abi.encodeCall(IERC165.supportsInterface, (type(IAppAuthV2).interfaceId)), abi.encode(false) ); assertEq(kms.currentAppEpoch(policy), 0); // The authorization paths still report the rejection instead of reverting. @@ -678,7 +732,7 @@ contract DstackKmsTest is Test { (uint256 rootEpoch,) = kms.currentRootEpoch(); assertEq(rootEpoch, 0); AuthorizationRequest memory request = AuthorizationRequest( - info, keccak256("app_get_key"), OptionalUint256(false, 0), OptionalUint256(false, 0) + appInfoV2(info), keccak256("app_get_key"), OptionalUint256(false, 0), OptionalUint256(false, 0) ); AuthorizationResult memory result = kms.authorizeApp(request); assertFalse(result.authorized); @@ -716,7 +770,10 @@ contract DstackKmsTest is Test { assertEq(result.currentRootEpochHash, hash1); AuthorizationRequest memory request = AuthorizationRequest( - authorizationInfo(user), keccak256("kms_serve"), OptionalUint256(false, 0), OptionalUint256(false, 0) + appInfoV2(authorizationInfo(user)), + keccak256("kms_serve"), + OptionalUint256(false, 0), + OptionalUint256(false, 0) ); result = kms.authorizeApp(request); assertEq(result.rejectionReason, "Invalid authorization operation"); @@ -950,7 +1007,7 @@ contract DstackKmsTest is Test { returns (AuthorizationRequest memory) { return AuthorizationRequest( - info, keccak256("app_get_key"), OptionalUint256(true, appEpoch), OptionalUint256(true, root) + appInfoV2(info), keccak256("app_get_key"), OptionalUint256(true, appEpoch), OptionalUint256(true, root) ); } @@ -1079,14 +1136,14 @@ contract DstackKmsTest is Test { AuthorizationRequest memory request = epochRequest(info, 0, 0); vm.mockCall( info.appId, - abi.encodeCall(IAppEpochAuth.authorizeApp, (request)), + abi.encodeCall(IAppAuthV2.authorizeApp, (request)), abi.encode(AuthorizationResult(false, "key retrieval denied", "", 0, bytes32(0), 0, 0, bytes32(0))) ); assertEq(kms.authorizeApp(request).rejectionReason, "key retrieval denied"); request.operation = keccak256("app_get_cert"); vm.mockCall( info.appId, - abi.encodeCall(IAppEpochAuth.authorizeApp, (request)), + abi.encodeCall(IAppAuthV2.authorizeApp, (request)), abi.encode(AuthorizationResult(true, "", "", 0, bytes32(0), 0, 0, bytes32(0))) ); assertTrue(kms.authorizeApp(request).authorized); @@ -1135,14 +1192,14 @@ contract DstackKmsTest is Test { // The KMS overwrites whatever the policy reports for fields it owns. vm.mockCall( info.appId, - abi.encodeCall(IAppEpochAuth.authorizeApp, (request)), + abi.encodeCall(IAppAuthV2.authorizeApp, (request)), abi.encode( AuthorizationResult(true, "", "policy-gateway", 9, bytes32(uint256(9)), 3, 9, bytes32(uint256(9))) ) ); - vm.expectCall(info.appId, abi.encodeCall(IAppEpochAuth.authorizeApp, (request)), 1); + vm.expectCall(info.appId, abi.encodeCall(IAppAuthV2.authorizeApp, (request)), 1); // The app epoch comes from the forwarded decision only. - vm.expectCall(info.appId, abi.encodeCall(IAppEpochAuth.currentAppEpoch, ()), 0); + vm.expectCall(info.appId, abi.encodeCall(IAppAuthV2.currentAppEpoch, ()), 0); AuthorizationResult memory result = kms.authorizeApp(request); assertTrue(result.authorized); assertEq(result.currentAppEpoch, 3); @@ -1190,7 +1247,7 @@ contract DstackKmsTest is Test { registerNextRootEpoch(); info.appId = kms.deployAndRegisterApp(user, false, false, info.deviceId, info.composeHash); vm.stopPrank(); - bytes memory forwarded = abi.encodeWithSelector(IAppEpochAuth.authorizeApp.selector); + bytes memory forwarded = abi.encodeWithSelector(IAppAuthV2.authorizeApp.selector); vm.expectCall(info.appId, forwarded, 0); assertEq(kms.authorizeApp(epochRequest(info, 0, 0)).rejectionReason, "OS image is not allowed"); AuthorizationRequest memory request = epochRequest(info, 0, 0); @@ -1211,7 +1268,7 @@ contract DstackKmsTest is Test { vm.prank(user); DstackApp(info.appId).advanceAppEpoch(0); vm.mockCall( - info.appId, abi.encodeCall(IERC165.supportsInterface, (type(IAppEpochAuth).interfaceId)), abi.encode(false) + info.appId, abi.encodeCall(IERC165.supportsInterface, (type(IAppAuthV2).interfaceId)), abi.encode(false) ); vm.mockCall(info.appId, abi.encodeCall(IAppAuth.isAppAllowed, (info)), abi.encode(true, "")); assertTrue(kms.authorizeApp(epochRequest(info, 0, 0)).authorized); @@ -1223,7 +1280,7 @@ contract DstackKmsTest is Test { assertEq(result.rejectionReason, "Requested app epoch is not active"); vm.clearMockedCalls(); vm.mockCallRevert( - info.appId, abi.encodeCall(IAppEpochAuth.authorizeApp, (epochRequest(info, 0, 0))), "policy unavailable" + info.appId, abi.encodeCall(IAppAuthV2.authorizeApp, (epochRequest(info, 0, 0))), "policy unavailable" ); vm.expectRevert(bytes("policy unavailable")); kms.authorizeApp(epochRequest(info, 0, 0)); diff --git a/dstack/kms/auth-eth/test/DstackKmsEpoch.symbolic.t.sol b/dstack/kms/auth-eth/test/DstackKmsEpoch.symbolic.t.sol index da39b1004..06ed66839 100644 --- a/dstack/kms/auth-eth/test/DstackKmsEpoch.symbolic.t.sol +++ b/dstack/kms/auth-eth/test/DstackKmsEpoch.symbolic.t.sol @@ -11,6 +11,7 @@ import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import "../contracts/AuthorizationOperation.sol"; import "../contracts/DstackApp.sol"; import "../contracts/DstackKms.sol"; +import "./AppInfoV2.sol"; function _appAuth( DstackApp target, @@ -22,7 +23,7 @@ function _appAuth( returns (bool, string memory) { AuthorizationResult memory result = target.authorizeApp( - AuthorizationRequest(info, operation, OptionalUint256(true, epoch), OptionalUint256(false, 0)) + AuthorizationRequest(appInfoV2(info), operation, OptionalUint256(true, epoch), OptionalUint256(false, 0)) ); return (result.authorized, result.rejectionReason); } @@ -134,7 +135,9 @@ contract DstackKmsEpochSymbolicTest is Test { pure returns (AuthorizationRequest memory) { - return AuthorizationRequest(info, operation, OptionalUint256(appSet, appEpoch), OptionalUint256(rootSet, root)); + return AuthorizationRequest( + appInfoV2(info), operation, OptionalUint256(appSet, appEpoch), OptionalUint256(rootSet, root) + ); } /// Calldata of one KMS policy setter: 0-5 write the row of `epoch`, 6-11 @@ -566,7 +569,7 @@ contract DstackKmsEpochSymbolicTest is Test { } // --------------------------------------------------------------- - // An app policy predating app epochs (no IAppEpochAuth) is decided by + // An app policy predating app epochs (no IAppAuthV2) is decided by // its isAppAllowed and only for app epoch 0: an explicit nonzero app // epoch is refused, and otherwise the decision is the KMS checks at // the selected root epoch and the policy's own answer. diff --git a/dstack/kms/auth-eth/test/LegacyAppCompat.t.sol b/dstack/kms/auth-eth/test/LegacyAppCompat.t.sol index 59750a092..a9db161d1 100644 --- a/dstack/kms/auth-eth/test/LegacyAppCompat.t.sol +++ b/dstack/kms/auth-eth/test/LegacyAppCompat.t.sol @@ -10,6 +10,7 @@ import "forge-std/Test.sol"; import "openzeppelin-foundry-upgrades/Upgrades.sol"; import "../contracts/DstackKms.sol"; import "../contracts/DstackApp.sol"; +import "./AppInfoV2.sol"; // The epoch-less setters of the released app policies. interface ILegacyApp { @@ -76,7 +77,7 @@ contract LegacyAppCompatTest is Test { info.tcbStatus = "UpToDate"; info.advisoryIds = new string[](0); return AuthorizationRequest( - info, keccak256("app_get_key"), OptionalUint256(appSet, appEpoch), OptionalUint256(false, 0) + appInfoV2(info), keccak256("app_get_key"), OptionalUint256(appSet, appEpoch), OptionalUint256(false, 0) ); } @@ -124,7 +125,7 @@ contract LegacyAppCompatTest is Test { address app = deployLegacyApp(release); assertTrue(IERC165(app).supportsInterface(basicManagement)); assertFalse(IERC165(app).supportsInterface(type(IAppEpochManagement).interfaceId)); - assertFalse(IERC165(app).supportsInterface(type(IAppEpochAuth).interfaceId)); + assertFalse(IERC165(app).supportsInterface(type(IAppAuthV2).interfaceId)); } DstackApp current = DstackApp( Upgrades.deployUUPSProxy( @@ -136,7 +137,7 @@ contract LegacyAppCompatTest is Test { ); assertFalse(current.supportsInterface(basicManagement)); assertTrue(current.supportsInterface(type(IAppEpochManagement).interfaceId)); - assertTrue(current.supportsInterface(type(IAppEpochAuth).interfaceId)); + assertTrue(current.supportsInterface(type(IAppAuthV2).interfaceId)); } function test_UpgradedV059AppKeepsItsPolicyAtAppEpochZero() public { diff --git a/dstack/kms/auth-eth/test/ManageScript.t.sol b/dstack/kms/auth-eth/test/ManageScript.t.sol index 503e3a878..f030ebb1d 100644 --- a/dstack/kms/auth-eth/test/ManageScript.t.sol +++ b/dstack/kms/auth-eth/test/ManageScript.t.sol @@ -51,5 +51,25 @@ contract ManageScriptTest is Test { script = addComposeHash(legacy); vm.expectRevert(bytes("this app predates app epochs: only APP_EPOCH=0 applies")); script.run(); + + // TEE variant policies take `any` or names, and exist only in current apps. + vm.setEnv("APP_EPOCH", "0"); + vm.setEnv("APP_CONTRACT_ADDR", vm.toString(current)); + vm.setEnv("TEE_VARIANTS", "dstack-tdx,dstack-gcp-tdx"); + SetTeeVariants teeScript = new SetTeeVariants(); + teeScript.setUp(); + teeScript.run(); + assertFalse(DstackApp(current).allowAnyTeeVariant()); + string[] memory names = DstackApp(current).allowedTeeVariants(); + assertEq(names.length, 2); + assertEq(names[1], "dstack-gcp-tdx"); + vm.setEnv("TEE_VARIANTS", "any"); + teeScript.run(); + assertTrue(DstackApp(current).allowAnyTeeVariant()); + vm.setEnv("APP_CONTRACT_ADDR", vm.toString(legacy)); + teeScript = new SetTeeVariants(); + teeScript.setUp(); + vm.expectRevert(bytes("this app predates TEE variant policies")); + teeScript.run(); } }