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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,16 @@ 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` (`0x853d2b79`). The `Manage.s.sol` scripts require
`ROOT_EPOCH` or `APP_EPOCH`, or a `FROM_*`/`TO_*` range.
`IAppEpochManagement` (`0xc65f31b6`). 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
open to any host.
- App contracts deployed before app epochs (v0.5.x, v0.6.0) keep working with
the upgraded `DstackKms` and KMS, which authorize them for app epoch 0 only.
`Manage.s.sol` detects them by interface ID and calls their epoch-less
setters. Upgrading one to the current `DstackApp` keeps its compose hashes,
devices and `allowAnyDevice` as app epoch 0.
setters. Upgrading one to the current `DstackApp` keeps its compose hashes and
devices as app epoch 0, and its `allowAnyDevice` for every app epoch.
- DstackKms, DstackApp: ownership transfers take two steps for proxies upgraded
from v0.5.9: `transferOwnership` nominates, and the new owner must call
`acceptOwnership`.
Expand Down Expand Up @@ -118,7 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`removeKmsDevice`. Use the `*AtEpoch` or `*AtEpochs` forms; before any
rotation, `addOsImageHashAtEpoch(0, hash)` is the old call.
- **Breaking:** DstackApp: the epoch-less `addComposeHash`, `removeComposeHash`,
`addDevice`, `removeDevice` and `setAllowAnyDevice`, and with them
`addDevice` and `removeDevice`, and with them
`IAppAuthBasicManagement`: an upgraded app no longer reports interface ID
`0x8fd37527`, and calls to those functions revert. Use the `*AtEpoch` or
`*AtEpochs` forms (`IAppEpochManagement`); at app epoch 0,
Expand Down
18 changes: 12 additions & 6 deletions docs/app-key-epochs.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ Authorization checks only the selected row, with O(1) add, removal and lookup:

- `DstackKms`: OS images, KMS aggregated measurements and KMS device IDs are
indexed by **root epoch**. OS image limits also apply to app key requests.
- `DstackApp`: compose hashes, device IDs and `allowAnyDevice` are indexed by
**app epoch**. Each required material must be allowed in the selected row.
- `DstackApp`: compose hashes and device IDs are indexed by **app epoch**. Each
required material must be allowed in the selected row. `allowAnyDevice` is a
single switch over every app epoch: which hosts may run the app is not a
property of a key generation, and a per-epoch switch could leave an older
epoch open to any host after the current one is restricted.

There is no implicit access in either direction. Rotating keys starts an empty
row; approving a new row neither copies old approvals nor grants historical keys.
Expand All @@ -61,7 +64,7 @@ range, so one transaction revokes a value from all rows, with
`remove*AtEpochs(0, latest, …)` in `DstackKms` and
`remove*AtEpochs(0, currentAppEpoch, …)` in `DstackApp`. `DstackKms` writes
registered root epochs (before any is registered, only epoch zero); `DstackApp`
writes app epochs up to the next one, as do `setAllowAnyDeviceAtEpoch(s)`.
writes app epochs up to the next one.
`DstackApp` reports these setters as the ERC-165 interface
`IAppEpochManagement` and no longer reports `IAppAuthBasicManagement`
(`0x8fd37527`), whose epoch-less setters it removed. The scripts in
Expand All @@ -85,7 +88,9 @@ it could not get keys for.
The `*AtEpoch` getters read the selected row, including legacy epoch zero.
`EpochPolicyChanged` events include the row and the resulting permission. Legacy
bool getters and events describe **epoch zero only**; changing a nonzero row does
not change its legacy bool or emit a misleading legacy material event.
not change its legacy bool or emit a misleading legacy material event. The
exception is `allowAnyDevice` and its `AllowAnyDeviceSet` event, which cover every
app epoch.
Root policy rows from epoch 1 belong to the epoch's registration, so a row can
be written only once the epoch is registered (register, prepare the row, then
activate). A popped epoch's rows do not carry over: the next registration of the
Expand All @@ -110,8 +115,9 @@ The original bool mappings and `allowAnyDevice` retain their storage locations a
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
owners, pending owners, policy flags, root registries and app epoch counters are
preserved. Even if the pre-upgrade counter is nonzero, old global approvals grant
only row zero after upgrading: explicitly populate the intended nonzero rows.
preserved. Even if the pre-upgrade counter is nonzero, old global material
approvals grant only row zero after upgrading: explicitly populate the intended
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
Expand Down
10 changes: 5 additions & 5 deletions docs/onchain-governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ to the next epoch. Removing a value from rows `0` through the current epoch revo
| `removeComposeHashAtEpoch(uint256,bytes32)` / `removeComposeHashAtEpochs(uint256,uint256,bytes32)` | Remove a compose hash from one app epoch or a range |
| `addDeviceAtEpoch(uint256,bytes32)` / `addDeviceAtEpochs(uint256,uint256,bytes32)` | Whitelist a device ID |
| `removeDeviceAtEpoch(uint256,bytes32)` / `removeDeviceAtEpochs(uint256,uint256,bytes32)` | Remove a device |
| `setAllowAnyDeviceAtEpoch(uint256,bool)` / `setAllowAnyDeviceAtEpochs(uint256,uint256,bool)` | Allow any device to run this app |
| `setAllowAnyDevice(bool)` | Allow any device to run this app, 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 |
Expand All @@ -189,15 +189,15 @@ 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 <app-id> "supportsInterface(bytes4)(bool)" 0x853d2b79 --rpc-url $RPC_URL # IAppEpochManagement: epoch setters
cast call <app-id> "supportsInterface(bytes4)(bool)" 0xc65f31b6 --rpc-url $RPC_URL # IAppEpochManagement: epoch setters
cast call <app-id> "supportsInterface(bytes4)(bool)" 0x8fd37527 --rpc-url $RPC_URL # IAppAuthBasicManagement: epoch-less setters
```

