@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
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @enactprotocol/trust
|
|
2
|
+
|
|
3
|
+
Sigstore integration, attestations, and verification for Enact.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides:
|
|
8
|
+
- Sigstore integration for keyless signing and verification
|
|
9
|
+
- Attestation generation and validation (in-toto, SLSA)
|
|
10
|
+
- Bundle signature verification
|
|
11
|
+
- Certificate chain validation
|
|
12
|
+
- Trust policy enforcement
|
|
13
|
+
|
|
14
|
+
## Status
|
|
15
|
+
|
|
16
|
+
Currently in Phase 1 (scaffolding). Full implementation will be completed in Phase 2.
|
|
17
|
+
|
|
18
|
+
## Development
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Build
|
|
22
|
+
bun run build
|
|
23
|
+
|
|
24
|
+
# Test
|
|
25
|
+
bun test
|
|
26
|
+
|
|
27
|
+
# Type check
|
|
28
|
+
bun run typecheck
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Planned Features (Phase 2)
|
|
32
|
+
|
|
33
|
+
- [ ] Hash utilities with streaming support
|
|
34
|
+
- [ ] Sigstore keyless signing via OIDC
|
|
35
|
+
- [ ] Bundle verification with certificate chain validation
|
|
36
|
+
- [ ] In-toto attestation format support
|
|
37
|
+
- [ ] SLSA provenance support
|
|
38
|
+
- [ ] Comprehensive test coverage
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@enactprotocol/trust",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Sigstore integration, attestations, and verification for Enact",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc --build",
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"open": "^11.0.0",
|
|
22
|
+
"openid-client": "5",
|
|
23
|
+
"sigstore": "^4.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^22.10.1",
|
|
27
|
+
"typescript": "^5.7.2"
|
|
28
|
+
},
|
|
29
|
+
"keywords": ["cryptography", "sigstore", "attestation", "verification"],
|
|
30
|
+
"author": "Enact Protocol",
|
|
31
|
+
"license": "Apache-2.0"
|
|
32
|
+
}
|
package/src/hash.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash utilities for content integrity verification
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { type Hash, createHash } from "node:crypto";
|
|
6
|
+
import { createReadStream, statSync } from "node:fs";
|
|
7
|
+
import type { FileHashOptions, HashAlgorithm, HashResult } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Hash a string or buffer using the specified algorithm
|
|
11
|
+
*
|
|
12
|
+
* @param content - The content to hash
|
|
13
|
+
* @param algorithm - The hash algorithm to use (default: sha256)
|
|
14
|
+
* @returns Hash result with algorithm and digest
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const result = hashContent("hello world");
|
|
19
|
+
* console.log(result.digest); // "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function hashContent(
|
|
23
|
+
content: string | Buffer,
|
|
24
|
+
algorithm: HashAlgorithm = "sha256"
|
|
25
|
+
): HashResult {
|
|
26
|
+
const hash: Hash = createHash(algorithm);
|
|
27
|
+
hash.update(content);
|
|
28
|
+
const digest = hash.digest("hex");
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
algorithm,
|
|
32
|
+
digest,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Hash a buffer directly
|
|
38
|
+
*
|
|
39
|
+
* @param buffer - The buffer to hash
|
|
40
|
+
* @param algorithm - The hash algorithm to use (default: sha256)
|
|
41
|
+
* @returns Hash result with algorithm and digest
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const buffer = Buffer.from("hello world");
|
|
46
|
+
* const result = hashBuffer(buffer);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function hashBuffer(buffer: Buffer, algorithm: HashAlgorithm = "sha256"): HashResult {
|
|
50
|
+
return hashContent(buffer, algorithm);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hash a file using streaming to support large files
|
|
55
|
+
*
|
|
56
|
+
* @param filePath - Path to the file to hash
|
|
57
|
+
* @param options - Hashing options including algorithm and progress callback
|
|
58
|
+
* @returns Promise resolving to hash result
|
|
59
|
+
*
|
|
60
|
+
* @throws Error if file doesn't exist or cannot be read
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const result = await hashFile("/path/to/file.txt", {
|
|
65
|
+
* algorithm: "sha256",
|
|
66
|
+
* onProgress: (read, total) => {
|
|
67
|
+
* console.log(`${(read / total * 100).toFixed(1)}% complete`);
|
|
68
|
+
* }
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export async function hashFile(
|
|
73
|
+
filePath: string,
|
|
74
|
+
options: FileHashOptions = {}
|
|
75
|
+
): Promise<HashResult> {
|
|
76
|
+
const { algorithm = "sha256", onProgress } = options;
|
|
77
|
+
|
|
78
|
+
// Validate file exists and get size
|
|
79
|
+
let fileSize: number;
|
|
80
|
+
try {
|
|
81
|
+
const stats = statSync(filePath);
|
|
82
|
+
if (!stats.isFile()) {
|
|
83
|
+
throw new Error(`Path is not a file: ${filePath}`);
|
|
84
|
+
}
|
|
85
|
+
fileSize = stats.size;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof Error) {
|
|
88
|
+
throw new Error(`Failed to access file: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const hash: Hash = createHash(algorithm);
|
|
95
|
+
const stream = createReadStream(filePath);
|
|
96
|
+
let bytesRead = 0;
|
|
97
|
+
|
|
98
|
+
stream.on("data", (chunk: string | Buffer) => {
|
|
99
|
+
hash.update(chunk);
|
|
100
|
+
const chunkSize = typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
|
|
101
|
+
bytesRead += chunkSize;
|
|
102
|
+
|
|
103
|
+
if (onProgress) {
|
|
104
|
+
onProgress(bytesRead, fileSize);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
stream.on("end", () => {
|
|
109
|
+
const digest = hash.digest("hex");
|
|
110
|
+
resolve({
|
|
111
|
+
algorithm,
|
|
112
|
+
digest,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
stream.on("error", (error: Error) => {
|
|
117
|
+
reject(new Error(`Failed to read file: ${error.message}`));
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @enactprotocol/trust
|
|
3
|
+
*
|
|
4
|
+
* Sigstore integration, attestation generation/verification,
|
|
5
|
+
* and trust system for Enact.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const version = "0.1.0";
|
|
9
|
+
|
|
10
|
+
// Hash utilities
|
|
11
|
+
export { hashContent, hashBuffer, hashFile } from "./hash";
|
|
12
|
+
|
|
13
|
+
// Key management
|
|
14
|
+
export { generateKeyPair, isValidPEMKey, getKeyTypeFromPEM } from "./keys";
|
|
15
|
+
|
|
16
|
+
// Sigstore integration (attestation signing, verification, trust policies)
|
|
17
|
+
export * from "./sigstore";
|
|
18
|
+
|
|
19
|
+
// TypeScript types for hash/key operations
|
|
20
|
+
export type {
|
|
21
|
+
HashAlgorithm,
|
|
22
|
+
HashResult,
|
|
23
|
+
FileHashOptions,
|
|
24
|
+
KeyType,
|
|
25
|
+
KeyFormat,
|
|
26
|
+
KeyPair,
|
|
27
|
+
KeyGenerationOptions,
|
|
28
|
+
} from "./types";
|
package/src/keys.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key management utilities for cryptographic operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
6
|
+
import type { KeyGenerationOptions, KeyPair, KeyType } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a new cryptographic key pair
|
|
10
|
+
*
|
|
11
|
+
* @param options - Key generation options
|
|
12
|
+
* @returns Generated key pair with public and private keys
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* // Generate RSA key pair
|
|
17
|
+
* const rsaKeys = generateKeyPair({
|
|
18
|
+
* type: "rsa",
|
|
19
|
+
* modulusLength: 2048
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Generate Ed25519 key pair
|
|
23
|
+
* const ed25519Keys = generateKeyPair({
|
|
24
|
+
* type: "ed25519"
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function generateKeyPair(options: KeyGenerationOptions): KeyPair {
|
|
29
|
+
const { type, format = "pem", modulusLength = 2048, passphrase } = options;
|
|
30
|
+
|
|
31
|
+
const keyPairOptions = getKeyPairOptions(type, modulusLength, format, passphrase);
|
|
32
|
+
const nodeKeyType = getNodeKeyType(type);
|
|
33
|
+
|
|
34
|
+
// TypeScript has very strict overloads for generateKeyPairSync
|
|
35
|
+
// We use any here as we've validated the types through our own KeyType
|
|
36
|
+
// biome-ignore lint/suspicious/noExplicitAny: Node.js crypto API has complex overloads
|
|
37
|
+
const { publicKey, privateKey } = generateKeyPairSync(nodeKeyType as any, keyPairOptions as any);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
publicKey: publicKey.toString(),
|
|
41
|
+
privateKey: privateKey.toString(),
|
|
42
|
+
type,
|
|
43
|
+
format,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert our KeyType to Node.js crypto key type
|
|
49
|
+
*/
|
|
50
|
+
function getNodeKeyType(type: KeyType): "rsa" | "ed25519" | "ec" {
|
|
51
|
+
switch (type) {
|
|
52
|
+
case "rsa":
|
|
53
|
+
return "rsa";
|
|
54
|
+
case "ed25519":
|
|
55
|
+
return "ed25519";
|
|
56
|
+
case "ecdsa":
|
|
57
|
+
return "ec";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get key pair generation options for Node.js crypto
|
|
63
|
+
*/
|
|
64
|
+
function getKeyPairOptions(
|
|
65
|
+
type: KeyType,
|
|
66
|
+
modulusLength: number,
|
|
67
|
+
format: "pem" | "der" | "jwk",
|
|
68
|
+
passphrase?: string
|
|
69
|
+
): Parameters<typeof generateKeyPairSync>[1] {
|
|
70
|
+
const baseOptions: Record<string, unknown> = {
|
|
71
|
+
publicKeyEncoding: {
|
|
72
|
+
type: "spki",
|
|
73
|
+
format,
|
|
74
|
+
},
|
|
75
|
+
privateKeyEncoding: {
|
|
76
|
+
type: "pkcs8",
|
|
77
|
+
format,
|
|
78
|
+
...(passphrase && {
|
|
79
|
+
cipher: "aes-256-cbc",
|
|
80
|
+
passphrase,
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Add type-specific options
|
|
86
|
+
if (type === "rsa") {
|
|
87
|
+
return {
|
|
88
|
+
...baseOptions,
|
|
89
|
+
modulusLength,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (type === "ecdsa") {
|
|
94
|
+
return {
|
|
95
|
+
...baseOptions,
|
|
96
|
+
namedCurve: "prime256v1", // Also known as secp256r1 or P-256
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Ed25519 doesn't need additional options
|
|
101
|
+
return baseOptions;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate a PEM-formatted key
|
|
106
|
+
*
|
|
107
|
+
* @param key - The key to validate (public or private)
|
|
108
|
+
* @param expectedType - Expected key type (public or private)
|
|
109
|
+
* @returns True if key is valid PEM format
|
|
110
|
+
*/
|
|
111
|
+
export function isValidPEMKey(
|
|
112
|
+
key: string,
|
|
113
|
+
expectedType: "public" | "private" = "private"
|
|
114
|
+
): boolean {
|
|
115
|
+
if (expectedType === "public") {
|
|
116
|
+
return key.includes("-----BEGIN PUBLIC KEY-----") && key.includes("-----END PUBLIC KEY-----");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
(key.includes("-----BEGIN PRIVATE KEY-----") && key.includes("-----END PRIVATE KEY-----")) ||
|
|
121
|
+
(key.includes("-----BEGIN ENCRYPTED PRIVATE KEY-----") &&
|
|
122
|
+
key.includes("-----END ENCRYPTED PRIVATE KEY-----"))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parse key type from a PEM key string
|
|
128
|
+
*
|
|
129
|
+
* @param key - PEM-formatted key string
|
|
130
|
+
* @returns Detected key type or undefined if cannot be determined
|
|
131
|
+
*/
|
|
132
|
+
export function getKeyTypeFromPEM(key: string): KeyType | undefined {
|
|
133
|
+
// Check if it's a valid PEM key first
|
|
134
|
+
if (!isValidPEMKey(key, "private") && !isValidPEMKey(key, "public")) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check for explicit algorithm markers
|
|
139
|
+
if (key.includes("RSA")) {
|
|
140
|
+
return "rsa";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Length-based heuristic (RSA keys are much longer)
|
|
144
|
+
// RSA 2048: ~1700 chars, RSA 4096: ~3200 chars
|
|
145
|
+
// Ed25519: ~120 chars
|
|
146
|
+
// ECDSA P-256: ~200-300 chars
|
|
147
|
+
if (key.length > 1000) {
|
|
148
|
+
return "rsa";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (key.length < 200) {
|
|
152
|
+
return "ed25519";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ECDSA falls somewhere in between
|
|
156
|
+
return "ecdsa";
|
|
157
|
+
}
|