@calimero-network/mero-js 25.0.7 → 25.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Which agent images to trust: a mero-kms release's signed agent allowlist,
3
+ * `kms-agent-attestation-policy.<profile>.json`, and a quote verifier built
4
+ * from it.
5
+ *
6
+ * The KMS release workflow measures the release's agent image and signs its
7
+ * allowlist with cosign keyless, beside the rest of the release. The allowlist
8
+ * can therefore come from anywhere (a release asset, mdma's mirror, a file in
9
+ * the app): {@link verifySignedAgentPolicy} checks the signature here, against
10
+ * the Sigstore trust root embedded in this package, and refuses anything else.
11
+ */
12
+ import { type SignerIdentity } from '../sealed/sigstore.js';
13
+ import { type DcapCollateral, type DcapVerify, type QuoteVerifier } from '../sealed/verify.js';
14
+ /** The workflow that signs every mero-kms release, agent allowlists included. */
15
+ export declare const KMS_RELEASE_SIGNER: Readonly<SignerIdentity>;
16
+ /** An agent allowlist, as the KMS release publishes it. Every list must be non-empty. */
17
+ export interface AgentPolicy {
18
+ schema_version?: number;
19
+ role?: string;
20
+ profile?: string;
21
+ /** The mero-kms release it belongs to. */
22
+ version?: string;
23
+ allowed_tcb_statuses: string[];
24
+ allowed_mrtd: string[];
25
+ allowed_rtmr0: string[];
26
+ allowed_rtmr1: string[];
27
+ allowed_rtmr2: string[];
28
+ allowed_rtmr3: string[];
29
+ }
30
+ export interface VerifySignedAgentPolicyOptions {
31
+ /** Require this profile; defaults to `locked-read-only`, the only one whose host cannot reach in. */
32
+ profile?: string;
33
+ /** Require the file's `version` to be this mero-kms release. */
34
+ expectedVersion?: string;
35
+ /** The certificate identity required. Defaults to {@link KMS_RELEASE_SIGNER}'s. */
36
+ identity?: string;
37
+ /** The OIDC issuer required. Defaults to {@link KMS_RELEASE_SIGNER}'s. */
38
+ issuer?: string;
39
+ }
40
+ /**
41
+ * Verify an agent allowlist against its cosign bundle, as `cosign verify-blob`
42
+ * would, and return it parsed. Rejects unless it was signed by the KMS release
43
+ * workflow, it is an agent allowlist (`role: "agent"`) for the profile asked
44
+ * for, every list in it is non-empty, and `expectedVersion`, when given, is its
45
+ * `version`.
46
+ */
47
+ export declare function verifySignedAgentPolicy(policy: string | Uint8Array, bundle: string | object, options?: VerifySignedAgentPolicyOptions): Promise<AgentPolicy>;
48
+ export interface AgentQuoteVerifierOptions {
49
+ /** `verify` from `@phala/dcap-qvl`, or anything that verifies a quote the same way. */
50
+ dcapVerify: DcapVerify;
51
+ /**
52
+ * The gate sends no collateral: fetch it, e.g.
53
+ * `(quote) => getCollateral(PHALA_PCCS_URL, quote)` from `@phala/dcap-qvl`.
54
+ */
55
+ fetchCollateral: (quote: Uint8Array) => Promise<DcapCollateral>;
56
+ /** The current time in milliseconds; defaults to `Date.now`. For tests. */
57
+ now?: () => number;
58
+ }
59
+ /**
60
+ * A verifier for {@link attestAgent} that accepts a quote only from an agent
61
+ * image in `policy`: its signature and collateral verify, its TCB status is
62
+ * one the policy allows (never `Revoked`), it is not a debug TD, and each of
63
+ * MRTD and RTMR0-3 is in the policy's list. Mock quotes never pass.
64
+ */
65
+ export declare function agentQuoteVerifier(policy: AgentPolicy, options: AgentQuoteVerifierOptions): QuoteVerifier;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Which agent images to trust: a mero-kms release's signed agent allowlist,
3
+ * `kms-agent-attestation-policy.<profile>.json`, and a quote verifier built
4
+ * from it.
5
+ *
6
+ * The KMS release workflow measures the release's agent image and signs its
7
+ * allowlist with cosign keyless, beside the rest of the release. The allowlist
8
+ * can therefore come from anywhere (a release asset, mdma's mirror, a file in
9
+ * the app): {@link verifySignedAgentPolicy} checks the signature here, against
10
+ * the Sigstore trust root embedded in this package, and refuses anything else.
11
+ */
12
+ import { verifyCosignBundle } from '../sealed/sigstore.js';
13
+ import { PRODUCTION_PROFILE, createQuoteVerifier, } from '../sealed/verify.js';
14
+ /** The workflow that signs every mero-kms release, agent allowlists included. */
15
+ export const KMS_RELEASE_SIGNER = Object.freeze({
16
+ identity: 'https://github.com/calimero-network/mero-tee/.github/workflows/release-kms.yaml@refs/heads/master',
17
+ issuer: 'https://token.actions.githubusercontent.com',
18
+ });
19
+ const LISTS = [
20
+ 'allowed_tcb_statuses',
21
+ 'allowed_mrtd',
22
+ 'allowed_rtmr0',
23
+ 'allowed_rtmr1',
24
+ 'allowed_rtmr2',
25
+ 'allowed_rtmr3',
26
+ ];
27
+ /**
28
+ * Verify an agent allowlist against its cosign bundle, as `cosign verify-blob`
29
+ * would, and return it parsed. Rejects unless it was signed by the KMS release
30
+ * workflow, it is an agent allowlist (`role: "agent"`) for the profile asked
31
+ * for, every list in it is non-empty, and `expectedVersion`, when given, is its
32
+ * `version`.
33
+ */
34
+ export async function verifySignedAgentPolicy(policy, bundle, options = {}) {
35
+ const body = typeof policy === 'string' ? new TextEncoder().encode(policy) : policy;
36
+ await verifyCosignBundle(body, bundle, {
37
+ identity: options.identity ?? KMS_RELEASE_SIGNER.identity,
38
+ issuer: options.issuer ?? KMS_RELEASE_SIGNER.issuer,
39
+ });
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(new TextDecoder().decode(body));
43
+ }
44
+ catch {
45
+ throw new Error('The signed agent policy is not JSON');
46
+ }
47
+ if (parsed?.role !== 'agent') {
48
+ throw new Error(`The signed file is a ${JSON.stringify(parsed?.role)} policy, not an agent one`);
49
+ }
50
+ const profile = options.profile ?? PRODUCTION_PROFILE;
51
+ if (parsed.profile !== profile) {
52
+ throw new Error(`The signed agent policy is for the ${JSON.stringify(parsed.profile)} profile, not ${profile}`);
53
+ }
54
+ if (options.expectedVersion !== undefined && parsed.version !== options.expectedVersion) {
55
+ throw new Error(`The signed agent policy is release ${JSON.stringify(parsed.version)}, not ${options.expectedVersion}`);
56
+ }
57
+ checkLists(parsed);
58
+ return parsed;
59
+ }
60
+ function checkLists(policy) {
61
+ for (const list of LISTS) {
62
+ if (!Array.isArray(policy?.[list]) || policy[list].length === 0) {
63
+ // As the gate's own verifier: an empty list accepts nothing.
64
+ throw new Error(`The agent policy's ${list} is empty, so no agent would be accepted`);
65
+ }
66
+ }
67
+ }
68
+ /** DCAP's spelling of each TCB status; the policy may use any case. */
69
+ const TCB_STATUSES = [
70
+ 'UpToDate',
71
+ 'SWHardeningNeeded',
72
+ 'ConfigurationNeeded',
73
+ 'ConfigurationAndSWHardeningNeeded',
74
+ 'OutOfDate',
75
+ 'OutOfDateConfigurationNeeded',
76
+ ];
77
+ function tcbStatus(value) {
78
+ const found = TCB_STATUSES.find((status) => status.toLowerCase() === value.trim().toLowerCase());
79
+ if (!found)
80
+ throw new Error(`The agent policy allows an unknown TCB status ${JSON.stringify(value)}`);
81
+ return found;
82
+ }
83
+ const register = (value) => value.trim().replace(/^0x/i, '');
84
+ /**
85
+ * A verifier for {@link attestAgent} that accepts a quote only from an agent
86
+ * image in `policy`: its signature and collateral verify, its TCB status is
87
+ * one the policy allows (never `Revoked`), it is not a debug TD, and each of
88
+ * MRTD and RTMR0-3 is in the policy's list. Mock quotes never pass.
89
+ */
90
+ export function agentQuoteVerifier(policy, options) {
91
+ checkLists(policy);
92
+ return createQuoteVerifier({
93
+ dcapVerify: options.dcapVerify,
94
+ fetchCollateral: options.fetchCollateral,
95
+ now: options.now,
96
+ allowedTcbStatuses: policy.allowed_tcb_statuses.map(tcbStatus),
97
+ allowedMrtd: policy.allowed_mrtd.map(register),
98
+ allowedRtmr0: policy.allowed_rtmr0.map(register),
99
+ allowedRtmr1: policy.allowed_rtmr1.map(register),
100
+ allowedRtmr2: policy.allowed_rtmr2.map(register),
101
+ allowedRtmr3: policy.allowed_rtmr3.map(register),
102
+ });
103
+ }