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'
1616const client = new DstackClient ()
1717const result = await client .getKey (' /oracle' , ' ethereum' )
1818
19- // Derive an Ethereum wallet
2019const 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
4644const result = await client .getKey (' /oracle' , ' ethereum' )
4745
@@ -50,44 +48,86 @@ result.signature_chain[0] // App signature: appKey signs derivedPubkey
5048result .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
100140await 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