Skip to content

Commit be6ed43

Browse files
authored
Merge pull request #959 from Dstack-TEE/codex/feat-kms-auth-policy-audit
[STACKED on #958] feat(kms): emit authorization policy audit events
2 parents 1270d5d + bf73944 commit be6ed43

4 files changed

Lines changed: 373 additions & 2 deletions

File tree

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ contract DstackApp is
4040
event UpgradesDisabled();
4141
event AllowAnyDeviceSet(bool allowAny);
4242
event RequireTcbUpToDateSet(bool requireUpToDate);
43+
/// @notice Additive audit event for reconstructing authorization policy.
44+
event PolicyChanged(address indexed actor, bytes32 indexed policy, bytes32 indexed value, bool enabled);
4345

4446
/// @custom:oz-upgrades-unsafe-allow constructor
4547
constructor() {
@@ -94,12 +96,14 @@ contract DstackApp is
9496
if (initialDeviceId != bytes32(0)) {
9597
allowedDeviceIds[initialDeviceId] = true;
9698
emit DeviceAdded(initialDeviceId);
99+
_emitPolicy("device", initialDeviceId, true);
97100
}
98101

99102
// Add initial compose hash if provided
100103
if (initialComposeHash != bytes32(0)) {
101104
allowedComposeHashes[initialComposeHash] = true;
102105
emit ComposeHashAdded(initialComposeHash);
106+
_emitPolicy("compose-hash", initialComposeHash, true);
103107
}
104108

105109
__Ownable_init(initialOwner);
@@ -130,44 +134,55 @@ contract DstackApp is
130134
}
131135

132136
// Function to authorize upgrades (required by UUPSUpgradeable)
133-
function _authorizeUpgrade(address) internal view override onlyOwner {
137+
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
134138
require(!_upgradesDisabled, "Upgrades are permanently disabled");
139+
_emitPolicy("implementation-upgrade", bytes32(uint256(uint160(newImplementation))), true);
140+
}
141+
142+
function _emitPolicy(string memory policy, bytes32 value, bool enabled) internal {
143+
emit PolicyChanged(msg.sender, keccak256(bytes(policy)), value, enabled);
135144
}
136145

137146
// Add a compose hash to allowed list
138147
function addComposeHash(bytes32 composeHash) external onlyOwner {
139148
allowedComposeHashes[composeHash] = true;
140149
emit ComposeHashAdded(composeHash);
150+
_emitPolicy("compose-hash", composeHash, true);
141151
}
142152

143153
// Remove a compose hash from allowed list
144154
function removeComposeHash(bytes32 composeHash) external onlyOwner {
145155
allowedComposeHashes[composeHash] = false;
146156
emit ComposeHashRemoved(composeHash);
157+
_emitPolicy("compose-hash", composeHash, false);
147158
}
148159

149160
// Set whether any device is allowed to boot this app
150161
function setAllowAnyDevice(bool _allowAnyDevice) external onlyOwner {
151162
allowAnyDevice = _allowAnyDevice;
152163
emit AllowAnyDeviceSet(_allowAnyDevice);
164+
_emitPolicy("allow-any-device", bytes32(0), _allowAnyDevice);
153165
}
154166

155167
// Set whether TCB status must be UpToDate to boot this app
156168
function setRequireTcbUpToDate(bool _requireUpToDate) external onlyOwner {
157169
requireTcbUpToDate = _requireUpToDate;
158170
emit RequireTcbUpToDateSet(_requireUpToDate);
171+
_emitPolicy("require-tcb-up-to-date", bytes32(0), _requireUpToDate);
159172
}
160173

161174
// Add a device ID to allowed list
162175
function addDevice(bytes32 deviceId) external onlyOwner {
163176
allowedDeviceIds[deviceId] = true;
164177
emit DeviceAdded(deviceId);
178+
_emitPolicy("device", deviceId, true);
165179
}
166180

167181
// Remove a device ID from allowed list
168182
function removeDevice(bytes32 deviceId) external onlyOwner {
169183
allowedDeviceIds[deviceId] = false;
170184
emit DeviceRemoved(deviceId);
185+
_emitPolicy("device", deviceId, false);
171186
}
172187

173188
// Check if an app is allowed to boot
@@ -202,6 +217,7 @@ contract DstackApp is
202217
function disableUpgrades() external onlyOwner {
203218
_upgradesDisabled = true;
204219
emit UpgradesDisabled();
220+
_emitPolicy("upgrades-disabled", bytes32(0), true);
205221
}
206222

207223
// Add storage gap for upgradeable contracts

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
5959
// slither-disable-next-line unindexed-event-address
6060
event AppImplementationSet(address implementation);
6161
event AppDeployedViaFactory(address indexed appId, address indexed deployer);
62+
/// @notice Additive audit event for reconstructing authorization policy.
63+
/// @dev `value` is the affected bytes32 value, address, or hash of dynamic public data.
64+
event PolicyChanged(address indexed actor, bytes32 indexed policy, bytes32 indexed value, bool enabled);
6265

6366
/// @custom:oz-upgrades-unsafe-allow constructor
6467
constructor() {
@@ -75,6 +78,7 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
7578
if (_appImplementation != address(0)) {
7679
appImplementation = _appImplementation;
7780
emit AppImplementationSet(_appImplementation);
81+
_emitPolicy("app-implementation", bytes32(uint256(uint160(_appImplementation))), true);
7882
}
7983
}
8084

@@ -96,12 +100,19 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
96100
}
97101

