Skip to content

Commit 29dab51

Browse files
committed
improve flow and testing of example 2
1 parent c1f957e commit 29dab51

3 files changed

Lines changed: 200 additions & 131 deletions

File tree

tutorial/02-kms-and-signing/README.md

Lines changed: 153 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Tutorial 02: KMS and Signing
22

3-
Derive persistent keys that survive restarts and produce on-chain verifiable signatures.
3+
Derive persistent keys that survive restarts and produce verifiable signatures.
44

55
## The Problem
66

@@ -16,7 +16,6 @@ import { DstackClient } from '@phala/dstack-sdk'
1616
const client = new DstackClient()
1717
const result = await client.getKey('/oracle', 'ethereum')
1818

19-
// Derive an Ethereum wallet
2019
const privateKey = '0x' + Buffer.from(result.key).toString('hex').slice(0, 64)
2120
```
2221

@@ -25,23 +24,22 @@ The derived key is:
2524
- **Unique to your app**: Different apps (compose hashes) get different keys
2625
- **Verifiable**: Signature chain proves the key came from KMS
2726

28-
## How It Works
27+
## Signature Chain
28+
29+
The KMS returns a **signature chain** proving derivation:
2930

3031
```
31-
┌─────────────────────────────────────────────────────────────┐
32-
│ KMS │
33-
│ (runs in its own TEE, holds root keys) │
34-
│ │
35-
│ Root Key ──derives──▶ App Key ──derives──▶ Your Key │
36-
│ │ │ │ │
37-
│ │ │ └─ getKey('/oracle')
38-
│ │ └─ tied to appId (compose hash)
39-
│ └─ KMS root, known on-chain
40-
└─────────────────────────────────────────────────────────────┘
32+
KMS Root (known on-chain)
33+
34+
│ signs: "dstack-kms-issued:" + appId + appPubkey
35+
36+
App Key (recovered from kmsSignature)
37+
38+
│ signs: "ethereum:" + derivedPubkeyHex
39+
40+
Derived Key → signs your messages
4141
```
4242

43-
The KMS returns a **signature chain** proving derivation:
44-
4543
```javascript
4644
const result = await client.getKey('/oracle', 'ethereum')
4745

@@ -50,44 +48,86 @@ result.signature_chain[0] // App signature: appKey signs derivedPubkey
5048
result.signature_chain[1] // KMS signature: kmsRoot signs appPubkey
5149
```
5250

53-
## On-Chain Verification
51+
## Try It
5452

55-
The signature chain lets smart contracts verify a signature came from a TEE:
53+
```bash
54+
pip install -r requirements.txt
55+
phala simulator start
5656

57+
docker compose build
58+
docker compose run --rm -p 8080:8080 \
59+
-v ~/.phala-cloud/simulator/0.5.3/dstack.sock:/var/run/dstack.sock \
60+
app
5761
```
58-
KMS Root (known on-chain: 0x2f83172A...)
59-
60-
│ signs: "dstack-kms-issued:" + appId + appPubkey
61-
62-
App Key (recovered from kmsSignature)
63-
64-
│ signs: "ethereum:" + derivedPubkeyHex
65-
66-
Derived Key → signs your messages
62+
63+
In another terminal:
64+
65+
```bash
66+
python3 test_local.py
6767
```
6868

69-
A contract can:
70-
1. Recover `appPubkey` from `kmsSignature` and verify it matches KMS root
71-
2. Recover `derivedPubkey` from `appSignature`
72-
3. Verify the message signature against `derivedPubkey`
69+
Output:
70+
```
71+
TEE Oracle Signature Chain Verification
72+
============================================================
73+
Oracle URL: http://localhost:8080
74+
KMS Root: 0x8f2cF602C9695b23130367ed78d8F557554de7C5
75+
76+
Fetching from oracle...
77+
Got price: $97234.0
78+
Source: api.coingecko.com
79+
80+
Verifying Signature Chain
81+
==================================================
82+
App ID: 0x...
83+
Derived Pubkey: 02a1b2c3d4e5f6...
84+
85+
Step 1: App signature over derived key
86+
App Address: 0x...
87+
88+
Step 2: KMS signature over app key
89+
Recovered KMS: 0x8f2cF602C9695b23130367ed78d8F557554de7C5
90+
Expected KMS: 0x8f2cF602C9695b23130367ed78d8F557554de7C5
91+
OK: KMS signature verified
92+
93+
Step 3: Message signature
94+
Recovered signer: 0x...
95+
Expected signer: 0x...
96+
OK: Message signature verified
97+
98+
============================================================
99+
All verifications passed:
100+
- KMS signed the app key
101+
- App key signed the derived key
102+
- Derived key signed the oracle message
103+
```
73104

74-
## Minimal Example
105+
## Verifying the Signature Chain
75106

76-
```javascript
77-
import { DstackClient } from '@phala/dstack-sdk'
78-
import { privateKeyToAccount } from 'viem/accounts'
107+
The verification steps:
79108

80-
const client = new DstackClient()
109+
1. **App signature** — Recover the app public key from `appSignature` over the message `"{purpose}:{derivedPubkeyHex}"`
81110

