@originator-profile/sign 0.4.0-beta.1

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/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Profile Sign
2
+
3
+ 鍵の生成と署名するためのパッケージです。
package/dist/index.cjs ADDED
@@ -0,0 +1,286 @@
1
+ 'use strict';
2
+
3
+ var securingMechanism = require('@originator-profile/securing-mechanism');
4
+
5
+ const supportedHashAlgorithms = {
6
+ /** SHA-256 hash algorithm */
7
+ sha256: "SHA-256",
8
+ /** SHA-384 hash algorithm */
9
+ sha384: "SHA-384",
10
+ /** SHA-512 hash algorithm */
11
+ sha512: "SHA-512"
12
+ };
13
+ const IntegrityMetadataRegex = /^(?<alg>sha256|sha384|sha512)-(?<val>(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)(?:[?](?<opt>[\x21-\x7e]*))?$/;
14
+ class IntegrityMetadata {
15
+ /** Hash algorithm */
16
+ alg;
17
+ /** The base64-encoded hash value of the resource */
18
+ val;
19
+ /** Optional additional attributes */
20
+ opt;
21
+ /**
22
+ * Creates an instance of `IntegrityMetadata` from a given object or string.
23
+ * @param integrity The integrity metadata input, which can be a string or object.
24
+ * @example
25
+ * ```js
26
+ * new IntegrityMetadata("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=")
27
+ * ```
28
+ *
29
+ * or
30
+ *
31
+ * ```js
32
+ * new IntegrityMetadata({
33
+ * alg: "sha256",
34
+ * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
35
+ * })
36
+ * ```
37
+ */
38
+ constructor(integrity) {
39
+ const integrityString = typeof integrity === "object" && integrity !== null ? IntegrityMetadata.stringify(integrity) : String(integrity ?? "").trim();
40
+ const {
41
+ alg = "",
42
+ val = "",
43
+ opt
44
+ } = IntegrityMetadataRegex.exec(integrityString)?.groups ?? {};
45
+ Object.assign(this, {
46
+ alg,
47
+ val,
48
+ opt: opt?.split("?") ?? []
49
+ });
50
+ }
51
+ /**
52
+ * Compares the current integrity metadata with another object or string.
53
+ * @param integrity The integrity metadata to compare with.
54
+ * @returns `true` if the integrity metadata matches, `false` otherwise.
55
+ * @example
56
+ * ```js
57
+ * integrityMetadata.match("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=")
58
+ * ```
59
+ *
60
+ * or
61
+ *
62
+ * ```js
63
+ * integrityMetadata.match({
64
+ * alg: "sha256",
65
+ * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
66
+ * })
67
+ * ```
68
+ */
69
+ match(integrity) {
70
+ const { alg, val } = new IntegrityMetadata(integrity);
71
+ if (!alg) return false;
72
+ if (!val) return false;
73
+ if (!(alg in supportedHashAlgorithms)) return false;
74
+ return alg === this.alg && val === this.val;
75
+ }
76
+ /**
77
+ * Converts the integrity metadata into a string representation.
78
+ * @returns The string representation of the integrity metadata.
79
+ */
80
+ toString() {
81
+ return IntegrityMetadata.stringify(this);
82
+ }
83
+ /**
84
+ * Converts the integrity metadata into a JSON string.
85
+ * @returns The JSON string representation of the integrity metadata.
86
+ */
87
+ toJSON() {
88
+ return this.toString();
89
+ }
90
+ /**
91
+ * Static method to stringify an integrity metadata object.
92
+ * @param integrity The integrity metadata object to stringify.
93
+ * @returns The stringified integrity metadata.
94
+ * @example
95
+ * ```js
96
+ * IntegrityMetadata.stringify({
97
+ * alg: "sha256",
98
+ * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
99
+ * }) // "sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM="
100
+ * ```
101
+ */
102
+ static stringify({ alg, val, opt = [] }) {
103
+ if (!alg) return "";
104
+ if (!val) return "";
105
+ if (!(alg in supportedHashAlgorithms)) return "";
106
+ return `${alg}-${[val, ...opt].join("?")}`;
107
+ }
108
+ }
109
+ async function createIntegrityMetadata(hashAlgorithm, data, opt = []) {
110
+ const alg = hashAlgorithm.toLowerCase();
111
+ if (!(alg in supportedHashAlgorithms)) {
112
+ return new IntegrityMetadata("");
113
+ }
114
+ const hashAlgorithmIdentifier = supportedHashAlgorithms[alg];
115
+ const arrayBuffer = await crypto.subtle.digest(hashAlgorithmIdentifier, data);
116
+ const val = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
117
+ const integrity = IntegrityMetadata.stringify({ alg, val, opt });
118
+ return new IntegrityMetadata(integrity);
119
+ }
120
+
121
+ async function createDigestSri(alg, resource, fetcher = fetch) {
122
+ if (!(alg in supportedHashAlgorithms)) {
123
+ return { id: resource.id };
124
+ }
125
+ const meta = await Promise.all(
126
+ [resource.content ?? resource.id].flat().map(async (content) => {
127
+ const res = await fetcher(content);
128
+ const data = await res.arrayBuffer();
129
+ return await createIntegrityMetadata(alg, data);
130
+ })
131
+ );
132
+ return {
133
+ id: resource.id,
134
+ digestSRI: meta.join(" ")
135
+ };
136
+ }
137
+
138
+ async function fetchAndSetDigestSri(alg, content) {
139
+ if (!content) return content;
140
+ if (typeof content.digestSRI !== "string") {
141
+ Object.assign(
142
+ content,
143
+ await createDigestSri(alg, content)
144
+ );
145
+ }
146
+ delete content.content;
147
+ return content;
148
+ }
149
+
150
+ const fetchHtmlContent = async (elements) => {
151
+ const text = elements.map((element) => element.outerHTML).join("");
152
+ return [new Response(text)];
153
+ };
154
+ const fetchTextContent = async (elements) => {
155
+ const text = elements.map((element) => element.textContent ?? "").join("");
156
+ return [new Response(text)];
157
+ };
158
+ const fetchVisibleTextContent = async (elements) => {
159
+ const text = elements.map((element) => element.innerText).join("");
160
+ return [new Response(text)];
161
+ };
162
+ const fetchExternalResource = async (elements, fetcher = fetch) => {
163
+ return await Promise.all(
164
+ elements.map(async (element) => {
165
+ const el = element;
166
+ const src = el.currentSrc || el.src;
167
+ if (!src) {
168
+ throw new Error("Element has no src or currentSrc property");
169
+ }
170
+ return await fetcher(src);
171
+ })
172
+ );
173
+ };
174
+ const selectByCss = (params) => {
175
+ return Array.from(
176
+ params.document.querySelectorAll(params.cssSelector)
177
+ );
178
+ };
179
+ const selectByIntegrity = (params) => {
180
+ return selectByCss({
181
+ ...params,
182
+ cssSelector: `[integrity=${JSON.stringify(String(params.integrity))}]`
183
+ });
184
+ };
185
+ async function createIntegrity(alg, { content = "", ...target }, doc = document) {
186
+ if (![
187
+ "TextTargetIntegrity",
188
+ "VisibleTextTargetIntegrity",
189
+ "HtmlTargetIntegrity",
190
+ "ExternalResourceTargetIntegrity"
191
+ ].includes(target.type)) {
192
+ return null;
193
+ }
194
+ if (!(alg in supportedHashAlgorithms)) {
195
+ return null;
196
+ }
197
+ if (target.type === "ExternalResourceTargetIntegrity") {
198
+ const meta2 = await Promise.all(
199
+ [content].flat().map(async (content2) => {
200
+ const res2 = URL.canParse(content2) ? await fetch(content2) : new Response(content2);
201
+ const data2 = await res2.arrayBuffer();
202
+ return await createIntegrityMetadata(alg, data2);
203
+ })
204
+ );
205
+ return {
206
+ ...target,
207
+ integrity: meta2.join(" ")
208
+ };
209
+ }
210
+ const { contentFetcher, elementSelector } = {
211
+ HtmlTargetIntegrity: {
212
+ contentFetcher: fetchHtmlContent,
213
+ elementSelector: selectByCss
214
+ },
215
+ TextTargetIntegrity: {
216
+ contentFetcher: fetchTextContent,
217
+ elementSelector: selectByCss
218
+ },
219
+ VisibleTextTargetIntegrity: {
220
+ contentFetcher: fetchVisibleTextContent,
221
+ elementSelector: selectByCss
222
+ }
223
+ }[target.type];
224
+ const elements = elementSelector({ ...target, document: doc });
225
+ if (elements.length === 0) return null;
226
+ const [res] = await contentFetcher(elements);
227
+ if (!res) return null;
228
+ const data = await res.arrayBuffer();
229
+ const meta = await createIntegrityMetadata(alg, data);
230
+ return {
231
+ ...target,
232
+ integrity: meta.toString()
233
+ };
234
+ }
235
+
236
+ class IntegrityCalculationError extends Error {
237
+ }
238
+ async function fetchAndSetTargetIntegrity(alg, obj, documentProvider = async () => document) {
239
+ const target = await Promise.all(
240
+ obj.target.map(async (raw, i) => {
241
+ if (raw.integrity) {
242
+ const { content: _, ...target3 } = raw;
243
+ return target3;
244
+ }
245
+ const doc = await documentProvider(raw);
246
+ const target2 = await createIntegrity(alg, raw, doc);
247
+ if (!target2) {
248
+ throw new IntegrityCalculationError(
249
+ `Failed to create integrity for element target[${i}].`
250
+ );
251
+ }
252
+ return target2;
253
+ })
254
+ );
255
+ return Object.assign(obj, { target });
256
+ }
257
+
258
+ async function signCa(uca, privateKey, {
259
+ alg = "ES256",
260
+ issuedAt = /* @__PURE__ */ new Date(),
261
+ expiredAt,
262
+ integrityAlg = "sha256",
263
+ documentProvider = async () => document
264
+ }) {
265
+ await fetchAndSetDigestSri(integrityAlg, uca.credentialSubject.image);
266
+ await fetchAndSetTargetIntegrity(integrityAlg, uca, documentProvider);
267
+ return await securingMechanism.signJwtVc(uca, privateKey, { alg, issuedAt, expiredAt });
268
+ }
269
+
270
+ async function signCp(cp, privateKey, options) {
271
+ return securingMechanism.signJwtVc(cp, privateKey, options);
272
+ }
273
+
274
+ exports.IntegrityCalculationError = IntegrityCalculationError;
275
+ exports.createDigestSri = createDigestSri;
276
+ exports.createIntegrity = createIntegrity;
277
+ exports.fetchAndSetDigestSri = fetchAndSetDigestSri;
278
+ exports.fetchAndSetTargetIntegrity = fetchAndSetTargetIntegrity;
279
+ exports.fetchExternalResource = fetchExternalResource;
280
+ exports.fetchHtmlContent = fetchHtmlContent;
281
+ exports.fetchTextContent = fetchTextContent;
282
+ exports.fetchVisibleTextContent = fetchVisibleTextContent;
283
+ exports.selectByCss = selectByCss;
284
+ exports.selectByIntegrity = selectByIntegrity;
285
+ exports.signCa = signCa;
286
+ exports.signCp = signCp;
@@ -0,0 +1,199 @@
1
+ import { Image, RawTarget, Target, UnsignedContentAttestation, Jwk, CoreProfile } from '@originator-profile/model';
2
+
3
+ /**
4
+ * Represents the available hash algorithms used for Subresource Integrity.
5
+ * @see {@link https://www.w3.org/TR/CSP2/#hash_algo}
6
+ */
7
+ type HashAlgorithm = "sha256" | "sha384" | "sha512";
8
+
9
+ type DigestSriContent = Image;
10
+ type ContentFetcher = (elements: ReadonlyArray<HTMLElement>, fetcher?: typeof fetch) => Promise<ReadonlyArray<Response>>;
11
+ type ElementSelector = (params: {
12
+ cssSelector?: string;
13
+ integrity?: string;
14
+ document: Document;
15
+ }) => ReadonlyArray<HTMLElement>;
16
+ /** 文脈に応じて Document を提供する関数 */
17
+ type DocumentProvider = (raw: RawTarget) => Promise<Document>;
18
+
19
+ /**
20
+ * `digestSRI` の作成
21
+ *
22
+ * `content` にアクセスし `digestSRI` を計算します。
23
+ * なお、`content` プロパティは削除されます。
24
+ * `content` プロパティが存在しない場合、`id` にアクセスし `digestSRI` 計算します。
25
+ *
26
+ * 複数のコンテンツが指定された場合、それぞれのハッシュ値がスペース区切りで結合されます。
27
+ *
28
+ * @see {@link https://www.w3.org/TR/SRI/#the-integrity-attribute}
29
+ * @example
30
+ * 単一コンテンツ
31
+ * ```ts
32
+ * const resource = {
33
+ * id: "<URL>",
34
+ * content: "<コンテンツ (URL)>", // 省略可能
35
+ * };
36
+ *
37
+ * const { digestSRI } = await createDigestSri("sha256", resource);
38
+ * console.log(digestSRI); // sha256-...
39
+ * ```
40
+ *
41
+ * @example
42
+ * 複数コンテンツ
43
+ * ```ts
44
+ * const resource = {
45
+ * id: "<URL>",
46
+ * content: ["<コンテンツURL1>", "<コンテンツURL2>"],
47
+ * };
48
+ *
49
+ * const { digestSRI } = await createDigestSri("sha256", resource);
50
+ * console.log(digestSRI); // sha256-... sha256-...
51
+ * ```
52
+ */
53
+ declare function createDigestSri(alg: HashAlgorithm, resource: {
54
+ /** URL */
55
+ id: string;
56
+ /** コンテンツ (URL) */
57
+ content?: Array<string> | string;
58
+ }, fetcher?: typeof fetch): Promise<DigestSriContent>;
59
+
60
+ /**
61
+ * オブジェクトへの `digestSRI` の割り当て
62
+ *
63
+ * `digestSRI` を省略した場合、`content` にアクセスし `digestSRI` を計算します。
64
+ * なお、`content` プロパティは削除されます。
65
+ * `content` プロパティが存在しない場合、`id` にアクセスし `digestSRI` 計算します。
66
+ * @see {@link https://www.w3.org/TR/SRI/#the-integrity-attribute}
67
+ * @example
68
+ * ```ts
69
+ * const resource = {
70
+ * id: "<URL>",
71
+ * content: "<コンテンツ (URL)>", // 省略可能
72
+ * };
73
+ *
74
+ * await fetchAndSetDigestSri("sha256", resource);
75
+ *
76
+ * console.log(resource);
77
+ * // {
78
+ * // id: "<URL>",
79
+ * // digestSRI: "sha256-..."
80
+ * // }
81
+ * ```
82
+ */
83
+ declare function fetchAndSetDigestSri(alg: HashAlgorithm, content: unknown): Promise<DigestSriContent | undefined>;
84
+
85
+ /** Integrityの計算に失敗 (例: 検証対象が存在しない) エラー */
86
+ declare class IntegrityCalculationError extends Error {
87
+ }
88
+ /**
89
+ * 未署名 Content Attestation への Target Integrity の割り当て
90
+ * target[].integrity を省略した場合、type に準じて content から integrity を計算します。
91
+ * 一方、target[].integrity が含まれる場合、その値をそのまま使用します。
92
+ * なお、いずれも target[].content プロパティが削除される点にご注意ください。
93
+ * @see {@link https://docs.originator-profile.org/opb/content-integrity-descriptor/}
94
+ * @throws {IntegrityCalculationError} Integrityの計算に失敗 (例: 検証対象が存在しない)
95
+ * @example
96
+ * ```ts
97
+ * const uca = {
98
+ * // ...
99
+ * target: [
100
+ * {
101
+ * type: "<Target Integrityの種別>",
102
+ * cssSelector: "<CSS セレクター>",
103
+ * },
104
+ * ],
105
+ * };
106
+ *
107
+ * await fetchAndSetTargetIntegrity("sha256", uca);
108
+ *
109
+ * console.log(uca.target);
110
+ * // [
111
+ * // {
112
+ * // type: "<Target Integrityの種別>",
113
+ * // cssSelector: "<CSS セレクター>",
114
+ * // integrity: "sha256-..."
115
+ * // }
116
+ * // ]
117
+ * ```
118
+ */
119
+ declare function fetchAndSetTargetIntegrity<T extends {
120
+ target: ReadonlyArray<RawTarget>;
121
+ }>(alg: HashAlgorithm, obj: T, documentProvider?: DocumentProvider): Promise<T & {
122
+ target: ReadonlyArray<Target>;
123
+ }>;
124
+
125
+ /** element.outerHTML and join("") */
126
+ declare const fetchHtmlContent: ContentFetcher;
127
+ /** element.textContent and join("") */
128
+ declare const fetchTextContent: ContentFetcher;
129
+ /** element.innerText and join("") */
130
+ declare const fetchVisibleTextContent: ContentFetcher;
131
+ /**
132
+ * Fetches external resources from elements by using their `currentSrc` or `src` property.
133
+ * HTMLImageElement (<img>) and HTMLMediaElement (<video>, <audio>) support the `currentSrc` property,
134
+ * which represents the actual source URL currently in use after source selection (e.g., <img srcset>, <video> with multiple <source>).
135
+ * `currentSrc` is preferred over `src` because it reflects the final selected resource, ensuring integrity checks are performed on the actual loaded content.
136
+ * Falls back to `src` if `currentSrc` is not available.
137
+ */
138
+ declare const fetchExternalResource: ContentFetcher;
139
+ declare const selectByCss: ElementSelector;
140
+ declare const selectByIntegrity: ElementSelector;
141
+ /**
142
+ * Target Integrity の作成
143
+ *
144
+ * `ExternalResourceTargetIntegrity` で複数のコンテンツが指定された場合、それぞれのハッシュ値がスペース区切りで結合されます。
145
+ *
146
+ * @see {@link https://docs.originator-profile.org/opb/content-integrity-descriptor/}
147
+ * @example
148
+ * 基本的な使用例
149
+ * ```ts
150
+ * const content = {
151
+ * type: "HtmlTargetIntegrity", // or ***TargetIntegrity
152
+ * cssSelector: "<CSS セレクター>",
153
+ * };
154
+ *
155
+ * const { integrity } = await createIntegrity("sha256", content);
156
+ * console.log(integrity); // sha256-...
157
+ * ```
158
+ *
159
+ * @example
160
+ * ExternalResourceTargetIntegrity で複数コンテンツ
161
+ * ```ts
162
+ * const content = {
163
+ * type: "ExternalResourceTargetIntegrity",
164
+ * content: ["<コンテンツURL1>", "<コンテンツURL2>"],
165
+ * };
166
+ *
167
+ * const { integrity } = await createIntegrity("sha256", content);
168
+ * console.log(integrity); // sha256-... sha256-...
169
+ * ```
170
+ */
171
+ declare function createIntegrity(alg: HashAlgorithm, { content, ...target }: RawTarget, doc?: Document): Promise<Target | null>;
172
+
173
+ /**
174
+ * Content Attestation への署名
175
+ * @param uca 未署名 Content Attestation オブジェクト
176
+ * @param privateKey プライベート鍵
177
+ * @return JWT でエンコードされた Content Attestation
178
+ */
179
+ declare function signCa(uca: UnsignedContentAttestation, privateKey: Jwk, { alg, issuedAt, expiredAt, integrityAlg, documentProvider, }: {
180
+ alg?: string;
181
+ issuedAt?: Date;
182
+ expiredAt: Date;
183
+ integrityAlg?: HashAlgorithm;
184
+ documentProvider?: DocumentProvider;
185
+ }): Promise<string>;
186
+
187
+ /**
188
+ * CP への署名
189
+ * @param cp CoreProfile オブジェクト
190
+ * @param privateKey プライベート鍵
191
+ * @return JWT でエンコードされた CP
192
+ */
193
+ declare function signCp(cp: CoreProfile, privateKey: Jwk, options: {
194
+ alg?: string;
195
+ issuedAt: Date;
196
+ expiredAt: Date;
197
+ }): Promise<string>;
198
+
199
+ export { type ContentFetcher, type DigestSriContent, type DocumentProvider, type ElementSelector, IntegrityCalculationError, createDigestSri, createIntegrity, fetchAndSetDigestSri, fetchAndSetTargetIntegrity, fetchExternalResource, fetchHtmlContent, fetchTextContent, fetchVisibleTextContent, selectByCss, selectByIntegrity, signCa, signCp };
@@ -0,0 +1,199 @@
1
+ import { Image, RawTarget, Target, UnsignedContentAttestation, Jwk, CoreProfile } from '@originator-profile/model';
2
+
3
+ /**
4
+ * Represents the available hash algorithms used for Subresource Integrity.
5
+ * @see {@link https://www.w3.org/TR/CSP2/#hash_algo}
6
+ */
7
+ type HashAlgorithm = "sha256" | "sha384" | "sha512";
8
+
9
+ type DigestSriContent = Image;
10
+ type ContentFetcher = (elements: ReadonlyArray<HTMLElement>, fetcher?: typeof fetch) => Promise<ReadonlyArray<Response>>;
11
+ type ElementSelector = (params: {
12
+ cssSelector?: string;
13
+ integrity?: string;
14
+ document: Document;
15
+ }) => ReadonlyArray<HTMLElement>;
16
+ /** 文脈に応じて Document を提供する関数 */
17
+ type DocumentProvider = (raw: RawTarget) => Promise<Document>;
18
+
19
+ /**
20
+ * `digestSRI` の作成
21
+ *
22
+ * `content` にアクセスし `digestSRI` を計算します。
23
+ * なお、`content` プロパティは削除されます。
24
+ * `content` プロパティが存在しない場合、`id` にアクセスし `digestSRI` 計算します。
25
+ *
26
+ * 複数のコンテンツが指定された場合、それぞれのハッシュ値がスペース区切りで結合されます。
27
+ *
28
+ * @see {@link https://www.w3.org/TR/SRI/#the-integrity-attribute}
29
+ * @example
30
+ * 単一コンテンツ
31
+ * ```ts
32
+ * const resource = {
33
+ * id: "<URL>",
34
+ * content: "<コンテンツ (URL)>", // 省略可能
35
+ * };
36
+ *
37
+ * const { digestSRI } = await createDigestSri("sha256", resource);
38
+ * console.log(digestSRI); // sha256-...
39
+ * ```
40
+ *
41
+ * @example
42
+ * 複数コンテンツ
43
+ * ```ts
44
+ * const resource = {
45
+ * id: "<URL>",
46
+ * content: ["<コンテンツURL1>", "<コンテンツURL2>"],
47
+ * };
48
+ *
49
+ * const { digestSRI } = await createDigestSri("sha256", resource);
50
+ * console.log(digestSRI); // sha256-... sha256-...
51
+ * ```
52
+ */
53
+ declare function createDigestSri(alg: HashAlgorithm, resource: {
54
+ /** URL */
55
+ id: string;
56
+ /** コンテンツ (URL) */
57
+ content?: Array<string> | string;
58
+ }, fetcher?: typeof fetch): Promise<DigestSriContent>;
59
+
60
+ /**
61
+ * オブジェクトへの `digestSRI` の割り当て
62
+ *
63
+ * `digestSRI` を省略した場合、`content` にアクセスし `digestSRI` を計算します。
64
+ * なお、`content` プロパティは削除されます。
65
+ * `content` プロパティが存在しない場合、`id` にアクセスし `digestSRI` 計算します。
66
+ * @see {@link https://www.w3.org/TR/SRI/#the-integrity-attribute}
67
+ * @example
68
+ * ```ts
69
+ * const resource = {
70
+ * id: "<URL>",
71
+ * content: "<コンテンツ (URL)>", // 省略可能
72
+ * };
73
+ *
74
+ * await fetchAndSetDigestSri("sha256", resource);
75
+ *
76
+ * console.log(resource);
77
+ * // {
78
+ * // id: "<URL>",
79
+ * // digestSRI: "sha256-..."
80
+ * // }
81
+ * ```
82
+ */
83
+ declare function fetchAndSetDigestSri(alg: HashAlgorithm, content: unknown): Promise<DigestSriContent | undefined>;
84
+
85
+ /** Integrityの計算に失敗 (例: 検証対象が存在しない) エラー */
86
+ declare class IntegrityCalculationError extends Error {
87
+ }
88
+ /**
89
+ * 未署名 Content Attestation への Target Integrity の割り当て
90
+ * target[].integrity を省略した場合、type に準じて content から integrity を計算します。
91
+ * 一方、target[].integrity が含まれる場合、その値をそのまま使用します。
92
+ * なお、いずれも target[].content プロパティが削除される点にご注意ください。
93
+ * @see {@link https://docs.originator-profile.org/opb/content-integrity-descriptor/}
94
+ * @throws {IntegrityCalculationError} Integrityの計算に失敗 (例: 検証対象が存在しない)
95
+ * @example
96
+ * ```ts
97
+ * const uca = {
98
+ * // ...
99
+ * target: [
100
+ * {
101
+ * type: "<Target Integrityの種別>",
102
+ * cssSelector: "<CSS セレクター>",
103
+ * },
104
+ * ],
105
+ * };
106
+ *
107
+ * await fetchAndSetTargetIntegrity("sha256", uca);
108
+ *
109
+ * console.log(uca.target);
110
+ * // [
111
+ * // {
112
+ * // type: "<Target Integrityの種別>",
113
+ * // cssSelector: "<CSS セレクター>",
114
+ * // integrity: "sha256-..."
115
+ * // }
116
+ * // ]
117
+ * ```
118
+ */
119
+ declare function fetchAndSetTargetIntegrity<T extends {
120
+ target: ReadonlyArray<RawTarget>;
121
+ }>(alg: HashAlgorithm, obj: T, documentProvider?: DocumentProvider): Promise<T & {
122
+ target: ReadonlyArray<Target>;
123
+ }>;
124
+
125
+ /** element.outerHTML and join("") */
126
+ declare const fetchHtmlContent: ContentFetcher;
127
+ /** element.textContent and join("") */
128
+ declare const fetchTextContent: ContentFetcher;
129
+ /** element.innerText and join("") */
130
+ declare const fetchVisibleTextContent: ContentFetcher;
131
+ /**
132
+ * Fetches external resources from elements by using their `currentSrc` or `src` property.
133
+ * HTMLImageElement (<img>) and HTMLMediaElement (<video>, <audio>) support the `currentSrc` property,
134
+ * which represents the actual source URL currently in use after source selection (e.g., <img srcset>, <video> with multiple <source>).
135
+ * `currentSrc` is preferred over `src` because it reflects the final selected resource, ensuring integrity checks are performed on the actual loaded content.
136
+ * Falls back to `src` if `currentSrc` is not available.
137
+ */
138
+ declare const fetchExternalResource: ContentFetcher;
139
+ declare const selectByCss: ElementSelector;
140
+ declare const selectByIntegrity: ElementSelector;
141
+ /**
142
+ * Target Integrity の作成
143
+ *
144
+ * `ExternalResourceTargetIntegrity` で複数のコンテンツが指定された場合、それぞれのハッシュ値がスペース区切りで結合されます。
145
+ *
146
+ * @see {@link https://docs.originator-profile.org/opb/content-integrity-descriptor/}
147
+ * @example
148
+ * 基本的な使用例
149
+ * ```ts
150
+ * const content = {
151
+ * type: "HtmlTargetIntegrity", // or ***TargetIntegrity
152
+ * cssSelector: "<CSS セレクター>",
153
+ * };
154
+ *
155
+ * const { integrity } = await createIntegrity("sha256", content);
156
+ * console.log(integrity); // sha256-...
157
+ * ```
158
+ *
159
+ * @example
160
+ * ExternalResourceTargetIntegrity で複数コンテンツ
161
+ * ```ts
162
+ * const content = {
163
+ * type: "ExternalResourceTargetIntegrity",
164
+ * content: ["<コンテンツURL1>", "<コンテンツURL2>"],
165
+ * };
166
+ *
167
+ * const { integrity } = await createIntegrity("sha256", content);
168
+ * console.log(integrity); // sha256-... sha256-...
169
+ * ```
170
+ */
171
+ declare function createIntegrity(alg: HashAlgorithm, { content, ...target }: RawTarget, doc?: Document): Promise<Target | null>;
172
+
173
+ /**
174
+ * Content Attestation への署名
175
+ * @param uca 未署名 Content Attestation オブジェクト
176
+ * @param privateKey プライベート鍵
177
+ * @return JWT でエンコードされた Content Attestation
178
+ */
179
+ declare function signCa(uca: UnsignedContentAttestation, privateKey: Jwk, { alg, issuedAt, expiredAt, integrityAlg, documentProvider, }: {
180
+ alg?: string;
181
+ issuedAt?: Date;
182
+ expiredAt: Date;
183
+ integrityAlg?: HashAlgorithm;
184
+ documentProvider?: DocumentProvider;
185
+ }): Promise<string>;
186
+
187
+ /**
188
+ * CP への署名
189
+ * @param cp CoreProfile オブジェクト
190
+ * @param privateKey プライベート鍵
191
+ * @return JWT でエンコードされた CP
192
+ */
193
+ declare function signCp(cp: CoreProfile, privateKey: Jwk, options: {
194
+ alg?: string;
195
+ issuedAt: Date;
196
+ expiredAt: Date;
197
+ }): Promise<string>;
198
+
199
+ export { type ContentFetcher, type DigestSriContent, type DocumentProvider, type ElementSelector, IntegrityCalculationError, createDigestSri, createIntegrity, fetchAndSetDigestSri, fetchAndSetTargetIntegrity, fetchExternalResource, fetchHtmlContent, fetchTextContent, fetchVisibleTextContent, selectByCss, selectByIntegrity, signCa, signCp };
package/dist/index.mjs ADDED
@@ -0,0 +1,272 @@
1
+ import { signJwtVc } from '@originator-profile/securing-mechanism';
2
+
3
+ const supportedHashAlgorithms = {
4
+ /** SHA-256 hash algorithm */
5
+ sha256: "SHA-256",
6
+ /** SHA-384 hash algorithm */
7
+ sha384: "SHA-384",
8
+ /** SHA-512 hash algorithm */
9
+ sha512: "SHA-512"
10
+ };
11
+ const IntegrityMetadataRegex = /^(?<alg>sha256|sha384|sha512)-(?<val>(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)(?:[?](?<opt>[\x21-\x7e]*))?$/;
12
+ class IntegrityMetadata {
13
+ /** Hash algorithm */
14
+ alg;
15
+ /** The base64-encoded hash value of the resource */
16
+ val;
17
+ /** Optional additional attributes */
18
+ opt;
19
+ /**
20
+ * Creates an instance of `IntegrityMetadata` from a given object or string.
21
+ * @param integrity The integrity metadata input, which can be a string or object.
22
+ * @example
23
+ * ```js
24
+ * new IntegrityMetadata("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=")
25
+ * ```
26
+ *
27
+ * or
28
+ *
29
+ * ```js
30
+ * new IntegrityMetadata({
31
+ * alg: "sha256",
32
+ * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
33
+ * })
34
+ * ```
35
+ */
36
+ constructor(integrity) {
37
+ const integrityString = typeof integrity === "object" && integrity !== null ? IntegrityMetadata.stringify(integrity) : String(integrity ?? "").trim();
38
+ const {
39
+ alg = "",
40
+ val = "",
41
+ opt
42
+ } = IntegrityMetadataRegex.exec(integrityString)?.groups ?? {};
43
+ Object.assign(this, {
44
+ alg,
45
+ val,
46
+ opt: opt?.split("?") ?? []
47
+ });
48
+ }
49
+ /**
50
+ * Compares the current integrity metadata with another object or string.
51
+ * @param integrity The integrity metadata to compare with.
52
+ * @returns `true` if the integrity metadata matches, `false` otherwise.
53
+ * @example
54
+ * ```js
55
+ * integrityMetadata.match("sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=")
56
+ * ```
57
+ *
58
+ * or
59
+ *
60
+ * ```js
61
+ * integrityMetadata.match({
62
+ * alg: "sha256",
63
+ * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
64
+ * })
65
+ * ```
66
+ */
67
+ match(integrity) {
68
+ const { alg, val } = new IntegrityMetadata(integrity);
69
+ if (!alg) return false;
70
+ if (!val) return false;
71
+ if (!(alg in supportedHashAlgorithms)) return false;
72
+ return alg === this.alg && val === this.val;
73
+ }
74
+ /**
75
+ * Converts the integrity metadata into a string representation.
76
+ * @returns The string representation of the integrity metadata.
77
+ */
78
+ toString() {
79
+ return IntegrityMetadata.stringify(this);
80
+ }
81
+ /**
82
+ * Converts the integrity metadata into a JSON string.
83
+ * @returns The JSON string representation of the integrity metadata.
84
+ */
85
+ toJSON() {
86
+ return this.toString();
87
+ }
88
+ /**
89
+ * Static method to stringify an integrity metadata object.
90
+ * @param integrity The integrity metadata object to stringify.
91
+ * @returns The stringified integrity metadata.
92
+ * @example
93
+ * ```js
94
+ * IntegrityMetadata.stringify({
95
+ * alg: "sha256",
96
+ * val: "MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM=",
97
+ * }) // "sha256-MV9b23bQeMQ7isAGTkoBZGErH853yGk0W/yUx1iU7dM="
98
+ * ```
99
+ */
100
+ static stringify({ alg, val, opt = [] }) {
101
+ if (!alg) return "";
102
+ if (!val) return "";
103
+ if (!(alg in supportedHashAlgorithms)) return "";
104
+ return `${alg}-${[val, ...opt].join("?")}`;
105
+ }
106
+ }
107
+ async function createIntegrityMetadata(hashAlgorithm, data, opt = []) {
108
+ const alg = hashAlgorithm.toLowerCase();
109
+ if (!(alg in supportedHashAlgorithms)) {
110
+ return new IntegrityMetadata("");
111
+ }
112
+ const hashAlgorithmIdentifier = supportedHashAlgorithms[alg];
113
+ const arrayBuffer = await crypto.subtle.digest(hashAlgorithmIdentifier, data);
114
+ const val = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
115
+ const integrity = IntegrityMetadata.stringify({ alg, val, opt });
116
+ return new IntegrityMetadata(integrity);
117
+ }
118
+
119
+ async function createDigestSri(alg, resource, fetcher = fetch) {
120
+ if (!(alg in supportedHashAlgorithms)) {
121
+ return { id: resource.id };
122
+ }
123
+ const meta = await Promise.all(
124
+ [resource.content ?? resource.id].flat().map(async (content) => {
125
+ const res = await fetcher(content);
126
+ const data = await res.arrayBuffer();
127
+ return await createIntegrityMetadata(alg, data);
128
+ })
129
+ );
130
+ return {
131
+ id: resource.id,
132
+ digestSRI: meta.join(" ")
133
+ };
134
+ }
135
+
136
+ async function fetchAndSetDigestSri(alg, content) {
137
+ if (!content) return content;
138
+ if (typeof content.digestSRI !== "string") {
139
+ Object.assign(
140
+ content,
141
+ await createDigestSri(alg, content)
142
+ );
143
+ }
144
+ delete content.content;
145
+ return content;
146
+ }
147
+
148
+ const fetchHtmlContent = async (elements) => {
149
+ const text = elements.map((element) => element.outerHTML).join("");
150
+ return [new Response(text)];
151
+ };
152
+ const fetchTextContent = async (elements) => {
153
+ const text = elements.map((element) => element.textContent ?? "").join("");
154
+ return [new Response(text)];
155
+ };
156
+ const fetchVisibleTextContent = async (elements) => {
157
+ const text = elements.map((element) => element.innerText).join("");
158
+ return [new Response(text)];
159
+ };
160
+ const fetchExternalResource = async (elements, fetcher = fetch) => {
161
+ return await Promise.all(
162
+ elements.map(async (element) => {
163
+ const el = element;
164
+ const src = el.currentSrc || el.src;
165
+ if (!src) {
166
+ throw new Error("Element has no src or currentSrc property");
167
+ }
168
+ return await fetcher(src);
169
+ })
170
+ );
171
+ };
172
+ const selectByCss = (params) => {
173
+ return Array.from(
174
+ params.document.querySelectorAll(params.cssSelector)
175
+ );
176
+ };
177
+ const selectByIntegrity = (params) => {
178
+ return selectByCss({
179
+ ...params,
180
+ cssSelector: `[integrity=${JSON.stringify(String(params.integrity))}]`
181
+ });
182
+ };
183
+ async function createIntegrity(alg, { content = "", ...target }, doc = document) {
184
+ if (![
185
+ "TextTargetIntegrity",
186
+ "VisibleTextTargetIntegrity",
187
+ "HtmlTargetIntegrity",
188
+ "ExternalResourceTargetIntegrity"
189
+ ].includes(target.type)) {
190
+ return null;
191
+ }
192
+ if (!(alg in supportedHashAlgorithms)) {
193
+ return null;
194
+ }
195
+ if (target.type === "ExternalResourceTargetIntegrity") {
196
+ const meta2 = await Promise.all(
197
+ [content].flat().map(async (content2) => {
198
+ const res2 = URL.canParse(content2) ? await fetch(content2) : new Response(content2);
199
+ const data2 = await res2.arrayBuffer();
200
+ return await createIntegrityMetadata(alg, data2);
201
+ })
202
+ );
203
+ return {
204
+ ...target,
205
+ integrity: meta2.join(" ")
206
+ };
207
+ }
208
+ const { contentFetcher, elementSelector } = {
209
+ HtmlTargetIntegrity: {
210
+ contentFetcher: fetchHtmlContent,
211
+ elementSelector: selectByCss
212
+ },
213
+ TextTargetIntegrity: {
214
+ contentFetcher: fetchTextContent,
215
+ elementSelector: selectByCss
216
+ },
217
+ VisibleTextTargetIntegrity: {
218
+ contentFetcher: fetchVisibleTextContent,
219
+ elementSelector: selectByCss
220
+ }
221
+ }[target.type];
222
+ const elements = elementSelector({ ...target, document: doc });
223
+ if (elements.length === 0) return null;
224
+ const [res] = await contentFetcher(elements);
225
+ if (!res) return null;
226
+ const data = await res.arrayBuffer();
227
+ const meta = await createIntegrityMetadata(alg, data);
228
+ return {
229
+ ...target,
230
+ integrity: meta.toString()
231
+ };
232
+ }
233
+
234
+ class IntegrityCalculationError extends Error {
235
+ }
236
+ async function fetchAndSetTargetIntegrity(alg, obj, documentProvider = async () => document) {
237
+ const target = await Promise.all(
238
+ obj.target.map(async (raw, i) => {
239
+ if (raw.integrity) {
240
+ const { content: _, ...target3 } = raw;
241
+ return target3;
242
+ }
243
+ const doc = await documentProvider(raw);
244
+ const target2 = await createIntegrity(alg, raw, doc);
245
+ if (!target2) {
246
+ throw new IntegrityCalculationError(
247
+ `Failed to create integrity for element target[${i}].`
248
+ );
249
+ }
250
+ return target2;
251
+ })
252
+ );
253
+ return Object.assign(obj, { target });
254
+ }
255
+
256
+ async function signCa(uca, privateKey, {
257
+ alg = "ES256",
258
+ issuedAt = /* @__PURE__ */ new Date(),
259
+ expiredAt,
260
+ integrityAlg = "sha256",
261
+ documentProvider = async () => document
262
+ }) {
263
+ await fetchAndSetDigestSri(integrityAlg, uca.credentialSubject.image);
264
+ await fetchAndSetTargetIntegrity(integrityAlg, uca, documentProvider);
265
+ return await signJwtVc(uca, privateKey, { alg, issuedAt, expiredAt });
266
+ }
267
+
268
+ async function signCp(cp, privateKey, options) {
269
+ return signJwtVc(cp, privateKey, options);
270
+ }
271
+
272
+ export { IntegrityCalculationError, createDigestSri, createIntegrity, fetchAndSetDigestSri, fetchAndSetTargetIntegrity, fetchExternalResource, fetchHtmlContent, fetchTextContent, fetchVisibleTextContent, selectByCss, selectByIntegrity, signCa, signCp };
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@originator-profile/sign",
3
+ "version": "0.4.0-beta.1",
4
+ "private": false,
5
+ "type": "module",
6
+ "exports": {
7
+ "require": {
8
+ "types": "./dist/index.d.cts",
9
+ "default": "./dist/index.cjs"
10
+ },
11
+ "import": {
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "dependencies": {
20
+ "jose": "^6.0.10",
21
+ "@originator-profile/cryptography": "0.4.0-beta.1",
22
+ "@originator-profile/securing-mechanism": "0.4.0-beta.1",
23
+ "@originator-profile/model": "0.4.0-beta.1"
24
+ },
25
+ "devDependencies": {
26
+ "date-fns": "^4.1.0",
27
+ "eslint": "^9.25.1",
28
+ "pkgroll": "^2.12.2",
29
+ "typescript": "^5.8.3",
30
+ "vitest": "^4.0.0",
31
+ "websri": "^1.0.1",
32
+ "@originator-profile/tsconfig": "0.4.0-beta.1",
33
+ "eslint-config-originator-profile": "0.4.0-beta.1"
34
+ },
35
+ "scripts": {
36
+ "build": "pkgroll --clean-dist --target=node20",
37
+ "test": "vitest run",
38
+ "lint": "eslint --fix .",
39
+ "type-check": "tsc --noEmit"
40
+ }
41
+ }