dsh-standards 1.4.1 → 1.6.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/40-checker/fixtures/security/sec15-clean/index.mjs +3 -0
- package/40-checker/fixtures/security/sec15-clean/package.json +19 -0
- package/40-checker/fixtures/security/sec15-missing-ref/index.mjs +3 -0
- package/40-checker/fixtures/security/sec15-missing-ref/package.json +22 -0
- package/40-checker/fixtures/security/sec15-node/index.mjs +3 -0
- package/40-checker/fixtures/security/sec15-node/package.json +22 -0
- package/40-checker/fixtures/security/sec15-node/scripts/build.mjs +1 -0
- package/40-checker/fixtures/security/sec15-pipe/index.mjs +3 -0
- package/40-checker/fixtures/security/sec15-pipe/package.json +22 -0
- package/40-checker/fixtures/security/sec16-client/index.mjs +3 -0
- package/40-checker/fixtures/security/sec16-client/lib/client.js +3 -0
- package/40-checker/fixtures/security/sec16-client/package.json +21 -0
- package/40-checker/fixtures/security/sec16-declared/index.mjs +3 -0
- package/40-checker/fixtures/security/sec16-declared/package.json +19 -0
- package/40-checker/fixtures/security/sec16-eval/index.mjs +3 -0
- package/40-checker/fixtures/security/sec16-eval/package.json +19 -0
- package/40-checker/fixtures/security/sec16-require/index.mjs +4 -0
- package/40-checker/fixtures/security/sec16-require/package.json +19 -0
- package/40-checker/fixtures/security/sec17-env/index.mjs +5 -0
- package/40-checker/fixtures/security/sec17-env/package.json +19 -0
- package/40-checker/fixtures/security/sec17-seam/index.mjs +3 -0
- package/40-checker/fixtures/security/sec17-seam/package.json +19 -0
- package/40-checker/fixtures/security/sec17-ssh/index.mjs +6 -0
- package/40-checker/fixtures/security/sec17-ssh/package.json +19 -0
- package/40-checker/fixtures/security/sec18-bad/index.mjs +3 -0
- package/40-checker/fixtures/security/sec18-bad/package.json +21 -0
- package/40-checker/fixtures/security/sec18-good/index.mjs +3 -0
- package/40-checker/fixtures/security/sec18-good/package.json +26 -0
- package/40-checker/fixtures/security/sec18-undeclared/index.mjs +3 -0
- package/40-checker/fixtures/security/sec18-undeclared/package.json +26 -0
- package/40-checker/fixtures/security/sec19-blob/index.mjs +2 -0
- package/40-checker/fixtures/security/sec19-blob/package.json +19 -0
- package/40-checker/fixtures/security/sec19-clean/index.mjs +2 -0
- package/40-checker/fixtures/security/sec19-clean/package.json +19 -0
- package/40-checker/fixtures/security/sec19-wasm/blob.wasm +0 -0
- package/40-checker/fixtures/security/sec19-wasm/index.mjs +3 -0
- package/40-checker/fixtures/security/sec19-wasm/package.json +19 -0
- package/40-checker/fixtures/security/sec20-unwrapped/index.mjs +4 -0
- package/40-checker/fixtures/security/sec20-unwrapped/package.json +23 -0
- package/40-checker/fixtures/security/sec20-wrapped/index.mjs +3 -0
- package/40-checker/fixtures/security/sec20-wrapped/package.json +23 -0
- package/40-checker/fixtures/security/sec21-known/index.mjs +3 -0
- package/40-checker/fixtures/security/sec21-known/package.json +23 -0
- package/40-checker/fixtures/security/sec21-typosquat/index.mjs +3 -0
- package/40-checker/fixtures/security/sec21-typosquat/package.json +22 -0
- package/40-checker/fixtures/security/sec22-attested/index.mjs +3 -0
- package/40-checker/fixtures/security/sec22-attested/package.json +22 -0
- package/40-checker/fixtures/security/sec22-none/index.mjs +3 -0
- package/40-checker/fixtures/security/sec22-none/package.json +19 -0
- package/40-checker/scripts/sign-evidence.mjs +169 -0
- package/40-checker/src/cli.mjs +402 -73
- package/40-checker/src/declarations.mjs +10 -10
- package/40-checker/src/flow-evidence.mjs +21 -0
- package/40-checker/src/index.mjs +2 -1
- package/40-checker/src/report.mjs +116 -4
- package/40-checker/src/runtime-child.mjs +68 -0
- package/40-checker/src/runtime.mjs +3 -1
- package/40-checker/src/sandbox-run.mjs +406 -35
- package/40-checker/src/security.mjs +1065 -0
- package/40-checker/src/signature.mjs +249 -0
- package/40-checker/src/util.mjs +53 -0
- package/CHANGELOG.md +34 -0
- package/docs/SIGNING.md +137 -0
- package/docs/coverage.md +39 -6
- package/package.json +1 -1
- package/spec/coverage-contract.md +33 -11
- package/spec/dsc-clauses.json +410 -9
- package/spec/remediation.json +196 -0
- package/spec/schemas/conformance.schema.json +80 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sign-evidence.mjs — Ed25519 key generation, evidence signing, verification.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* node 40-checker/scripts/sign-evidence.mjs keygen --out <dir> [--force]
|
|
7
|
+
* node 40-checker/scripts/sign-evidence.mjs sign <evidence.json> --key <private.pem> --out <signed.json> [--at <iso>]
|
|
8
|
+
* node 40-checker/scripts/sign-evidence.mjs verify <signed.json> [--pub <public.pem>] [--json]
|
|
9
|
+
*
|
|
10
|
+
* `verify` prints exactly `valid`, `invalid`, or `missing` on stdout (unless
|
|
11
|
+
* `--json`) and exits 0 only for `valid`. The signature covers canonical JSON
|
|
12
|
+
* with the `signature` field removed; see src/signature.mjs.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import {
|
|
18
|
+
generateEd25519KeyPair, keyIdFromPublicKey, loadPrivateKey, loadPublicKey,
|
|
19
|
+
signEvidence, verifyEvidenceSignature,
|
|
20
|
+
} from '../src/signature.mjs'
|
|
21
|
+
|
|
22
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
|
|
24
|
+
function usage() {
|
|
25
|
+
return `sign-evidence — Ed25519 signatures for dsh-flow-evidence/2
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
node 40-checker/scripts/sign-evidence.mjs keygen --out <dir> [--force]
|
|
29
|
+
node 40-checker/scripts/sign-evidence.mjs sign <evidence.json> --key <private.pem> --out <signed.json> [--at <iso>]
|
|
30
|
+
node 40-checker/scripts/sign-evidence.mjs verify <signed.json> [--pub <public.pem>] [--json]
|
|
31
|
+
|
|
32
|
+
Keygen writes ed25519.pem (PKCS#8 private) and ed25519.pub.pem (SPKI public)
|
|
33
|
+
with mode 0600. Refuses to overwrite unless --force is given.
|
|
34
|
+
|
|
35
|
+
Sign covers canonical JSON of every field except \`signature\` and records
|
|
36
|
+
{ alg, keyId, publicKey, sig, at }; keyId is sha256(SPKI public key PEM)[:16].
|
|
37
|
+
|
|
38
|
+
Verify recomputes the payload and checks the signature. Pass --pub to pin the
|
|
39
|
+
trust anchor; without it the embedded public key is used (self-asserted).
|
|
40
|
+
Exit codes: 0 valid, 1 invalid/missing, 2 usage or input error.`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parse(argv, { flags = [], values = [] } = {}) {
|
|
44
|
+
const positional = []
|
|
45
|
+
const options = {}
|
|
46
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
47
|
+
const arg = argv[i]
|
|
48
|
+
if (flags.includes(arg)) {
|
|
49
|
+
options[arg] = true
|
|
50
|
+
} else if (values.includes(arg)) {
|
|
51
|
+
const value = argv[i + 1]
|
|
52
|
+
if (value === undefined) throw new Error(`${arg} needs a value`)
|
|
53
|
+
options[arg] = value
|
|
54
|
+
i += 1
|
|
55
|
+
} else if (arg.startsWith('--')) {
|
|
56
|
+
throw new Error(`unknown option: ${arg}`)
|
|
57
|
+
} else {
|
|
58
|
+
positional.push(arg)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { positional, options }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readJsonFile(file) {
|
|
65
|
+
const text = fs.readFileSync(file, 'utf8')
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(text)
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(`not valid JSON (${file}): ${error.message}`)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeJsonFile(file, value) {
|
|
74
|
+
fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true })
|
|
75
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main(argv = process.argv.slice(2)) {
|
|
79
|
+
const [command, ...rest] = argv
|
|
80
|
+
try {
|
|
81
|
+
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
82
|
+
console.log(usage())
|
|
83
|
+
return command ? 0 : 2
|
|
84
|
+
}
|
|
85
|
+
if (command === 'keygen') {
|
|
86
|
+
const { options } = parse(rest, { flags: ['--force'], values: ['--out'] })
|
|
87
|
+
if (!options['--out']) throw new Error('keygen needs --out <dir>')
|
|
88
|
+
const outDir = path.resolve(options['--out'])
|
|
89
|
+
const privateFile = path.join(outDir, 'ed25519.pem')
|
|
90
|
+
const publicFile = path.join(outDir, 'ed25519.pub.pem')
|
|
91
|
+
if (!options['--force'] && (fs.existsSync(privateFile) || fs.existsSync(publicFile))) {
|
|
92
|
+
throw new Error(`refusing to overwrite existing key in ${outDir}; pass --force`)
|
|
93
|
+
}
|
|
94
|
+
const pair = generateEd25519KeyPair()
|
|
95
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
96
|
+
fs.writeFileSync(privateFile, pair.privateKeyPem, { mode: 0o600 })
|
|
97
|
+
fs.writeFileSync(publicFile, pair.publicKeyPem, { mode: 0o600 })
|
|
98
|
+
// writeFileSync mode is filtered by the umask; force the requested mode.
|
|
99
|
+
fs.chmodSync(privateFile, 0o600)
|
|
100
|
+
fs.chmodSync(publicFile, 0o600)
|
|
101
|
+
const publicKey = loadPublicKey(fs.readFileSync(publicFile, 'utf8'))
|
|
102
|
+
const keyId = keyIdFromPublicKey(publicKey)
|
|
103
|
+
console.log(JSON.stringify({ privateKey: privateFile, publicKey: publicFile, keyId }, null, 2))
|
|
104
|
+
return 0
|
|
105
|
+
}
|
|
106
|
+
if (command === 'sign') {
|
|
107
|
+
const { positional, options } = parse(rest, { values: ['--key', '--out', '--at'] })
|
|
108
|
+
if (positional.length !== 1) throw new Error('sign needs exactly one <evidence.json>')
|
|
109
|
+
if (!options['--key']) throw new Error('sign needs --key <private.pem>')
|
|
110
|
+
if (!options['--out']) throw new Error('sign needs --out <signed.json>')
|
|
111
|
+
let at = new Date().toISOString()
|
|
112
|
+
if (options['--at']) {
|
|
113
|
+
const parsed = Date.parse(options['--at'])
|
|
114
|
+
if (!Number.isFinite(parsed)) throw new Error(`--at is not a valid ISO-8601 timestamp: ${options['--at']}`)
|
|
115
|
+
at = new Date(parsed).toISOString()
|
|
116
|
+
}
|
|
117
|
+
const document = readJsonFile(path.resolve(positional[0]))
|
|
118
|
+
const privateKey = loadPrivateKey(fs.readFileSync(options['--key'], 'utf8'))
|
|
119
|
+
const signed = signEvidence(document, privateKey, { at })
|
|
120
|
+
const out = path.resolve(options['--out'])
|
|
121
|
+
writeJsonFile(out, signed)
|
|
122
|
+
console.log(JSON.stringify({ signed: out, keyId: signed.signature.keyId, alg: signed.signature.alg, at }, null, 2))
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
if (command === 'verify') {
|
|
126
|
+
const { positional, options } = parse(rest, { flags: ['--json'], values: ['--pub'] })
|
|
127
|
+
if (positional.length !== 1) throw new Error('verify needs exactly one <signed.json>')
|
|
128
|
+
let document
|
|
129
|
+
try {
|
|
130
|
+
document = readJsonFile(path.resolve(positional[0]))
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(`invalid\n${error.message}`)
|
|
133
|
+
return 2
|
|
134
|
+
}
|
|
135
|
+
let publicKey = null
|
|
136
|
+
if (options['--pub']) {
|
|
137
|
+
try {
|
|
138
|
+
publicKey = loadPublicKey(fs.readFileSync(options['--pub'], 'utf8'))
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const status = error?.code === 'ENOENT' ? 'missing' : 'invalid'
|
|
141
|
+
if (options['--json']) {
|
|
142
|
+
console.log(JSON.stringify({ status, reason: `cannot read --pub key: ${error.message}` }, null, 2))
|
|
143
|
+
} else {
|
|
144
|
+
console.log(status)
|
|
145
|
+
}
|
|
146
|
+
console.error(`cannot read --pub key: ${error.message}`)
|
|
147
|
+
return 1
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const result = verifyEvidenceSignature(document, { publicKey })
|
|
151
|
+
if (options['--json']) console.log(JSON.stringify(result, null, 2))
|
|
152
|
+
else console.log(result.status)
|
|
153
|
+
if (result.status !== 'valid' && result.reason) console.error(result.reason)
|
|
154
|
+
return result.status === 'valid' ? 0 : 1
|
|
155
|
+
}
|
|
156
|
+
console.error(`unknown command: ${command}`)
|
|
157
|
+
console.error(usage())
|
|
158
|
+
return 2
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(error.message)
|
|
161
|
+
return 2
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
166
|
+
process.exitCode = main()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { HERE, main, parse, readJsonFile, writeJsonFile }
|