@enactprotocol/trust 2.0.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/README.md +38 -0
- package/package.json +32 -0
- package/src/hash.ts +120 -0
- package/src/index.ts +28 -0
- package/src/keys.ts +157 -0
- package/src/sigstore/attestation.ts +501 -0
- package/src/sigstore/cosign.ts +564 -0
- package/src/sigstore/index.ts +139 -0
- package/src/sigstore/oauth/client.ts +89 -0
- package/src/sigstore/oauth/index.ts +95 -0
- package/src/sigstore/oauth/server.ts +163 -0
- package/src/sigstore/policy.ts +450 -0
- package/src/sigstore/signing.ts +569 -0
- package/src/sigstore/types.ts +613 -0
- package/src/sigstore/verification.ts +355 -0
- package/src/types.ts +80 -0
- package/tests/hash.test.ts +180 -0
- package/tests/index.test.ts +8 -0
- package/tests/keys.test.ts +147 -0
- package/tests/sigstore/attestation.test.ts +369 -0
- package/tests/sigstore/policy.test.ts +260 -0
- package/tests/sigstore/signing.test.ts +220 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sigstore verification module
|
|
3
|
+
*
|
|
4
|
+
* This module provides verification capabilities for Sigstore bundles and attestations.
|
|
5
|
+
* It verifies signatures, certificates, and transparency log entries.
|
|
6
|
+
*
|
|
7
|
+
* NOTE: This implementation bypasses TUF (The Update Framework) and uses bundled trusted
|
|
8
|
+
* roots directly. This is necessary for Bun compatibility because TUF verification fails
|
|
9
|
+
* with BoringSSL's stricter signature algorithm requirements.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { bundleFromJSON } from "@sigstore/bundle";
|
|
15
|
+
import { TrustedRoot } from "@sigstore/protobuf-specs";
|
|
16
|
+
import { Verifier, toSignedEntity, toTrustMaterial } from "@sigstore/verify";
|
|
17
|
+
import { extractIdentityFromBundle } from "./signing";
|
|
18
|
+
import type {
|
|
19
|
+
ExpectedIdentity,
|
|
20
|
+
OIDCIdentity,
|
|
21
|
+
SigstoreBundle,
|
|
22
|
+
VerificationDetails,
|
|
23
|
+
VerificationOptions,
|
|
24
|
+
VerificationResult,
|
|
25
|
+
} from "./types";
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Constants
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the path to bundled TUF seeds
|
|
33
|
+
* We need to navigate from the @sigstore/tuf main entry point to find seeds.json
|
|
34
|
+
*/
|
|
35
|
+
function getTufSeedsPath(): string {
|
|
36
|
+
// The package.json main points to dist/index.js, but seeds.json is at package root
|
|
37
|
+
const tufPkgPath = require.resolve("@sigstore/tuf/package.json");
|
|
38
|
+
return path.join(path.dirname(tufPkgPath), "seeds.json");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Trusted Root Management
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Load the trusted root from bundled TUF seeds
|
|
47
|
+
* This bypasses TUF's online verification which fails with BoringSSL
|
|
48
|
+
*/
|
|
49
|
+
async function loadTrustedRoot(): Promise<ReturnType<typeof TrustedRoot.fromJSON>> {
|
|
50
|
+
const seedsPath = getTufSeedsPath();
|
|
51
|
+
const seeds = JSON.parse(fs.readFileSync(seedsPath, "utf8"));
|
|
52
|
+
const seedData = seeds["https://tuf-repo-cdn.sigstore.dev"];
|
|
53
|
+
const trustedRootB64 = seedData.targets["trusted_root.json"];
|
|
54
|
+
const trustedRootJson = JSON.parse(Buffer.from(trustedRootB64, "base64").toString());
|
|
55
|
+
return TrustedRoot.fromJSON(trustedRootJson);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create trust material from the bundled trusted root
|
|
60
|
+
*/
|
|
61
|
+
async function createTrustMaterial() {
|
|
62
|
+
const trustedRoot = await loadTrustedRoot();
|
|
63
|
+
return toTrustMaterial(trustedRoot);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Verification Functions
|
|
68
|
+
// ============================================================================
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Verify a Sigstore bundle
|
|
72
|
+
*
|
|
73
|
+
* @param bundle - The Sigstore bundle to verify
|
|
74
|
+
* @param artifact - Optional artifact data (for message signature bundles)
|
|
75
|
+
* @param options - Verification options
|
|
76
|
+
* @returns Verification result with detailed checks
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* const result = await verifyBundle(bundle, artifact, {
|
|
81
|
+
* expectedIdentity: {
|
|
82
|
+
* subjectAlternativeName: "user@example.com",
|
|
83
|
+
* issuer: "https://accounts.google.com"
|
|
84
|
+
* }
|
|
85
|
+
* });
|
|
86
|
+
* if (result.verified) {
|
|
87
|
+
* console.log("Bundle verified successfully");
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export async function verifyBundle(
|
|
92
|
+
bundle: SigstoreBundle,
|
|
93
|
+
artifact?: Buffer,
|
|
94
|
+
options: VerificationOptions = {}
|
|
95
|
+
): Promise<VerificationResult> {
|
|
96
|
+
const details: VerificationDetails = {
|
|
97
|
+
signatureValid: false,
|
|
98
|
+
certificateValid: false,
|
|
99
|
+
certificateWithinValidity: false,
|
|
100
|
+
rekorEntryValid: false,
|
|
101
|
+
inclusionProofValid: false,
|
|
102
|
+
errors: [],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// Create trust material from bundled roots
|
|
107
|
+
const trustMaterial = await createTrustMaterial();
|
|
108
|
+
|
|
109
|
+
// Create verifier
|
|
110
|
+
const verifier = new Verifier(trustMaterial);
|
|
111
|
+
|
|
112
|
+
// Convert bundle to proper format
|
|
113
|
+
const parsedBundle = bundleFromJSON(bundle);
|
|
114
|
+
const signedEntity = toSignedEntity(parsedBundle, artifact);
|
|
115
|
+
|
|
116
|
+
// Perform verification
|
|
117
|
+
verifier.verify(signedEntity);
|
|
118
|
+
|
|
119
|
+
// If we get here, verification passed
|
|
120
|
+
details.signatureValid = true;
|
|
121
|
+
details.certificateValid = true;
|
|
122
|
+
details.certificateWithinValidity = true;
|
|
123
|
+
details.rekorEntryValid = true;
|
|
124
|
+
details.inclusionProofValid = true;
|
|
125
|
+
|
|
126
|
+
// Extract identity from bundle
|
|
127
|
+
const identity = extractIdentityFromBundle(bundle);
|
|
128
|
+
|
|
129
|
+
// Check identity if expected identity is provided
|
|
130
|
+
if (options.expectedIdentity) {
|
|
131
|
+
details.identityMatches = matchesExpectedIdentity(identity, options.expectedIdentity);
|
|
132
|
+
if (!details.identityMatches) {
|
|
133
|
+
details.errors.push("Identity does not match expected values");
|
|
134
|
+
const result: VerificationResult = {
|
|
135
|
+
verified: false,
|
|
136
|
+
error: "Identity mismatch",
|
|
137
|
+
details,
|
|
138
|
+
};
|
|
139
|
+
if (identity) result.identity = identity;
|
|
140
|
+
const timestamp = extractTimestampFromBundle(bundle);
|
|
141
|
+
if (timestamp) result.timestamp = timestamp;
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const result: VerificationResult = {
|
|
147
|
+
verified: true,
|
|
148
|
+
details,
|
|
149
|
+
};
|
|
150
|
+
if (identity) result.identity = identity;
|
|
151
|
+
const timestamp = extractTimestampFromBundle(bundle);
|
|
152
|
+
if (timestamp) result.timestamp = timestamp;
|
|
153
|
+
return result;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
156
|
+
details.errors.push(errorMessage);
|
|
157
|
+
|
|
158
|
+
// Try to determine which check failed based on error message
|
|
159
|
+
categorizeVerificationError(errorMessage, details);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
verified: false,
|
|
163
|
+
error: errorMessage,
|
|
164
|
+
details,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create a reusable verifier for multiple verifications
|
|
171
|
+
*
|
|
172
|
+
* @param options - Verification options
|
|
173
|
+
* @returns A verifier object that can verify multiple bundles
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* const verifier = await createBundleVerifier({
|
|
178
|
+
* expectedIdentity: { issuer: "https://accounts.google.com" }
|
|
179
|
+
* });
|
|
180
|
+
*
|
|
181
|
+
* // Verify multiple bundles efficiently
|
|
182
|
+
* for (const bundle of bundles) {
|
|
183
|
+
* verifier.verify(bundle);
|
|
184
|
+
* }
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
export async function createBundleVerifier(options: VerificationOptions = {}) {
|
|
188
|
+
// Create trust material once and reuse
|
|
189
|
+
const trustMaterial = await createTrustMaterial();
|
|
190
|
+
const verifier = new Verifier(trustMaterial);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
/**
|
|
194
|
+
* Verify a bundle using the cached verifier
|
|
195
|
+
*/
|
|
196
|
+
verify: async (bundle: SigstoreBundle, artifact?: Buffer): Promise<VerificationResult> => {
|
|
197
|
+
const details: VerificationDetails = {
|
|
198
|
+
signatureValid: false,
|
|
199
|
+
certificateValid: false,
|
|
200
|
+
certificateWithinValidity: false,
|
|
201
|
+
rekorEntryValid: false,
|
|
202
|
+
inclusionProofValid: false,
|
|
203
|
+
errors: [],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Convert bundle to proper format
|
|
208
|
+
const parsedBundle = bundleFromJSON(bundle);
|
|
209
|
+
const signedEntity = toSignedEntity(parsedBundle, artifact);
|
|
210
|
+
|
|
211
|
+
// Perform verification
|
|
212
|
+
verifier.verify(signedEntity);
|
|
213
|
+
|
|
214
|
+
details.signatureValid = true;
|
|
215
|
+
details.certificateValid = true;
|
|
216
|
+
details.certificateWithinValidity = true;
|
|
217
|
+
details.rekorEntryValid = true;
|
|
218
|
+
details.inclusionProofValid = true;
|
|
219
|
+
|
|
220
|
+
const identity = extractIdentityFromBundle(bundle);
|
|
221
|
+
|
|
222
|
+
if (options.expectedIdentity) {
|
|
223
|
+
details.identityMatches = matchesExpectedIdentity(identity, options.expectedIdentity);
|
|
224
|
+
if (!details.identityMatches) {
|
|
225
|
+
details.errors.push("Identity does not match expected values");
|
|
226
|
+
const result: VerificationResult = {
|
|
227
|
+
verified: false,
|
|
228
|
+
error: "Identity mismatch",
|
|
229
|
+
details,
|
|
230
|
+
};
|
|
231
|
+
if (identity) result.identity = identity;
|
|
232
|
+
const timestamp = extractTimestampFromBundle(bundle);
|
|
233
|
+
if (timestamp) result.timestamp = timestamp;
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const result: VerificationResult = {
|
|
239
|
+
verified: true,
|
|
240
|
+
details,
|
|
241
|
+
};
|
|
242
|
+
if (identity) result.identity = identity;
|
|
243
|
+
const timestamp = extractTimestampFromBundle(bundle);
|
|
244
|
+
if (timestamp) result.timestamp = timestamp;
|
|
245
|
+
return result;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
248
|
+
details.errors.push(errorMessage);
|
|
249
|
+
categorizeVerificationError(errorMessage, details);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
verified: false,
|
|
253
|
+
error: errorMessage,
|
|
254
|
+
details,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Quick verification check - returns boolean only
|
|
263
|
+
*
|
|
264
|
+
* @param bundle - The Sigstore bundle to verify
|
|
265
|
+
* @param artifact - Optional artifact data
|
|
266
|
+
* @returns True if verification passes, false otherwise
|
|
267
|
+
*/
|
|
268
|
+
export async function isVerified(bundle: SigstoreBundle, artifact?: Buffer): Promise<boolean> {
|
|
269
|
+
try {
|
|
270
|
+
const trustMaterial = await createTrustMaterial();
|
|
271
|
+
const verifier = new Verifier(trustMaterial);
|
|
272
|
+
const parsedBundle = bundleFromJSON(bundle);
|
|
273
|
+
const signedEntity = toSignedEntity(parsedBundle, artifact);
|
|
274
|
+
verifier.verify(signedEntity);
|
|
275
|
+
return true;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ============================================================================
|
|
282
|
+
// Helper Functions
|
|
283
|
+
// ============================================================================
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if an identity matches expected values
|
|
287
|
+
*/
|
|
288
|
+
function matchesExpectedIdentity(
|
|
289
|
+
identity: OIDCIdentity | undefined,
|
|
290
|
+
expected: ExpectedIdentity
|
|
291
|
+
): boolean {
|
|
292
|
+
if (!identity) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Check issuer
|
|
297
|
+
if (expected.issuer && identity.issuer !== expected.issuer) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Check subject alternative name (could be email or URI)
|
|
302
|
+
if (expected.subjectAlternativeName) {
|
|
303
|
+
const san = expected.subjectAlternativeName;
|
|
304
|
+
if (identity.email !== san && identity.subject !== san) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check GitHub workflow repository
|
|
310
|
+
if (expected.workflowRepository && identity.workflowRepository !== expected.workflowRepository) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Check GitHub workflow ref
|
|
315
|
+
if (expected.workflowRef && identity.workflowRef !== expected.workflowRef) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Extract timestamp from a Sigstore bundle
|
|
324
|
+
*/
|
|
325
|
+
function extractTimestampFromBundle(bundle: SigstoreBundle): Date | undefined {
|
|
326
|
+
// Try to get timestamp from transparency log entry
|
|
327
|
+
const tlogEntry = bundle.verificationMaterial?.tlogEntries?.[0];
|
|
328
|
+
if (tlogEntry?.integratedTime) {
|
|
329
|
+
const timestamp = Number.parseInt(tlogEntry.integratedTime, 10);
|
|
330
|
+
if (!Number.isNaN(timestamp)) {
|
|
331
|
+
return new Date(timestamp * 1000);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Categorize a verification error to update details
|
|
340
|
+
*/
|
|
341
|
+
function categorizeVerificationError(errorMessage: string, details: VerificationDetails): void {
|
|
342
|
+
const lowerError = errorMessage.toLowerCase();
|
|
343
|
+
|
|
344
|
+
if (lowerError.includes("signature")) {
|
|
345
|
+
details.signatureValid = false;
|
|
346
|
+
} else if (lowerError.includes("certificate") && lowerError.includes("expired")) {
|
|
347
|
+
details.certificateWithinValidity = false;
|
|
348
|
+
} else if (lowerError.includes("certificate")) {
|
|
349
|
+
details.certificateValid = false;
|
|
350
|
+
} else if (lowerError.includes("rekor") || lowerError.includes("transparency")) {
|
|
351
|
+
details.rekorEntryValid = false;
|
|
352
|
+
} else if (lowerError.includes("inclusion") || lowerError.includes("proof")) {
|
|
353
|
+
details.inclusionProofValid = false;
|
|
354
|
+
}
|
|
355
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for security operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Hash Types
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Supported hash algorithms
|
|
11
|
+
*/
|
|
12
|
+
export type HashAlgorithm = "sha256" | "sha512";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Result of a hash operation
|
|
16
|
+
*/
|
|
17
|
+
export interface HashResult {
|
|
18
|
+
/** The hash algorithm used */
|
|
19
|
+
algorithm: HashAlgorithm;
|
|
20
|
+
/** The hash digest in hexadecimal format */
|
|
21
|
+
digest: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Options for file hashing operations
|
|
26
|
+
*/
|
|
27
|
+
export interface FileHashOptions {
|
|
28
|
+
/** Hash algorithm to use (default: sha256) */
|
|
29
|
+
algorithm?: HashAlgorithm;
|
|
30
|
+
/** Progress callback for large files */
|
|
31
|
+
onProgress?: (bytesRead: number, totalBytes: number) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Key Management Types
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Supported key types
|
|
40
|
+
*/
|
|
41
|
+
export type KeyType = "rsa" | "ed25519" | "ecdsa";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Key format for storage
|
|
45
|
+
*/
|
|
46
|
+
export type KeyFormat = "pem" | "der" | "jwk";
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A cryptographic key pair
|
|
50
|
+
*/
|
|
51
|
+
export interface KeyPair {
|
|
52
|
+
/** Public key */
|
|
53
|
+
publicKey: string;
|
|
54
|
+
/** Private key (encrypted or plain) */
|
|
55
|
+
privateKey: string;
|
|
56
|
+
/** Key type */
|
|
57
|
+
type: KeyType;
|
|
58
|
+
/** Key format */
|
|
59
|
+
format: KeyFormat;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Options for key generation
|
|
64
|
+
*/
|
|
65
|
+
export interface KeyGenerationOptions {
|
|
66
|
+
/** Key type to generate */
|
|
67
|
+
type: KeyType;
|
|
68
|
+
/** Output format */
|
|
69
|
+
format?: KeyFormat;
|
|
70
|
+
/** RSA key size in bits (only for RSA keys) */
|
|
71
|
+
modulusLength?: number;
|
|
72
|
+
/** Passphrase for encrypting private key */
|
|
73
|
+
passphrase?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Placeholder for other types
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
export type SecurityConfig = Record<string, unknown>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { hashBuffer, hashContent, hashFile } from "../src/hash";
|
|
5
|
+
|
|
6
|
+
const TEST_DIR = join(import.meta.dir, "fixtures");
|
|
7
|
+
const TEST_FILE = join(TEST_DIR, "test.txt");
|
|
8
|
+
const LARGE_FILE = join(TEST_DIR, "large.txt");
|
|
9
|
+
const BINARY_FILE = join(TEST_DIR, "binary.dat");
|
|
10
|
+
|
|
11
|
+
describe("hash utilities", () => {
|
|
12
|
+
beforeAll(() => {
|
|
13
|
+
// Create test directory and files
|
|
14
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
15
|
+
writeFileSync(TEST_FILE, "hello world");
|
|
16
|
+
|
|
17
|
+
// Create a larger file for streaming tests
|
|
18
|
+
writeFileSync(LARGE_FILE, "x".repeat(1024 * 1024)); // 1MB file
|
|
19
|
+
|
|
20
|
+
// Create a binary file
|
|
21
|
+
const binaryData = Buffer.from([0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd]);
|
|
22
|
+
writeFileSync(BINARY_FILE, binaryData);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(() => {
|
|
26
|
+
// Clean up test files
|
|
27
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("hashContent", () => {
|
|
31
|
+
test("hashes string content with sha256", () => {
|
|
32
|
+
const result = hashContent("hello world");
|
|
33
|
+
|
|
34
|
+
expect(result.algorithm).toBe("sha256");
|
|
35
|
+
expect(result.digest).toBe(
|
|
36
|
+
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("hashes string content with sha512", () => {
|
|
41
|
+
const result = hashContent("hello world", "sha512");
|
|
42
|
+
|
|
43
|
+
expect(result.algorithm).toBe("sha512");
|
|
44
|
+
expect(result.digest).toBe(
|
|
45
|
+
"309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f"
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("hashes buffer content", () => {
|
|
50
|
+
const buffer = Buffer.from("hello world");
|
|
51
|
+
const result = hashContent(buffer);
|
|
52
|
+
|
|
53
|
+
expect(result.digest).toBe(
|
|
54
|
+
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("produces consistent hashes for same content", () => {
|
|
59
|
+
const result1 = hashContent("test content");
|
|
60
|
+
const result2 = hashContent("test content");
|
|
61
|
+
|
|
62
|
+
expect(result1.digest).toBe(result2.digest);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("produces different hashes for different content", () => {
|
|
66
|
+
const result1 = hashContent("content 1");
|
|
67
|
+
const result2 = hashContent("content 2");
|
|
68
|
+
|
|
69
|
+
expect(result1.digest).not.toBe(result2.digest);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("handles empty string", () => {
|
|
73
|
+
const result = hashContent("");
|
|
74
|
+
|
|
75
|
+
expect(result.digest).toBe(
|
|
76
|
+
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("handles unicode content", () => {
|
|
81
|
+
const result = hashContent("Hello δΈη π");
|
|
82
|
+
|
|
83
|
+
expect(result.algorithm).toBe("sha256");
|
|
84
|
+
expect(result.digest).toHaveLength(64); // sha256 produces 64 hex chars
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("hashBuffer", () => {
|
|
89
|
+
test("hashes buffer data", () => {
|
|
90
|
+
const buffer = Buffer.from("hello world");
|
|
91
|
+
const result = hashBuffer(buffer);
|
|
92
|
+
|
|
93
|
+
expect(result.digest).toBe(
|
|
94
|
+
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("hashes binary data", () => {
|
|
99
|
+
const buffer = Buffer.from([0x00, 0x01, 0x02, 0xff]);
|
|
100
|
+
const result = hashBuffer(buffer);
|
|
101
|
+
|
|
102
|
+
expect(result.algorithm).toBe("sha256");
|
|
103
|
+
expect(result.digest).toHaveLength(64);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("supports sha512 algorithm", () => {
|
|
107
|
+
const buffer = Buffer.from("test");
|
|
108
|
+
const result = hashBuffer(buffer, "sha512");
|
|
109
|
+
|
|
110
|
+
expect(result.algorithm).toBe("sha512");
|
|
111
|
+
expect(result.digest).toHaveLength(128); // sha512 produces 128 hex chars
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("hashFile", () => {
|
|
116
|
+
test("hashes file content", async () => {
|
|
117
|
+
const result = await hashFile(TEST_FILE);
|
|
118
|
+
|
|
119
|
+
expect(result.algorithm).toBe("sha256");
|
|
120
|
+
expect(result.digest).toBe(
|
|
121
|
+
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("hashes file with sha512", async () => {
|
|
126
|
+
const result = await hashFile(TEST_FILE, { algorithm: "sha512" });
|
|
127
|
+
|
|
128
|
+
expect(result.algorithm).toBe("sha512");
|
|
129
|
+
expect(result.digest).toHaveLength(128);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("hashes large file with streaming", async () => {
|
|
133
|
+
const result = await hashFile(LARGE_FILE);
|
|
134
|
+
|
|
135
|
+
expect(result.algorithm).toBe("sha256");
|
|
136
|
+
expect(result.digest).toHaveLength(64);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("hashes binary file", async () => {
|
|
140
|
+
const result = await hashFile(BINARY_FILE);
|
|
141
|
+
|
|
142
|
+
expect(result.algorithm).toBe("sha256");
|
|
143
|
+
expect(result.digest).toHaveLength(64);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("calls progress callback during hashing", async () => {
|
|
147
|
+
const progressCalls: Array<{ read: number; total: number }> = [];
|
|
148
|
+
|
|
149
|
+
await hashFile(LARGE_FILE, {
|
|
150
|
+
onProgress: (bytesRead, totalBytes) => {
|
|
151
|
+
progressCalls.push({ read: bytesRead, total: totalBytes });
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(progressCalls.length).toBeGreaterThan(0);
|
|
156
|
+
|
|
157
|
+
// Last call should have read all bytes
|
|
158
|
+
const lastCall = progressCalls[progressCalls.length - 1];
|
|
159
|
+
if (lastCall) {
|
|
160
|
+
expect(lastCall.read).toBe(lastCall.total);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("throws error for non-existent file", async () => {
|
|
165
|
+
await expect(hashFile("/nonexistent/file.txt")).rejects.toThrow("Failed to access file");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("throws error for directory path", async () => {
|
|
169
|
+
await expect(hashFile(TEST_DIR)).rejects.toThrow("Path is not a file");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("produces same hash as hashContent for same data", async () => {
|
|
173
|
+
const content = "hello world";
|
|
174
|
+
const contentResult = hashContent(content);
|
|
175
|
+
const fileResult = await hashFile(TEST_FILE);
|
|
176
|
+
|
|
177
|
+
expect(fileResult.digest).toBe(contentResult.digest);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|