@freedomofpress/sigstore-browser 0.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.
- package/.github/workflows/conformance.yml +29 -0
- package/.github/workflows/publish.yml +45 -0
- package/README.md +10 -0
- package/dist/asn1/error.d.ts +5 -0
- package/dist/asn1/error.d.ts.map +1 -0
- package/dist/asn1/error.js +20 -0
- package/dist/asn1/error.js.map +1 -0
- package/dist/asn1/index.d.ts +2 -0
- package/dist/asn1/index.d.ts.map +1 -0
- package/dist/asn1/index.js +17 -0
- package/dist/asn1/index.js.map +1 -0
- package/dist/asn1/length.d.ts +4 -0
- package/dist/asn1/length.d.ts.map +1 -0
- package/dist/asn1/length.js +59 -0
- package/dist/asn1/length.js.map +1 -0
- package/dist/asn1/obj.d.ts +15 -0
- package/dist/asn1/obj.d.ts.map +1 -0
- package/dist/asn1/obj.js +153 -0
- package/dist/asn1/obj.js.map +1 -0
- package/dist/asn1/parse.d.ts +7 -0
- package/dist/asn1/parse.d.ts.map +1 -0
- package/dist/asn1/parse.js +118 -0
- package/dist/asn1/parse.js.map +1 -0
- package/dist/asn1/tag.d.ts +17 -0
- package/dist/asn1/tag.d.ts.map +1 -0
- package/dist/asn1/tag.js +86 -0
- package/dist/asn1/tag.js.map +1 -0
- package/dist/bundle.d.ts +87 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.js +2 -0
- package/dist/bundle.js.map +1 -0
- package/dist/canonicalize.d.ts +2 -0
- package/dist/canonicalize.d.ts.map +1 -0
- package/dist/canonicalize.js +64 -0
- package/dist/canonicalize.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +217 -0
- package/dist/cli.js.map +1 -0
- package/dist/crypto.d.ts +5 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +260 -0
- package/dist/crypto.js.map +1 -0
- package/dist/dsse.d.ts +2 -0
- package/dist/dsse.d.ts.map +1 -0
- package/dist/dsse.js +42 -0
- package/dist/dsse.js.map +1 -0
- package/dist/encoding.d.ts +17 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +112 -0
- package/dist/encoding.js.map +1 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +79 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces.d.ts +104 -0
- package/dist/interfaces.d.ts.map +1 -0
- package/dist/interfaces.js +26 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/oid.d.ts +5 -0
- package/dist/oid.d.ts.map +1 -0
- package/dist/oid.js +25 -0
- package/dist/oid.js.map +1 -0
- package/dist/pem.d.ts +3 -0
- package/dist/pem.d.ts.map +1 -0
- package/dist/pem.js +41 -0
- package/dist/pem.js.map +1 -0
- package/dist/rfc3161/error.d.ts +3 -0
- package/dist/rfc3161/error.d.ts.map +1 -0
- package/dist/rfc3161/error.js +18 -0
- package/dist/rfc3161/error.js.map +1 -0
- package/dist/rfc3161/index.d.ts +2 -0
- package/dist/rfc3161/index.d.ts.map +1 -0
- package/dist/rfc3161/index.js +17 -0
- package/dist/rfc3161/index.js.map +1 -0
- package/dist/rfc3161/timestamp.d.ts +36 -0
- package/dist/rfc3161/timestamp.d.ts.map +1 -0
- package/dist/rfc3161/timestamp.js +203 -0
- package/dist/rfc3161/timestamp.js.map +1 -0
- package/dist/rfc3161/tstinfo.d.ts +13 -0
- package/dist/rfc3161/tstinfo.d.ts.map +1 -0
- package/dist/rfc3161/tstinfo.js +47 -0
- package/dist/rfc3161/tstinfo.js.map +1 -0
- package/dist/sigstore.d.ts +33 -0
- package/dist/sigstore.d.ts.map +1 -0
- package/dist/sigstore.js +531 -0
- package/dist/sigstore.js.map +1 -0
- package/dist/stream.d.ts +22 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +114 -0
- package/dist/stream.js.map +1 -0
- package/dist/timestamp/tsa.d.ts +24 -0
- package/dist/timestamp/tsa.d.ts.map +1 -0
- package/dist/timestamp/tsa.js +150 -0
- package/dist/timestamp/tsa.js.map +1 -0
- package/dist/tlog/body.d.ts +8 -0
- package/dist/tlog/body.d.ts.map +1 -0
- package/dist/tlog/body.js +45 -0
- package/dist/tlog/body.js.map +1 -0
- package/dist/tlog/checkpoint.d.ts +23 -0
- package/dist/tlog/checkpoint.d.ts.map +1 -0
- package/dist/tlog/checkpoint.js +187 -0
- package/dist/tlog/checkpoint.js.map +1 -0
- package/dist/tlog/dsse.d.ts +4 -0
- package/dist/tlog/dsse.d.ts.map +1 -0
- package/dist/tlog/dsse.js +85 -0
- package/dist/tlog/dsse.js.map +1 -0
- package/dist/tlog/hashedrekord.d.ts +4 -0
- package/dist/tlog/hashedrekord.d.ts.map +1 -0
- package/dist/tlog/hashedrekord.js +63 -0
- package/dist/tlog/hashedrekord.js.map +1 -0
- package/dist/tlog/intoto.d.ts +4 -0
- package/dist/tlog/intoto.d.ts.map +1 -0
- package/dist/tlog/intoto.js +50 -0
- package/dist/tlog/intoto.js.map +1 -0
- package/dist/tlog/merkle.d.ts +3 -0
- package/dist/tlog/merkle.d.ts.map +1 -0
- package/dist/tlog/merkle.js +112 -0
- package/dist/tlog/merkle.js.map +1 -0
- package/dist/trust/tuf-root.json +1 -0
- package/dist/trust/tuf.d.ts +95 -0
- package/dist/trust/tuf.d.ts.map +1 -0
- package/dist/trust/tuf.js +150 -0
- package/dist/trust/tuf.js.map +1 -0
- package/dist/x509/cert.d.ts +52 -0
- package/dist/x509/cert.d.ts.map +1 -0
- package/dist/x509/cert.js +256 -0
- package/dist/x509/cert.js.map +1 -0
- package/dist/x509/chain.d.ts +20 -0
- package/dist/x509/chain.d.ts.map +1 -0
- package/dist/x509/chain.js +141 -0
- package/dist/x509/chain.js.map +1 -0
- package/dist/x509/ext.d.ts +48 -0
- package/dist/x509/ext.d.ts.map +1 -0
- package/dist/x509/ext.js +164 -0
- package/dist/x509/ext.js.map +1 -0
- package/dist/x509/index.d.ts +4 -0
- package/dist/x509/index.d.ts.map +1 -0
- package/dist/x509/index.js +19 -0
- package/dist/x509/index.js.map +1 -0
- package/dist/x509/sct.d.ts +25 -0
- package/dist/x509/sct.d.ts.map +1 -0
- package/dist/x509/sct.js +123 -0
- package/dist/x509/sct.js.map +1 -0
- package/eslint.config.mjs +31 -0
- package/package.json +39 -0
- package/src/asn1/error.ts +18 -0
- package/src/asn1/index.ts +16 -0
- package/src/asn1/length.ts +71 -0
- package/src/asn1/obj.ts +184 -0
- package/src/asn1/parse.ts +146 -0
- package/src/asn1/tag.ts +105 -0
- package/src/bundle.ts +99 -0
- package/src/canonicalize.ts +61 -0
- package/src/cli.ts +285 -0
- package/src/crypto.ts +310 -0
- package/src/dsse.ts +49 -0
- package/src/encoding.ts +141 -0
- package/src/errors.ts +82 -0
- package/src/index.browser.test.ts +219 -0
- package/src/index.ts +14 -0
- package/src/interfaces.ts +118 -0
- package/src/oid.ts +28 -0
- package/src/pem.ts +48 -0
- package/src/rfc3161/error.ts +16 -0
- package/src/rfc3161/index.ts +17 -0
- package/src/rfc3161/timestamp.ts +270 -0
- package/src/rfc3161/tstinfo.ts +80 -0
- package/src/sigstore.ts +728 -0
- package/src/stream.ts +141 -0
- package/src/timestamp/tsa.ts +205 -0
- package/src/tlog/body.ts +65 -0
- package/src/tlog/checkpoint.ts +248 -0
- package/src/tlog/dsse.ts +147 -0
- package/src/tlog/hashedrekord.ts +130 -0
- package/src/tlog/intoto.ts +102 -0
- package/src/tlog/merkle.ts +152 -0
- package/src/trust/README.md +38 -0
- package/src/trust/tuf-root.json +1 -0
- package/src/trust/tuf.ts +223 -0
- package/src/types/tuf-browser.d.ts +13 -0
- package/src/x509/cert.ts +319 -0
- package/src/x509/chain.ts +212 -0
- package/src/x509/ext.ts +212 -0
- package/src/x509/index.ts +19 -0
- package/src/x509/sct.ts +154 -0
- package/tsconfig.json +18 -0
- package/vite.config.playwright.ts +26 -0
- package/vite.config.ts +18 -0
package/src/tlog/dsse.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DSSE transparency log entry verification
|
|
3
|
+
*
|
|
4
|
+
* Based on sigstore-js:
|
|
5
|
+
* https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/tlog/dsse.ts
|
|
6
|
+
*
|
|
7
|
+
* Key differences from sigstore-js:
|
|
8
|
+
* - Browser-compatible: uses Uint8Array instead of Buffer for binary data
|
|
9
|
+
* - Direct bundle field comparison instead of SignatureContent abstraction
|
|
10
|
+
* - Uses crypto.subtle.digest for hash computation instead of Node.js crypto
|
|
11
|
+
* - Adds verifyDSSE002Body() - new functionality (reference only supports v0.0.1)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { base64ToUint8Array, hexToUint8Array, toArrayBuffer, uint8ArrayEqual } from "../encoding.js";
|
|
15
|
+
import { HashAlgorithms } from "../interfaces.js";
|
|
16
|
+
import type { SigstoreBundle } from "../bundle.js";
|
|
17
|
+
import type { RekorEntry } from "./body.js";
|
|
18
|
+
|
|
19
|
+
interface DSSESpec {
|
|
20
|
+
signatures?: Array<{
|
|
21
|
+
signature: string;
|
|
22
|
+
verifier?: string;
|
|
23
|
+
keyid?: string;
|
|
24
|
+
}>;
|
|
25
|
+
payloadHash?: {
|
|
26
|
+
algorithm: string;
|
|
27
|
+
value: string;
|
|
28
|
+
};
|
|
29
|
+
envelopeHash?: {
|
|
30
|
+
algorithm: string;
|
|
31
|
+
value: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface DSSEEntry extends RekorEntry {
|
|
36
|
+
apiVersion: "0.0.1" | "0.0.2";
|
|
37
|
+
kind: "dsse";
|
|
38
|
+
spec: DSSESpec;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function verifyDSSEBody(
|
|
42
|
+
entry: RekorEntry,
|
|
43
|
+
bundle: SigstoreBundle
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
const dsseEntry = entry as DSSEEntry;
|
|
46
|
+
|
|
47
|
+
switch (dsseEntry.apiVersion) {
|
|
48
|
+
case "0.0.1":
|
|
49
|
+
return verifyDSSE001Body(dsseEntry, bundle);
|
|
50
|
+
case "0.0.2":
|
|
51
|
+
return verifyDSSE002Body(dsseEntry, bundle);
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(
|
|
54
|
+
`Unsupported dsse version: ${dsseEntry.apiVersion}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function verifyDSSE001Body(
|
|
60
|
+
entry: DSSEEntry,
|
|
61
|
+
bundle: SigstoreBundle
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
if (!bundle.dsseEnvelope) {
|
|
64
|
+
throw new Error("Bundle missing dsseEnvelope for DSSE entry");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!entry.spec.signatures || entry.spec.signatures.length !== 1) {
|
|
68
|
+
throw new Error("DSSE entry must have exactly one signature");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const tlogSig = entry.spec.signatures[0].signature;
|
|
72
|
+
const tlogSigBytes = base64ToUint8Array(tlogSig);
|
|
73
|
+
|
|
74
|
+
if (bundle.dsseEnvelope.signatures.length === 0) {
|
|
75
|
+
throw new Error("Bundle DSSE envelope missing signatures");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const bundleSigBytes = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
|
|
79
|
+
|
|
80
|
+
if (!uint8ArrayEqual(tlogSigBytes, bundleSigBytes)) {
|
|
81
|
+
throw new Error("DSSE signature mismatch between TLog entry and bundle");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const tlogHash = entry.spec.payloadHash?.value || "";
|
|
85
|
+
if (!tlogHash) {
|
|
86
|
+
throw new Error("DSSE entry missing payloadHash");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tlogHashBytes = hexToUint8Array(tlogHash);
|
|
90
|
+
|
|
91
|
+
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
92
|
+
const bundleHashBytes = new Uint8Array(
|
|
93
|
+
await crypto.subtle.digest(HashAlgorithms.SHA256, toArrayBuffer(payloadBytes))
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
|
|
97
|
+
throw new Error("DSSE payload hash mismatch between TLog entry and bundle");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// New functionality for DSSE v0.0.2 (not in sigstore-js reference, which only supports v0.0.1)
|
|
102
|
+
async function verifyDSSE002Body(
|
|
103
|
+
entry: DSSEEntry,
|
|
104
|
+
bundle: SigstoreBundle
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
if (!bundle.dsseEnvelope) {
|
|
107
|
+
throw new Error("Bundle missing dsseEnvelope for DSSE v0.0.2 entry");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const spec = (entry.spec as any).dsseV002;
|
|
111
|
+
if (!spec) {
|
|
112
|
+
throw new Error("DSSE v0.0.2 entry missing dsseV002 spec");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!spec.signatures || spec.signatures.length !== 1) {
|
|
116
|
+
throw new Error("DSSE v0.0.2 entry must have exactly one signature");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const tlogSig = spec.signatures[0].content;
|
|
120
|
+
const tlogSigBytes = base64ToUint8Array(tlogSig);
|
|
121
|
+
|
|
122
|
+
if (bundle.dsseEnvelope.signatures.length === 0) {
|
|
123
|
+
throw new Error("Bundle DSSE envelope missing signatures");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const bundleSigBytes = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
|
|
127
|
+
|
|
128
|
+
if (!uint8ArrayEqual(tlogSigBytes, bundleSigBytes)) {
|
|
129
|
+
throw new Error("DSSE signature mismatch between TLog entry and bundle (v0.0.2)");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const tlogHash = spec.payloadHash?.digest || "";
|
|
133
|
+
if (!tlogHash) {
|
|
134
|
+
throw new Error("DSSE v0.0.2 entry missing payloadHash");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const tlogHashBytes = base64ToUint8Array(tlogHash);
|
|
138
|
+
|
|
139
|
+
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
140
|
+
const bundleHashBytes = new Uint8Array(
|
|
141
|
+
await crypto.subtle.digest(HashAlgorithms.SHA256, toArrayBuffer(payloadBytes))
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
|
|
145
|
+
throw new Error("DSSE payload hash mismatch between TLog entry and bundle (v0.0.2)");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* HashedRekord transparency log entry verification
|
|
3
|
+
*
|
|
4
|
+
* Based on sigstore-js:
|
|
5
|
+
* https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/tlog/hashedrekord.ts
|
|
6
|
+
*
|
|
7
|
+
* Key differences from sigstore-js:
|
|
8
|
+
* - Browser-compatible: uses Uint8Array instead of Buffer for binary data
|
|
9
|
+
* - Direct bundle field comparison instead of SignatureContent abstraction
|
|
10
|
+
* - Adds verifyHashedRekordV002Body() - new functionality (reference only supports v0.0.1)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { base64ToUint8Array, hexToUint8Array, uint8ArrayEqual } from "../encoding.js";
|
|
14
|
+
import type { SigstoreBundle } from "../bundle.js";
|
|
15
|
+
import type { RekorEntry } from "./body.js";
|
|
16
|
+
|
|
17
|
+
interface HashedRekordSpec {
|
|
18
|
+
signature: {
|
|
19
|
+
content: string;
|
|
20
|
+
publicKey: {
|
|
21
|
+
content: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
data: {
|
|
25
|
+
hash: {
|
|
26
|
+
algorithm: string;
|
|
27
|
+
value: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface HashedRekordEntry extends RekorEntry {
|
|
33
|
+
apiVersion: "0.0.1" | "0.0.2";
|
|
34
|
+
kind: "hashedrekord";
|
|
35
|
+
spec: HashedRekordSpec | HashedRekordV002Spec;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface HashedRekordV002Spec {
|
|
39
|
+
hashedRekordV002: {
|
|
40
|
+
signature: {
|
|
41
|
+
content: string;
|
|
42
|
+
verifier: {
|
|
43
|
+
x509Certificate: {
|
|
44
|
+
rawBytes: string;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
data: {
|
|
49
|
+
algorithm: string;
|
|
50
|
+
digest: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function verifyHashedRekordBody(
|
|
56
|
+
entry: RekorEntry,
|
|
57
|
+
bundle: SigstoreBundle
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const hashedRekordEntry = entry as HashedRekordEntry;
|
|
60
|
+
|
|
61
|
+
switch (hashedRekordEntry.apiVersion) {
|
|
62
|
+
case "0.0.1":
|
|
63
|
+
return verifyHashedRekordV001Body(hashedRekordEntry, bundle);
|
|
64
|
+
case "0.0.2":
|
|
65
|
+
return verifyHashedRekordV002Body(hashedRekordEntry, bundle);
|
|
66
|
+
default:
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Unsupported hashedrekord version: ${hashedRekordEntry.apiVersion}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function verifyHashedRekordV001Body(
|
|
74
|
+
entry: HashedRekordEntry,
|
|
75
|
+
bundle: SigstoreBundle
|
|
76
|
+
): void {
|
|
77
|
+
const spec = entry.spec as HashedRekordSpec;
|
|
78
|
+
if (!bundle.messageSignature) {
|
|
79
|
+
throw new Error("Bundle missing messageSignature for hashedrekord entry");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const tlogSig = spec.signature.content || "";
|
|
83
|
+
const tlogSigBytes = base64ToUint8Array(tlogSig);
|
|
84
|
+
const bundleSigBytes = base64ToUint8Array(bundle.messageSignature.signature);
|
|
85
|
+
|
|
86
|
+
if (!uint8ArrayEqual(tlogSigBytes, bundleSigBytes)) {
|
|
87
|
+
throw new Error("Signature mismatch between TLog entry and bundle");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const tlogDigest = spec.data.hash?.value || "";
|
|
91
|
+
const tlogDigestBytes = hexToUint8Array(tlogDigest);
|
|
92
|
+
const bundleDigestBytes = base64ToUint8Array(
|
|
93
|
+
bundle.messageSignature.messageDigest.digest
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!uint8ArrayEqual(tlogDigestBytes, bundleDigestBytes)) {
|
|
97
|
+
throw new Error("Digest mismatch between TLog entry and bundle");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// New functionality for HashedRekord v0.0.2 (not in sigstore-js reference, which only supports v0.0.1)
|
|
102
|
+
function verifyHashedRekordV002Body(
|
|
103
|
+
entry: HashedRekordEntry,
|
|
104
|
+
bundle: SigstoreBundle
|
|
105
|
+
): void {
|
|
106
|
+
const spec = (entry.spec as HashedRekordV002Spec).hashedRekordV002;
|
|
107
|
+
if (!bundle.messageSignature) {
|
|
108
|
+
throw new Error("Bundle missing messageSignature for hashedrekord v0.0.2 entry");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// NOTE: HashedRekord v0.0.2 uses single base64 encoding (unlike intoto which is double-encoded)
|
|
112
|
+
// Verified against Sigstore conformance test suite bundles
|
|
113
|
+
const tlogSig = spec.signature.content || "";
|
|
114
|
+
const tlogSigBytes = base64ToUint8Array(tlogSig);
|
|
115
|
+
const bundleSigBytes = base64ToUint8Array(bundle.messageSignature.signature);
|
|
116
|
+
|
|
117
|
+
if (!uint8ArrayEqual(tlogSigBytes, bundleSigBytes)) {
|
|
118
|
+
throw new Error("Signature mismatch between TLog entry and bundle (v0.0.2)");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tlogDigest = spec.data.digest || "";
|
|
122
|
+
const tlogDigestBytes = base64ToUint8Array(tlogDigest);
|
|
123
|
+
const bundleDigestBytes = base64ToUint8Array(
|
|
124
|
+
bundle.messageSignature.messageDigest.digest
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (!uint8ArrayEqual(tlogDigestBytes, bundleDigestBytes)) {
|
|
128
|
+
throw new Error("Digest mismatch between TLog entry and bundle (v0.0.2)");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Intoto transparency log entry verification
|
|
3
|
+
*
|
|
4
|
+
* Based on sigstore-js:
|
|
5
|
+
* https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/tlog/intoto.ts
|
|
6
|
+
*
|
|
7
|
+
* Key differences from sigstore-js:
|
|
8
|
+
* - Browser-compatible: uses Uint8Array instead of Buffer for binary data
|
|
9
|
+
* - Direct bundle field comparison instead of SignatureContent abstraction
|
|
10
|
+
* - Uses crypto.subtle.digest for hash computation instead of Node.js crypto
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { base64Decode, base64ToUint8Array, hexToUint8Array, toArrayBuffer, uint8ArrayEqual } from "../encoding.js";
|
|
14
|
+
import { HashAlgorithms } from "../interfaces.js";
|
|
15
|
+
import type { SigstoreBundle } from "../bundle.js";
|
|
16
|
+
import type { RekorEntry } from "./body.js";
|
|
17
|
+
|
|
18
|
+
interface IntotoEnvelope {
|
|
19
|
+
payload: string;
|
|
20
|
+
payloadType: string;
|
|
21
|
+
signatures: Array<{
|
|
22
|
+
keyid?: string;
|
|
23
|
+
sig: string;
|
|
24
|
+
publicKey?: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface IntotoSpec {
|
|
29
|
+
content: {
|
|
30
|
+
envelope: IntotoEnvelope;
|
|
31
|
+
hash?: {
|
|
32
|
+
algorithm: string;
|
|
33
|
+
value: string;
|
|
34
|
+
};
|
|
35
|
+
payloadHash?: {
|
|
36
|
+
algorithm: string;
|
|
37
|
+
value: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
publicKey?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface IntotoEntry extends RekorEntry {
|
|
44
|
+
apiVersion: "0.0.2";
|
|
45
|
+
kind: "intoto";
|
|
46
|
+
spec: IntotoSpec;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function verifyIntotoBody(
|
|
50
|
+
entry: RekorEntry,
|
|
51
|
+
bundle: SigstoreBundle
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
const intotoEntry = entry as IntotoEntry;
|
|
54
|
+
|
|
55
|
+
if (intotoEntry.apiVersion !== "0.0.2") {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Unsupported intoto version: ${intotoEntry.apiVersion}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!bundle.dsseEnvelope) {
|
|
62
|
+
throw new Error("Bundle missing dsseEnvelope for intoto entry");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const tlogEnvelope = intotoEntry.spec.content.envelope;
|
|
66
|
+
|
|
67
|
+
if (!tlogEnvelope.signatures || tlogEnvelope.signatures.length !== 1) {
|
|
68
|
+
throw new Error("Intoto entry must have exactly one signature");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The signature in the intoto tlog entry is double-base64-encoded:
|
|
72
|
+
// 1. Raw signature bytes are base64-encoded (normal)
|
|
73
|
+
// 2. That base64 string is base64-encoded again (for storage in JSON)
|
|
74
|
+
// So we decode twice: base64 -> string -> base64 -> bytes
|
|
75
|
+
const tlogSigBase64 = tlogEnvelope.signatures[0].sig;
|
|
76
|
+
const tlogSigDecoded = base64Decode(tlogSigBase64); // First decode: base64 -> UTF-8 string (which contains base64)
|
|
77
|
+
const tlogSigBytes = base64ToUint8Array(tlogSigDecoded); // Second decode: base64 string -> raw bytes
|
|
78
|
+
|
|
79
|
+
if (bundle.dsseEnvelope.signatures.length === 0) {
|
|
80
|
+
throw new Error("Bundle DSSE envelope missing signatures");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const bundleSigBytes = base64ToUint8Array(bundle.dsseEnvelope.signatures[0].sig);
|
|
84
|
+
|
|
85
|
+
if (!uint8ArrayEqual(tlogSigBytes, bundleSigBytes)) {
|
|
86
|
+
throw new Error("Intoto signature mismatch between TLog entry and bundle");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (intotoEntry.spec.content.payloadHash) {
|
|
90
|
+
const tlogHash = intotoEntry.spec.content.payloadHash.value;
|
|
91
|
+
const tlogHashBytes = hexToUint8Array(tlogHash);
|
|
92
|
+
|
|
93
|
+
const payloadBytes = base64ToUint8Array(bundle.dsseEnvelope.payload);
|
|
94
|
+
const bundleHashBytes = new Uint8Array(
|
|
95
|
+
await crypto.subtle.digest(HashAlgorithms.SHA256, toArrayBuffer(payloadBytes))
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (!uint8ArrayEqual(tlogHashBytes, bundleHashBytes)) {
|
|
99
|
+
throw new Error("Intoto payload hash mismatch between TLog entry and bundle");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Adapted from sigstore-js for browser compatibility:
|
|
3
|
+
https://github.com/sigstore/sigstore-js/blob/main/packages/verify/src/timestamp/merkle.ts
|
|
4
|
+
|
|
5
|
+
Key differences:
|
|
6
|
+
- Uses Uint8Array instead of Buffer
|
|
7
|
+
- Uses async Web Crypto API (crypto.subtle.digest) instead of sync Node.js crypto
|
|
8
|
+
- Base64 decoding via custom base64ToUint8Array instead of Buffer.from
|
|
9
|
+
- Custom uint8ArrayEqual instead of crypto.bufferEqual
|
|
10
|
+
*/
|
|
11
|
+
import { base64ToUint8Array, toArrayBuffer, uint8ArrayEqual } from "../encoding.js";
|
|
12
|
+
import { HashAlgorithms } from "../interfaces.js";
|
|
13
|
+
import type { TLogEntry } from "../bundle.js";
|
|
14
|
+
|
|
15
|
+
const RFC6962_LEAF_HASH_PREFIX = new Uint8Array([0x00]);
|
|
16
|
+
const RFC6962_NODE_HASH_PREFIX = new Uint8Array([0x01]);
|
|
17
|
+
|
|
18
|
+
export async function verifyMerkleInclusion(
|
|
19
|
+
entry: TLogEntry
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
if (!entry.inclusionProof) {
|
|
22
|
+
throw new Error("Missing inclusion proof");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const inclusionProof = entry.inclusionProof;
|
|
26
|
+
const logIndex = BigInt(inclusionProof.logIndex);
|
|
27
|
+
const treeSize = BigInt(inclusionProof.treeSize);
|
|
28
|
+
|
|
29
|
+
if (logIndex < 0n || logIndex >= treeSize) {
|
|
30
|
+
throw new Error(`Invalid log index: ${logIndex}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Figure out which subset of hashes corresponds to the inner and border
|
|
34
|
+
// nodes
|
|
35
|
+
const { inner, border } = decompInclProof(logIndex, treeSize);
|
|
36
|
+
|
|
37
|
+
if (inclusionProof.hashes.length !== inner + border) {
|
|
38
|
+
throw new Error("Invalid hash count in inclusion proof");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const innerHashes = inclusionProof.hashes
|
|
42
|
+
.slice(0, inner)
|
|
43
|
+
.map((h) => base64ToUint8Array(h));
|
|
44
|
+
const borderHashes = inclusionProof.hashes
|
|
45
|
+
.slice(inner)
|
|
46
|
+
.map((h) => base64ToUint8Array(h));
|
|
47
|
+
|
|
48
|
+
// The entry's hash is the leaf hash
|
|
49
|
+
const leafHash = await hashLeaf(base64ToUint8Array(entry.canonicalizedBody));
|
|
50
|
+
|
|
51
|
+
// Chain the hashes belonging to the inner and border portions
|
|
52
|
+
const calculatedHash = await chainBorderRight(
|
|
53
|
+
await chainInner(leafHash, innerHashes, logIndex),
|
|
54
|
+
borderHashes
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const rootHash = base64ToUint8Array(inclusionProof.rootHash);
|
|
58
|
+
|
|
59
|
+
// Calculated hash should match the root hash in the inclusion proof
|
|
60
|
+
if (!uint8ArrayEqual(calculatedHash, rootHash)) {
|
|
61
|
+
throw new Error("Calculated root hash does not match inclusion proof");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Breaks down inclusion proof for a leaf at the specified index in a tree of
|
|
66
|
+
// the specified size. The split point is where paths to the index leaf and
|
|
67
|
+
// the (size - 1) leaf diverge. Returns lengths of the bottom and upper proof
|
|
68
|
+
// parts.
|
|
69
|
+
function decompInclProof(
|
|
70
|
+
index: bigint,
|
|
71
|
+
size: bigint
|
|
72
|
+
): { inner: number; border: number } {
|
|
73
|
+
const inner = innerProofSize(index, size);
|
|
74
|
+
const border = onesCount(index >> BigInt(inner));
|
|
75
|
+
return { inner, border };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Computes a subtree hash for a node on or below the tree's right border.
|
|
79
|
+
// Assumes the provided proof hashes are ordered from lower to higher levels
|
|
80
|
+
// and seed is the initial hash of the node specified by the index.
|
|
81
|
+
async function chainInner(
|
|
82
|
+
seed: Uint8Array,
|
|
83
|
+
hashes: Uint8Array[],
|
|
84
|
+
index: bigint
|
|
85
|
+
): Promise<Uint8Array> {
|
|
86
|
+
let acc = seed;
|
|
87
|
+
for (let i = 0; i < hashes.length; i++) {
|
|
88
|
+
const h = hashes[i];
|
|
89
|
+
if ((index >> BigInt(i)) & BigInt(1)) {
|
|
90
|
+
acc = await hashChildren(h, acc);
|
|
91
|
+
} else {
|
|
92
|
+
acc = await hashChildren(acc, h);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return acc;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Computes a subtree hash for nodes along the tree's right border.
|
|
99
|
+
async function chainBorderRight(
|
|
100
|
+
seed: Uint8Array,
|
|
101
|
+
hashes: Uint8Array[]
|
|
102
|
+
): Promise<Uint8Array> {
|
|
103
|
+
let acc = seed;
|
|
104
|
+
for (const h of hashes) {
|
|
105
|
+
acc = await hashChildren(h, acc);
|
|
106
|
+
}
|
|
107
|
+
return acc;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function innerProofSize(index: bigint, size: bigint): number {
|
|
111
|
+
return bitLength(index ^ (size - BigInt(1)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Counts the number of ones in the binary representation of the given number.
|
|
115
|
+
// https://en.wikipedia.org/wiki/Hamming_weight
|
|
116
|
+
function onesCount(num: bigint): number {
|
|
117
|
+
return num.toString(2).split("1").length - 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Returns the number of bits necessary to represent an integer in binary.
|
|
121
|
+
function bitLength(n: bigint): number {
|
|
122
|
+
if (n === 0n) {
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
return n.toString(2).length;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Hashing logic according to RFC6962.
|
|
129
|
+
// https://datatracker.ietf.org/doc/html/rfc6962#section-2
|
|
130
|
+
async function hashChildren(
|
|
131
|
+
left: Uint8Array,
|
|
132
|
+
right: Uint8Array
|
|
133
|
+
): Promise<Uint8Array> {
|
|
134
|
+
const data = new Uint8Array(
|
|
135
|
+
RFC6962_NODE_HASH_PREFIX.length + left.length + right.length
|
|
136
|
+
);
|
|
137
|
+
data.set(RFC6962_NODE_HASH_PREFIX, 0);
|
|
138
|
+
data.set(left, RFC6962_NODE_HASH_PREFIX.length);
|
|
139
|
+
data.set(right, RFC6962_NODE_HASH_PREFIX.length + left.length);
|
|
140
|
+
|
|
141
|
+
const hash = await crypto.subtle.digest(HashAlgorithms.SHA256, toArrayBuffer(data));
|
|
142
|
+
return new Uint8Array(hash);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function hashLeaf(leaf: Uint8Array): Promise<Uint8Array> {
|
|
146
|
+
const data = new Uint8Array(RFC6962_LEAF_HASH_PREFIX.length + leaf.length);
|
|
147
|
+
data.set(RFC6962_LEAF_HASH_PREFIX, 0);
|
|
148
|
+
data.set(leaf, RFC6962_LEAF_HASH_PREFIX.length);
|
|
149
|
+
|
|
150
|
+
const hash = await crypto.subtle.digest(HashAlgorithms.SHA256, toArrayBuffer(data));
|
|
151
|
+
return new Uint8Array(hash);
|
|
152
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Trust Material
|
|
2
|
+
|
|
3
|
+
This directory contains trust material for Sigstore verification.
|
|
4
|
+
|
|
5
|
+
## tuf-root.json
|
|
6
|
+
|
|
7
|
+
**Source**: Extracted from [sigstore-js/packages/tuf/seeds.json](https://github.com/sigstore/sigstore-js/blob/main/packages/tuf/seeds.json)
|
|
8
|
+
|
|
9
|
+
**Purpose**: Bootstrap TUF (The Update Framework) client with a pinned root of trust for the Sigstore TUF repository.
|
|
10
|
+
|
|
11
|
+
**Content**: Base64-encoded TUF `root.json` metadata for `https://tuf-repo-cdn.sigstore.dev`
|
|
12
|
+
- Type: TUF root metadata (version 13)
|
|
13
|
+
- Expires: 2026-01-22
|
|
14
|
+
- Contains: 5 root signing keys with threshold signature requirement
|
|
15
|
+
|
|
16
|
+
**Security**: This file prevents MITM attacks during TUF initialization by embedding the initial root metadata instead of fetching it from the network. The TUF client uses this to verify all subsequent TUF metadata updates.
|
|
17
|
+
|
|
18
|
+
**Updates**: Should be updated when sigstore-js updates its embedded TUF root. Check the sigstore-js repository for the latest version.
|
|
19
|
+
|
|
20
|
+
## Trust Material Flow
|
|
21
|
+
|
|
22
|
+
1. **TUF Bootstrap**: `tuf-root.json` → TUF client initialization
|
|
23
|
+
2. **TUF Metadata Updates**: TUF client fetches and verifies latest metadata
|
|
24
|
+
3. **Sigstore Trust Material**: TUF delivers `trusted_root.json` (CAs, CT logs, TLogs, TSAs)
|
|
25
|
+
4. **Verification**: Sigstore verifier uses the trust material to verify signatures
|
|
26
|
+
|
|
27
|
+
This matches the sigstore-js architecture: TUF provides always up-to-date trust material rather than using static embedded files that can become outdated.
|
|
28
|
+
|
|
29
|
+
## Why No Static `trusted_root.json`?
|
|
30
|
+
|
|
31
|
+
Unlike some implementations, we do **not** embed a static `trusted_root.json` file. Here's why:
|
|
32
|
+
|
|
33
|
+
- **Outdated Material**: Static trust roots become outdated (e.g., expired certificates, new CAs, rotated keys)
|
|
34
|
+
- **Reviewer Issues**: Previous embedded files had incorrect `validFor` dates and mismatched certificate subjects
|
|
35
|
+
- **TUF Purpose**: TUF exists specifically to deliver fresh, verified trust material - using it is the correct approach
|
|
36
|
+
- **Matches Reference**: sigstore-js uses TUF exclusively, with no static fallback
|
|
37
|
+
|
|
38
|
+
The CLI always uses TUF to fetch `trusted_root.json`, ensuring correctness and freshness.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"ewogInNpZ25hdHVyZXMiOiBbCiAgewogICAia2V5aWQiOiAiNmYyNjAwODlkNTkyM2RhZjIwMTY2Y2E2NTdjNTQzYWY2MTgzNDZhYjk3MTg4NGE5OTk2MmIwMTk4OGJiZTBjMyIsCiAgICJzaWciOiAiIgogIH0sCiAgewogICAia2V5aWQiOiAiZTcxYTU0ZDU0MzgzNWJhODZhZGFkOTQ2MDM3OWM3NjQxZmI4NzI2ZDE2NGVhNzY2ODAxYTFjNTIyYWJhN2VhMiIsCiAgICJzaWciOiAiMzA0NTAyMjEwMGJiZGRkNDY0ZjgwNjZjZWI4OGJhNzg3Mzc1YzEyY2Q2MzMwNjgwZTA4YzI5MTA3MDNlNjUzOGM3MWNjNzlhZDIwMjIwNTE5MGIwNmU0NTM3ZmU5NjFiM2VmODFmZTY4ZWRjZDAwODljMTlmOTE5YWZlZDQyM2I5YWFmZDcwMDY0MTE1MyIKICB9LAogIHsKICAgImtleWlkIjogIjIyZjRjYWVjNmQ4ZTZmOTU1NWFmNjZiM2Q0YzNjYjA2YTNiYjIzZmRjN2UzOWM5MTZjNjFmNDYyZTZmNTJiMDYiLAogICAic2lnIjogIjMwNDQwMjIwNjkzMDZjZDUyNTdmNzMyYTc0MGMxYWZlNjBhOGU0MzNjNWRlNThlYWZlYWRiZTk5YzMzNmM5YzcxZDE5OGNmODAyMjAwZDc3Mzk1M2FlN2RiYzQ4ZDNlNWJhZDlhNmY2NGJhZmZmMTk2YjdlMmFkNGE1MmExOTUxOTM2N2Q0N2RjMDQyIgogIH0sCiAgewogICAia2V5aWQiOiAiNjE2NDM4MzgxMjViNDQwYjQwZGI2OTQyZjVjYjVhMzFjMGRjMDQzNjgzMTZlYjJhYWE1OGI5NTkwNGE1ODIyMiIsCiAgICJzaWciOiAiMzA0NDAyMjA0ZDIxYTJlYzgwZGY2NmU2MWY2ZmUyOTEyOTUxZGM0N2RmODM2MDM2ZjhjMGFiMTA4MTZkMzc1ZTcxZGJmNzllMDIyMDU0N2FkY2UxYWZkZjA0ZTY3OTRlZmEyMDNkZDUyNjRjNmY3ZTBlZjc4ZTU3ZmU5MzRiMGQyNmNiOTk0ZWVjNzYiCiAgfSwKICB7CiAgICJrZXlpZCI6ICJhNjg3ZTViZjRmYWI4MmIwZWU1OGQ0NmUwNWM5NTM1MTQ1YTJjOWFmYjQ1OGY0M2Q0MmI0NWNhMGZkY2UyYTcwIiwKICAgInNpZyI6ICIzMDQ1MDIyMDYwODI2NDk2NTU3MTQ0ZWIxNjQ5ODkzZWQ1ZjZmNGVhNTQ1MzZmZWIwY2E4MmY4Yjg5YWU2NDFiZTM5NzQzZTUwMjIxMDBhZDcxMThiNWU5ZDQ4MzczMjYyMDZlNDEyZmM2ZGEyOTk5OTI1ZDExMDMyOGE3YzE2NmIwNmM2MjQzMzZjOTNmIgogIH0sCiAgewogICAia2V5aWQiOiAiMTgzZTY0ZjM3NjcwZGMxM2NhMGQyODk5NWEzMDUzZjM3NDA5NTRkZGNlNDQzMjFhNDFlNDY1MzRjZjQ0ZTYzMiIsCiAgICJzaWciOiAiMzA0NjAyMjEwMGQ4MTc5NDM5YzJlNzNlYjBjMTczM2FiZWU3ZmFmODMyZGNhZWE3MjYzZWRjYjQ5MTk4OTFjM2EyNDdmMDU5MjMwMjIxMDBlMWE0MzdlMDc5N2U4MDNmOWI3MmRjOWQyZDkyMTU1YjBhMjI3MGMyNGVmZGQ1ZjRiM2E1ZDhmMGIwZjQzMWE3IgogIH0KIF0sCiAic2lnbmVkIjogewogICJfdHlwZSI6ICJyb290IiwKICAiY29uc2lzdGVudF9zbmFwc2hvdCI6IHRydWUsCiAgImV4cGlyZXMiOiAiMjAyNi0wMS0yMlQxMzowNTo1OVoiLAogICJrZXlzIjogewogICAiMGM4NzQzMmMzYmYwOWZkOTkxODlmZGMzMmZhNWVhZWRmNGU0YTVmYWM3YmFiNzNmYTA0YTJlMGZjNjRhZjZmNSI6IHsKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dHlwZSI6ICJlY2RzYSIsCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRVdSaUdyNStqKzNKNVNzSCtadHI1bkUySDJ3TzdcbkJWK25PM3M5M2dMY2ExOHFUT3pIWTFvV3lBR0R5a01Tc0dUVUJTdDlEK0FuMEtmS3NEMm1mU000MlE9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iCiAgICB9LAogICAgInNjaGVtZSI6ICJlY2RzYS1zaGEyLW5pc3RwMjU2IiwKICAgICJ4LXR1Zi1vbi1jaS1vbmxpbmUtdXJpIjogImdjcGttczpwcm9qZWN0cy9zaWdzdG9yZS1yb290LXNpZ25pbmcvbG9jYXRpb25zL2dsb2JhbC9rZXlSaW5ncy9yb290L2NyeXB0b0tleXMvdGltZXN0YW1wL2NyeXB0b0tleVZlcnNpb25zLzEiCiAgIH0sCiAgICIxODNlNjRmMzc2NzBkYzEzY2EwZDI4OTk1YTMwNTNmMzc0MDk1NGRkY2U0NDMyMWE0MWU0NjUzNGNmNDRlNjMyIjogewogICAgImtleXR5cGUiOiAiZWNkc2EiLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogIi0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVNeHBQT0pDSVo1b3RHNDEwNmZHSnNlRVFpM1Y5XG5wa01ZUTR1eVY5VGoxTTdXSFhJeUxHK2prZnZ1RzBnbFExSlpiUlpaQlYzZ0FSNHNvamRHSElTZW93PT1cbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIgogICAgfSwKICAgICJzY2hlbWUiOiAiZWNkc2Etc2hhMi1uaXN0cDI1NiIsCiAgICAieC10dWYtb24tY2kta2V5b3duZXIiOiAiQGxhbmNlIgogICB9LAogICAiMjJmNGNhZWM2ZDhlNmY5NTU1YWY2NmIzZDRjM2NiMDZhM2JiMjNmZGM3ZTM5YzkxNmM2MWY0NjJlNmY1MmIwNiI6IHsKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dHlwZSI6ICJlY2RzYSIsCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRXpCelZPbUhDUG9qTVZMU0kzNjRXaWlWOE5QckRcbjZJZ1J4Vmxpc2t6L3YreTNKRVI1bWNWR2NPTmxpRGNXTUM1SjJsZkhtalBOUGhiNEg3eG04THpmU0E9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iCiAgICB9LAogICAgInNjaGVtZSI6ICJlY2RzYS1zaGEyLW5pc3RwMjU2IiwKICAgICJ4LXR1Zi1vbi1jaS1rZXlvd25lciI6ICJAc2FudGlhZ290b3JyZXMiCiAgIH0sCiAgICI2MTY0MzgzODEyNWI0NDBiNDBkYjY5NDJmNWNiNWEzMWMwZGMwNDM2ODMxNmViMmFhYTU4Yjk1OTA0YTU4MjIyIjogewogICAgImtleWlkX2hhc2hfYWxnb3JpdGhtcyI6IFsKICAgICAic2hhMjU2IiwKICAgICAic2hhNTEyIgogICAgXSwKICAgICJrZXl0eXBlIjogImVjZHNhIiwKICAgICJrZXl2YWwiOiB7CiAgICAgInB1YmxpYyI6ICItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFaW5pa1NzQVFtWWtOZUg1ZVlxL0NuSXpMYWFjT1xueGxTYWF3UURPd3FLeS90Q3F4cTV4eFBTSmMyMUs0V0loczlHeU9rS2Z6dWVZM0dJTHpjTUpaNGNXdz09XG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIKICAgIH0sCiAgICAic2NoZW1lIjogImVjZHNhLXNoYTItbmlzdHAyNTYiLAogICAgIngtdHVmLW9uLWNpLWtleW93bmVyIjogIkBib2JjYWxsYXdheSIKICAgfSwKICAgImE2ODdlNWJmNGZhYjgyYjBlZTU4ZDQ2ZTA1Yzk1MzUxNDVhMmM5YWZiNDU4ZjQzZDQyYjQ1Y2EwZmRjZTJhNzAiOiB7CiAgICAia2V5aWRfaGFzaF9hbGdvcml0aG1zIjogWwogICAgICJzaGEyNTYiLAogICAgICJzaGE1MTIiCiAgICBdLAogICAgImtleXR5cGUiOiAiZWNkc2EiLAogICAgImtleXZhbCI6IHsKICAgICAicHVibGljIjogIi0tLS0tQkVHSU4gUFVCTElDIEtFWS0tLS0tXG5NRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUUwZ2hyaDkyTHcxWXIzaWRHVjVXcUN0TURCOEN4XG4rRDhoZEM0dzJaTE5JcGxWUm9WR0xza1lhM2doZU15T2ppSjhrUGkxNWFRMi8vN1Arb2o3VXZKUEd3PT1cbi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLVxuIgogICAgfSwKICAgICJzY2hlbWUiOiAiZWNkc2Etc2hhMi1uaXN0cDI1NiIsCiAgICAieC10dWYtb24tY2kta2V5b3duZXIiOiAiQGpvc2h1YWdsIgogICB9LAogICAiZTcxYTU0ZDU0MzgzNWJhODZhZGFkOTQ2MDM3OWM3NjQxZmI4NzI2ZDE2NGVhNzY2ODAxYTFjNTIyYWJhN2VhMiI6IHsKICAgICJrZXlpZF9oYXNoX2FsZ29yaXRobXMiOiBbCiAgICAgInNoYTI1NiIsCiAgICAgInNoYTUxMiIKICAgIF0sCiAgICAia2V5dHlwZSI6ICJlY2RzYSIsCiAgICAia2V5dmFsIjogewogICAgICJwdWJsaWMiOiAiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cbk1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRUVYc3ozU1pYRmI4ak1WNDJqNnBKbHlqYmpSOEtcbk4zQndvY2V4cTZMTUliNXFzV0tPUXZMTjE2TlVlZkxjNEhzd09vdW1Sc1ZWYWFqU3BRUzZmb2JrUnc9PVxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXG4iCiAgICB9LAogICAgInNjaGVtZSI6ICJlY2RzYS1zaGEyLW5pc3RwMjU2IiwKICAgICJ4LXR1Zi1vbi1jaS1rZXlvd25lciI6ICJAbW5tNjc4IgogICB9CiAgfSwKICAicm9sZXMiOiB7CiAgICJyb290IjogewogICAgImtleWlkcyI6IFsKICAgICAiZTcxYTU0ZDU0MzgzNWJhODZhZGFkOTQ2MDM3OWM3NjQxZmI4NzI2ZDE2NGVhNzY2ODAxYTFjNTIyYWJhN2VhMiIsCiAgICAgIjIyZjRjYWVjNmQ4ZTZmOTU1NWFmNjZiM2Q0YzNjYjA2YTNiYjIzZmRjN2UzOWM5MTZjNjFmNDYyZTZmNTJiMDYiLAogICAgICI2MTY0MzgzODEyNWI0NDBiNDBkYjY5NDJmNWNiNWEzMWMwZGMwNDM2ODMxNmViMmFhYTU4Yjk1OTA0YTU4MjIyIiwKICAgICAiYTY4N2U1YmY0ZmFiODJiMGVlNThkNDZlMDVjOTUzNTE0NWEyYzlhZmI0NThmNDNkNDJiNDVjYTBmZGNlMmE3MCIsCiAgICAgIjE4M2U2NGYzNzY3MGRjMTNjYTBkMjg5OTVhMzA1M2YzNzQwOTU0ZGRjZTQ0MzIxYTQxZTQ2NTM0Y2Y0NGU2MzIiCiAgICBdLAogICAgInRocmVzaG9sZCI6IDMKICAgfSwKICAgInNuYXBzaG90IjogewogICAgImtleWlkcyI6IFsKICAgICAiMGM4NzQzMmMzYmYwOWZkOTkxODlmZGMzMmZhNWVhZWRmNGU0YTVmYWM3YmFiNzNmYTA0YTJlMGZjNjRhZjZmNSIKICAgIF0sCiAgICAidGhyZXNob2xkIjogMSwKICAgICJ4LXR1Zi1vbi1jaS1leHBpcnktcGVyaW9kIjogMzY1MCwKICAgICJ4LXR1Zi1vbi1jaS1zaWduaW5nLXBlcmlvZCI6IDM2NQogICB9LAogICAidGFyZ2V0cyI6IHsKICAgICJrZXlpZHMiOiBbCiAgICAgImU3MWE1NGQ1NDM4MzViYTg2YWRhZDk0NjAzNzljNzY0MWZiODcyNmQxNjRlYTc2NjgwMWExYzUyMmFiYTdlYTIiLAogICAgICIyMmY0Y2FlYzZkOGU2Zjk1NTVhZjY2YjNkNGMzY2IwNmEzYmIyM2ZkYzdlMzljOTE2YzYxZjQ2MmU2ZjUyYjA2IiwKICAgICAiNjE2NDM4MzgxMjViNDQwYjQwZGI2OTQyZjVjYjVhMzFjMGRjMDQzNjgzMTZlYjJhYWE1OGI5NTkwNGE1ODIyMiIsCiAgICAgImE2ODdlNWJmNGZhYjgyYjBlZTU4ZDQ2ZTA1Yzk1MzUxNDVhMmM5YWZiNDU4ZjQzZDQyYjQ1Y2EwZmRjZTJhNzAiLAogICAgICIxODNlNjRmMzc2NzBkYzEzY2EwZDI4OTk1YTMwNTNmMzc0MDk1NGRkY2U0NDMyMWE0MWU0NjUzNGNmNDRlNjMyIgogICAgXSwKICAgICJ0aHJlc2hvbGQiOiAzCiAgIH0sCiAgICJ0aW1lc3RhbXAiOiB7CiAgICAia2V5aWRzIjogWwogICAgICIwYzg3NDMyYzNiZjA5ZmQ5OTE4OWZkYzMyZmE1ZWFlZGY0ZTRhNWZhYzdiYWI3M2ZhMDRhMmUwZmM2NGFmNmY1IgogICAgXSwKICAgICJ0aHJlc2hvbGQiOiAxLAogICAgIngtdHVmLW9uLWNpLWV4cGlyeS1wZXJpb2QiOiA3LAogICAgIngtdHVmLW9uLWNpLXNpZ25pbmctcGVyaW9kIjogNgogICB9CiAgfSwKICAic3BlY192ZXJzaW9uIjogIjEuMCIsCiAgInZlcnNpb24iOiAxMywKICAieC10dWYtb24tY2ktZXhwaXJ5LXBlcmlvZCI6IDE5NywKICAieC10dWYtb24tY2ktc2lnbmluZy1wZXJpb2QiOiA0NgogfQp9"
|