82-
// Derive a persistent wallet
83-
const result = await client.getKey('/wallet', 'ethereum')
84-
const privateKey = '0x' + Buffer.from(result.key).toString('hex').slice(0, 64)
85-
const account = privateKeyToAccount(privateKey)
111+
2. **KMS signature** — Recover the KMS signer from `kmsSignature` over the message `"dstack-kms-issued:" + appId + appPubkeyCompressed`. Compare against known KMS root.
112+
113+
3. **Message signature** — Recover the signer from the message signature. Compare against address derived from `derivedPubkey`.
86114

87-
console.log('Address:', account.address) // Same every restart
115+
If all three pass, the signature chain is valid: the message was signed by a key derived from KMS for this specific app.
116+
117+
## On-Chain Verification
88118

89-
// Sign a message
90-
const signature = await account.signMessage({ message: 'hello' })
119+
The same verification can run in a smart contract. See [04-onchain-oracle](../04-onchain-oracle) for `TeeOracle.sol` which implements:
120+
121+
```solidity
122+
function verify(
123+
bytes32 messageHash,
124+
bytes calldata messageSignature,
125+
bytes calldata appSignature,
126+
bytes calldata kmsSignature,
127+
bytes calldata derivedCompressedPubkey,
128+
bytes calldata appCompressedPubkey,
129+
string calldata purpose
130+
) public view returns (bool isValid)
91131
```
92132

93133
## Key Paths
@@ -100,20 +140,84 @@ await client.getKey('/wallet/fees') // Fee payer
100140
await client.getKey('/signing/oracle') // Oracle signatures
101141
```
102142

103-
## Try It
143+
## Multi-Node Deployment
144+
145+
Multiple TEE nodes can derive the **same key** if they share the same `appId`. This enables redundancy and load balancing while maintaining a single signing identity.
146+
147+
### Deploy with allowAnyDevice
148+
149+
The simplest multi-node setup uses `allowAnyDevice=true`, which lets any TEE with the correct compose hash join:
104150

105151
```bash
106-
phala simulator start
152+
# Deploy first node (deploys AppAuth contract with allowAnyDevice=true)
153+
export PRIVATE_KEY="0x..."
154+
python3 deploy_with_contract.py
155+
```
107156

108-
docker compose run --rm \
109-
-v ~/.phala-cloud/simulator/0.5.3/dstack.sock:/var/run/dstack.sock \
110-
app
157+
Output:
158+
```
159+
SUCCESS! Save this for deploying replicas:
160+
APP_ID=0xc96d55b03ede924c89154348be9dcffd52304af0
161+
COMPOSE_HASH=0x392b8a1f...
162+
```
163+
164+
### Deploy Replicas
165+
166+
Edit `deploy_replica.py` with the APP_ID from above, then:
167+
168+
```bash
169+
python3 deploy_replica.py
170+
```
171+
172+
Both nodes now derive the same key:
173+
```
174+
Node 1: Oracle signer: 0x7a3B... (same!)
175+
Node 2: Oracle signer: 0x7a3B... (same!)
176+
```
177+
178+
### How It Works
179+
180+
```
181+
┌─────────────────────────────────────────────────────────────┐
182+
│ AppAuth Contract (allowAnyDevice=true) │
183+
│ │
184+
│ allowedComposeHashes[0x392b...] = true │
185+
│ allowAnyDevice = true │
186+
│ │
187+
│ isAppAllowed(bootInfo): │
188+
│ if composeHash in allowedComposeHashes → ALLOW │
189+
│ (device ID doesn't matter) │
190+
└─────────────────────────────────────────────────────────────┘
191+
│ │
192+
▼ ▼
193+
┌──────────┐ ┌──────────┐
194+
│ Node 1 │ │ Node 2 │
195+
│ prod5 │ │ prod9 │
196+
└──────────┘ └──────────┘
197+
│ │
198+
│ getKey("/oracle") │ getKey("/oracle")
199+
▼ ▼
200+
Same derived key Same derived key
201+
```
202+
203+
For more controlled multi-node setups (owner-approved devices, custom AppAuth), see [04-onchain-oracle](../04-onchain-oracle).
204+
205+
## Files
206+
207+
```
208+
02-kms-and-signing/
209+
├── docker-compose.yaml # Oracle with signing
210+
├── test_local.py # Signature chain verification
211+
├── deploy_with_contract.py # Deploy with allowAnyDevice=true
212+
├── deploy_replica.py # Deploy replica using existing appId
213+
├── requirements.txt # Python dependencies
214+
└── README.md
111215
```
112216

113217
## Next Steps
114218

115-
- [01-attestation](../01-attestation): Understand TDX quotes and verification
116-
- [04-onchain-oracle](../04-onchain-oracle): Full oracle with signature chain verification contract
219+
- [03-gateway-and-tls](../03-gateway-and-tls): Custom domains and TLS
220+
- [04-onchain-oracle](../04-onchain-oracle): On-chain verification contract, AppAuth deployment
117221

118222
## References
119223

0 commit comments

Comments
 (0)