Status: implemented
Implemented — this change has shipped.
RFC 0002: Module signing
Design change, 2026-08-27 — the shipped scheme is not Sigstore. Everything below the "Proposed change" heading describes a Sigstore-based design. It is preserved as written, because it is still the right long-term target and the motivation section is unchanged. What shipped is a
uwmd-keystorescheme built on RFC 0010's machinery, for reasons set out in Why not Sigstore, yet. Theschemediscriminator was added precisely so that addingsigstorelater is additive rather than breaking.
Summary
Modules extend the standard with new sections, calculations, validations, view models, and agent layers. A malicious or accidentally corrupted module manifest can therefore inject calculations or validations into a host's pipeline. This RFC proposes a Sigstore-style signature on module manifests, verified by the host according to its policy. Signatures are advisory at the protocol level — what to do with an unsigned or invalid-signature module is a host-policy decision, not a normative requirement.
Motivation
Today, ModuleManifest carries no integrity surface. A host that loads a module manifest from disk, npm, or a URL has no built-in way to:
- Verify the module is unmodified since the author published it.
- Verify the publisher is who the module claims (
authors: [...]). - Make a policy decision about whether to load it.
For a CRE underwriting host running modules from third-party authors, this is a real risk: a tampered module could declare a validations rule that silently flips a deal from "blocking" to "warning," or inject a calculation whose formula reads sensitive frontmatter and writes it into a derived value that gets surfaced to the user.
The point of this RFC is not to enforce signatures — that's a host decision — but to give hosts a standard surface to verify against, so two conforming hosts agree on what "signature valid" means.
Proposed change
Protocol spec — Part X (Module System)
Add an optional signature block to ModuleManifest:
export interface ModuleSignature {
/** Signing scheme. v2 ships with 'sigstore' only. */
scheme: 'sigstore';
/** Base64-encoded signature over the canonical manifest bytes. */
signature: string;
/** PEM-encoded signing certificate (Sigstore-issued, OIDC-bound). */
certificate: string;
/** Sigstore Rekor transparency log entry ID. */
rekor_log_id: string;
/** Identity claim (OIDC subject) the signer authenticated as. */
identity: string;
/** Issuer of the OIDC token (e.g., 'https://accounts.google.com'). */
issuer: string;
}
export interface ModuleManifest {
// ... existing fields
signature?: ModuleSignature;
}Canonical manifest bytes for signing = the manifest serialized with signature field omitted, JSON Canonical Form (RFC 8785).
Verification
Add verifyModuleSignature(manifest, options) to @uwmd/core:
export interface VerifyOptions {
/** Trust roots for the Sigstore Fulcio CA. Defaults to the public Sigstore root. */
trust_root?: TrustRoot;
/** Maximum age of the Rekor entry. Defaults to no limit. */
max_age_seconds?: number;
}
export interface VerifyResult {
ok: boolean;
/** Populated when ok=false. */
error?: ProtocolError;
/** Verified identity claims when ok=true. */
identity?: { subject: string; issuer: string; rekor_log_id: string };
}
export function verifyModuleSignature(
manifest: ModuleManifest,
options?: VerifyOptions,
): Promise<VerifyResult>;The function:
- Strips
signaturefrom the manifest, computes JSON Canonical Form. - Verifies the signature against the certificate.
- Verifies the certificate chains to a Sigstore Fulcio root.
- Verifies the Rekor inclusion proof.
- Returns
ok: truewith identity claims, orok: falsewith aProtocolErrorcarrying one of:MOD-SIG-INVALID,MOD-SIG-CHAIN,MOD-SIG-REKOR,MOD-SIG-CANON.
Host policy
The protocol does not mandate that hosts verify. It mandates:
A host that claims to verify module signatures MUST invoke
verifyModuleSignature(or an equivalent that produces identical verdicts) and MUST NOT load a module whose signature fails verification under the host's declared policy.
A host's ImplementationManifest.capabilities gains the optional module-signature-verification capability. Hosts that declare it must honor the verification semantics.
Compatibility analysis
- Existing
.uw.mdfiles — no impact (modules are referenced from frontmatter; the file content does not change). - Existing module manifests — backwards compatible. Manifests without a
signaturefield continue to load. A host's policy decides whether unsigned modules are allowed. - Existing implementers — no behavior change unless they opt into signature verification. Tier-3 calc hosts and Tier-4 agent hosts that load modules can adopt incrementally.
- Module authors — adding a signature is opt-in. The publishing tooling (a future
uwmd module signsubcommand, separate RFC) handles the OIDC flow and signature attachment.
No deprecation path required — purely additive.
Conformance impact
New fixtures in conformance/tier-3-calc-host/fixtures/:
module-signed-valid/— a real Sigstore-signed manifest, expectedverifyModuleSignaturereturnsok: true.module-signed-tampered/— same manifest with one byte flipped, expectedMOD-SIG-INVALID.module-signed-expired-cert/— signature whose cert is past Sigstore's short-lived window, expectedMOD-SIG-CHAIN.module-unsigned/— manifest with nosignaturefield, expectedverifyModuleSignaturereturnsok: falsewith codeMOD-SIG-MISSING(so hosts can distinguish "no signature" from "bad signature").
Existing module fixtures continue to load (they're unsigned; the calc-host tier doesn't require verification).
Reference implementation
- Files affected:
packages/uwmd-core/src/protocol.ts—ModuleSignaturetype, extendModuleManifest.packages/uwmd-core/src/module-signing.ts(new) —verifyModuleSignature.spec/schemas/module-manifest.schema.json— additivesignaturefield.spec/schemas/module-signature.schema.json(new) — normative shape.spec/UW_PROTOCOL_v1.md— Part X signing subsection.
- Dependencies:
@sigstore/verifyand@sigstore/protobuf-specs(Node ESM). Browser support deferred —verifyModuleSignatureruns server-side in v2. - API surface:
verifyModuleSignature(manifest, options), typesModuleSignature/VerifyOptions/VerifyResult. - Test plan: ship a fixture-signed manifest; assert verification round-trips. Negative tests for each error code. CI uses Sigstore's mock identity provider.
Alternatives considered
PGP signatures. Rejected — PGP key management is the reason most projects are abandoning PGP. Sigstore's keyless flow eliminates the long-lived-key footgun.
Detached signature file (
.sig). Rejected — couples the signature to a delivery channel (filesystem) and complicates verification when manifests travel through registries that don't ship sidecar files.Cosign-style OCI artifact signing only. Rejected as the only path — works for modules distributed as container images, breaks for modules distributed as plain
.json. Sigstore's bundle format covers both.In-band signature using JWS. Considered. The tradeoff is JWS produces a self-contained token but loses the Rekor transparency log. Sigstore bundles include both, so we get the JWS-style self-containment plus public auditability.
Make verification mandatory. Rejected — would lock out air-gapped hosts and increase the conformance bar in a way unrelated to the rest of the protocol. Verification is a capability, not a tier requirement.
Unresolved questions
- Sigstore root rotation. Sigstore's trust root changes over time. Should
@uwmd/coreship a vendored root snapshot, or fetch it at verify-time? Recommend vendored snapshot with a release-cycle update cadence. - Identity policy. Hosts likely want to allow only signatures from specific identities (e.g.,
*@uwmd.org). TheVerifyOptionssurface should grow anallowed_identities: string[]regex/glob list. Out of scope for this RFC; lands in a follow-up host-policy RFC. - Browser support. Sigstore verification in the browser is feasible but heavyweight (~500 kB bundle). Recommend deferring browser-side verification until a smaller verifier is available; until then, browser hosts that need verification do it server-side and pass the verdict in.
- Module dependency signing. A module's
depends_onlist points at other modules. Should the host transitively verify all dependencies? Recommend yes, but the policy logic is a host decision; the protocol gives the verifier, not the policy engine.
Why not Sigstore, yet
RFC 0010 shipped on 2026-08-27, three days before this one, and changed what the cheap option is.
Sigstore verification needs a Fulcio trust root and a Rekor inclusion proof. That means one of two things, and both are bad here:
- A vendored root snapshot. Sigstore's trust root rotates. A snapshot in
@uwmd/coreis a standing obligation to ship a release on somebody else's cadence, and a stale snapshot fails closed — every signed module suddenly unverifiable, for a reason nothing in this repo caused. - Fetching at verify time. A protocol whose stated non-goals include network transport (§I.2) would acquire a network dependency in its module loader, and the conformance corpus — offline and deterministic by construction — could not express a single fixture.
Against that, @uwmd/signing already existed, with a key store, three algorithms, and a canonicalization the whole project shares. Signing a manifest with it is the same act as signing a block, over different bytes. The result is offline, deterministic, fixture-able, and reuses a verifier that already has conformance coverage.
What is genuinely lost is Sigstore's two real advantages: keyless signing (no long-lived key for an author to manage or leak) and public transparency (a log anyone can audit). Those are worth having, and §X.1.2 reserves scheme: "sigstore" for them. This RFC's judgment is that a working keystore scheme now beats a Sigstore scheme that either never ships or ships with a trust-root maintenance burden nobody has signed up for.
The other four alternatives below are unaffected — PGP, sidecar .sig files, OCI-only signing, and JWS are all still rejected, for the reasons given.
Implementation notes (deviations from the proposal)
Shipped 2026-08-27, on top of RFC 0010.
- Scheme is
uwmd-keystore, notsigstore— see above. TheModuleSignatureshape is correspondingly the §V.11 block-signature shape (alg/kid/sig/signed_at) plusschemeand an optionalidentity, rather than the certificate / Rekor fields proposed above. - Error codes continue the
PROTO-MOD-NNNsequence —PROTO-MOD-068through072— rather than opening aMOD-SIG-*namespace. Every other module error in this repo isPROTO-MOD-NNN, and a second namespace for one feature would be a taxonomy split with nothing behind it. The distinctions the RFC asked for all survive, including the one it was most right about:missingis its own code, so a host can tell "unsigned" from "bad signature". unknown_keyis one verdict covering two situations — the host's store lacks thekid, and the host has no signature backend at all. Both mean "this host cannot establish anything about this module", and both must refuse under a checking policy. Splitting them would offer a distinction no host can act on differently.- Host policy is a named enum on the loader, not left entirely to the host:
ignore/verify-if-present/require, withloadModuleManifestAsyncandcreateModuleRegistryAsyncas the async siblings (verification needs Web Crypto; the sync loaders are untouched and pay nothing). The RFC's substance is intact — the protocol still mandates no policy — but naming the three that exist in practice keeps two hosts from inventing incompatible spellings of the same three. identityis explicitly advisory, with §X.1.5 saying so in normative text. The RFC deferred identity policy to a follow-up; what shipped is theallowedIdentitiesallow-list it sketched, plus the caveat that matters more than the feature: a signature proves the key holder asserted an identity, never that the assertion is true.- Fixtures live in
conformance/signing/modules/, notconformance/tier-3-calc-host/fixtures/. Module signing is not a tier-3 capability — a Tier-1 reader that loads modules has the same problem — and the fixtures share RFC 0010's test key, because a host that trusts a signer trusts them for both artifact kinds. Six scenarios, each run under all three policies rather than only the interesting one:04-unsignedloading underverify-if-presentand refusing underrequireis the entire policy distinction, and asserting one half would let the two collapse unnoticed. - A malformed signature is refused under
ignoretoo. Declining to verify is not a licence to admit a malformed object into a frozen manifest that other code will read.
Prior art
- Sigstore (sigstore.dev) — keyless code signing with transparency log. The model this RFC adopts.
- npm provenance — already integrated with Sigstore for
@uwmd/corereleases (.github/workflows/release.yml). This RFC extends the same model to module manifests. - PEP 740 (Python Package Index attestations) — same model adapted for PyPI.
- OCI 1.1 signatures (Cosign) — sibling tool from the same project; relevant if a future RFC ships modules as OCI artifacts.
- JOSE / JWS — the in-band-signing alternative considered above.