@calimero-network/mero-js 21.3.0 → 21.4.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,76 @@
1
+ /**
2
+ * The parts of the Sigstore public-good trust root that verifying a mero-tee
3
+ * release's cosign bundle needs: the Fulcio certificate authorities that issue
4
+ * the release workflow's short-lived signing certificates, and the Rekor log
5
+ * key that signs the bundle's signed entry timestamp. Each with the window in
6
+ * which the trust root says it was in use.
7
+ *
8
+ * Copied from the `trusted_root.json` that Sigstore publishes through its TUF
9
+ * repository (https://tuf-repo-cdn.sigstore.dev, target `trusted_root.json`),
10
+ * as shipped by the `sigstore` Python client 4.5.0 in
11
+ * `sigstore/_store/https%3A%2F%2Ftuf-repo-cdn.sigstore.dev/trusted_root.json`.
12
+ * Embedded rather than fetched: fetching it at run time would trust whoever
13
+ * serves it, the very thing this check exists to avoid.
14
+ *
15
+ * Left out, because a legacy cosign bundle never uses them: the CT logs (the
16
+ * certificate's SCT is not checked here, as core does not either), the
17
+ * timestamp authority, and the Rekor v2 log `log2025-1.rekor.sigstore.dev`,
18
+ * whose Ed25519 key signs checkpoints rather than signed entry timestamps. A
19
+ * bundle that names any log but those below is refused.
20
+ *
21
+ * To refresh it when Sigstore rotates a key or adds a CA: take the current
22
+ * `trusted_root.json` from a TUF client (`python -m sigstore` or `cosign
23
+ * initialize` both keep one, verified against the TUF root), and copy each
24
+ * `certificateAuthorities[].certChain.certificates[].rawBytes` with its
25
+ * `validFor`, and each ECDSA `tlogs[]` entry's `publicKey.rawBytes`,
26
+ * `publicKey.validFor` and `logId.keyId` (as hex). The tests check each log id
27
+ * is the SHA-256 of its key and each chain verifies.
28
+ */
29
+ export const FULCIO_AUTHORITIES = [
30
+ {
31
+ uri: 'https://fulcio.sigstore.dev',
32
+ validFor: { start: '2021-03-07T03:20:29Z', end: '2022-12-31T23:59:59.999Z' },
33
+ certChain: [
34
+ 'MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9y' +
35
+ 'ZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UE' +
36
+ 'ChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+p' +
37
+ 'NO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+' +
38
+ '2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYD' +
39
+ 'VR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqG' +
40
+ 'SM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjR' +
41
+ 'ay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ==',
42
+ ],
43
+ },
44
+ {
45
+ uri: 'https://fulcio.sigstore.dev',
46
+ validFor: { start: '2022-04-13T20:06:15Z' },
47
+ certChain: [
48
+ 'MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3Rv' +
49
+ 'cmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNV' +
50
+ 'BAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EE' +
51
+ 'ACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798' +
52
+ 'LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAww' +
53
+ 'CgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYD' +
54
+ 'VR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfx' +
55
+ 'Xx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsP' +
56
+ 'mygUY7Ii2zbdCdliiow=',
57
+ 'MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3Rv' +
58
+ 'cmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNV' +
59
+ 'BAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3' +
60
+ 'PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS' +
61
+ '5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud' +
62
+ 'DgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjO' +
63
+ 'PQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9' +
64
+ 'ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ',
65
+ ],
66
+ },
67
+ ];
68
+ export const REKOR_LOGS = [
69
+ {
70
+ baseUrl: 'https://rekor.sigstore.dev',
71
+ logId: 'c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d',
72
+ publicKey: 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufB' +
73
+ 'nhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==',
74
+ validFor: { start: '2021-01-12T11:53:27Z' },
75
+ },
76
+ ];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Verifying a cosign keyless signature, as `cosign verify-blob --bundle` does,
3
+ * with WebCrypto and the Sigstore trust root embedded in this package.
4
+ *
5
+ * mero-tee signs each release asset with cosign keyless: there is no release
6
+ * key, only a Fulcio certificate that lives ten minutes, issued to one GitHub
7
+ * Actions workflow run, and a Rekor log entry that proves the signature was
8
+ * made while the certificate was valid. So the checks are: the signature over
9
+ * the bytes verifies with the certificate's key; Rekor's signed entry
10
+ * timestamp verifies, and the entry it signs is this signature over these
11
+ * bytes by this certificate; the certificate chains to a Fulcio CA and was
12
+ * valid when Rekor logged it; and the certificate names the workflow expected.
13
+ * The same checks core runs in `crates/tee-release/src/sigstore_verify.rs`.
14
+ *
15
+ * Nothing here reads the clock. Every time is Rekor's `integratedTime`, which
16
+ * its signature covers, so a verification that passes once passes forever,
17
+ * whatever the client's clock says.
18
+ */
19
+ import { type Certificate } from './x509.js';
20
+ /** Who must have signed: the certificate's SAN URI and its OIDC issuer, each matched exactly. */
21
+ export interface SignerIdentity {
22
+ identity: string;
23
+ issuer: string;
24
+ }
25
+ /**
26
+ * Verify a cosign keyless `bundle` over `body`, signed by `signer`. Resolves
27
+ * with the time Rekor logged it (seconds); rejects with the reason otherwise.
28
+ * `signature`, when given, is the detached `.sig` published beside the file,
29
+ * and must be the bundle's.
30
+ */
31
+ export declare function verifyCosignBundle(body: Uint8Array, bundle: unknown, signer: SignerIdentity, signature?: string): Promise<{
32
+ integratedTime: number;
33
+ }>;
34
+ /**
35
+ * Check that `leaf` was issued by a Fulcio CA in the trust root, and that it,
36
+ * the CA's certificates and the CA itself (by the trust root's `validFor`)
37
+ * were all valid at `at` (milliseconds): when Rekor logged the signature, not
38
+ * now, since a Fulcio certificate lives ten minutes. Every signature on the
39
+ * way is checked, with the algorithm its certificate names.
40
+ */
41
+ export declare function chainsToFulcio(leaf: Certificate, at: number): Promise<void>;
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Verifying a cosign keyless signature, as `cosign verify-blob --bundle` does,
3
+ * with WebCrypto and the Sigstore trust root embedded in this package.
4
+ *
5
+ * mero-tee signs each release asset with cosign keyless: there is no release
6
+ * key, only a Fulcio certificate that lives ten minutes, issued to one GitHub
7
+ * Actions workflow run, and a Rekor log entry that proves the signature was
8
+ * made while the certificate was valid. So the checks are: the signature over
9
+ * the bytes verifies with the certificate's key; Rekor's signed entry
10
+ * timestamp verifies, and the entry it signs is this signature over these
11
+ * bytes by this certificate; the certificate chains to a Fulcio CA and was
12
+ * valid when Rekor logged it; and the certificate names the workflow expected.
13
+ * The same checks core runs in `crates/tee-release/src/sigstore_verify.rs`.
14
+ *
15
+ * Nothing here reads the clock. Every time is Rekor's `integratedTime`, which
16
+ * its signature covers, so a verification that passes once passes forever,
17
+ * whatever the client's clock says.
18
+ */
19
+ import { hex } from '../crypto/internal.js';
20
+ import { FULCIO_AUTHORITIES, REKOR_LOGS } from './sigstore-trust-root.js';
21
+ import { children, contents, expectTag, fromBase64, objectIdentifiers, parseCertificate, parseDer, pemCertificate, verifyEcdsa, } from './x509.js';
22
+ const SUBJECT_ALT_NAME = '2.5.29.17';
23
+ const EXTENDED_KEY_USAGE = '2.5.29.37';
24
+ const CODE_SIGNING = '1.3.6.1.5.5.7.3.3';
25
+ /** Fulcio's OIDC issuer extension, a DER UTF8String. */
26
+ const FULCIO_ISSUER_V2 = '1.3.6.1.4.1.57264.1.8';
27
+ /** Its deprecated predecessor, the raw issuer bytes; read only when the other is absent. */
28
+ const FULCIO_ISSUER_V1 = '1.3.6.1.4.1.57264.1.1';
29
+ const SAN_URI = 0x86;
30
+ const UTF8_STRING = 0x0c;
31
+ const SEQUENCE = 0x30;
32
+ const utf8 = new TextEncoder();
33
+ // Not `fatal`: not every runtime's TextDecoder takes options. A malformed byte
34
+ // decodes to U+FFFD, which no pinned identity holds, so it can only fail a match.
35
+ const fromUtf8 = (bytes) => new TextDecoder().decode(bytes);
36
+ /**
37
+ * Verify a cosign keyless `bundle` over `body`, signed by `signer`. Resolves
38
+ * with the time Rekor logged it (seconds); rejects with the reason otherwise.
39
+ * `signature`, when given, is the detached `.sig` published beside the file,
40
+ * and must be the bundle's.
41
+ */
42
+ export async function verifyCosignBundle(body, bundle, signer, signature) {
43
+ const { base64Signature, cert, rekorBundle } = cosignBundle(bundle);
44
+ const payload = rekorBundle.Payload;
45
+ if (signature !== undefined && signature.trim() !== base64Signature.trim()) {
46
+ throw new Error('The detached signature is not the one in the bundle');
47
+ }
48
+ const sig = fromBase64(base64Signature, 'The bundle signature');
49
+ const leafDer = pemCertificate(fromUtf8(fromBase64(cert, 'The bundle certificate')), 'The bundle certificate');
50
+ const leaf = parseCertificate(leafDer);
51
+ // Rekor vouches for the entry and its time: nothing below is trusted before this.
52
+ const log = REKOR_LOGS.find((candidate) => candidate.logId === payload.logID.toLowerCase());
53
+ if (!log)
54
+ throw new Error(`The bundle names Rekor log ${payload.logID}, which the Sigstore trust root does not know`);
55
+ const canonical = utf8.encode(`{"body":${JSON.stringify(payload.body)},"integratedTime":${payload.integratedTime},` +
56
+ `"logID":${JSON.stringify(payload.logID)},"logIndex":${payload.logIndex}}`);
57
+ const setVerifies = await verifyEcdsa(fromBase64(log.publicKey, 'The Rekor key'), 'P-256', 'SHA-256', fromBase64(rekorBundle.SignedEntryTimestamp, 'The signed entry timestamp'), canonical);
58
+ if (!setVerifies) {
59
+ throw new Error("Rekor's signed entry timestamp does not verify: the log entry, its time or its index was altered");
60
+ }
61
+ const at = payload.integratedTime * 1000;
62
+ if (!within(at, log.validFor)) {
63
+ throw new Error(`Rekor log ${log.baseUrl} was not in use at ${iso(at)}, when the entry says it was logged`);
64
+ }
65
+ // The entry Rekor signed is this signature, over these bytes, by this certificate.
66
+ const entry = hashedRekord(payload.body);
67
+ const digest = hex(new Uint8Array(await crypto.subtle.digest('SHA-256', body)));
68
+ if (entry.hash !== digest)
69
+ throw new Error("The file's SHA-256 is not the one Rekor logged: the file is not the one signed");
70
+ if (!equal(fromBase64(entry.signature, 'The logged signature'), sig)) {
71
+ throw new Error("The bundle's signature is not the one Rekor logged");
72
+ }
73
+ const loggedCert = pemCertificate(fromUtf8(fromBase64(entry.publicKey, 'The logged certificate')), 'The logged certificate');
74
+ if (!equal(loggedCert, leafDer))
75
+ throw new Error("The bundle's certificate is not the one Rekor logged");
76
+ if (leaf.curve !== 'P-256' || !(await verifyEcdsa(leaf.spki, 'P-256', 'SHA-256', sig, body))) {
77
+ throw new Error("The signature over the file does not verify with the bundle's certificate");
78
+ }
79
+ await chainsToFulcio(leaf, at);
80
+ checkSigner(leaf, signer);
81
+ return { integratedTime: payload.integratedTime };
82
+ }
83
+ /**
84
+ * Check that `leaf` was issued by a Fulcio CA in the trust root, and that it,
85
+ * the CA's certificates and the CA itself (by the trust root's `validFor`)
86
+ * were all valid at `at` (milliseconds): when Rekor logged the signature, not
87
+ * now, since a Fulcio certificate lives ten minutes. Every signature on the
88
+ * way is checked, with the algorithm its certificate names.
89
+ */
90
+ export async function chainsToFulcio(leaf, at) {
91
+ if (!validAt(leaf, at)) {
92
+ throw new Error(`The signing certificate was valid ${iso(leaf.notBefore)} to ${iso(leaf.notAfter)}, not at ${iso(at)} when Rekor logged it`);
93
+ }
94
+ const reasons = [];
95
+ for (const authority of FULCIO_AUTHORITIES) {
96
+ const chain = authority.certChain.map((der) => parseCertificate(fromBase64(der, 'A Fulcio certificate')));
97
+ if (!(await issuedBy(leaf, chain[0])))
98
+ continue;
99
+ // The issuer is found; from here a failure is this CA's, so say which.
100
+ const reason = await authorityRefusal(chain, authority.validFor, at);
101
+ if (!reason)
102
+ return;
103
+ reasons.push(`${authority.uri} (valid from ${authority.validFor.start}): ${reason}`);
104
+ }
105
+ throw new Error(reasons.length > 0
106
+ ? `The signing certificate's CA is not trusted at ${iso(at)}: ${reasons.join('; ')}`
107
+ : 'The signing certificate was not issued by a Fulcio CA in the Sigstore trust root');
108
+ }
109
+ async function authorityRefusal(chain, validFor, at) {
110
+ if (!within(at, validFor))
111
+ return 'the trust root says it was not issuing certificates then';
112
+ for (const [index, certificate] of chain.entries()) {
113
+ if (!validAt(certificate, at))
114
+ return `its certificate ${index} was not valid then`;
115
+ const parent = chain[index + 1] ?? certificate; // the root signs itself
116
+ if (!(await issuedBy(certificate, parent)))
117
+ return `its certificate ${index} is not signed by the next`;
118
+ }
119
+ return undefined;
120
+ }
121
+ async function issuedBy(child, parent) {
122
+ if (!equal(child.issuer, parent.subject))
123
+ return false;
124
+ return verifyEcdsa(parent.spki, parent.curve, child.signatureHash, child.signature, child.tbs);
125
+ }
126
+ function checkSigner(leaf, signer) {
127
+ const san = leaf.extensions.get(SUBJECT_ALT_NAME);
128
+ if (!san)
129
+ throw new Error('The signing certificate names no identity');
130
+ const names = children(expectTag(parseDer(san), SEQUENCE, 'The subject alternative name'));
131
+ const uris = names.filter((name) => name.tag === SAN_URI).map((name) => fromUtf8(contents(name)));
132
+ if (uris.length !== 1 || names.length !== 1 || uris[0] !== signer.identity) {
133
+ throw new Error(`The release was signed by ${JSON.stringify(uris.length === 1 ? uris[0] : uris)}, not by ${signer.identity}`);
134
+ }
135
+ const v2 = leaf.extensions.get(FULCIO_ISSUER_V2);
136
+ const v1 = leaf.extensions.get(FULCIO_ISSUER_V1);
137
+ const issuer = v2
138
+ ? fromUtf8(contents(expectTag(parseDer(v2), UTF8_STRING, 'The OIDC issuer')))
139
+ : v1
140
+ ? fromUtf8(v1)
141
+ : undefined;
142
+ if (issuer !== signer.issuer) {
143
+ throw new Error(`The signing certificate's OIDC issuer is ${issuer === undefined ? 'missing' : JSON.stringify(issuer)}, not ${signer.issuer}`);
144
+ }
145
+ const usage = leaf.extensions.get(EXTENDED_KEY_USAGE);
146
+ if (!usage || !objectIdentifiers(usage).includes(CODE_SIGNING)) {
147
+ throw new Error('The signing certificate is not for code signing');
148
+ }
149
+ }
150
+ function cosignBundle(value) {
151
+ let bundle = value;
152
+ if (typeof bundle === 'string') {
153
+ try {
154
+ bundle = JSON.parse(bundle);
155
+ }
156
+ catch {
157
+ throw new Error('The bundle is not JSON');
158
+ }
159
+ }
160
+ const b = bundle;
161
+ const p = b?.rekorBundle?.Payload;
162
+ const ok = typeof b?.base64Signature === 'string' &&
163
+ typeof b.cert === 'string' &&
164
+ typeof b.rekorBundle?.SignedEntryTimestamp === 'string' &&
165
+ typeof p?.body === 'string' &&
166
+ typeof p.logID === 'string' &&
167
+ /^[0-9a-fA-F]{64}$/.test(p.logID) &&
168
+ Number.isSafeInteger(p.integratedTime) &&
169
+ p.integratedTime >= 0 &&
170
+ Number.isSafeInteger(p.logIndex) &&
171
+ p.logIndex >= 0;
172
+ if (!ok)
173
+ throw new Error('The bundle is not a cosign bundle with a Rekor entry');
174
+ return b;
175
+ }
176
+ /** The fields of a logged `hashedrekord` entry that are compared. */
177
+ function hashedRekord(body) {
178
+ let entry;
179
+ try {
180
+ entry = JSON.parse(fromUtf8(fromBase64(body, 'The logged entry')));
181
+ }
182
+ catch {
183
+ throw new Error('The logged entry is not JSON');
184
+ }
185
+ const hash = entry?.spec?.data?.hash;
186
+ const signature = entry?.spec?.signature;
187
+ if (entry?.kind !== 'hashedrekord' ||
188
+ hash?.algorithm !== 'sha256' ||
189
+ typeof hash.value !== 'string' ||
190
+ typeof signature?.content !== 'string' ||
191
+ typeof signature.publicKey?.content !== 'string') {
192
+ throw new Error('The logged entry is not a SHA-256 hashedrekord');
193
+ }
194
+ return { hash: hash.value.toLowerCase(), signature: signature.content, publicKey: signature.publicKey.content };
195
+ }
196
+ function validAt(certificate, at) {
197
+ return certificate.notBefore <= at && at <= certificate.notAfter;
198
+ }
199
+ function within(at, window) {
200
+ return Date.parse(window.start) <= at && (window.end === undefined || at <= Date.parse(window.end));
201
+ }
202
+ function iso(ms) {
203
+ return new Date(ms).toISOString();
204
+ }
205
+ function equal(a, b) {
206
+ return a.length === b.length && a.every((byte, index) => byte === b[index]);
207
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Just enough DER and X.509 to check a Sigstore signing certificate with
3
+ * WebCrypto: read a certificate's signed part, validity, key and extensions,
4
+ * and verify the ECDSA signatures (P-256 and P-384) Sigstore uses.
5
+ *
6
+ * Written here, rather than taken from a library, so the SDK keeps no runtime
7
+ * dependencies and runs wherever WebCrypto does. It parses strictly and throws
8
+ * on anything it does not expect; it is not a general X.509 implementation.
9
+ */
10
+ export type Curve = 'P-256' | 'P-384';
11
+ export type Hash = 'SHA-256' | 'SHA-384';
12
+ /** One DER element: its tag, and where it and its contents lie in `bytes`. */
13
+ export interface Element {
14
+ tag: number;
15
+ bytes: Uint8Array;
16
+ /** Offset of the element's first (tag) byte. */
17
+ offset: number;
18
+ /** Offset of its contents. */
19
+ start: number;
20
+ /** Offset just past it. */
21
+ end: number;
22
+ }
23
+ /** Parse one complete DER element, refusing trailing bytes. */
24
+ export declare function parseDer(bytes: Uint8Array): Element;
25
+ export declare function children(element: Element): Element[];
26
+ /** `element` if it has `tag`; otherwise throws naming `what`. */
27
+ export declare function expectTag(element: Element | undefined, tag: number, what: string): Element;
28
+ export declare function contents(element: Element): Uint8Array;
29
+ export interface Certificate {
30
+ der: Uint8Array;
31
+ /** The signed part, exactly as the issuer signed it. */
32
+ tbs: Uint8Array;
33
+ /** The hash of the ECDSA signature the issuer made over `tbs`. */
34
+ signatureHash: Hash;
35
+ /** That signature, DER. */
36
+ signature: Uint8Array;
37
+ /** The issuer and subject names, DER, to compare byte for byte. */
38
+ issuer: Uint8Array;
39
+ subject: Uint8Array;
40
+ /** Validity, in milliseconds since the epoch, both ends inclusive. */
41
+ notBefore: number;
42
+ notAfter: number;
43
+ /** The subject's key, DER SubjectPublicKeyInfo, and its curve. */
44
+ spki: Uint8Array;
45
+ curve: Curve;
46
+ /** Each extension's value (the contents of its OCTET STRING), by object identifier. */
47
+ extensions: Map<string, Uint8Array>;
48
+ }
49
+ /**
50
+ * Parse a DER X.509 certificate with an ECDSA key, signed with ECDSA. Throws
51
+ * naming what is wrong.
52
+ */
53
+ export declare function parseCertificate(der: Uint8Array): Certificate;
54
+ /** The object identifiers in a DER `SEQUENCE OF OBJECT IDENTIFIER` (an extended key usage). */
55
+ export declare function objectIdentifiers(der: Uint8Array): string[];
56
+ /**
57
+ * An ECDSA signature as WebCrypto takes it (IEEE P1363: `r || s`, each the
58
+ * curve's size), from the DER `SEQUENCE { r INTEGER, s INTEGER }` X.509 and
59
+ * cosign use.
60
+ */
61
+ export declare function ecdsaSignatureToP1363(der: Uint8Array, curve: Curve): Uint8Array;
62
+ /** Verify a DER ECDSA `signature` over `data` with a DER SubjectPublicKeyInfo key. */
63
+ export declare function verifyEcdsa(spki: Uint8Array, curve: Curve, hash: Hash, signature: Uint8Array, data: Uint8Array): Promise<boolean>;
64
+ /** Decode base64 (standard alphabet), refusing anything else. */
65
+ export declare function fromBase64(value: string, what: string): Uint8Array;
66
+ /** The DER of the one `CERTIFICATE` block a PEM holds. */
67
+ export declare function pemCertificate(pem: string, what: string): Uint8Array;
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Just enough DER and X.509 to check a Sigstore signing certificate with
3
+ * WebCrypto: read a certificate's signed part, validity, key and extensions,
4
+ * and verify the ECDSA signatures (P-256 and P-384) Sigstore uses.
5
+ *
6
+ * Written here, rather than taken from a library, so the SDK keeps no runtime
7
+ * dependencies and runs wherever WebCrypto does. It parses strictly and throws
8
+ * on anything it does not expect; it is not a general X.509 implementation.
9
+ */
10
+ const SEQUENCE = 0x30;
11
+ const OID = 0x06;
12
+ const INTEGER = 0x02;
13
+ const BIT_STRING = 0x03;
14
+ const OCTET_STRING = 0x04;
15
+ const BOOLEAN = 0x01;
16
+ const UTC_TIME = 0x17;
17
+ const GENERALIZED_TIME = 0x18;
18
+ const CONTEXT_0 = 0xa0;
19
+ const CONTEXT_3 = 0xa3;
20
+ const EC_PUBLIC_KEY = '1.2.840.10045.2.1';
21
+ const CURVES = {
22
+ '1.2.840.10045.3.1.7': 'P-256',
23
+ '1.3.132.0.34': 'P-384',
24
+ };
25
+ const CURVE_SIZE = { 'P-256': 32, 'P-384': 48 };
26
+ /** The certificate signature algorithms accepted: ECDSA with SHA-256 or SHA-384. */
27
+ const SIGNATURE_HASHES = {
28
+ '1.2.840.10045.4.3.2': 'SHA-256',
29
+ '1.2.840.10045.4.3.3': 'SHA-384',
30
+ };
31
+ function read(bytes, offset, limit) {
32
+ if (offset + 2 > limit)
33
+ throw new Error('it is truncated');
34
+ const tag = bytes[offset];
35
+ if ((tag & 0x1f) === 0x1f)
36
+ throw new Error('it uses a tag this parser does not read');
37
+ let length = bytes[offset + 1];
38
+ let start = offset + 2;
39
+ if (length & 0x80) {
40
+ const count = length & 0x7f;
41
+ if (count === 0 || count > 4 || start + count > limit)
42
+ throw new Error('it has a length this parser does not read');
43
+ length = 0;
44
+ for (let i = 0; i < count; i += 1)
45
+ length = length * 256 + bytes[start + i];
46
+ start += count;
47
+ }
48
+ const end = start + length;
49
+ if (end > limit)
50
+ throw new Error('it is truncated');
51
+ return { tag, bytes, offset, start, end };
52
+ }
53
+ /** Parse one complete DER element, refusing trailing bytes. */
54
+ export function parseDer(bytes) {
55
+ const element = read(bytes, 0, bytes.length);
56
+ if (element.end !== bytes.length)
57
+ throw new Error('it has trailing bytes');
58
+ return element;
59
+ }
60
+ export function children(element) {
61
+ const out = [];
62
+ for (let offset = element.start; offset < element.end;) {
63
+ const child = read(element.bytes, offset, element.end);
64
+ out.push(child);
65
+ offset = child.end;
66
+ }
67
+ return out;
68
+ }
69
+ /** `element` if it has `tag`; otherwise throws naming `what`. */
70
+ export function expectTag(element, tag, what) {
71
+ if (!element || element.tag !== tag)
72
+ throw new Error(`${what} is not where it should be`);
73
+ return element;
74
+ }
75
+ export function contents(element) {
76
+ return element.bytes.slice(element.start, element.end);
77
+ }
78
+ /** The whole element, header included. */
79
+ function whole(element) {
80
+ return element.bytes.slice(element.offset, element.end);
81
+ }
82
+ function oid(element) {
83
+ const value = contents(expectTag(element, OID, 'An object identifier'));
84
+ if (value.length === 0 || value[value.length - 1] & 0x80)
85
+ throw new Error('it has a malformed object identifier');
86
+ const arcs = [];
87
+ let arc = 0;
88
+ for (const byte of value) {
89
+ arc = arc * 128 + (byte & 0x7f);
90
+ if (!(byte & 0x80)) {
91
+ arcs.push(arc);
92
+ arc = 0;
93
+ }
94
+ }
95
+ const first = arcs.shift();
96
+ const head = first < 80 ? [Math.floor(first / 40), first % 40] : [2, first - 80];
97
+ return [...head, ...arcs].join('.');
98
+ }
99
+ function time(element) {
100
+ const text = new TextDecoder().decode(contents(element));
101
+ const utc = element.tag === UTC_TIME ? /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(text) : null;
102
+ const generalized = element.tag === GENERALIZED_TIME ? /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/.exec(text) : null;
103
+ const parts = utc ?? generalized;
104
+ if (!parts)
105
+ throw new Error(`it holds the time ${JSON.stringify(text)}, which is not one a certificate may hold`);
106
+ let year = Number(parts[1]);
107
+ if (utc)
108
+ year += year < 50 ? 2000 : 1900;
109
+ const [month, day, hour, minute, second] = parts.slice(2).map(Number);
110
+ return Date.UTC(year, month - 1, day, hour, minute, second);
111
+ }
112
+ /**
113
+ * Parse a DER X.509 certificate with an ECDSA key, signed with ECDSA. Throws
114
+ * naming what is wrong.
115
+ */
116
+ export function parseCertificate(der) {
117
+ try {
118
+ const [tbsElement, algorithm, signature, ...rest] = children(expectTag(parseDer(der), SEQUENCE, 'The certificate'));
119
+ if (rest.length > 0)
120
+ throw new Error('it has fields after its signature');
121
+ const fields = children(expectTag(tbsElement, SEQUENCE, 'The signed part'));
122
+ // `version` is [0] EXPLICIT; everything after it is positional.
123
+ const first = fields[0]?.tag === CONTEXT_0 ? 1 : 0;
124
+ const [serial, innerAlgorithm, issuer, validity, subject, spki, ...optional] = fields.slice(first);
125
+ expectTag(serial, INTEGER, 'The serial number');
126
+ const algorithmId = oid(children(expectTag(algorithm, SEQUENCE, 'The signature algorithm'))[0]);
127
+ const innerId = oid(children(expectTag(innerAlgorithm, SEQUENCE, 'The signature algorithm'))[0]);
128
+ if (innerId !== algorithmId)
129
+ throw new Error('it names two different signature algorithms');
130
+ const signatureHash = SIGNATURE_HASHES[algorithmId];
131
+ if (!signatureHash)
132
+ throw new Error(`it is signed with ${algorithmId}, not ECDSA with SHA-256 or SHA-384`);
133
+ const [notBefore, notAfter] = children(expectTag(validity, SEQUENCE, 'The validity')).map(time);
134
+ if (notAfter === undefined)
135
+ throw new Error('its validity is incomplete');
136
+ const [keyAlgorithm, keyBits] = children(expectTag(spki, SEQUENCE, 'The public key'));
137
+ expectTag(keyBits, BIT_STRING, 'The public key');
138
+ const [keyType, keyCurve] = children(expectTag(keyAlgorithm, SEQUENCE, 'The key algorithm'));
139
+ if (oid(keyType) !== EC_PUBLIC_KEY)
140
+ throw new Error('its key is not an elliptic-curve key');
141
+ const curve = CURVES[oid(keyCurve)];
142
+ if (!curve)
143
+ throw new Error('its key is on a curve other than P-256 or P-384');
144
+ const extensions = new Map();
145
+ const extensionsField = optional.find((field) => field.tag === CONTEXT_3);
146
+ if (extensionsField) {
147
+ const [list] = children(extensionsField);
148
+ for (const extension of children(expectTag(list, SEQUENCE, 'The extensions'))) {
149
+ const parts = children(expectTag(extension, SEQUENCE, 'An extension'));
150
+ if (parts.length === 3)
151
+ expectTag(parts[1], BOOLEAN, 'An extension\'s critical flag');
152
+ else if (parts.length !== 2)
153
+ throw new Error('it has a malformed extension');
154
+ const id = oid(parts[0]);
155
+ if (extensions.has(id))
156
+ throw new Error(`it has extension ${id} twice`);
157
+ extensions.set(id, contents(expectTag(parts[parts.length - 1], OCTET_STRING, 'An extension value')));
158
+ }
159
+ }
160
+ const signatureBits = contents(expectTag(signature, BIT_STRING, 'The signature'));
161
+ if (signatureBits[0] !== 0)
162
+ throw new Error('its signature is not a whole number of bytes');
163
+ return {
164
+ der,
165
+ tbs: whole(tbsElement),
166
+ signatureHash,
167
+ signature: signatureBits.slice(1),
168
+ issuer: whole(expectTag(issuer, SEQUENCE, 'The issuer')),
169
+ subject: whole(expectTag(subject, SEQUENCE, 'The subject')),
170
+ notBefore,
171
+ notAfter,
172
+ spki: whole(spki),
173
+ curve,
174
+ extensions,
175
+ };
176
+ }
177
+ catch (error) {
178
+ throw new Error(`The certificate is malformed: ${error.message}`);
179
+ }
180
+ }
181
+ /** The object identifiers in a DER `SEQUENCE OF OBJECT IDENTIFIER` (an extended key usage). */
182
+ export function objectIdentifiers(der) {
183
+ return children(expectTag(parseDer(der), SEQUENCE, 'The list')).map(oid);
184
+ }
185
+ /**
186
+ * An ECDSA signature as WebCrypto takes it (IEEE P1363: `r || s`, each the
187
+ * curve's size), from the DER `SEQUENCE { r INTEGER, s INTEGER }` X.509 and
188
+ * cosign use.
189
+ */
190
+ export function ecdsaSignatureToP1363(der, curve) {
191
+ const size = CURVE_SIZE[curve];
192
+ const integers = children(expectTag(parseDer(der), SEQUENCE, 'The signature'));
193
+ if (integers.length !== 2)
194
+ throw new Error('The signature is not an ECDSA signature');
195
+ const out = new Uint8Array(size * 2);
196
+ integers.forEach((integer, index) => {
197
+ let value = contents(expectTag(integer, INTEGER, 'The signature'));
198
+ // r and s are positive: a set top bit with no leading zero is a negative number.
199
+ if (value.length === 0 || value[0] & 0x80)
200
+ throw new Error('The signature is not an ECDSA signature');
201
+ while (value.length > 1 && value[0] === 0)
202
+ value = value.subarray(1);
203
+ if (value.length > size)
204
+ throw new Error('The signature is not an ECDSA signature');
205
+ out.set(value, index * size + size - value.length);
206
+ });
207
+ return out;
208
+ }
209
+ /** Verify a DER ECDSA `signature` over `data` with a DER SubjectPublicKeyInfo key. */
210
+ export async function verifyEcdsa(spki, curve, hash, signature, data) {
211
+ let p1363;
212
+ try {
213
+ p1363 = ecdsaSignatureToP1363(signature, curve);
214
+ }
215
+ catch {
216
+ return false;
217
+ }
218
+ const key = await crypto.subtle.importKey('spki', spki, { name: 'ECDSA', namedCurve: curve }, false, ['verify']);
219
+ return crypto.subtle.verify({ name: 'ECDSA', hash }, key, p1363, data);
220
+ }
221
+ /** Decode base64 (standard alphabet), refusing anything else. */
222
+ export function fromBase64(value, what) {
223
+ const clean = value.replace(/\s+/g, '');
224
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(clean) || clean.length % 4 !== 0) {
225
+ throw new Error(`${what} is not base64`);
226
+ }
227
+ const binary = atob(clean);
228
+ const bytes = new Uint8Array(binary.length);
229
+ for (let i = 0; i < binary.length; i += 1)
230
+ bytes[i] = binary.charCodeAt(i);
231
+ return bytes;
232
+ }
233
+ /** The DER of the one `CERTIFICATE` block a PEM holds. */
234
+ export function pemCertificate(pem, what) {
235
+ const blocks = [...pem.matchAll(/-----BEGIN CERTIFICATE-----([^-]*)-----END CERTIFICATE-----/g)];
236
+ if (blocks.length !== 1 || pem.replace(blocks[0][0], '').trim() !== '') {
237
+ throw new Error(`${what} is not exactly one PEM certificate`);
238
+ }
239
+ return fromBase64(blocks[0][1], what);
240
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calimero-network/mero-js",
3
- "version": "21.3.0",
3
+ "version": "21.4.0",
4
4
  "description": "Pure JavaScript SDK for Calimero",
5
5
  "type": "module",
6
6
  "exports": {