Skip to content

Commit 2dab2f8

Browse files
committed
test: add KMS unit tests and cross-language serde contract tests
- Unit tests for validate_onchain_tcb_policy: empty policy, version checks, malformed JSON, invalid Rego, TcbPolicyDoc deserialization - Serde contract tests with shared JSON fixtures (kms/tests/fixtures/) validated by both Rust and TypeScript to catch field naming drift - TDX quote sample fixtures for building QuoteVerificationResult in tests - url_join utility test
1 parent 27c5d55 commit 2dab2f8

8 files changed

Lines changed: 247 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// SPDX-FileCopyrightText: © 2025 Phala Network <dstack@phala.network>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
/**
6+
* Serde contract tests: verify that TypeScript types correctly
7+
* parse/produce the same JSON fixtures used by the Rust KMS tests.
8+
*
9+
* Direction-aware:
10+
* - BootInfo: KMS→auth-eth → TS is the consumer → test deserialization
11+
* - BootResponse: auth-eth→KMS → TS is the producer → test serialization
12+
* - PolicyResponse: auth-eth→KMS → TS is the producer → test serialization
13+
*/
14+
15+
import { BootInfo, BootResponse, PolicyResponse } from '../src/types';
16+
import * as fs from 'fs';
17+
import * as path from 'path';
18+
19+
const FIXTURES_DIR = path.resolve(__dirname, '../../tests/fixtures');
20+
21+
describe('Serde contract tests (shared fixtures)', () => {
22+
describe('BootInfo (KMS→auth-eth: TS deserializes)', () => {
23+
it('should deserialize the shared fixture produced by Rust', () => {
24+
const json = JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, 'boot_info.json'), 'utf8'));
25+
const info: BootInfo = json;
26+
// Verify all required fields are accessible after deserialization
27+
expect(typeof info.mrAggregated).toBe('string');
28+
expect(typeof info.osImageHash).toBe('string');
29+
expect(typeof info.mrSystem).toBe('string');
30+
expect(typeof info.appId).toBe('string');
31+
expect(typeof info.composeHash).toBe('string');
32+
expect(typeof info.instanceId).toBe('string');
33+
expect(typeof info.deviceId).toBe('string');
34+
expect(typeof info.tcbStatus).toBe('string');
35+
expect(Array.isArray(info.advisoryIds)).toBe(true);
36+
expect(info.tcbStatus).toBe('UpToDate');
37+
expect(info.advisoryIds).toEqual(['INTEL-SA-00001']);
38+
});
39+
40+
it('should contain all fields expected by the server schema', () => {
41+
const json = JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, 'boot_info.json'), 'utf8'));
42+
const requiredFields = ['mrAggregated', 'osImageHash', 'appId', 'composeHash', 'instanceId', 'deviceId'];
43+
for (const field of requiredFields) {
44+
expect(json).toHaveProperty(field);
45+
}
46+
});
47+
});
48+
49+
describe('BootResponse (auth-eth→KMS: TS serializes)', () => {
50+
it('should produce JSON matching the shared fixture consumed by Rust', () => {
51+
const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, 'boot_response.json'), 'utf8'));
52+
// Construct the response as auth-eth would
53+
const resp: BootResponse = {
54+
isAllowed: true,
55+
gatewayAppId: '0x1234567890abcdef1234567890abcdef12345678',
56+
reason: '',
57+
};
58+
expect(JSON.parse(JSON.stringify(resp))).toEqual(fixture);
59+
});
60+
});
61+
62+
describe('PolicyResponse (auth-eth→KMS: TS serializes)', () => {
63+
it('should produce JSON matching the shared fixture consumed by Rust', () => {
64+
const fixture = JSON.parse(fs.readFileSync(path.join(FIXTURES_DIR, 'policy_response.json'), 'utf8'));
65+
// Construct the response as auth-eth would
66+
const resp: PolicyResponse = {
67+
tcbPolicy: '{"version":1,"intel_qal":[]}',
68+
};
69+
expect(JSON.parse(JSON.stringify(resp))).toEqual(fixture);
70+
});
71+
});
72+
});

