@attest-it/core 0.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 +126 -0
- package/dist/chunk-UWYR7JNE.js +212 -0
- package/dist/chunk-UWYR7JNE.js.map +1 -0
- package/dist/core-alpha.d.ts +711 -0
- package/dist/core-beta.d.ts +711 -0
- package/dist/core-public.d.ts +711 -0
- package/dist/core-unstripped.d.ts +711 -0
- package/dist/crypto-ITLMIMRJ.js +3 -0
- package/dist/crypto-ITLMIMRJ.js.map +1 -0
- package/dist/index.cjs +915 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +691 -0
- package/dist/index.d.ts +691 -0
- package/dist/index.js +629 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @attest-it/core
|
|
2
|
+
|
|
3
|
+
Core library for the attest-it human-gated test attestation system.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides the core business logic for attest-it:
|
|
8
|
+
|
|
9
|
+
- Configuration loading and validation
|
|
10
|
+
- Fingerprint computation for test files and packages
|
|
11
|
+
- Attestation file reading and writing with signing
|
|
12
|
+
- Cryptographic key generation and verification (via OpenSSL)
|
|
13
|
+
- Verification logic for CI pipelines
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @attest-it/core
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Most users should install the `attest-it` umbrella package instead, which includes both this core library and the CLI.
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Loading Configuration
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import { loadConfig, findConfigPath } from '@attest-it/core'
|
|
29
|
+
|
|
30
|
+
const configPath = await findConfigPath('/path/to/repo')
|
|
31
|
+
const config = await loadConfig(configPath)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Computing Fingerprints
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { computeFingerprint } from '@attest-it/core'
|
|
38
|
+
|
|
39
|
+
const result = await computeFingerprint({
|
|
40
|
+
packages: ['packages/my-app'],
|
|
41
|
+
basedir: '/path/to/repo',
|
|
42
|
+
ignore: ['**/*.test.ts'],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
console.log(result.fingerprint) // "sha256:abc123..."
|
|
46
|
+
console.log(result.fileCount) // 42
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Working with Attestations
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import {
|
|
53
|
+
readAndVerifyAttestations,
|
|
54
|
+
writeSignedAttestations,
|
|
55
|
+
createAttestation,
|
|
56
|
+
upsertAttestation,
|
|
57
|
+
} from '@attest-it/core'
|
|
58
|
+
|
|
59
|
+
// Read and verify existing attestations
|
|
60
|
+
const { attestations } = await readAndVerifyAttestations({
|
|
61
|
+
filepath: '.attest-it/attestations.json',
|
|
62
|
+
publicKeyPath: '.attest-it/pubkey.pem',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Create a new attestation
|
|
66
|
+
const newAttestation = createAttestation({
|
|
67
|
+
suite: 'desktop-tests',
|
|
68
|
+
fingerprint: 'sha256:abc123...',
|
|
69
|
+
command: 'pnpm vitest --project desktop',
|
|
70
|
+
exitCode: 0,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Add to attestations and save
|
|
74
|
+
const updated = upsertAttestation(attestations, newAttestation)
|
|
75
|
+
await writeSignedAttestations({
|
|
76
|
+
filepath: '.attest-it/attestations.json',
|
|
77
|
+
attestations: updated,
|
|
78
|
+
privateKeyPath: '~/.config/attest-it/key.pem',
|
|
79
|
+
})
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Verification
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { verifyAttestations } from '@attest-it/core'
|
|
86
|
+
|
|
87
|
+
const result = await verifyAttestations({
|
|
88
|
+
config,
|
|
89
|
+
repoRoot: '/path/to/repo',
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if (result.success) {
|
|
93
|
+
console.log('All attestations valid')
|
|
94
|
+
} else {
|
|
95
|
+
for (const suite of result.suites) {
|
|
96
|
+
if (suite.status !== 'VALID') {
|
|
97
|
+
console.log(`${suite.suite}: ${suite.status} - ${suite.message}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Key Generation
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { generateKeyPair, getDefaultPrivateKeyPath } from '@attest-it/core'
|
|
107
|
+
|
|
108
|
+
const paths = await generateKeyPair({
|
|
109
|
+
algorithm: 'ed25519',
|
|
110
|
+
publicPath: '.attest-it/pubkey.pem',
|
|
111
|
+
privatePath: getDefaultPrivateKeyPath(),
|
|
112
|
+
})
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## API Documentation
|
|
116
|
+
|
|
117
|
+
See the [API documentation](../../docs/api/core.md) for complete type definitions and function signatures.
|
|
118
|
+
|
|
119
|
+
## Requirements
|
|
120
|
+
|
|
121
|
+
- Node.js 20+
|
|
122
|
+
- OpenSSL (for cryptographic operations)
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
|
|
6
|
+
// src/crypto.ts
|
|
7
|
+
async function runOpenSSL(args, stdin) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const child = spawn("openssl", args, {
|
|
10
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
11
|
+
});
|
|
12
|
+
const stdoutChunks = [];
|
|
13
|
+
let stderr = "";
|
|
14
|
+
child.stdout.on("data", (chunk) => {
|
|
15
|
+
stdoutChunks.push(chunk);
|
|
16
|
+
});
|
|
17
|
+
child.stderr.on("data", (chunk) => {
|
|
18
|
+
stderr += chunk.toString();
|
|
19
|
+
});
|
|
20
|
+
child.on("error", (err) => {
|
|
21
|
+
reject(new Error(`Failed to spawn OpenSSL: ${err.message}`));
|
|
22
|
+
});
|
|
23
|
+
child.on("close", (code) => {
|
|
24
|
+
resolve({
|
|
25
|
+
exitCode: code ?? 1,
|
|
26
|
+
stdout: Buffer.concat(stdoutChunks),
|
|
27
|
+
stderr
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
child.stdin.end();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async function checkOpenSSL() {
|
|
34
|
+
const result = await runOpenSSL(["version"]);
|
|
35
|
+
if (result.exitCode !== 0) {
|
|
36
|
+
throw new Error(`OpenSSL check failed: ${result.stderr}`);
|
|
37
|
+
}
|
|
38
|
+
return result.stdout.toString().trim();
|
|
39
|
+
}
|
|
40
|
+
var openSSLChecked = false;
|
|
41
|
+
async function ensureOpenSSLAvailable() {
|
|
42
|
+
if (openSSLChecked) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await checkOpenSSL();
|
|
47
|
+
openSSLChecked = true;
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error(
|
|
50
|
+
"OpenSSL is not installed or not in PATH. Please install OpenSSL to use attest-it. On macOS: brew install openssl. On Ubuntu: apt-get install openssl"
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function getDefaultPrivateKeyPath() {
|
|
55
|
+
const homeDir = os.homedir();
|
|
56
|
+
if (process.platform === "win32") {
|
|
57
|
+
const appData = process.env.APPDATA ?? path.join(homeDir, "AppData", "Roaming");
|
|
58
|
+
return path.join(appData, "attest-it", "private.pem");
|
|
59
|
+
}
|
|
60
|
+
return path.join(homeDir, ".config", "attest-it", "private.pem");
|
|
61
|
+
}
|
|
62
|
+
function getDefaultPublicKeyPath() {
|
|
63
|
+
return path.join(process.cwd(), "attest-it-public.pem");
|
|
64
|
+
}
|
|
65
|
+
async function ensureDir(dirPath) {
|
|
66
|
+
try {
|
|
67
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
if (err instanceof Error && "code" in err && err.code !== "EEXIST") {
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function fileExists(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
await fs.access(filePath);
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function cleanupFiles(...paths) {
|
|
83
|
+
for (const filePath of paths) {
|
|
84
|
+
try {
|
|
85
|
+
await fs.unlink(filePath);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function generateKeyPair(options = {}) {
|
|
91
|
+
await ensureOpenSSLAvailable();
|
|
92
|
+
const {
|
|
93
|
+
algorithm = "ed25519",
|
|
94
|
+
privatePath = getDefaultPrivateKeyPath(),
|
|
95
|
+
publicPath = getDefaultPublicKeyPath(),
|
|
96
|
+
force = false
|
|
97
|
+
} = options;
|
|
98
|
+
const privateExists = await fileExists(privatePath);
|
|
99
|
+
const publicExists = await fileExists(publicPath);
|
|
100
|
+
if ((privateExists || publicExists) && !force) {
|
|
101
|
+
const existing = [privateExists ? privatePath : null, publicExists ? publicPath : null].filter(
|
|
102
|
+
Boolean
|
|
103
|
+
);
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Key files already exist: ${existing.join(", ")}. Use force: true to overwrite.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
await ensureDir(path.dirname(privatePath));
|
|
109
|
+
await ensureDir(path.dirname(publicPath));
|
|
110
|
+
try {
|
|
111
|
+
const genArgs = algorithm === "ed25519" ? ["genpkey", "-algorithm", "Ed25519", "-out", privatePath] : ["genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:2048", "-out", privatePath];
|
|
112
|
+
const genResult = await runOpenSSL(genArgs);
|
|
113
|
+
if (genResult.exitCode !== 0) {
|
|
114
|
+
throw new Error(`Failed to generate private key: ${genResult.stderr}`);
|
|
115
|
+
}
|
|
116
|
+
await setKeyPermissions(privatePath);
|
|
117
|
+
const pubResult = await runOpenSSL(["pkey", "-in", privatePath, "-pubout", "-out", publicPath]);
|
|
118
|
+
if (pubResult.exitCode !== 0) {
|
|
119
|
+
throw new Error(`Failed to extract public key: ${pubResult.stderr}`);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
privatePath,
|
|
123
|
+
publicPath
|
|
124
|
+
};
|
|
125
|
+
} catch (err) {
|
|
126
|
+
await cleanupFiles(privatePath, publicPath);
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async function sign(options) {
|
|
131
|
+
await ensureOpenSSLAvailable();
|
|
132
|
+
const { privateKeyPath, data } = options;
|
|
133
|
+
if (!await fileExists(privateKeyPath)) {
|
|
134
|
+
throw new Error(`Private key not found: ${privateKeyPath}`);
|
|
135
|
+
}
|
|
136
|
+
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
137
|
+
const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
|
|
138
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "attest-it-"));
|
|
139
|
+
const dataFile = path.join(tmpDir, "data.bin");
|
|
140
|
+
const sigFile = path.join(tmpDir, "sig.bin");
|
|
141
|
+
try {
|
|
142
|
+
await fs.writeFile(dataFile, processBuffer);
|
|
143
|
+
const result = await runOpenSSL([
|
|
144
|
+
"pkeyutl",
|
|
145
|
+
"-sign",
|
|
146
|
+
"-inkey",
|
|
147
|
+
privateKeyPath,
|
|
148
|
+
"-in",
|
|
149
|
+
dataFile,
|
|
150
|
+
"-out",
|
|
151
|
+
sigFile
|
|
152
|
+
]);
|
|
153
|
+
if (result.exitCode !== 0) {
|
|
154
|
+
throw new Error(`Failed to sign data: ${result.stderr}`);
|
|
155
|
+
}
|
|
156
|
+
const sigBuffer = await fs.readFile(sigFile);
|
|
157
|
+
return sigBuffer.toString("base64");
|
|
158
|
+
} finally {
|
|
159
|
+
try {
|
|
160
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function verify(options) {
|
|
166
|
+
await ensureOpenSSLAvailable();
|
|
167
|
+
const { publicKeyPath, data, signature } = options;
|
|
168
|
+
if (!await fileExists(publicKeyPath)) {
|
|
169
|
+
throw new Error(`Public key not found: ${publicKeyPath}`);
|
|
170
|
+
}
|
|
171
|
+
const dataBuffer = typeof data === "string" ? Buffer.from(data, "utf8") : data;
|
|
172
|
+
const processBuffer = dataBuffer.length === 0 ? Buffer.from([0]) : dataBuffer;
|
|
173
|
+
const sigBuffer = Buffer.from(signature, "base64");
|
|
174
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "attest-it-"));
|
|
175
|
+
const dataFile = path.join(tmpDir, "data.bin");
|
|
176
|
+
const sigFile = path.join(tmpDir, "sig.bin");
|
|
177
|
+
try {
|
|
178
|
+
await fs.writeFile(dataFile, processBuffer);
|
|
179
|
+
await fs.writeFile(sigFile, sigBuffer);
|
|
180
|
+
const result = await runOpenSSL([
|
|
181
|
+
"pkeyutl",
|
|
182
|
+
"-verify",
|
|
183
|
+
"-pubin",
|
|
184
|
+
"-inkey",
|
|
185
|
+
publicKeyPath,
|
|
186
|
+
"-sigfile",
|
|
187
|
+
sigFile,
|
|
188
|
+
"-in",
|
|
189
|
+
dataFile
|
|
190
|
+
]);
|
|
191
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
192
|
+
throw new Error(`Verification error: ${result.stderr}`);
|
|
193
|
+
}
|
|
194
|
+
return result.exitCode === 0;
|
|
195
|
+
} finally {
|
|
196
|
+
try {
|
|
197
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function setKeyPermissions(keyPath) {
|
|
203
|
+
if (process.platform === "win32") {
|
|
204
|
+
await fs.chmod(keyPath, 384);
|
|
205
|
+
} else {
|
|
206
|
+
await fs.chmod(keyPath, 384);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { checkOpenSSL, generateKeyPair, getDefaultPrivateKeyPath, getDefaultPublicKeyPath, setKeyPermissions, sign, verify };
|
|
211
|
+
//# sourceMappingURL=chunk-UWYR7JNE.js.map
|
|
212
|
+
//# sourceMappingURL=chunk-UWYR7JNE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/crypto.ts"],"names":[],"mappings":"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","file":"chunk-UWYR7JNE.js","sourcesContent":["/**\n * Cryptographic utilities for key generation, signing, and verification.\n *\n * @remarks\n * This module provides cryptographic operations using OpenSSL for key management\n * and signature verification. It supports Ed25519 and RSA algorithms.\n *\n * @packageDocumentation\n */\n\nimport { spawn } from 'node:child_process'\nimport * as fs from 'node:fs/promises'\nimport * as path from 'node:path'\nimport * as os from 'node:os'\n\n/**\n * Supported signature algorithms.\n * @public\n */\nexport type Algorithm = 'ed25519' | 'rsa'\n\n/**\n * Paths to a generated keypair.\n * @public\n */\nexport interface KeyPaths {\n /** Path to the private key file */\n privatePath: string\n /** Path to the public key file */\n publicPath: string\n}\n\n/**\n * Options for key generation.\n * @public\n */\nexport interface KeygenOptions {\n /** Algorithm to use (default: ed25519) */\n algorithm?: Algorithm\n /** Path for private key (default: OS-specific config dir) */\n privatePath?: string\n /** Path for public key (default: repo root) */\n publicPath?: string\n /** Overwrite existing keys (default: false) */\n force?: boolean\n}\n\n/**\n * Options for signing data.\n * @public\n */\nexport interface SignOptions {\n /** Path to the private key file */\n privateKeyPath: string\n /** Data to sign (string or Buffer) */\n data: string | Buffer\n}\n\n/**\n * Options for verifying signatures.\n * @public\n */\nexport interface VerifyOptions {\n /** Path to the public key file */\n publicKeyPath: string\n /** Original data that was signed */\n data: string | Buffer\n /** Base64-encoded signature to verify */\n signature: string\n}\n\n/**\n * Result from spawning an OpenSSL process.\n * @internal\n */\ninterface SpawnResult {\n /** Process exit code */\n exitCode: number\n /** Standard output as Buffer */\n stdout: Buffer\n /** Standard error as string */\n stderr: string\n}\n\n/**\n * Run OpenSSL with the given arguments.\n * @param args - Command-line arguments for OpenSSL\n * @param stdin - Optional data to write to stdin\n * @returns Process result with exit code and outputs\n * @internal\n */\nasync function runOpenSSL(args: string[], stdin?: Buffer): Promise<SpawnResult> {\n return new Promise((resolve, reject) => {\n const child = spawn('openssl', args, {\n stdio: ['pipe', 'pipe', 'pipe'],\n })\n\n const stdoutChunks: Buffer[] = []\n let stderr = ''\n\n child.stdout.on('data', (chunk: Buffer) => {\n stdoutChunks.push(chunk)\n })\n\n child.stderr.on('data', (chunk: Buffer) => {\n stderr += chunk.toString()\n })\n\n child.on('error', (err) => {\n reject(new Error(`Failed to spawn OpenSSL: ${err.message}`))\n })\n\n child.on('close', (code) => {\n resolve({\n exitCode: code ?? 1,\n stdout: Buffer.concat(stdoutChunks),\n stderr,\n })\n })\n\n if (stdin) {\n child.stdin.write(stdin)\n }\n child.stdin.end()\n })\n}\n\n/**\n * Check if OpenSSL is available and get version info.\n * @returns OpenSSL version string\n * @throws Error if OpenSSL is not available\n * @public\n */\nexport async function checkOpenSSL(): Promise<string> {\n const result = await runOpenSSL(['version'])\n\n if (result.exitCode !== 0) {\n throw new Error(`OpenSSL check failed: ${result.stderr}`)\n }\n\n return result.stdout.toString().trim()\n}\n\n/**\n * Cached result of OpenSSL availability check.\n * @internal\n */\nlet openSSLChecked = false\n\n/**\n * Ensure OpenSSL is available before performing cryptographic operations.\n * @throws Error with installation instructions if OpenSSL is not available\n * @internal\n */\nasync function ensureOpenSSLAvailable(): Promise<void> {\n if (openSSLChecked) {\n return\n }\n\n try {\n await checkOpenSSL()\n openSSLChecked = true\n } catch {\n throw new Error(\n 'OpenSSL is not installed or not in PATH. ' +\n 'Please install OpenSSL to use attest-it. ' +\n 'On macOS: brew install openssl. ' +\n 'On Ubuntu: apt-get install openssl',\n )\n }\n}\n\n/**\n * Get the default private key path based on OS.\n * - macOS/Linux: ~/.config/attest-it/private.pem\n * - Windows: %APPDATA%\\attest-it\\private.pem\n * @public\n */\nexport function getDefaultPrivateKeyPath(): string {\n const homeDir = os.homedir()\n\n if (process.platform === 'win32') {\n const appData = process.env.APPDATA ?? path.join(homeDir, 'AppData', 'Roaming')\n return path.join(appData, 'attest-it', 'private.pem')\n }\n\n return path.join(homeDir, '.config', 'attest-it', 'private.pem')\n}\n\n/**\n * Get the default public key path (in repo).\n * @public\n */\nexport function getDefaultPublicKeyPath(): string {\n return path.join(process.cwd(), 'attest-it-public.pem')\n}\n\n/**\n * Ensure a directory exists, creating it and parent directories if needed.\n * @param dirPath - Directory path to create\n * @internal\n */\nasync function ensureDir(dirPath: string): Promise<void> {\n try {\n await fs.mkdir(dirPath, { recursive: true })\n } catch (err) {\n if (err instanceof Error && 'code' in err && err.code !== 'EEXIST') {\n throw err\n }\n }\n}\n\n/**\n * Check if a file exists.\n * @param filePath - File path to check\n * @returns true if file exists\n * @internal\n */\nasync function fileExists(filePath: string): Promise<boolean> {\n try {\n await fs.access(filePath)\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Clean up one or more files, ignoring errors if files don't exist.\n * @param paths - File paths to delete\n * @internal\n */\nasync function cleanupFiles(...paths: string[]): Promise<void> {\n for (const filePath of paths) {\n try {\n await fs.unlink(filePath)\n } catch {\n // Ignore cleanup errors - file may not exist\n }\n }\n}\n\n/**\n * Generate a new keypair using OpenSSL.\n * @param options - Generation options\n * @returns Paths to generated keys\n * @throws Error if OpenSSL fails or keys exist without force\n * @public\n */\nexport async function generateKeyPair(options: KeygenOptions = {}): Promise<KeyPaths> {\n // Ensure OpenSSL is available before proceeding\n await ensureOpenSSLAvailable()\n\n const {\n algorithm = 'ed25519',\n privatePath = getDefaultPrivateKeyPath(),\n publicPath = getDefaultPublicKeyPath(),\n force = false,\n } = options\n\n // Check if keys already exist\n const privateExists = await fileExists(privatePath)\n const publicExists = await fileExists(publicPath)\n\n if ((privateExists || publicExists) && !force) {\n const existing = [privateExists ? privatePath : null, publicExists ? publicPath : null].filter(\n Boolean,\n )\n throw new Error(\n `Key files already exist: ${existing.join(', ')}. Use force: true to overwrite.`,\n )\n }\n\n // Ensure parent directories exist\n await ensureDir(path.dirname(privatePath))\n await ensureDir(path.dirname(publicPath))\n\n try {\n // Generate private key\n const genArgs =\n algorithm === 'ed25519'\n ? ['genpkey', '-algorithm', 'Ed25519', '-out', privatePath]\n : ['genpkey', '-algorithm', 'RSA', '-pkeyopt', 'rsa_keygen_bits:2048', '-out', privatePath]\n\n const genResult = await runOpenSSL(genArgs)\n if (genResult.exitCode !== 0) {\n throw new Error(`Failed to generate private key: ${genResult.stderr}`)\n }\n\n // Set restrictive permissions on private key\n await setKeyPermissions(privatePath)\n\n // Extract public key\n const pubResult = await runOpenSSL(['pkey', '-in', privatePath, '-pubout', '-out', publicPath])\n\n if (pubResult.exitCode !== 0) {\n throw new Error(`Failed to extract public key: ${pubResult.stderr}`)\n }\n\n return {\n privatePath,\n publicPath,\n }\n } catch (err) {\n // Clean up both key files on any failure\n await cleanupFiles(privatePath, publicPath)\n throw err\n }\n}\n\n/**\n * Sign data using a private key.\n * @param options - Signing options\n * @returns Base64-encoded signature\n * @throws Error if signing fails\n * @public\n */\nexport async function sign(options: SignOptions): Promise<string> {\n // Ensure OpenSSL is available before proceeding\n await ensureOpenSSLAvailable()\n\n const { privateKeyPath, data } = options\n\n // Check if private key exists\n if (!(await fileExists(privateKeyPath))) {\n throw new Error(`Private key not found: ${privateKeyPath}`)\n }\n\n // Convert data to Buffer\n const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data\n\n // OpenSSL pkeyutl cannot handle empty files, so we need to add a single byte\n // for empty data and document this limitation\n const processBuffer = dataBuffer.length === 0 ? Buffer.from([0x00]) : dataBuffer\n\n // Create temporary directory with OS-level uniqueness guarantees\n // This prevents TOCTOU race conditions that Math.random() would allow\n const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'attest-it-'))\n const dataFile = path.join(tmpDir, 'data.bin')\n const sigFile = path.join(tmpDir, 'sig.bin')\n\n try {\n // Write data to temp file\n await fs.writeFile(dataFile, processBuffer)\n\n // Sign the data\n const result = await runOpenSSL([\n 'pkeyutl',\n '-sign',\n '-inkey',\n privateKeyPath,\n '-in',\n dataFile,\n '-out',\n sigFile,\n ])\n\n if (result.exitCode !== 0) {\n throw new Error(`Failed to sign data: ${result.stderr}`)\n }\n\n // Read the signature\n const sigBuffer = await fs.readFile(sigFile)\n return sigBuffer.toString('base64')\n } finally {\n // Clean up temp directory and all files within it\n try {\n await fs.rm(tmpDir, { recursive: true, force: true })\n } catch {\n // Ignore cleanup errors - OS will eventually clean tmpdir\n }\n }\n}\n\n/**\n * Verify a signature using a public key.\n * @param options - Verification options\n * @returns true if signature is valid\n * @throws Error if verification fails (not just invalid signature)\n * @public\n */\nexport async function verify(options: VerifyOptions): Promise<boolean> {\n // Ensure OpenSSL is available before proceeding\n await ensureOpenSSLAvailable()\n\n const { publicKeyPath, data, signature } = options\n\n // Check if public key exists\n if (!(await fileExists(publicKeyPath))) {\n throw new Error(`Public key not found: ${publicKeyPath}`)\n }\n\n // Convert data to Buffer\n const dataBuffer = typeof data === 'string' ? Buffer.from(data, 'utf8') : data\n\n // OpenSSL pkeyutl cannot handle empty files, so we use the same workaround\n // as in sign() - add a single byte for empty data\n const processBuffer = dataBuffer.length === 0 ? Buffer.from([0x00]) : dataBuffer\n\n // Decode signature from base64\n const sigBuffer = Buffer.from(signature, 'base64')\n\n // Create temporary directory with OS-level uniqueness guarantees\n // This prevents TOCTOU race conditions that Math.random() would allow\n const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'attest-it-'))\n const dataFile = path.join(tmpDir, 'data.bin')\n const sigFile = path.join(tmpDir, 'sig.bin')\n\n try {\n // Write data and signature to temp files\n await fs.writeFile(dataFile, processBuffer)\n await fs.writeFile(sigFile, sigBuffer)\n\n // Verify the signature\n const result = await runOpenSSL([\n 'pkeyutl',\n '-verify',\n '-pubin',\n '-inkey',\n publicKeyPath,\n '-sigfile',\n sigFile,\n '-in',\n dataFile,\n ])\n\n // Exit code 0 means valid signature, 1 means invalid\n // Any other exit code or stderr output indicates an error\n if (result.exitCode !== 0 && result.exitCode !== 1) {\n throw new Error(`Verification error: ${result.stderr}`)\n }\n\n return result.exitCode === 0\n } finally {\n // Clean up temp directory and all files within it\n try {\n await fs.rm(tmpDir, { recursive: true, force: true })\n } catch {\n // Ignore cleanup errors - OS will eventually clean tmpdir\n }\n }\n}\n\n/**\n * Set restrictive permissions on a private key file.\n * @param keyPath - Path to the private key\n * @public\n */\nexport async function setKeyPermissions(keyPath: string): Promise<void> {\n // On Windows, use fs.chmod which has limited effect\n // On Unix, set to 0o600 (read/write for owner only)\n if (process.platform === 'win32') {\n // Windows doesn't support Unix-style permissions in the same way\n // But we still call chmod for consistency\n await fs.chmod(keyPath, 0o600)\n } else {\n await fs.chmod(keyPath, 0o600)\n }\n}\n"]}
|