98102
// Function to authorize upgrades (required by UUPSUpgradeable)
99-
function _authorizeUpgrade(address newImplementation) internal override onlyOwner { }
103+
function _authorizeUpgrade(address newImplementation) internal override onlyOwner {
104+
_emitPolicy("implementation-upgrade", bytes32(uint256(uint160(newImplementation))), true);
105+
}
106+
107+
function _emitPolicy(string memory policy, bytes32 value, bool enabled) internal {
108+
emit PolicyChanged(msg.sender, keccak256(bytes(policy)), value, enabled);
109+
}
100110

101111
// Function to set KMS information
102112
function setKmsInfo(KmsInfo memory info) external onlyOwner {
103113
kmsInfo = info;
104114
emit KmsInfoSet(info.k256Pubkey);
115+
_emitPolicy("kms-info", keccak256(info.k256Pubkey), true);
105116
}
106117

107118
// Function to set KMS quote
@@ -118,6 +129,7 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
118129
function setGatewayAppId(string memory appId) external onlyOwner {
119130
gatewayAppId = appId;
120131
emit GatewayAppIdSet(appId);
132+
_emitPolicy("gateway-app-id", keccak256(bytes(appId)), true);
121133
}
122134

123135
/// @notice Register an app address as known to this KMS.
@@ -133,13 +145,15 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
133145
require(appId != address(0), "Invalid app ID");
134146
registeredApps[appId] = true;
135147
emit AppRegistered(appId);
148+
_emitPolicy("registered-app", bytes32(uint256(uint160(appId))), true);
136149
}
137150

138151
// Function to set DstackApp implementation contract address
139152
function setAppImplementation(address _implementation) external onlyOwner {
140153
require(_implementation != address(0), "Invalid implementation address");
141154
appImplementation = _implementation;
142155
emit AppImplementationSet(_implementation);
156+
_emitPolicy("app-implementation", bytes32(uint256(uint160(_implementation))), true);
143157
}
144158

145159
// Factory method: Deploy and register DstackApp in single transaction
@@ -200,36 +214,42 @@ contract DstackKms is Initializable, Ownable2StepUpgradeable, UUPSUpgradeable, E
200214
function addKmsAggregatedMr(bytes32 mrAggregated) external onlyOwner {
201215
kmsAllowedAggregatedMrs[mrAggregated] = true;
202216
emit KmsAggregatedMrAdded(mrAggregated);
217+
_emitPolicy("kms-aggregated-mr", mrAggregated, true);
203218
}
204219

205220
// Function to deregister an aggregated MR measurement
206221
function removeKmsAggregatedMr(bytes32 mrAggregated) external onlyOwner {
207222
kmsAllowedAggregatedMrs[mrAggregated] = false;
208223
emit KmsAggregatedMrRemoved(mrAggregated);
224+
_emitPolicy("kms-aggregated-mr", mrAggregated, false);
209225
}
210226

211227
// Function to register a KMS device ID
212228
function addKmsDevice(bytes32 deviceId) external onlyOwner {
213229
kmsAllowedDeviceIds[deviceId] = true;
214230
emit KmsDeviceAdded(deviceId);
231+
_emitPolicy("kms-device", deviceId, true);
215232
}
216233

217234
// Function to deregister a KMS device ID
218235
function removeKmsDevice(bytes32 deviceId) external onlyOwner {
219236
kmsAllowedDeviceIds[deviceId] = false;
220237
emit KmsDeviceRemoved(deviceId);
238+
_emitPolicy("kms-device", deviceId, false);
221239
}
222240