kms/src/main_service.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,91 @@ impl RpcCall<KmsState> for RpcHandler {
491491
pub fn rpc_methods() -> &'static [&'static str] {
492492
<KmsServer<RpcHandler>>::supported_methods()
493493
}
494+
495+
#[cfg(test)]
496+
mod tests {
497+
use super::*;
498+
499+
/// Build a QuoteVerificationResult from the bundled TDX sample fixtures.
500+
fn sample_qvr() -> QuoteVerificationResult {
501+
let raw_quote = include_bytes!("../tests/fixtures/tdx_quote");
502+
let collateral: dcap_qvl::QuoteCollateralV3 =
503+
serde_json::from_slice(include_bytes!("../tests/fixtures/tdx_quote_collateral.json"))
504+
.expect("parse collateral");
505+
// Timestamp within the collateral validity window
506+
let now = 1_750_400_000;
507+
dcap_qvl::verify::QuoteVerifier::new_prod_default_crypto()
508+
.verify(raw_quote, collateral, now)
509+
.expect("verify sample quote")
510+
}
511+
512+
#[test]
513+
fn empty_policy_is_noop() {
514+
let qvr = sample_qvr();
515+
validate_onchain_tcb_policy(&qvr, "").unwrap();
516+
}
517+
518+
#[test]
519+
fn version1_empty_intel_qal_is_noop() {
520+
let qvr = sample_qvr();
521+
validate_onchain_tcb_policy(&qvr, r#"{"version":1,"intel_qal":[]}"#).unwrap();
522+
}
523+
524+
#[test]
525+
fn unknown_version_is_rejected() {
526+
let qvr = sample_qvr();
527+
let err = validate_onchain_tcb_policy(&qvr, r#"{"version":99,"intel_qal":[]}"#)
528+
.unwrap_err();
529+
assert!(
530+
err.to_string().contains("Unsupported TCB policy version 99"),
531+
"unexpected error: {err}"
532+
);
533+
}
534+
535+
#[test]
536+
fn malformed_json_is_rejected() {
537+
let qvr = sample_qvr();
538+
let err = validate_onchain_tcb_policy(&qvr, "not json").unwrap_err();
539+
assert!(
540+
err.to_string().contains("Failed to parse"),
541+
"unexpected error: {err}"
542+
);
543+
}
544+
545+
#[test]
546+
fn missing_version_field_is_rejected() {
547+
let qvr = sample_qvr();
548+
let err = validate_onchain_tcb_policy(&qvr, r#"{"intel_qal":[]}"#).unwrap_err();
549+
// version is required — serde fails, wrapped with "Failed to parse" context
550+
assert!(
551+
err.to_string().contains("Failed to parse"),
552+
"unexpected error: {err}"
553+
);
554+
}
555+
556+
#[test]
557+
fn tcb_policy_doc_deserialization() {
558+
let doc: TcbPolicyDoc =
559+
serde_json::from_str(r#"{"version":1,"intel_qal":["policy1","policy2"]}"#).unwrap();
560+
assert_eq!(doc.version, 1);
561+
assert_eq!(doc.intel_qal.len(), 2);
562+
563+
// intel_qal defaults to empty when omitted
564+
let doc: TcbPolicyDoc = serde_json::from_str(r#"{"version":1}"#).unwrap();
565+
assert!(doc.intel_qal.is_empty());
566+
}
567+
568+
#[test]
569+
fn invalid_rego_policy_is_rejected() {
570+
let qvr = sample_qvr();
571+
let err = validate_onchain_tcb_policy(
572+
&qvr,
573+
r#"{"version":1,"intel_qal":["not valid rego json"]}"#,
574+
)
575+
.unwrap_err();
576+
assert!(
577+
err.to_string().contains("Failed to build RegoPolicySet"),
578+
"unexpected error: {err}"
579+
);
580+
}
581+
}

kms/src/main_service/upgrade_authority.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,58 @@ fn url_join(url: &str, path: &str) -> String {
165165
url.push_str(path);
166166
url
167167
}
168+
169+
#[cfg(test)]
170+
mod tests {
171+
use super::*;
172+
173+
/// BootInfo: KMS is the producer (serializer).
174+
/// Verify that Rust serialization output matches the shared fixture exactly.
175+
#[test]
176+
fn boot_info_serialization_matches_fixture() {
177+
let fixture_json = include_str!("../../tests/fixtures/boot_info.json");
178+
// Deserialize to construct the value, then re-serialize and compare.
179+
// This proves Rust's serialization produces JSON that TS can consume.
180+
let info: BootInfo =
181+
serde_json::from_str(fixture_json).expect("deserialize BootInfo from fixture");
182+
let serialized = serde_json::to_value(&info).expect("serialize BootInfo");
183+
let expected: serde_json::Value =
184+
serde_json::from_str(fixture_json).expect("parse fixture");
185+
assert_eq!(
186+
serialized, expected,
187+
"Rust BootInfo serialization must match fixture (consumed by TS auth-eth)"
188+
);
189+
}
190+
191+
/// BootResponse: auth-eth is the producer, KMS is the consumer (deserializer).
192+
/// Verify that Rust can deserialize the shared fixture correctly.
193+
#[test]
194+
fn boot_response_deserialization_from_fixture() {
195+
let fixture_json = include_str!("../../tests/fixtures/boot_response.json");
196+
let resp: BootResponse =
197+
serde_json::from_str(fixture_json).expect("deserialize BootResponse from fixture");
198+
assert!(resp.is_allowed);
199+
assert_eq!(resp.gateway_app_id, "0x1234567890abcdef1234567890abcdef12345678");
200+
assert!(resp.reason.is_empty());
201+
}
202+
203+
/// PolicyResponse: auth-eth is the producer, KMS is the consumer (deserializer).
204+
/// Verify that Rust can deserialize the shared fixture correctly.
205+
#[test]
206+
fn policy_response_deserialization_from_fixture() {
207+
let fixture_json = include_str!("../../tests/fixtures/policy_response.json");
208+
let resp: PolicyResponse =
209+
serde_json::from_str(fixture_json).expect("deserialize PolicyResponse from fixture");
210+
assert!(!resp.tcb_policy.is_empty());
211+
// Verify the embedded JSON is valid
212+
let policy: serde_json::Value =
213+
serde_json::from_str(&resp.tcb_policy).expect("parse embedded tcbPolicy JSON");
214+
assert_eq!(policy["version"], 1);
215+
}
216+
217+
#[test]
218+
fn url_join_appends_path_correctly() {
219+
assert_eq!(url_join("http://host", "path"), "http://host/path");
220+
assert_eq!(url_join("http://host/", "path"), "http://host/path");
221+
}
222+
}

kms/tests/fixtures/boot_info.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"attestationMode": "dstack-tdx",
3+
"mrAggregated": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
4+
"osImageHash": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
5+
"mrSystem": "9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba",
6+
"appId": "1234567890123456789012345678901234567890",
7+
"composeHash": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
8+
"instanceId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
9+
"deviceId": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
10+
"keyProviderInfo": "",
11+
"tcbStatus": "UpToDate",
12+
"advisoryIds": ["INTEL-SA-00001"]
13+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"isAllowed": true,
3+
"gatewayAppId": "0x1234567890abcdef1234567890abcdef12345678",
4+
"reason": ""
5+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"tcbPolicy": "{\"version\":1,\"intel_qal\":[]}"
3+
}

kms/tests/fixtures/tdx_quote

4.89 KB
Binary file not shown.

0 commit comments

Comments
 (0)