Extend the AppAuth contract with custom authorization logic for your dstack apps.
dstack uses on-chain contracts to authorize which apps can access the KMS. The base DstackApp.sol contract provides simple compose-hash and device-id whitelisting. You can extend this with custom logic:
- NFT-gated membership (1 NFT = 1 authorized node)
- Timelock governance (delay before new compose hashes activate)
- Multi-sig approval
- On-chain voting
Every authorization contract implements IAppAuth:
interface IAppAuth {
struct AppBootInfo {
bytes32 appId;
bytes32 instanceId;
bytes32 composeHash;
bytes32 deviceId;
bytes32 mrAggregated;
bytes32 mrSystem;
bytes32 osImageHash;
string tcbStatus;
string[] advisoryIds;
}
function isAppAllowed(AppBootInfo calldata bootInfo)
external view returns (bool isAllowed, string memory reason);
}The KMS calls isAppAllowed() when an app requests keys. Your contract decides if the app should receive them based on whatever logic you implement.
The default contract checks two things:
- Compose hash whitelist — Is
bootInfo.composeHashin the allowed set? - Device whitelist — Is
bootInfo.deviceIdallowed (orallowAnyDeviceenabled)?
function isAppAllowed(AppBootInfo calldata bootInfo)
external view returns (bool, string memory)
{
if (!allowedComposeHashes[bootInfo.composeHash])
return (false, "Compose hash not allowed");
if (!allowAnyDevice && !allowedDevices[bootInfo.deviceId])
return (false, "Device not allowed");
return (true, "");
}Source: Dstack-TEE/dstack/kms/auth-eth/contracts
The dstack-nft-cluster project extends authorization with NFT membership:
contract DstackMembershipNFT is ERC721, IAppAuth {
mapping(uint256 => bytes32) public tokenToInstanceId;
mapping(bytes32 => string) public instanceToConnectionUrl;
function isAppAllowed(AppBootInfo calldata bootInfo)
external view returns (bool, string memory)
{
// Check if instanceId is registered to an NFT
if (!isInstanceRegistered(bootInfo.instanceId))
return (false, "Instance not registered to NFT");
// Verify signature chain from KMS
if (!verifySignatureChain(bootInfo))
return (false, "Invalid signature chain");
return (true, "");
}
function registerInstance(uint256 tokenId, string calldata name) external {
require(ownerOf(tokenId) == msg.sender, "Not token owner");
// ...
}
}This creates a "1 NFT = 1 node" model where token holders control cluster participation.
Add a delay before new compose hashes become active:
contract TimelockAppAuth is DstackApp {
uint256 public constant DELAY = 2 days;
mapping(bytes32 => uint256) public pendingComposeHashes;
function proposeComposeHash(bytes32 hash) external onlyOwner {
pendingComposeHashes[hash] = block.timestamp + DELAY;
}
function activateComposeHash(bytes32 hash) external {
require(pendingComposeHashes[hash] != 0, "Not proposed");
require(block.timestamp >= pendingComposeHashes[hash], "Too early");
allowedComposeHashes[hash] = true;
delete pendingComposeHashes[hash];
}
}Require multiple signers before adding compose hashes:
contract MultiSigAppAuth is DstackApp {
uint256 public threshold;
mapping(bytes32 => mapping(address => bool)) public approvals;
mapping(bytes32 => uint256) public approvalCount;
function approve(bytes32 hash) external {
require(isSigner[msg.sender], "Not a signer");
require(!approvals[hash][msg.sender], "Already approved");
approvals[hash][msg.sender] = true;
approvalCount[hash]++;
if (approvalCount[hash] >= threshold)
allowedComposeHashes[hash] = true;
}
}| Field | Description |
|---|---|
appId |
Hash of app configuration (compose-hash) |
instanceId |
Unique identifier for this running instance |
composeHash |
SHA-256 of app-compose.json manifest |
deviceId |
Hardware identifier of the TEE |
mrAggregated |
Combined measurement of firmware + OS |
mrSystem |
System-level measurement |
osImageHash |
Hash of the dstack OS image |
tcbStatus |
Intel TCB status (UpToDate, OutOfDate, etc.) |
advisoryIds |
List of applicable Intel security advisories |
Use these fields to implement sophisticated authorization policies. For example, reject apps running on outdated firmware:
if (keccak256(bytes(bootInfo.tcbStatus)) != keccak256("UpToDate"))
return (false, "TCB not up to date");- Deploy your custom contract to a supported chain (Base, Ethereum, etc.)
- Configure the KMS to use your contract address
- Deploy apps — they'll be authorized via your contract
For Phala Cloud's on-chain KMS, see Cloud vs On-chain KMS.
- IAppAuth interface
- DstackApp base contract
- dstack-nft-cluster — NFT-gated authorization example
- Key Management Protocol
- 01-attestation: Understand attestation verification
- 02-kms-and-signing: How apps derive keys from KMS