The `Manage.s.sol` app scripts check this themselves: on an older app they call the
epoch-less setter and accept only `APP_EPOCH=0` or no epoch; on a current app they
require `APP_EPOCH`, or `FROM_APP_EPOCH` and `TO_APP_EPOCH`. Upgrading an older app to
the current `DstackApp` keeps its compose hashes, devices and `allowAnyDevice` as app
epoch 0.
require `APP_EPOCH`, or `FROM_APP_EPOCH` and `TO_APP_EPOCH`. `SetAllowAnyDevice` takes
no epoch on either kind. Upgrading an older app to the current `DstackApp` keeps its
compose hashes and devices as app epoch 0, and its `allowAnyDevice` for every epoch.

### AppBootInfo Structure

Expand Down
38 changes: 10 additions & 28 deletions dstack/kms/auth-eth/contracts/DstackApp.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ contract DstackApp is
// State variable to track if upgrades are disabled
bool private _upgradesDisabled;

/// @notice Whether any device may boot this app. Epoch 0 only; for other epochs use the *AtEpoch getters and
/// EpochPolicyChanged.
/// @notice Whether any device may boot this app, at every app epoch.
bool public allowAnyDevice;

/// @notice Allowed device IDs for this app. Epoch 0 only; for other epochs use the *AtEpoch getters and
Expand All @@ -48,7 +47,6 @@ contract DstackApp is
// Epoch zero continues to use the original fields above after an upgrade.
mapping(uint256 => mapping(bytes32 => bool)) private _composeHashesByEpoch;
mapping(uint256 => mapping(bytes32 => bool)) private _deviceIdsByEpoch;
mapping(uint256 => bool) private _allowAnyDeviceByEpoch;

event EpochPolicyChanged(bytes32 indexed policy, bytes32 indexed value, uint256 indexed epoch, bool enabled);

Expand All @@ -58,7 +56,7 @@ contract DstackApp is
event DeviceAdded(bytes32 deviceId);
event DeviceRemoved(bytes32 deviceId);
event UpgradesDisabled();
/// @notice Epoch 0 only; for other epochs use the *AtEpoch getters and EpochPolicyChanged.
/// @notice Covers every app epoch; there is no EpochPolicyChanged counterpart.
event AllowAnyDeviceSet(bool allowAny);
event RequireTcbUpToDateSet(bool requireUpToDate);
/// @notice Additive audit event for reconstructing authorization policy.
Expand Down Expand Up @@ -172,10 +170,6 @@ contract DstackApp is
return epoch == 0 ? allowedDeviceIds[deviceId] : _deviceIdsByEpoch[epoch][deviceId];
}

function allowAnyDeviceAtEpoch(uint256 epoch) public view returns (bool) {
return epoch == 0 ? allowAnyDevice : _allowAnyDeviceByEpoch[epoch];
}

// A request may select any app epoch up to the current one and is decided
// on that epoch's row, so revoking a value takes every row that allows it:
// 0 through currentAppEpoch.
Expand Down Expand Up @@ -229,24 +223,12 @@ contract DstackApp is
emit EpochPolicyChanged(keccak256("compose-hash"), composeHash, epoch, allowed);
}

function setAllowAnyDeviceAtEpoch(uint256 epoch, bool allowed) public onlyOwner {
_setAllowAnyDevice(epoch, bytes32(0), allowed);
}

function setAllowAnyDeviceAtEpochs(uint256 fromEpoch, uint256 toEpoch, bool allowed) external onlyOwner {
_setAtEpochs(_setAllowAnyDevice, fromEpoch, toEpoch, bytes32(0), allowed);
}