223241
// Function to register an image measurement
224242
function addOsImageHash(bytes32 osImageHash) external onlyOwner {
225243
allowedOsImages[osImageHash] = true;
226244
emit OsImageHashAdded(osImageHash);
245+
_emitPolicy("os-image", osImageHash, true);
227246
}
228247

229248
// Function to deregister an image measurement
230249
function removeOsImageHash(bytes32 osImageHash) external onlyOwner {
231250
allowedOsImages[osImageHash] = false;
232251
emit OsImageHashRemoved(osImageHash);
252+
_emitPolicy("os-image", osImageHash, false);
233253
}
234254

235255
// Function to check if KMS is allowed to boot
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* SPDX-FileCopyrightText: © 2026 Phala Network <dstack@phala.network>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
pragma solidity ^0.8.24;
8+
9+
import "forge-std/Test.sol";
10+
import "openzeppelin-foundry-upgrades/Upgrades.sol";
11+
import "../contracts/DstackKms.sol";
12+
import "../contracts/DstackApp.sol";
13+
14+
contract EventAuditTest is Test {
15+
bytes32 private constant AUDIT_TOPIC = keccak256("PolicyChanged(address,bytes32,bytes32,bool)");
16+
17+
address private owner;
18+
address private outsider;
19+
DstackKms private kms;
20+
DstackApp private app;
21+
22+
function setUp() public {
23+
owner = makeAddr("audit-owner");
24+
outsider = makeAddr("audit-outsider");
25+
vm.startPrank(owner);
26+
DstackApp appImplementation = new DstackApp();
27+
kms = DstackKms(
28+
Upgrades.deployUUPSProxy(
29+
"DstackKms.sol", abi.encodeCall(DstackKms.initialize, (owner, address(appImplementation)))
30+
)
31+
);
32+
app = DstackApp(
33+
Upgrades.deployUUPSProxy(
34+
"DstackApp.sol",
35+
abi.encodeWithSignature(
36+
"initialize(address,bool,bool,bool,bytes32,bytes32)",
37+
owner,
38+
false,
39+
false,
40+
false,
41+
bytes32(0),
42+
bytes32(0)
43+
)
44+
)
45+
);
46+
vm.stopPrank();
47+
}
48+
49+
function test_KmsPolicyEventsReconstructQueriedState() public {
50+
bytes32 mr = keccak256("audit-mr");
51+
bytes32 device = keccak256("audit-device");
52+
bytes32 image = keccak256("audit-image");
53+
string memory gateway = "audit-gateway";
54+
55+
vm.recordLogs();
56+
vm.startPrank(owner);
57+
kms.addKmsAggregatedMr(mr);
58+
kms.addKmsDevice(device);
59+
kms.addOsImageHash(image);
60+
kms.setGatewayAppId(gateway);
61+
kms.registerApp(address(app));
62+
kms.removeKmsAggregatedMr(mr);
63+
vm.stopPrank();
64+
Vm.Log[] memory logs = vm.getRecordedLogs();
65+
66+
_assertAudit(logs, owner, "kms-aggregated-mr", mr, true);
67+
_assertAudit(logs, owner, "kms-device", device, true);
68+
_assertAudit(logs, owner, "os-image", image, true);
69+
_assertAudit(logs, owner, "gateway-app-id", keccak256(bytes(gateway)), true);
70+
_assertAudit(logs, owner, "registered-app", bytes32(uint256(uint160(address(app)))), true);
71+
_assertAudit(logs, owner, "kms-aggregated-mr", mr, false);
72+
73+
assertFalse(kms.kmsAllowedAggregatedMrs(mr));
74+
assertTrue(kms.kmsAllowedDeviceIds(device));
75+
assertTrue(kms.allowedOsImages(image));
76+
assertEq(kms.gatewayAppId(), gateway);
77+
assertTrue(kms.registeredApps(address(app)));
78+
}
79+
80+
function test_AppPolicyEventsReconstructQueriedState() public {
81+
bytes32 composeHash = keccak256("audit-compose");
82+
bytes32 device = keccak256("audit-app-device");
83+
84+
vm.recordLogs();
85+
vm.startPrank(owner);
86+
app.addComposeHash(composeHash);
87+
app.addDevice(device);
88+
app.setAllowAnyDevice(true);
89+
app.setRequireTcbUpToDate(true);
90+
app.removeDevice(device);
91+
app.disableUpgrades();
92+
vm.stopPrank();
93+
Vm.Log[] memory logs = vm.getRecordedLogs();
94+
95+
_assertAudit(logs, owner, "compose-hash", composeHash, true);
96+
_assertAudit(logs, owner, "device", device, true);
97+
_assertAudit(logs, owner, "allow-any-device", bytes32(0), true);
98+
_assertAudit(logs, owner, "require-tcb-up-to-date", bytes32(0), true);
99+
_assertAudit(logs, owner, "device", device, false);
100+
_assertAudit(logs, owner, "upgrades-disabled", bytes32(0), true);
101+
102+
assertTrue(app.allowedComposeHashes(composeHash));
103+
assertFalse(app.allowedDeviceIds(device));
104+
assertTrue(app.allowAnyDevice());
105+
assertTrue(app.requireTcbUpToDate());
106+
}
107+
108+
function test_InvalidMutationEmitsNoAuditAndLeavesNoPartialState() public {
109+
bytes32 image = keccak256("unauthorized-image");
110+
vm.recordLogs();
111+
vm.prank(outsider);
112+
vm.expectRevert();
113+
kms.addOsImageHash(image);
114+
Vm.Log[] memory logs = vm.getRecordedLogs();
115+
assertEq(_auditCount(logs), 0);
116+
assertFalse(kms.allowedOsImages(image));
117+
}
118+
119+
function test_ReorgDropsOrphanEventAndCanonicalEventRebuildsState() public {
120+
bytes32 orphaned = keccak256("orphaned-compose");
121+
bytes32 canonical = keccak256("canonical-compose");
122+
uint256 snapshot = vm.snapshotState();
123+
124+
vm.recordLogs();
125+
vm.prank(owner);
126+
app.addComposeHash(orphaned);
127+
Vm.Log[] memory orphanLogs = vm.getRecordedLogs();
128+
_assertAudit(orphanLogs, owner, "compose-hash", orphaned, true);
129+
assertTrue(app.allowedComposeHashes(orphaned));
130+
131+
assertTrue(vm.revertToState(snapshot));
132+
assertFalse(app.allowedComposeHashes(orphaned));
133+
134+
vm.recordLogs();
135+
vm.prank(owner);
136+
app.addComposeHash(canonical);
137+
Vm.Log[] memory canonicalLogs = vm.getRecordedLogs();
138+
_assertAudit(canonicalLogs, owner, "compose-hash", canonical, true);
139+
assertEq(_auditCount(canonicalLogs), 1);
140+
assertTrue(app.allowedComposeHashes(canonical));
141+
assertFalse(app.allowedComposeHashes(orphaned));
142+
}
143+
144+
function test_UpgradeAuditIncludesActorAndImplementation() public {
145+
vm.startPrank(owner);
146+
DstackApp replacement = new DstackApp();
147+
vm.recordLogs();
148+
app.upgradeToAndCall(address(replacement), "");
149+
Vm.Log[] memory logs = vm.getRecordedLogs();
150+
vm.stopPrank();
151+
152+
_assertAudit(logs, owner, "implementation-upgrade", bytes32(uint256(uint160(address(replacement)))), true);
153+
assertEq(Upgrades.getImplementationAddress(address(app)), address(replacement));
154+
}
155+
156+
function _assertAudit(
157+
Vm.Log[] memory logs,
158+
address actor,
159+
string memory policy,
160+
bytes32 value,
161+
bool enabled
162+
)
163+
private
164+
pure
165+
{
166+
bytes32 actorTopic = bytes32(uint256(uint160(actor)));
167+
bytes32 policyTopic = keccak256(bytes(policy));
168+
for (uint256 i = 0; i < logs.length; ++i) {
169+
Vm.Log memory entry = logs[i];
170+
if (
171+
entry.topics.length == 4 && entry.topics[0] == AUDIT_TOPIC && entry.topics[1] == actorTopic
172+
&& entry.topics[2] == policyTopic && entry.topics[3] == value
173+
&& abi.decode(entry.data, (bool)) == enabled
174+
) {
175+
return;
176+
}
177+
}
178+
revert("expected policy audit event not found");
179+
}
180+
181+
function _auditCount(Vm.Log[] memory logs) private pure returns (uint256 count) {
182+
for (uint256 i = 0; i < logs.length; ++i) {
183+
if (logs[i].topics.length == 4 && logs[i].topics[0] == AUDIT_TOPIC) ++count;
184+
}
185+
}
186+
}

0 commit comments

Comments
 (0)