@uwmd/signing 0.1.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 +190 -0
- package/dist/algorithms.d.ts +21 -0
- package/dist/algorithms.js +55 -0
- package/dist/base64.d.ts +9 -0
- package/dist/base64.js +60 -0
- package/dist/errors.d.ts +15 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +23 -0
- package/dist/keys.d.ts +67 -0
- package/dist/keys.js +102 -0
- package/dist/keystore-file.d.ts +32 -0
- package/dist/keystore-file.js +120 -0
- package/dist/modules.d.ts +29 -0
- package/dist/modules.js +56 -0
- package/dist/sign.d.ts +46 -0
- package/dist/sign.js +81 -0
- package/dist/test-helpers.d.ts +4 -0
- package/dist/test-helpers.js +49 -0
- package/dist/verify.d.ts +45 -0
- package/dist/verify.js +106 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# @uwmd/signing
|
|
2
|
+
|
|
3
|
+
Block, receipt, and module-manifest signatures for
|
|
4
|
+
[UW Markdown](https://uwmd.org) — the implementation of **protocol §V.11**
|
|
5
|
+
([RFC 0010](../../docs/rfcs/0010-signed-blocks.md)) and **§X.1**
|
|
6
|
+
([RFC 0002](../../docs/rfcs/0002-module-signing.md)).
|
|
7
|
+
|
|
8
|
+
## Why this is a separate package
|
|
9
|
+
|
|
10
|
+
`@uwmd/core` is deliberately zero-cryptography. Reading, validating, editing,
|
|
11
|
+
and computing over a `.uw.md` file requires no crypto, and the overwhelming
|
|
12
|
+
majority of adopters should never take a crypto dependency to do any of it.
|
|
13
|
+
|
|
14
|
+
Signed blocks are for the deployments that genuinely need chain of custody:
|
|
15
|
+
regulated lender data rooms, multi-party deal flow where sponsor, lender, and
|
|
16
|
+
appraiser each sign their own sections, and agent-host accountability where a
|
|
17
|
+
signature proves *which* agent instance wrote a block rather than merely what
|
|
18
|
+
the `actor` field claims.
|
|
19
|
+
|
|
20
|
+
Core owns the normative, crypto-free half — the wire shape (`_meta.signature`)
|
|
21
|
+
and the canonical signing input. This package owns the algorithms. Two seams
|
|
22
|
+
connect them:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
verifyChain(parsed, { signatureVerifier }) // blocks
|
|
26
|
+
verifyReceipt(receipt, source, { signatureVerifier }) // receipts (RFC 0016)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Without a verifier, core reports signatures as **present and unchecked** —
|
|
30
|
+
never as valid.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @uwmd/signing
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Needs Node ≥ 18.4 (Ed25519 in Web Crypto) or a modern browser. `ed25519`,
|
|
39
|
+
`es256`, and `es384` are the admitted algorithms.
|
|
40
|
+
|
|
41
|
+
## Signing a block
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { computeBlockHash, parseUWFile } from '@uwmd/core';
|
|
45
|
+
import { generateSigningKeyPair, signBlock, stampBlockSignature } from '@uwmd/signing';
|
|
46
|
+
|
|
47
|
+
const { signing, verifying } = await generateSigningKeyPair('ed25519', 'sponsor-2026');
|
|
48
|
+
|
|
49
|
+
const parsed = parseUWFile(source);
|
|
50
|
+
const block = parsed.sections.rent_roll;
|
|
51
|
+
|
|
52
|
+
// A block must already carry `content_hash`: a signature over an absent hash
|
|
53
|
+
// commits to nothing, and validates as INT-05.
|
|
54
|
+
const signature = await signBlock(block, signing);
|
|
55
|
+
const signed = stampBlockSignature(block, signature);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`signBlock` does not mutate the block, and `generateSigningKeyPair` is for
|
|
59
|
+
tests and local development — production keys belong in an HSM or cloud KMS
|
|
60
|
+
behind a custom `KeyStore`.
|
|
61
|
+
|
|
62
|
+
## Verifying
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { verifyChain } from '@uwmd/core';
|
|
66
|
+
import { createBlockSignatureVerifier, loadKeyStoreFile } from '@uwmd/signing';
|
|
67
|
+
|
|
68
|
+
const store = await loadKeyStoreFile('./keystore.json');
|
|
69
|
+
const result = await verifyChain(parsed, {
|
|
70
|
+
signatureVerifier: createBlockSignatureVerifier(store),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
result.signatures_present; // how many blocks carry a signature
|
|
74
|
+
result.signatures_verified; // how many actually verified
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or from the CLI:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uwmd verify deal.uwx.md --signing --keystore=./keystore.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### What each code means
|
|
84
|
+
|
|
85
|
+
| Code | Severity | Trigger |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `INT-05` | error | `signature` present with no `content_hash`. |
|
|
88
|
+
| `INT-06` | error | `kid` names a key the store does not hold. |
|
|
89
|
+
| `INT-07` | error | The signature does not verify, the algorithm is unadmitted, or the stamped `content_hash` no longer recomputes. |
|
|
90
|
+
| `INT-08` | warning | The algorithm is in the deployment's deprecation list. |
|
|
91
|
+
|
|
92
|
+
`INT-06` and `INT-07` are deliberately distinct. "I cannot check this" and
|
|
93
|
+
"this is forged" call for opposite responses — load a key versus reject the
|
|
94
|
+
document — and a verifier that merges them tells an operator to re-sign when
|
|
95
|
+
the real fix is to configure their key store.
|
|
96
|
+
|
|
97
|
+
## Key store format
|
|
98
|
+
|
|
99
|
+
Key *distribution* is out of scope: a public key that travels inside the
|
|
100
|
+
document it authenticates proves nothing. The file format below is a
|
|
101
|
+
**reference**, not a normative one — back a `KeyStore` with an HSM and never
|
|
102
|
+
touch it.
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"keystore_version": "1",
|
|
107
|
+
"keys": [
|
|
108
|
+
{
|
|
109
|
+
"kid": "sponsor-2026",
|
|
110
|
+
"alg": "ed25519",
|
|
111
|
+
"public_key_jwk": { "kty": "OKP", "crv": "Ed25519", "x": "..." }
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"kid": "lender-2026",
|
|
115
|
+
"alg": "es256",
|
|
116
|
+
"public_key_spki": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Exactly one of `public_key_jwk` / `public_key_spki` per entry. Every key is
|
|
123
|
+
imported eagerly at load time, so a typo fails once, loudly, instead of
|
|
124
|
+
appearing later as a per-block `INT-07` that reads like tampering.
|
|
125
|
+
|
|
126
|
+
**Rotation** is "issue new blocks under a new `kid` and keep the old key
|
|
127
|
+
loaded". A `kid` names one key and MUST NOT be reused; blocks signed under a
|
|
128
|
+
retired `kid` stay verifiable for as long as the store retains it.
|
|
129
|
+
|
|
130
|
+
## Module manifests
|
|
131
|
+
|
|
132
|
+
Module signing (RFC 0002, protocol §X.1) uses the same keys and the same
|
|
133
|
+
verifier. The signed bytes are the RFC 8785 canonical manifest with `signature`
|
|
134
|
+
removed.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { loadModuleManifestAsync } from '@uwmd/core';
|
|
138
|
+
import { createModuleSignatureVerifier, signModule, stampModuleSignature } from '@uwmd/signing';
|
|
139
|
+
|
|
140
|
+
const signed = stampModuleSignature(
|
|
141
|
+
manifest,
|
|
142
|
+
await signModule(manifest, signing, { identity: 'modules@example.org' }),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const result = await loadModuleManifestAsync(signed, {
|
|
146
|
+
signaturePolicy: 'require', // or 'verify-if-present' / 'ignore'
|
|
147
|
+
signatureVerifier: createModuleSignatureVerifier(store),
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The three policies are `ignore` (default), `verify-if-present` (an unsigned
|
|
152
|
+
module loads, a broken signature refuses), and `require`. Both checking
|
|
153
|
+
policies refuse when the key store lacks the `kid` — a host that cannot check a
|
|
154
|
+
signature has established nothing about the module.
|
|
155
|
+
|
|
156
|
+
`identity` is **advisory**. A signature proves the key holder asserted it,
|
|
157
|
+
never that the assertion is true; an `allowedIdentities` allow-list is worth
|
|
158
|
+
only as much as your decision to bind that `kid` to that identity.
|
|
159
|
+
|
|
160
|
+
## Receipts
|
|
161
|
+
|
|
162
|
+
Receipt signing (RFC 0016) uses the same keys:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { verifyReceipt } from '@uwmd/core';
|
|
166
|
+
import { createReceiptSignatureVerifier, signReceipt, stampReceiptSignature } from '@uwmd/signing';
|
|
167
|
+
|
|
168
|
+
const signed = stampReceiptSignature(receipt, await signReceipt(receipt, signing));
|
|
169
|
+
|
|
170
|
+
const verdict = await verifyReceipt(signed, source, {
|
|
171
|
+
signatureVerifier: createReceiptSignatureVerifier(store),
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Without a verifier, a signed receipt verifies as `unverifiable` with `RCP-08`.
|
|
176
|
+
|
|
177
|
+
## Limits
|
|
178
|
+
|
|
179
|
+
- **`signed_at` is self-asserted.** Audit-grade non-repudiation needs a
|
|
180
|
+
timestamping authority countersignature, which protocol 1.x does not define.
|
|
181
|
+
- **No selective disclosure.** Signing a redacted block would need Merkle
|
|
182
|
+
commitments over fields; the current design has none.
|
|
183
|
+
- **Re-rooting a chain requires re-signing.** `content_hash` covers `_meta`,
|
|
184
|
+
and `parent_hash` lives there — see the erratum in RFC 0010.
|
|
185
|
+
- **No Sigstore.** Keyless signing needs a Fulcio trust root and a Rekor
|
|
186
|
+
inclusion proof — a vendored snapshot that fails closed when stale, or
|
|
187
|
+
network access at verify time. `scheme: "sigstore"` is reserved in §X.1.2 so
|
|
188
|
+
adding it later stays additive.
|
|
189
|
+
|
|
190
|
+
MIT. Part of the [UW Markdown](https://github.com/UWMD-OSP/UW-Markdown) monorepo.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { UWSignatureAlgorithm } from '@uwmd/core';
|
|
2
|
+
export interface AlgorithmParams {
|
|
3
|
+
/** Passed to `importKey` / `generateKey`. */
|
|
4
|
+
readonly keyParams: EcKeyImportParams | Algorithm;
|
|
5
|
+
/** Passed to `sign` / `verify`. */
|
|
6
|
+
readonly signParams: EcdsaParams | Algorithm;
|
|
7
|
+
/** JWK `kty` the key material must declare. */
|
|
8
|
+
readonly kty: 'OKP' | 'EC';
|
|
9
|
+
/** JWK `crv` the key material must declare. */
|
|
10
|
+
readonly crv: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function algorithmParams(alg: string): AlgorithmParams;
|
|
13
|
+
export declare function isKnownAlgorithm(alg: string): alg is UWSignatureAlgorithm;
|
|
14
|
+
/**
|
|
15
|
+
* The runtime's Web Crypto implementation.
|
|
16
|
+
*
|
|
17
|
+
* Node exposes it as a global from 18.0; Ed25519 specifically landed in 18.4.
|
|
18
|
+
* An older runtime fails here with a code rather than a `TypeError` on
|
|
19
|
+
* `undefined.subtle`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function subtle(): SubtleCrypto;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Algorithm parameters for the three algorithms protocol §V.11 admits.
|
|
2
|
+
//
|
|
3
|
+
// The shortlist is borrowed from PASETO v4 and JOSE: one EdDSA curve and two
|
|
4
|
+
// NIST P-curves, all three present in stable Web Crypto. Deliberately no RSA —
|
|
5
|
+
// a 256-byte signature inline in a `_meta` block is a size the format should
|
|
6
|
+
// not have to carry, and nothing in CRE needs it.
|
|
7
|
+
//
|
|
8
|
+
// ECDSA here is the JOSE convention: raw `r || s` (IEEE P1363), which is what
|
|
9
|
+
// Web Crypto produces and consumes. DER-wrapped ECDSA signatures are NOT
|
|
10
|
+
// interchangeable and will fail verification rather than silently pass.
|
|
11
|
+
import { SigningError } from './errors.js';
|
|
12
|
+
const PARAMS = {
|
|
13
|
+
ed25519: {
|
|
14
|
+
keyParams: { name: 'Ed25519' },
|
|
15
|
+
signParams: { name: 'Ed25519' },
|
|
16
|
+
kty: 'OKP',
|
|
17
|
+
crv: 'Ed25519',
|
|
18
|
+
},
|
|
19
|
+
es256: {
|
|
20
|
+
keyParams: { name: 'ECDSA', namedCurve: 'P-256' },
|
|
21
|
+
signParams: { name: 'ECDSA', hash: 'SHA-256' },
|
|
22
|
+
kty: 'EC',
|
|
23
|
+
crv: 'P-256',
|
|
24
|
+
},
|
|
25
|
+
es384: {
|
|
26
|
+
keyParams: { name: 'ECDSA', namedCurve: 'P-384' },
|
|
27
|
+
signParams: { name: 'ECDSA', hash: 'SHA-384' },
|
|
28
|
+
kty: 'EC',
|
|
29
|
+
crv: 'P-384',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export function algorithmParams(alg) {
|
|
33
|
+
const params = PARAMS[alg];
|
|
34
|
+
if (!params) {
|
|
35
|
+
throw new SigningError('SIG_ALGORITHM_UNSUPPORTED', `Unknown signature algorithm '${alg}'. Protocol 1.x admits ed25519, es256, es384.`);
|
|
36
|
+
}
|
|
37
|
+
return params;
|
|
38
|
+
}
|
|
39
|
+
export function isKnownAlgorithm(alg) {
|
|
40
|
+
return alg in PARAMS;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The runtime's Web Crypto implementation.
|
|
44
|
+
*
|
|
45
|
+
* Node exposes it as a global from 18.0; Ed25519 specifically landed in 18.4.
|
|
46
|
+
* An older runtime fails here with a code rather than a `TypeError` on
|
|
47
|
+
* `undefined.subtle`.
|
|
48
|
+
*/
|
|
49
|
+
export function subtle() {
|
|
50
|
+
const provider = globalThis.crypto?.subtle;
|
|
51
|
+
if (!provider) {
|
|
52
|
+
throw new SigningError('SIG_NO_CRYPTO', 'No Web Crypto provider (globalThis.crypto.subtle) in this runtime; @uwmd/signing needs Node >= 18.4 or a browser.');
|
|
53
|
+
}
|
|
54
|
+
return provider;
|
|
55
|
+
}
|
package/dist/base64.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function toBase64Url(bytes: Uint8Array): string;
|
|
2
|
+
export declare function fromBase64Url(text: string): Uint8Array;
|
|
3
|
+
/**
|
|
4
|
+
* Decode either base64 or base64url. Key material arrives from openssl (base64,
|
|
5
|
+
* padded) far more often than from a JS encoder, and refusing the common form
|
|
6
|
+
* on a purely cosmetic difference would be a bad trade for zero safety.
|
|
7
|
+
*/
|
|
8
|
+
export declare function fromBase64Any(text: string): Uint8Array;
|
|
9
|
+
export declare function utf8(text: string): Uint8Array;
|
package/dist/base64.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Base64url (RFC 4648 §5, unpadded) and base64 helpers.
|
|
2
|
+
//
|
|
3
|
+
// Hand-rolled rather than reaching for Node's Buffer: this package is
|
|
4
|
+
// browser-usable, and `_meta.signature.sig` must round-trip identically on both
|
|
5
|
+
// sides or every signature written by a browser fails in Node.
|
|
6
|
+
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
7
|
+
const B64 = `${ALPHABET}+/`;
|
|
8
|
+
const B64URL = `${ALPHABET}-_`;
|
|
9
|
+
function encodeWith(bytes, alphabet, pad) {
|
|
10
|
+
let out = '';
|
|
11
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
12
|
+
const b0 = bytes[i];
|
|
13
|
+
const b1 = bytes[i + 1];
|
|
14
|
+
const b2 = bytes[i + 2];
|
|
15
|
+
const triple = (b0 << 16) | ((b1 ?? 0) << 8) | (b2 ?? 0);
|
|
16
|
+
out += alphabet[(triple >> 18) & 63];
|
|
17
|
+
out += alphabet[(triple >> 12) & 63];
|
|
18
|
+
out += b1 === undefined ? (pad ? '=' : '') : alphabet[(triple >> 6) & 63];
|
|
19
|
+
out += b2 === undefined ? (pad ? '=' : '') : alphabet[triple & 63];
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function decodeWith(text, alphabet) {
|
|
24
|
+
const clean = text.replace(/=+$/, '');
|
|
25
|
+
const bytes = [];
|
|
26
|
+
let acc = 0;
|
|
27
|
+
let bits = 0;
|
|
28
|
+
for (const ch of clean) {
|
|
29
|
+
const value = alphabet.indexOf(ch);
|
|
30
|
+
if (value < 0)
|
|
31
|
+
throw new Error(`invalid base64 character '${ch}'`);
|
|
32
|
+
acc = (acc << 6) | value;
|
|
33
|
+
bits += 6;
|
|
34
|
+
if (bits >= 8) {
|
|
35
|
+
bits -= 8;
|
|
36
|
+
bytes.push((acc >> bits) & 0xff);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return Uint8Array.from(bytes);
|
|
40
|
+
}
|
|
41
|
+
export function toBase64Url(bytes) {
|
|
42
|
+
return encodeWith(bytes, B64URL, false);
|
|
43
|
+
}
|
|
44
|
+
export function fromBase64Url(text) {
|
|
45
|
+
return decodeWith(text, B64URL);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decode either base64 or base64url. Key material arrives from openssl (base64,
|
|
49
|
+
* padded) far more often than from a JS encoder, and refusing the common form
|
|
50
|
+
* on a purely cosmetic difference would be a bad trade for zero safety.
|
|
51
|
+
*/
|
|
52
|
+
export function fromBase64Any(text) {
|
|
53
|
+
const normalized = text.trim().replace(/\s+/g, '');
|
|
54
|
+
return /[-_]/.test(normalized)
|
|
55
|
+
? decodeWith(normalized, B64URL)
|
|
56
|
+
: decodeWith(normalized, B64);
|
|
57
|
+
}
|
|
58
|
+
export function utf8(text) {
|
|
59
|
+
return new TextEncoder().encode(text);
|
|
60
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SigningErrorCode =
|
|
2
|
+
/** No Web Crypto provider (`crypto.subtle`) in this runtime. */
|
|
3
|
+
'SIG_NO_CRYPTO'
|
|
4
|
+
/** The runtime rejected the algorithm — e.g. Ed25519 on a pre-18.4 Node. */
|
|
5
|
+
| 'SIG_ALGORITHM_UNSUPPORTED'
|
|
6
|
+
/** Key material could not be imported (wrong format, wrong curve, corrupt). */
|
|
7
|
+
| 'SIG_BAD_KEY'
|
|
8
|
+
/** The block cannot produce a signing input — almost always a missing hash. */
|
|
9
|
+
| 'SIG_UNSIGNABLE'
|
|
10
|
+
/** A key-store document is malformed. */
|
|
11
|
+
| 'SIG_BAD_KEYSTORE';
|
|
12
|
+
export declare class SigningError extends Error {
|
|
13
|
+
readonly code: SigningErrorCode;
|
|
14
|
+
constructor(code: SigningErrorCode, message: string);
|
|
15
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Typed errors for the signing package.
|
|
2
|
+
//
|
|
3
|
+
// Mirrors the repo convention (`ProtocolError`, `CalcError`, `ExcelEmitError`):
|
|
4
|
+
// a bare `Error` from a crypto path is unactionable, because the four things
|
|
5
|
+
// that go wrong here — no provider, unusable key material, an unsignable block,
|
|
6
|
+
// an unreadable key store — call for four different fixes.
|
|
7
|
+
export class SigningError extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(`[${code}] ${message}`);
|
|
11
|
+
this.name = 'SigningError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { SigningError } from './errors.js';
|
|
2
|
+
export type { SigningErrorCode } from './errors.js';
|
|
3
|
+
export { toBase64Url, fromBase64Url, fromBase64Any } from './base64.js';
|
|
4
|
+
export { isKnownAlgorithm } from './algorithms.js';
|
|
5
|
+
export { InMemoryKeyStore, importPublicKey, importPrivateKey, generateSigningKeyPair, exportPublicKeyJwk, } from './keys.js';
|
|
6
|
+
export type { KeyStore, SignerKey, SigningKey, PublicKeyMaterial } from './keys.js';
|
|
7
|
+
export { loadKeyStoreDocument, parseKeyStore, loadKeyStoreFile, } from './keystore-file.js';
|
|
8
|
+
export type { KeyStoreDocument, KeyStoreEntry } from './keystore-file.js';
|
|
9
|
+
export { signBlock, stampBlockSignature, signReceipt, stampReceiptSignature } from './sign.js';
|
|
10
|
+
export type { SignBlockOptions } from './sign.js';
|
|
11
|
+
export { verifyBlockSignature, createBlockSignatureVerifier, createReceiptSignatureVerifier, } from './verify.js';
|
|
12
|
+
export type { BlockVerification, SigVerifyError } from './verify.js';
|
|
13
|
+
export { signModule, stampModuleSignature, createModuleSignatureVerifier } from './modules.js';
|
|
14
|
+
export type { SignModuleOptions } from './modules.js';
|
|
15
|
+
export declare const SIGNING_PACKAGE_NAME: "@uwmd/signing";
|
|
16
|
+
export declare const SIGNING_VERSION = "0.1.0";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @uwmd/signing — block and receipt signatures for UW Markdown (RFC 0010).
|
|
2
|
+
//
|
|
3
|
+
// Why this is a separate package: `@uwmd/core` is deliberately
|
|
4
|
+
// zero-cryptography. Signed blocks are for regulated lender data rooms and
|
|
5
|
+
// multi-party deal flow, not the everyday case, and the overwhelming majority
|
|
6
|
+
// of adopters should never take a crypto dependency to read an underwriting
|
|
7
|
+
// file. Core defines the wire shape and the canonical signing input (both
|
|
8
|
+
// normative, both crypto-free); the algorithms live here.
|
|
9
|
+
//
|
|
10
|
+
// Two seams connect them, and neither is a back door: `verifyChain(parsed,
|
|
11
|
+
// { signatureVerifier })` for blocks, and `verifyReceipt(..., {
|
|
12
|
+
// signatureVerifier })` for receipts. Without a verifier, core reports that
|
|
13
|
+
// signatures were *present and unchecked* rather than treating them as valid.
|
|
14
|
+
export { SigningError } from './errors.js';
|
|
15
|
+
export { toBase64Url, fromBase64Url, fromBase64Any } from './base64.js';
|
|
16
|
+
export { isKnownAlgorithm } from './algorithms.js';
|
|
17
|
+
export { InMemoryKeyStore, importPublicKey, importPrivateKey, generateSigningKeyPair, exportPublicKeyJwk, } from './keys.js';
|
|
18
|
+
export { loadKeyStoreDocument, parseKeyStore, loadKeyStoreFile, } from './keystore-file.js';
|
|
19
|
+
export { signBlock, stampBlockSignature, signReceipt, stampReceiptSignature } from './sign.js';
|
|
20
|
+
export { verifyBlockSignature, createBlockSignatureVerifier, createReceiptSignatureVerifier, } from './verify.js';
|
|
21
|
+
export { signModule, stampModuleSignature, createModuleSignatureVerifier } from './modules.js';
|
|
22
|
+
export const SIGNING_PACKAGE_NAME = '@uwmd/signing';
|
|
23
|
+
export const SIGNING_VERSION = '0.1.0';
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { UWSignatureAlgorithm } from '@uwmd/core';
|
|
2
|
+
/** A public key a verifier trusts, resolved from a `kid`. */
|
|
3
|
+
export interface SignerKey {
|
|
4
|
+
kid: string;
|
|
5
|
+
alg: UWSignatureAlgorithm;
|
|
6
|
+
publicKey: CryptoKey;
|
|
7
|
+
}
|
|
8
|
+
/** A private key a signer holds. Never serialized by this package. */
|
|
9
|
+
export interface SigningKey {
|
|
10
|
+
kid: string;
|
|
11
|
+
alg: UWSignatureAlgorithm;
|
|
12
|
+
privateKey: CryptoKey;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Resolves a `kid` to the public key a verifier should check against.
|
|
16
|
+
*
|
|
17
|
+
* `null` means "I do not hold this key" and MUST NOT be conflated with "the
|
|
18
|
+
* signature is bad" — they are INT-06 and INT-07 respectively, and a verifier
|
|
19
|
+
* that merges them tells an operator to re-sign when the real fix is to load a
|
|
20
|
+
* key.
|
|
21
|
+
*/
|
|
22
|
+
export interface KeyStore {
|
|
23
|
+
resolve(kid: string): Promise<SignerKey | null>;
|
|
24
|
+
}
|
|
25
|
+
/** Key material as it appears in a key-store document. */
|
|
26
|
+
export type PublicKeyMaterial =
|
|
27
|
+
/** JWK — the portable form; `kty`/`crv` are checked against `alg`. */
|
|
28
|
+
{
|
|
29
|
+
jwk: JsonWebKey;
|
|
30
|
+
}
|
|
31
|
+
/** PKIX SubjectPublicKeyInfo, base64 or base64url, with or without padding. */
|
|
32
|
+
| {
|
|
33
|
+
spki: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function importPublicKey(alg: string, material: PublicKeyMaterial): Promise<CryptoKey>;
|
|
36
|
+
export declare function importPrivateKey(alg: string, material: {
|
|
37
|
+
jwk: JsonWebKey;
|
|
38
|
+
} | {
|
|
39
|
+
pkcs8: string;
|
|
40
|
+
}): Promise<CryptoKey>;
|
|
41
|
+
/** An in-memory {@link KeyStore}. The reference verifier for tests and hosts. */
|
|
42
|
+
export declare class InMemoryKeyStore implements KeyStore {
|
|
43
|
+
private readonly keys;
|
|
44
|
+
constructor(keys?: Iterable<SignerKey>);
|
|
45
|
+
add(key: SignerKey): this;
|
|
46
|
+
resolve(kid: string): Promise<SignerKey | null>;
|
|
47
|
+
/**
|
|
48
|
+
* Key ids currently held. Key *rotation* is "issue under a new kid and keep
|
|
49
|
+
* the old one loaded" (RFC 0010 §Unresolved questions), so a store routinely
|
|
50
|
+
* holds several keys for one logical signer and this is how an operator sees
|
|
51
|
+
* that it does.
|
|
52
|
+
*/
|
|
53
|
+
get kids(): string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Generate a key pair for one of the admitted algorithms.
|
|
57
|
+
*
|
|
58
|
+
* Present for tests, local development, and `uwmd keygen`-style tooling — not a
|
|
59
|
+
* key-management system. Production keys belong in an HSM or cloud KMS behind a
|
|
60
|
+
* custom {@link KeyStore}.
|
|
61
|
+
*/
|
|
62
|
+
export declare function generateSigningKeyPair(alg: UWSignatureAlgorithm, kid: string): Promise<{
|
|
63
|
+
signing: SigningKey;
|
|
64
|
+
verifying: SignerKey;
|
|
65
|
+
}>;
|
|
66
|
+
/** Export a public key as a JWK, for writing into a key-store document. */
|
|
67
|
+
export declare function exportPublicKeyJwk(key: SignerKey): Promise<JsonWebKey>;
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Key import/export and the key-store contract.
|
|
2
|
+
//
|
|
3
|
+
// Key *distribution* is deliberately out of scope — RFC 0010 §Alternatives
|
|
4
|
+
// rejects inline public keys precisely because a key that travels with the
|
|
5
|
+
// document it authenticates proves nothing. What this file defines is the
|
|
6
|
+
// narrow thing the spec does need: how a `kid` resolves to usable key material.
|
|
7
|
+
import { algorithmParams, isKnownAlgorithm, subtle } from './algorithms.js';
|
|
8
|
+
import { fromBase64Any } from './base64.js';
|
|
9
|
+
import { SigningError } from './errors.js';
|
|
10
|
+
export async function importPublicKey(alg, material) {
|
|
11
|
+
return importKey(alg, material, 'public');
|
|
12
|
+
}
|
|
13
|
+
export async function importPrivateKey(alg, material) {
|
|
14
|
+
const normalized = 'jwk' in material ? { jwk: material.jwk } : { spki: material.pkcs8 };
|
|
15
|
+
return importKey(alg, normalized, 'private');
|
|
16
|
+
}
|
|
17
|
+
async function importKey(alg, material, kind) {
|
|
18
|
+
if (!isKnownAlgorithm(alg)) {
|
|
19
|
+
throw new SigningError('SIG_ALGORITHM_UNSUPPORTED', `Unknown signature algorithm '${alg}'.`);
|
|
20
|
+
}
|
|
21
|
+
const params = algorithmParams(alg);
|
|
22
|
+
const usages = kind === 'public' ? ['verify'] : ['sign'];
|
|
23
|
+
try {
|
|
24
|
+
if ('jwk' in material) {
|
|
25
|
+
assertJwkMatchesAlgorithm(alg, material.jwk, params.kty, params.crv);
|
|
26
|
+
return await subtle().importKey('jwk', material.jwk, params.keyParams, true, usages);
|
|
27
|
+
}
|
|
28
|
+
const bytes = fromBase64Any(material.spki);
|
|
29
|
+
const format = kind === 'public' ? 'spki' : 'pkcs8';
|
|
30
|
+
return await subtle().importKey(format, bytes, params.keyParams, true, usages);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof SigningError)
|
|
34
|
+
throw error;
|
|
35
|
+
throw new SigningError('SIG_BAD_KEY', `Could not import the ${kind} key for '${alg}': ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Reject a JWK whose curve disagrees with the declared `alg` *before* handing
|
|
40
|
+
* it to Web Crypto.
|
|
41
|
+
*
|
|
42
|
+
* Without this, a P-256 key labelled `es384` imports cleanly on some runtimes
|
|
43
|
+
* and then fails every verification, which reads as "the document was tampered
|
|
44
|
+
* with" when the truth is "the key store is mislabelled".
|
|
45
|
+
*/
|
|
46
|
+
function assertJwkMatchesAlgorithm(alg, jwk, kty, crv) {
|
|
47
|
+
if (jwk.kty !== kty || jwk.crv !== crv) {
|
|
48
|
+
throw new SigningError('SIG_BAD_KEY', `Key declares alg '${alg}' (expects kty=${kty}, crv=${crv}) but the JWK is kty=${String(jwk.kty)}, crv=${String(jwk.crv)}.`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** An in-memory {@link KeyStore}. The reference verifier for tests and hosts. */
|
|
52
|
+
export class InMemoryKeyStore {
|
|
53
|
+
keys = new Map();
|
|
54
|
+
constructor(keys = []) {
|
|
55
|
+
for (const key of keys)
|
|
56
|
+
this.add(key);
|
|
57
|
+
}
|
|
58
|
+
add(key) {
|
|
59
|
+
this.keys.set(key.kid, key);
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
async resolve(kid) {
|
|
63
|
+
return this.keys.get(kid) ?? null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Key ids currently held. Key *rotation* is "issue under a new kid and keep
|
|
67
|
+
* the old one loaded" (RFC 0010 §Unresolved questions), so a store routinely
|
|
68
|
+
* holds several keys for one logical signer and this is how an operator sees
|
|
69
|
+
* that it does.
|
|
70
|
+
*/
|
|
71
|
+
get kids() {
|
|
72
|
+
return [...this.keys.keys()];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Generate a key pair for one of the admitted algorithms.
|
|
77
|
+
*
|
|
78
|
+
* Present for tests, local development, and `uwmd keygen`-style tooling — not a
|
|
79
|
+
* key-management system. Production keys belong in an HSM or cloud KMS behind a
|
|
80
|
+
* custom {@link KeyStore}.
|
|
81
|
+
*/
|
|
82
|
+
export async function generateSigningKeyPair(alg, kid) {
|
|
83
|
+
const params = algorithmParams(alg);
|
|
84
|
+
let pair;
|
|
85
|
+
try {
|
|
86
|
+
pair = (await subtle().generateKey(params.keyParams, true, [
|
|
87
|
+
'sign',
|
|
88
|
+
'verify',
|
|
89
|
+
]));
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new SigningError('SIG_ALGORITHM_UNSUPPORTED', `This runtime cannot generate '${alg}' keys: ${error.message}`);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
signing: { kid, alg, privateKey: pair.privateKey },
|
|
96
|
+
verifying: { kid, alg, publicKey: pair.publicKey },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** Export a public key as a JWK, for writing into a key-store document. */
|
|
100
|
+
export async function exportPublicKeyJwk(key) {
|
|
101
|
+
return subtle().exportKey('jwk', key.publicKey);
|
|
102
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { UWSignatureAlgorithm } from '@uwmd/core';
|
|
2
|
+
import { type KeyStore } from './keys.js';
|
|
3
|
+
/** One entry. Exactly one of `public_key_jwk` / `public_key_spki` is required. */
|
|
4
|
+
export interface KeyStoreEntry {
|
|
5
|
+
kid: string;
|
|
6
|
+
alg: UWSignatureAlgorithm;
|
|
7
|
+
public_key_jwk?: JsonWebKey;
|
|
8
|
+
/** PKIX SubjectPublicKeyInfo, base64 (what `openssl pkey -pubout` emits). */
|
|
9
|
+
public_key_spki?: string;
|
|
10
|
+
/** Free-text, for humans reading the file. Never consulted by verification. */
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface KeyStoreDocument {
|
|
14
|
+
/** `"1"` today. A reader MUST refuse a version it does not know. */
|
|
15
|
+
keystore_version: string;
|
|
16
|
+
keys: KeyStoreEntry[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse and import a key-store document into an {@link InMemoryKeyStore}.
|
|
20
|
+
*
|
|
21
|
+
* Every key is imported eagerly. Deferring import to first use would turn a
|
|
22
|
+
* typo in the store into a per-block INT-07, which reads as tampering; failing
|
|
23
|
+
* once, loudly, at load time says what is actually wrong.
|
|
24
|
+
*/
|
|
25
|
+
export declare function loadKeyStoreDocument(document: unknown): Promise<KeyStore>;
|
|
26
|
+
/** Parse a key-store document from JSON text. */
|
|
27
|
+
export declare function parseKeyStore(json: string): Promise<KeyStore>;
|
|
28
|
+
/**
|
|
29
|
+
* Read a key-store document from disk. Node-only by construction — the import
|
|
30
|
+
* is dynamic so that bundling this module for a browser does not pull in `fs`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function loadKeyStoreFile(path: string): Promise<KeyStore>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// The reference key-store document: a JSON file mapping `kid` to a public key.
|
|
2
|
+
//
|
|
3
|
+
// RFC 0010 takes no position on key distribution, so this is explicitly a
|
|
4
|
+
// *reference* format, not a normative one — an adopter backing their store with
|
|
5
|
+
// an HSM implements `KeyStore` directly and never touches this file. It exists
|
|
6
|
+
// so that `uwmd verify --signing --keystore=<path>` has something to read and
|
|
7
|
+
// so the conformance fixtures have a portable way to ship a public key.
|
|
8
|
+
import { isKnownAlgorithm } from './algorithms.js';
|
|
9
|
+
import { SigningError } from './errors.js';
|
|
10
|
+
import { InMemoryKeyStore, importPublicKey } from './keys.js';
|
|
11
|
+
const SUPPORTED_KEYSTORE_VERSIONS = ['1'];
|
|
12
|
+
/**
|
|
13
|
+
* Parse and import a key-store document into an {@link InMemoryKeyStore}.
|
|
14
|
+
*
|
|
15
|
+
* Every key is imported eagerly. Deferring import to first use would turn a
|
|
16
|
+
* typo in the store into a per-block INT-07, which reads as tampering; failing
|
|
17
|
+
* once, loudly, at load time says what is actually wrong.
|
|
18
|
+
*/
|
|
19
|
+
export async function loadKeyStoreDocument(document) {
|
|
20
|
+
const parsed = assertKeyStoreDocument(document);
|
|
21
|
+
const keys = [];
|
|
22
|
+
for (const entry of parsed.keys) {
|
|
23
|
+
const material = entry.public_key_jwk
|
|
24
|
+
? { jwk: entry.public_key_jwk }
|
|
25
|
+
: { spki: entry.public_key_spki };
|
|
26
|
+
keys.push({
|
|
27
|
+
kid: entry.kid,
|
|
28
|
+
alg: entry.alg,
|
|
29
|
+
publicKey: await importPublicKey(entry.alg, material),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return new InMemoryKeyStore(keys);
|
|
33
|
+
}
|
|
34
|
+
/** Parse a key-store document from JSON text. */
|
|
35
|
+
export async function parseKeyStore(json) {
|
|
36
|
+
let document;
|
|
37
|
+
try {
|
|
38
|
+
document = JSON.parse(json);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
throw new SigningError('SIG_BAD_KEYSTORE', `Key store is not valid JSON: ${error.message}`);
|
|
42
|
+
}
|
|
43
|
+
return loadKeyStoreDocument(document);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Read a key-store document from disk. Node-only by construction — the import
|
|
47
|
+
* is dynamic so that bundling this module for a browser does not pull in `fs`.
|
|
48
|
+
*/
|
|
49
|
+
export async function loadKeyStoreFile(path) {
|
|
50
|
+
const { readFileSync } = await import('node:fs');
|
|
51
|
+
let text;
|
|
52
|
+
try {
|
|
53
|
+
text = readFileSync(path, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new SigningError('SIG_BAD_KEYSTORE', `Could not read key store '${path}': ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
return parseKeyStore(text);
|
|
59
|
+
}
|
|
60
|
+
function assertKeyStoreDocument(value) {
|
|
61
|
+
const fail = (message) => {
|
|
62
|
+
throw new SigningError('SIG_BAD_KEYSTORE', message);
|
|
63
|
+
};
|
|
64
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
65
|
+
return fail('Key store must be a JSON object.');
|
|
66
|
+
}
|
|
67
|
+
const doc = value;
|
|
68
|
+
const version = doc['keystore_version'];
|
|
69
|
+
if (typeof version !== 'string')
|
|
70
|
+
return fail('keystore_version must be a string.');
|
|
71
|
+
if (!SUPPORTED_KEYSTORE_VERSIONS.includes(version)) {
|
|
72
|
+
return fail(`Unsupported keystore_version '${version}'; this reader knows ${SUPPORTED_KEYSTORE_VERSIONS.join(', ')}.`);
|
|
73
|
+
}
|
|
74
|
+
const keys = doc['keys'];
|
|
75
|
+
if (!Array.isArray(keys))
|
|
76
|
+
return fail('keys must be an array.');
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const entries = [];
|
|
79
|
+
for (const [index, raw] of keys.entries()) {
|
|
80
|
+
const at = `keys[${index}]`;
|
|
81
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
82
|
+
return fail(`${at} must be an object.`);
|
|
83
|
+
}
|
|
84
|
+
const entry = raw;
|
|
85
|
+
const kid = entry['kid'];
|
|
86
|
+
if (typeof kid !== 'string' || kid.length === 0)
|
|
87
|
+
return fail(`${at}.kid must be a non-empty string.`);
|
|
88
|
+
// A duplicate kid is refused rather than last-wins: the whole point of a
|
|
89
|
+
// kid is that it names one key, and silently picking one of two makes
|
|
90
|
+
// verification depend on file order.
|
|
91
|
+
if (seen.has(kid))
|
|
92
|
+
return fail(`${at}.kid '${kid}' is declared more than once.`);
|
|
93
|
+
seen.add(kid);
|
|
94
|
+
const alg = entry['alg'];
|
|
95
|
+
if (typeof alg !== 'string' || !isKnownAlgorithm(alg)) {
|
|
96
|
+
return fail(`${at}.alg must be one of ed25519, es256, es384.`);
|
|
97
|
+
}
|
|
98
|
+
const jwk = entry['public_key_jwk'];
|
|
99
|
+
const spki = entry['public_key_spki'];
|
|
100
|
+
const hasJwk = jwk !== undefined;
|
|
101
|
+
const hasSpki = spki !== undefined;
|
|
102
|
+
if (hasJwk === hasSpki) {
|
|
103
|
+
return fail(`${at} must carry exactly one of public_key_jwk or public_key_spki.`);
|
|
104
|
+
}
|
|
105
|
+
if (hasJwk && (typeof jwk !== 'object' || jwk === null || Array.isArray(jwk))) {
|
|
106
|
+
return fail(`${at}.public_key_jwk must be an object.`);
|
|
107
|
+
}
|
|
108
|
+
if (hasSpki && typeof spki !== 'string') {
|
|
109
|
+
return fail(`${at}.public_key_spki must be a base64 string.`);
|
|
110
|
+
}
|
|
111
|
+
entries.push({
|
|
112
|
+
kid,
|
|
113
|
+
alg,
|
|
114
|
+
...(hasJwk ? { public_key_jwk: jwk } : {}),
|
|
115
|
+
...(hasSpki ? { public_key_spki: spki } : {}),
|
|
116
|
+
...(typeof entry['description'] === 'string' ? { description: entry['description'] } : {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
return { keystore_version: version, keys: entries };
|
|
120
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ModuleManifest, type ModuleSignature, type ModuleSignatureVerifier } from '@uwmd/core';
|
|
2
|
+
import type { KeyStore, SigningKey } from './keys.js';
|
|
3
|
+
export interface SignModuleOptions {
|
|
4
|
+
/** ISO 8601 instant to stamp as `signed_at`. Defaults to now. */
|
|
5
|
+
signedAt?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Identity claim to embed. Advisory: a signature proves the key holder
|
|
8
|
+
* asserted this, never that the assertion is true.
|
|
9
|
+
*/
|
|
10
|
+
identity?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Sign a module manifest, returning the detached signature.
|
|
14
|
+
*
|
|
15
|
+
* Does not mutate the manifest — use {@link stampModuleSignature}. A manifest
|
|
16
|
+
* that already carries a `signature` can be re-signed: the payload omits the
|
|
17
|
+
* field, so the old signature does not perturb the new one.
|
|
18
|
+
*/
|
|
19
|
+
export declare function signModule(manifest: ModuleManifest, key: SigningKey, options?: SignModuleOptions): Promise<ModuleSignature>;
|
|
20
|
+
/** Return a copy of `manifest` carrying `signature`. */
|
|
21
|
+
export declare function stampModuleSignature(manifest: ModuleManifest, signature: ModuleSignature): ModuleManifest;
|
|
22
|
+
/**
|
|
23
|
+
* A {@link ModuleSignatureVerifier} for `verifyModuleSignature` and the async
|
|
24
|
+
* module loaders.
|
|
25
|
+
*
|
|
26
|
+
* Scheme and shape are core's job — it has already run `checkSignatureShape`
|
|
27
|
+
* before reaching here — so this checks the cryptography and nothing else.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createModuleSignatureVerifier(store: KeyStore): ModuleSignatureVerifier;
|
package/dist/modules.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Module manifest signing and verification (RFC 0002, protocol §X.1).
|
|
2
|
+
//
|
|
3
|
+
// Deliberately thin. A module signature is the same act as a block signature
|
|
4
|
+
// over different bytes, so this file is mostly a change of payload — the key
|
|
5
|
+
// handling, the algorithm table, and the actual `subtle.verify` call are the
|
|
6
|
+
// ones `sign.ts` / `verify.ts` already use. Two verifiers would mean two
|
|
7
|
+
// chances to disagree about what a valid signature is.
|
|
8
|
+
import { MODULE_SIGNATURE_SCHEME, moduleSigningPayload, } from '@uwmd/core';
|
|
9
|
+
import { verifyRawSignature } from './verify.js';
|
|
10
|
+
import { signPayload } from './sign.js';
|
|
11
|
+
/**
|
|
12
|
+
* Sign a module manifest, returning the detached signature.
|
|
13
|
+
*
|
|
14
|
+
* Does not mutate the manifest — use {@link stampModuleSignature}. A manifest
|
|
15
|
+
* that already carries a `signature` can be re-signed: the payload omits the
|
|
16
|
+
* field, so the old signature does not perturb the new one.
|
|
17
|
+
*/
|
|
18
|
+
export async function signModule(manifest, key, options = {}) {
|
|
19
|
+
const signedAt = options.signedAt ?? new Date().toISOString();
|
|
20
|
+
return {
|
|
21
|
+
scheme: MODULE_SIGNATURE_SCHEME,
|
|
22
|
+
alg: key.alg,
|
|
23
|
+
kid: key.kid,
|
|
24
|
+
sig: await signPayload(moduleSigningPayload(manifest), key),
|
|
25
|
+
signed_at: signedAt,
|
|
26
|
+
...(options.identity !== undefined ? { identity: options.identity } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Return a copy of `manifest` carrying `signature`. */
|
|
30
|
+
export function stampModuleSignature(manifest, signature) {
|
|
31
|
+
return { ...manifest, signature };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A {@link ModuleSignatureVerifier} for `verifyModuleSignature` and the async
|
|
35
|
+
* module loaders.
|
|
36
|
+
*
|
|
37
|
+
* Scheme and shape are core's job — it has already run `checkSignatureShape`
|
|
38
|
+
* before reaching here — so this checks the cryptography and nothing else.
|
|
39
|
+
*/
|
|
40
|
+
export function createModuleSignatureVerifier(store) {
|
|
41
|
+
return {
|
|
42
|
+
async verify(payload, signature) {
|
|
43
|
+
const verdict = await verifyRawSignature(payload, signature.alg, signature.kid, signature.sig, store);
|
|
44
|
+
if (verdict.ok)
|
|
45
|
+
return { ok: true };
|
|
46
|
+
// `algorithm_mismatch` folds into `bad_signature` at this seam: core's
|
|
47
|
+
// ModuleSignatureVerifier contract has three outcomes, and a key whose
|
|
48
|
+
// curve disagrees with the manifest's claim is a signature that does not
|
|
49
|
+
// verify, not a shape problem core could have caught.
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: verdict.reason === 'unknown_kid' ? 'unknown_kid' : verdict.reason === 'malformed' ? 'malformed' : 'bad_signature',
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/sign.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type UWBlock, type UWBlockSignature, type UWReceipt, type UWReceiptSignature } from '@uwmd/core';
|
|
2
|
+
import type { SigningKey } from './keys.js';
|
|
3
|
+
export interface SignBlockOptions {
|
|
4
|
+
/**
|
|
5
|
+
* ISO 8601 instant to stamp as `signed_at`. Defaults to now.
|
|
6
|
+
*
|
|
7
|
+
* Injectable because a signature is only reproducible if its inputs are, and
|
|
8
|
+
* `signed_at` is one of them — conformance fixtures pin it.
|
|
9
|
+
*/
|
|
10
|
+
signedAt?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Stamp `content_hash` from the block's current content when it is absent.
|
|
13
|
+
*
|
|
14
|
+
* Off by default, and deliberately so: signing a block whose hash you just
|
|
15
|
+
* computed yourself is a different act from signing a hash somebody else
|
|
16
|
+
* committed to, and quietly doing the former would let a caller sign content
|
|
17
|
+
* that never passed `verifyChain`.
|
|
18
|
+
*/
|
|
19
|
+
stampContentHash?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Sign a block, returning the detached {@link UWBlockSignature}.
|
|
23
|
+
*
|
|
24
|
+
* The block is not mutated — use {@link stampBlockSignature} to attach the
|
|
25
|
+
* result. Splitting them keeps the byte-preservation invariant honest: the
|
|
26
|
+
* caller decides when the document changes.
|
|
27
|
+
*/
|
|
28
|
+
export declare function signBlock(block: UWBlock, key: SigningKey, options?: SignBlockOptions): Promise<UWBlockSignature>;
|
|
29
|
+
/**
|
|
30
|
+
* Return a copy of `block` carrying `signature` (and, when `signBlock` computed
|
|
31
|
+
* one, the `content_hash` it committed to).
|
|
32
|
+
*/
|
|
33
|
+
export declare function stampBlockSignature(block: UWBlock, signature: UWBlockSignature, contentHash?: string): UWBlock;
|
|
34
|
+
/**
|
|
35
|
+
* Sign a verification receipt (RFC 0016). The payload is the receipt with
|
|
36
|
+
* `signature: null`, canonicalized — `receiptSigningPayload` in core is the
|
|
37
|
+
* single definition of that, and this package never restates it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function signReceipt(receipt: UWReceipt, key: SigningKey): Promise<UWReceiptSignature>;
|
|
40
|
+
/** Attach a receipt signature, returning a new receipt. */
|
|
41
|
+
export declare function stampReceiptSignature(receipt: UWReceipt, signature: UWReceiptSignature): UWReceipt;
|
|
42
|
+
/**
|
|
43
|
+
* Sign arbitrary canonical bytes. Shared by blocks, receipts, and module
|
|
44
|
+
* manifests — the three artifacts differ only in what they canonicalize.
|
|
45
|
+
*/
|
|
46
|
+
export declare function signPayload(payload: string, key: SigningKey): Promise<string>;
|
package/dist/sign.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Signing: produce a detached signature over a block or a receipt.
|
|
2
|
+
import { canonicalBlockSigningInput, computeBlockHash, receiptSigningPayload, } from '@uwmd/core';
|
|
3
|
+
import { algorithmParams, subtle } from './algorithms.js';
|
|
4
|
+
import { toBase64Url, utf8 } from './base64.js';
|
|
5
|
+
import { SigningError } from './errors.js';
|
|
6
|
+
/**
|
|
7
|
+
* Sign a block, returning the detached {@link UWBlockSignature}.
|
|
8
|
+
*
|
|
9
|
+
* The block is not mutated — use {@link stampBlockSignature} to attach the
|
|
10
|
+
* result. Splitting them keeps the byte-preservation invariant honest: the
|
|
11
|
+
* caller decides when the document changes.
|
|
12
|
+
*/
|
|
13
|
+
export async function signBlock(block, key, options = {}) {
|
|
14
|
+
let contentHash = block.meta.content_hash;
|
|
15
|
+
if (typeof contentHash !== 'string' || contentHash.length === 0) {
|
|
16
|
+
if (!options.stampContentHash) {
|
|
17
|
+
throw new SigningError('SIG_UNSIGNABLE', `Block '${block.meta.section}' has no _meta.content_hash. Stamp one first (or pass stampContentHash) — a signature over an absent hash commits to nothing and validates as INT-05.`);
|
|
18
|
+
}
|
|
19
|
+
contentHash = await computeBlockHash(block);
|
|
20
|
+
}
|
|
21
|
+
const signedAt = options.signedAt ?? new Date().toISOString();
|
|
22
|
+
const payload = canonicalBlockSigningInput({
|
|
23
|
+
content_hash: contentHash,
|
|
24
|
+
section: block.meta.section,
|
|
25
|
+
actor: block.meta.actor,
|
|
26
|
+
timestamp: block.meta.timestamp,
|
|
27
|
+
kid: key.kid,
|
|
28
|
+
signed_at: signedAt,
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
alg: key.alg,
|
|
32
|
+
kid: key.kid,
|
|
33
|
+
sig: await signPayload(payload, key),
|
|
34
|
+
signed_at: signedAt,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Return a copy of `block` carrying `signature` (and, when `signBlock` computed
|
|
39
|
+
* one, the `content_hash` it committed to).
|
|
40
|
+
*/
|
|
41
|
+
export function stampBlockSignature(block, signature, contentHash) {
|
|
42
|
+
return {
|
|
43
|
+
...block,
|
|
44
|
+
meta: {
|
|
45
|
+
...block.meta,
|
|
46
|
+
...(contentHash ? { content_hash: contentHash } : {}),
|
|
47
|
+
signature,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Sign a verification receipt (RFC 0016). The payload is the receipt with
|
|
53
|
+
* `signature: null`, canonicalized — `receiptSigningPayload` in core is the
|
|
54
|
+
* single definition of that, and this package never restates it.
|
|
55
|
+
*/
|
|
56
|
+
export async function signReceipt(receipt, key) {
|
|
57
|
+
return {
|
|
58
|
+
algorithm: key.alg,
|
|
59
|
+
key_id: key.kid,
|
|
60
|
+
value: await signPayload(receiptSigningPayload(receipt), key),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Attach a receipt signature, returning a new receipt. */
|
|
64
|
+
export function stampReceiptSignature(receipt, signature) {
|
|
65
|
+
return { ...receipt, signature };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Sign arbitrary canonical bytes. Shared by blocks, receipts, and module
|
|
69
|
+
* manifests — the three artifacts differ only in what they canonicalize.
|
|
70
|
+
*/
|
|
71
|
+
export async function signPayload(payload, key) {
|
|
72
|
+
const params = algorithmParams(key.alg);
|
|
73
|
+
let bytes;
|
|
74
|
+
try {
|
|
75
|
+
bytes = await subtle().sign(params.signParams, key.privateKey, utf8(payload));
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw new SigningError('SIG_BAD_KEY', `Signing with key '${key.kid}' (${key.alg}) failed: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
return toBase64Url(new Uint8Array(bytes));
|
|
81
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ParsedUWFile, type UWBlock } from '@uwmd/core';
|
|
2
|
+
export declare function makeBlock(sectionId: string, content: Record<string, unknown>, meta?: Partial<UWBlock['meta']>): UWBlock;
|
|
3
|
+
export declare function hashedBlock(sectionId: string, content: Record<string, unknown>, meta?: Partial<UWBlock['meta']>): Promise<UWBlock>;
|
|
4
|
+
export declare function makeFile(sections: Record<string, UWBlock>, superseded?: Record<string, UWBlock[]>): ParsedUWFile;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Block/file fixtures shared by this package's tests.
|
|
2
|
+
//
|
|
3
|
+
// Not exported from `index.ts`: these are test scaffolding, and shipping a
|
|
4
|
+
// block factory in the public API would invite production code to build blocks
|
|
5
|
+
// that never went through the parser.
|
|
6
|
+
import { computeBlockHash } from '@uwmd/core';
|
|
7
|
+
export function makeBlock(sectionId, content, meta = {}) {
|
|
8
|
+
return {
|
|
9
|
+
annotation: { section: sectionId },
|
|
10
|
+
content,
|
|
11
|
+
meta: {
|
|
12
|
+
section: sectionId,
|
|
13
|
+
version: 1,
|
|
14
|
+
superseded: false,
|
|
15
|
+
source: 'manual',
|
|
16
|
+
agent_id: null,
|
|
17
|
+
agent_version: null,
|
|
18
|
+
actor: 'human/jared',
|
|
19
|
+
timestamp: '2026-08-27T00:00:00Z',
|
|
20
|
+
confidence: 'medium',
|
|
21
|
+
human_review_required: false,
|
|
22
|
+
flags: [],
|
|
23
|
+
input_hash: null,
|
|
24
|
+
notes: null,
|
|
25
|
+
...meta,
|
|
26
|
+
},
|
|
27
|
+
prose: '',
|
|
28
|
+
rawJson: '',
|
|
29
|
+
lineStart: 1,
|
|
30
|
+
lineEnd: 1,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export async function hashedBlock(sectionId, content, meta = {}) {
|
|
34
|
+
const block = makeBlock(sectionId, content, meta);
|
|
35
|
+
return { ...block, meta: { ...block.meta, content_hash: await computeBlockHash(block) } };
|
|
36
|
+
}
|
|
37
|
+
export function makeFile(sections, superseded = {}) {
|
|
38
|
+
return {
|
|
39
|
+
frontmatter: { asset_class: 'multifamily' },
|
|
40
|
+
sections,
|
|
41
|
+
prose: {},
|
|
42
|
+
pipeline_log: [],
|
|
43
|
+
custom_calculations: [],
|
|
44
|
+
custom_scenarios: [],
|
|
45
|
+
extensions: {},
|
|
46
|
+
superseded,
|
|
47
|
+
raw: '',
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type BlockSigFailure, type BlockSignatureVerifier, type BlockSigVerdict, type ReceiptSignatureVerifier, type UWBlock } from '@uwmd/core';
|
|
2
|
+
import type { KeyStore } from './keys.js';
|
|
3
|
+
/** RFC 0010's `SigVerifyError`, plus the hash check core cannot do alone. */
|
|
4
|
+
export type SigVerifyError = BlockSigFailure | 'content_hash_mismatch';
|
|
5
|
+
export type BlockVerification = {
|
|
6
|
+
ok: true;
|
|
7
|
+
kid: string;
|
|
8
|
+
} | {
|
|
9
|
+
ok: false;
|
|
10
|
+
reason: SigVerifyError;
|
|
11
|
+
kid?: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Verify a block's signature end to end: recompute the block's `content_hash`,
|
|
15
|
+
* then check the signature over the canonical signing input.
|
|
16
|
+
*
|
|
17
|
+
* The hash recomputation is the part that makes this meaningful. The signature
|
|
18
|
+
* only commits to a hash *value*; without re-deriving that value from the block
|
|
19
|
+
* in front of you, a tampered block with its original signature and original
|
|
20
|
+
* stamped hash verifies happily.
|
|
21
|
+
*/
|
|
22
|
+
export declare function verifyBlockSignature(block: UWBlock, store: KeyStore): Promise<BlockVerification>;
|
|
23
|
+
/**
|
|
24
|
+
* A {@link BlockSignatureVerifier} for `verifyChain(parsed, { signatureVerifier })`.
|
|
25
|
+
*
|
|
26
|
+
* Scoped narrower than {@link verifyBlockSignature} on purpose: `verifyChain`
|
|
27
|
+
* already recomputes every `content_hash` and reports a mismatch as INT-04, so
|
|
28
|
+
* repeating that here would report one tampered block twice under two codes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createBlockSignatureVerifier(store: KeyStore): BlockSignatureVerifier;
|
|
31
|
+
/**
|
|
32
|
+
* A {@link ReceiptSignatureVerifier} for `verifyReceipt(..., { signatureVerifier })`.
|
|
33
|
+
*
|
|
34
|
+
* Without one, a signed receipt verifies as `unverifiable` with RCP-08 — the
|
|
35
|
+
* one advertised receipt feature that shipped unimplemented until this package
|
|
36
|
+
* existed.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createReceiptSignatureVerifier(store: KeyStore): ReceiptSignatureVerifier;
|
|
39
|
+
/**
|
|
40
|
+
* The one place a signature is actually checked. Blocks and receipts carry the
|
|
41
|
+
* same three facts under different key names (`alg`/`kid`/`sig` vs
|
|
42
|
+
* `algorithm`/`key_id`/`value`); flattening them here is what keeps the two
|
|
43
|
+
* surfaces from growing two subtly different verifiers.
|
|
44
|
+
*/
|
|
45
|
+
export declare function verifyRawSignature(payload: string, alg: string, kid: string, sig: string, store: KeyStore): Promise<BlockSigVerdict>;
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Verification: check a detached signature against a key store.
|
|
2
|
+
import { blockSigningPayload, computeBlockHash, } from '@uwmd/core';
|
|
3
|
+
import { algorithmParams, isKnownAlgorithm, subtle } from './algorithms.js';
|
|
4
|
+
import { fromBase64Url, utf8 } from './base64.js';
|
|
5
|
+
/**
|
|
6
|
+
* Verify a block's signature end to end: recompute the block's `content_hash`,
|
|
7
|
+
* then check the signature over the canonical signing input.
|
|
8
|
+
*
|
|
9
|
+
* The hash recomputation is the part that makes this meaningful. The signature
|
|
10
|
+
* only commits to a hash *value*; without re-deriving that value from the block
|
|
11
|
+
* in front of you, a tampered block with its original signature and original
|
|
12
|
+
* stamped hash verifies happily.
|
|
13
|
+
*/
|
|
14
|
+
export async function verifyBlockSignature(block, store) {
|
|
15
|
+
const signature = block.meta.signature;
|
|
16
|
+
if (!signature)
|
|
17
|
+
return { ok: false, reason: 'malformed' };
|
|
18
|
+
if (!isWellFormed(signature))
|
|
19
|
+
return { ok: false, reason: 'malformed', kid: signature.kid };
|
|
20
|
+
const payload = blockSigningPayload(block);
|
|
21
|
+
if (payload === null)
|
|
22
|
+
return { ok: false, reason: 'malformed', kid: signature.kid };
|
|
23
|
+
const recomputed = await computeBlockHash(block);
|
|
24
|
+
if (recomputed !== block.meta.content_hash) {
|
|
25
|
+
return { ok: false, reason: 'content_hash_mismatch', kid: signature.kid };
|
|
26
|
+
}
|
|
27
|
+
const verdict = await verifyDetached(payload, signature, store);
|
|
28
|
+
return verdict.ok ? { ok: true, kid: signature.kid } : { ...verdict, kid: signature.kid };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A {@link BlockSignatureVerifier} for `verifyChain(parsed, { signatureVerifier })`.
|
|
32
|
+
*
|
|
33
|
+
* Scoped narrower than {@link verifyBlockSignature} on purpose: `verifyChain`
|
|
34
|
+
* already recomputes every `content_hash` and reports a mismatch as INT-04, so
|
|
35
|
+
* repeating that here would report one tampered block twice under two codes.
|
|
36
|
+
*/
|
|
37
|
+
export function createBlockSignatureVerifier(store) {
|
|
38
|
+
return {
|
|
39
|
+
async verify(payload, signature) {
|
|
40
|
+
if (!isWellFormed(signature))
|
|
41
|
+
return { ok: false, reason: 'malformed' };
|
|
42
|
+
return verifyDetached(payload, signature, store);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A {@link ReceiptSignatureVerifier} for `verifyReceipt(..., { signatureVerifier })`.
|
|
48
|
+
*
|
|
49
|
+
* Without one, a signed receipt verifies as `unverifiable` with RCP-08 — the
|
|
50
|
+
* one advertised receipt feature that shipped unimplemented until this package
|
|
51
|
+
* existed.
|
|
52
|
+
*/
|
|
53
|
+
export function createReceiptSignatureVerifier(store) {
|
|
54
|
+
return {
|
|
55
|
+
async verify(receipt, signedPayload) {
|
|
56
|
+
const signature = receipt.signature;
|
|
57
|
+
if (!signature)
|
|
58
|
+
return false;
|
|
59
|
+
const verdict = await verifyRawSignature(signedPayload, signature.algorithm, signature.key_id, signature.value, store);
|
|
60
|
+
return verdict.ok;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function verifyDetached(payload, signature, store) {
|
|
65
|
+
return verifyRawSignature(payload, signature.alg, signature.kid, signature.sig, store);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The one place a signature is actually checked. Blocks and receipts carry the
|
|
69
|
+
* same three facts under different key names (`alg`/`kid`/`sig` vs
|
|
70
|
+
* `algorithm`/`key_id`/`value`); flattening them here is what keeps the two
|
|
71
|
+
* surfaces from growing two subtly different verifiers.
|
|
72
|
+
*/
|
|
73
|
+
export async function verifyRawSignature(payload, alg, kid, sig, store) {
|
|
74
|
+
if (!isKnownAlgorithm(alg))
|
|
75
|
+
return { ok: false, reason: 'algorithm_mismatch' };
|
|
76
|
+
const key = await store.resolve(kid);
|
|
77
|
+
if (!key)
|
|
78
|
+
return { ok: false, reason: 'unknown_kid' };
|
|
79
|
+
if (key.alg !== alg)
|
|
80
|
+
return { ok: false, reason: 'algorithm_mismatch' };
|
|
81
|
+
let sigBytes;
|
|
82
|
+
try {
|
|
83
|
+
sigBytes = fromBase64Url(sig);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return { ok: false, reason: 'malformed' };
|
|
87
|
+
}
|
|
88
|
+
const params = algorithmParams(alg);
|
|
89
|
+
let ok;
|
|
90
|
+
try {
|
|
91
|
+
ok = await subtle().verify(params.signParams, key.publicKey, sigBytes, utf8(payload));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Web Crypto throws on a signature of the wrong length for the curve
|
|
95
|
+
// rather than returning false. That is malformed input, not a bad key.
|
|
96
|
+
return { ok: false, reason: 'malformed' };
|
|
97
|
+
}
|
|
98
|
+
return ok ? { ok: true } : { ok: false, reason: 'bad_signature' };
|
|
99
|
+
}
|
|
100
|
+
function isWellFormed(signature) {
|
|
101
|
+
return (typeof signature.alg === 'string' &&
|
|
102
|
+
typeof signature.kid === 'string' &&
|
|
103
|
+
signature.kid.length > 0 &&
|
|
104
|
+
typeof signature.sig === 'string' &&
|
|
105
|
+
signature.sig.length > 0);
|
|
106
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uwmd/signing",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Block and receipt signing for UW Markdown (RFC 0010) — Ed25519 / ES256 / ES384 over the normative signing inputs, with a file-backed reference key store. Kept out of @uwmd/core so the library stays crypto-free.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/UWMD-OSP/UW-Markdown.git",
|
|
8
|
+
"directory": "packages/uwmd-signing"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://uwmd.org",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
|
|
16
|
+
"./keystore-file": { "import": "./dist/keystore-file.js", "types": "./dist/keystore-file.d.ts" }
|
|
17
|
+
},
|
|
18
|
+
"files": ["dist", "README.md"],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"typecheck:tests": "tsc -p tsconfig.test.json"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": { "@uwmd/core": "1.8.0" },
|
|
25
|
+
"devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.4.0", "vitest": "^3.2.6" },
|
|
26
|
+
"engines": { "node": ">=18.4.0" },
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"keywords": [
|
|
29
|
+
"uwmd",
|
|
30
|
+
"underwriting",
|
|
31
|
+
"commercial-real-estate",
|
|
32
|
+
"signing",
|
|
33
|
+
"ed25519",
|
|
34
|
+
"provenance"
|
|
35
|
+
]
|
|
36
|
+
}
|