function _setAllowAnyDevice(uint256 epoch, bytes32, bool allowed) private {
_requireWritableEpoch(epoch);
if (epoch == 0) {
allowAnyDevice = allowed;
emit AllowAnyDeviceSet(allowed);
_emitPolicy("allow-any-device", bytes32(0), allowed);
} else {
_allowAnyDeviceByEpoch[epoch] = allowed;
}
emit EpochPolicyChanged(keccak256("allow-any-device"), bytes32(0), epoch, allowed);
// Covers every app epoch: restricting devices is a decision about hosts, not
// key generations, and must not leave an older epoch open to any host.
function setAllowAnyDevice(bool allowed) external onlyOwner {
allowAnyDevice = allowed;
emit AllowAnyDeviceSet(allowed);
_emitPolicy("allow-any-device", bytes32(0), allowed);
}

// Set whether TCB status must be UpToDate to boot this app
Expand Down Expand Up @@ -338,7 +320,7 @@ contract DstackApp is
}

// Check if device is allowed (when device restriction is enabled)
if (!allowAnyDeviceAtEpoch(appEpoch) && !allowedDeviceIdsAtEpoch(appEpoch, bootInfo.deviceId)) {
if (!allowAnyDevice && !allowedDeviceIdsAtEpoch(appEpoch, bootInfo.deviceId)) {
return (false, "Device not allowed");
}

Expand All @@ -362,5 +344,5 @@ contract DstackApp is
}

// Add storage gap for upgradeable contracts
uint256[45] private __gap;
uint256[46] private __gap;
}
6 changes: 3 additions & 3 deletions dstack/kms/auth-eth/contracts/IAppEpochManagement.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

pragma solidity ^0.8.24;

/// Epoch-scoped allowlist management. Each setter names the app epoch rows it
/// 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.
/// Replaces the epoch-less IAppAuthBasicManagement (0x8fd37527), which policies
/// predating app epochs still report; tools tell the two apart by interface ID.
interface IAppEpochManagement {
Expand All @@ -19,6 +20,5 @@ interface IAppEpochManagement {
function addDeviceAtEpochs(uint256 fromEpoch, uint256 toEpoch, bytes32 deviceId) external;
function removeDeviceAtEpoch(uint256 epoch, bytes32 deviceId) external;
function removeDeviceAtEpochs(uint256 fromEpoch, uint256 toEpoch, bytes32 deviceId) external;
function setAllowAnyDeviceAtEpoch(uint256 epoch, bool allowed) external;
function setAllowAnyDeviceAtEpochs(uint256 fromEpoch, uint256 toEpoch, bool allowed) external;
function setAllowAnyDevice(bool allowed) external;
}
11 changes: 6 additions & 5 deletions dstack/kms/auth-eth/docs/formal-verification.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,18 +142,19 @@ and admits one app per epoch, then one symbolic owner write where noted):
(one epoch, or a symbolic range), including epoch 0's legacy fields.
- [x] `check_AuthorizeApp_MatchesPolicy` — for both app operations,
`authorizeApp` allows exactly when the epoch is active, the TCB
requirement holds, and that epoch admits the compose hash and device.
requirement holds, that epoch admits the compose hash, and the device
is allowed by `allowAnyDevice` or that epoch's device row.
- [x] `check_PolicyWrite_TakesEffect` — each `*AtEpoch` setter on an epoch
up to the next one, and each `*AtEpochs` setter on every row of a range
ending at most there, either sets the value it was given or reverts on
a zero ID.
- [x] `check_FullRangeRemoval_RevokesAtEveryAppEpoch` — after removing a
compose hash, or a device and the any-device flag, from rows 0 through
the next epoch, no app epoch authorizes it, for either app operation.
compose hash from rows 0 through the next epoch, or a device from those
rows and clearing `allowAnyDevice`, no app epoch authorizes it, for
either app operation.

Mutation-tested: requiring both the any-device flag and a listed device,
ignoring the value of a per-epoch `allowAnyDevice` write, dropping a setter's
epoch bound or widening it past the next epoch, aliasing epoch 1 onto the
dropping a setter's epoch bound or widening it past the next epoch, aliasing epoch 1 onto the
legacy fields, letting `isAppAllowed` decide at a nonzero epoch or read the
prepared next row, a permissionless
`advanceAppEpoch` and dropping its stale-transaction guard each produce a
Expand Down
6 changes: 3 additions & 3 deletions dstack/kms/auth-eth/docs/specification.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Plus inherited storage in ERC-7201 namespaces: `OwnableUpgradeable`,
| Slot | Name | Type | Writable by |
|---|---|---|---|
| 0 | `allowedComposeHashes` | `mapping(bytes32 => bool)` | `addComposeHashAtEpoch(s)`, `removeComposeHashAtEpoch(s)` at epoch 0 (owner); 5/6-arg initializer |
| 1 | `_upgradesDisabled` + `allowAnyDevice` (packed) | `bool` + `bool` | `disableUpgrades` (owner, monotonic); `setAllowAnyDeviceAtEpoch(s)` at epoch 0 (owner); initializer |
| 1 | `_upgradesDisabled` + `allowAnyDevice` (packed) | `bool` + `bool` | `disableUpgrades` (owner, monotonic); `setAllowAnyDevice` (owner, every app epoch); initializer |
| 2 | `allowedDeviceIds` | `mapping(bytes32 => bool)` | `addDeviceAtEpoch(s)`, `removeDeviceAtEpoch(s)` at epoch 0 (owner); initializer |
| 3 | `requireTcbUpToDate` | `bool` | `setRequireTcbUpToDate` (owner); 6-arg initializer only |
| 4 | `__gap[49]` | `uint256[49]` | (none) |
Expand Down Expand Up @@ -238,8 +238,8 @@ byte sequence — no case variations, no trailing nulls, no whitespace.
### 3.11 Owner-only App mutations

For each of the `*AtEpoch` and `*AtEpochs` forms of `addComposeHash`,
`removeComposeHash`, `addDevice`, `removeDevice` and `setAllowAnyDevice`, and
`setRequireTcbUpToDate`, `disableUpgrades`:
`removeComposeHash`, `addDevice` and `removeDevice`, and for `setAllowAnyDevice`,
`setRequireTcbUpToDate` and `disableUpgrades`:

- **pre**: `msg.sender == owner()`.
- **frame**: every storage cell except the one mapped to the operation.
Expand Down
18 changes: 8 additions & 10 deletions dstack/kms/auth-eth/script/Manage.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ interface ILegacyAppManagement {
function removeComposeHash(bytes32 composeHash) external;
function addDevice(bytes32 deviceId) external;
function removeDevice(bytes32 deviceId) external;
function setAllowAnyDevice(bool allowed) external;
}

// Base contract with common functionality
Expand Down Expand Up @@ -238,16 +237,15 @@ contract AddDevice is BaseScript {
contract SetAllowAnyDevice is BaseScript {
function run() external {
bool allow = vm.envBool("ALLOW_ANY_DEVICE");
require(address(app) != address(0), "APP_CONTRACT_ADDR not set");
require(
!vm.envExists("APP_EPOCH") && !vm.envExists("FROM_APP_EPOCH") && !vm.envExists("TO_APP_EPOCH"),
"allowAnyDevice covers every app epoch: unset APP_EPOCH, FROM_APP_EPOCH and TO_APP_EPOCH"
);

ILegacyAppManagement legacy = legacyApp();
if (address(legacy) != address(0)) {
vm.broadcast(vm.envUint("PRIVATE_KEY"));
legacy.setAllowAnyDevice(allow);
} else {
(uint256 from, uint256 to) = appEpochs();
vm.broadcast(vm.envUint("PRIVATE_KEY"));
app.setAllowAnyDeviceAtEpochs(from, to, allow);
}
// Apps with and without app epochs share this setter.
vm.broadcast(vm.envUint("PRIVATE_KEY"));
app.setAllowAnyDevice(allow);

console.log("Set allowAnyDevice to:", allow);
}
Expand Down
2 changes: 1 addition & 1 deletion dstack/kms/auth-eth/script/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ forge script script/Manage.s.sol:AddDevice --broadcast --rpc-url $RPC_URL

#### Set Allow Any Device
```bash
KMS_CONTRACT_ADDR=0x... APP_CONTRACT_ADDR=0x... APP_EPOCH=0 ALLOW_ANY_DEVICE=true \
KMS_CONTRACT_ADDR=0x... APP_CONTRACT_ADDR=0x... ALLOW_ANY_DEVICE=true \
forge script script/Manage.s.sol:SetAllowAnyDevice --broadcast --rpc-url $RPC_URL
```

Expand Down
2 changes: 1 addition & 1 deletion dstack/kms/auth-eth/test/DstackApp.symbolic.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ contract DstackAppSymbolicTest is Test {
} else if (which == 3) {
data = abi.encodeWithSelector(DstackApp.removeDeviceAtEpoch.selector, uint256(0), word);
} else if (which == 4) {
data = abi.encodeWithSelector(DstackApp.setAllowAnyDeviceAtEpoch.selector, uint256(0), flag);
data = abi.encodeWithSelector(DstackApp.setAllowAnyDevice.selector, flag);
} else if (which == 5) {
data = abi.encodeWithSelector(DstackApp.setRequireTcbUpToDate.selector, flag);
} else if (which == 6) {
Expand Down
Loading
Loading