Skip to content

Developer Artifact Signing

**Open source · Supply-chain security · GPG replacement**

Overview

Developer Artifact Signing

Open source · Supply-chain security · GPG replacement

The problem

Every week: another popular npm / PyPI / crate package’s maintainer reports “I lost my signing key” or “my key was compromised, invalidate everything I’ve published.”

The status quo for open-source artifact signing:

  • GPG keys still dominate. A single key, stored God-knows-where by the maintainer. Lost key = downstream chaos. Compromised key = everyone downstream has to re-verify against a new key.
  • Sigstore / cosign is a major improvement (short-lived certificates tied to OIDC identity) but centralizes trust in Fulcio / Rekor. Also doesn’t address the “what if the maintainer leaves the project” or “what if there are multiple maintainers” case.
  • Package-registry-level signatures (npm signed registry) tie signing to the registry; the registry becomes a single point.

What’s actually needed:

  1. Guardian-recoverable signing keys. Lose your HSM? Your team’s chosen guardians can rotate you to a new key without nuking all your downstream consumers.
  2. Multi-maintainer signing. A package maintained by 3 people should be able to publish from any of them, without each consumer having to manage 3 separate GPG keys.
  3. Cryptographic chain of releases. The v2.0.1 release was signed by a key that was a rotation from v2.0.0’s key. Downstream consumers can follow the chain.
  4. Revocation that actually propagates. Compromised key? Revocation visible to every consumer within seconds, not “they need to notice the Twitter post.”
  5. Ecosystem-wide upgrades. “As of January 2027, all packages in this ecosystem must attest their build via sigstore”, coordinated activation across millions of downstream consumers.

Why Quidnug fits

A package maintainer is a quid. Their project is a quid (owned by the maintainer’s team). Each release is a title. Guardian recovery handles key loss. Events track the release lifecycle.

ProblemQuidnug primitive
”Lost signing key”Guardian recovery (co-maintainers as guardians)
“Multi-maintainer signing”Guardian set of maintainers, threshold 1+
“Release chain continuity”Anchor rotation chain
”Revoke compromised release”release.revoked event on the title
”Coordinate ecosystem upgrade”Fork-block transaction
”Consumer trust in maintainer”Relational trust edges
”Cross-registry signing”One quid identity, multiple registry mappings

High-level architecture

┌───────────────────────────────────────────┐
│ Developer / Org Quids │
│ │
│ Alice's quid (maintainer of "webapp-js") │
│ GuardianSet: {Bob, Carol, backup-HSM} │
│ Threshold: 1 (for routine release) │
│ Recovery: {Bob+Carol req'd, delay=24h}│
└───────────────────────────────────────────┘
│ publishes signed TITLE
┌───────────────────────────────────────────┐
│ Release title: │
│ "webapp-js@2.3.1" │
│ attributes: │
│ - artifactHash: <sha256 of tarball> │
│ - version: "2.3.1" │
│ - repository: github.com/acme/webapp │
│ - commitHash: <git sha> │
└───────────────────────────────────────────┘
┌─────────────────────┴───────────────────┐
│ Event stream: │
│ - release.published │
│ - release.sbom-attested │
│ - release.vulnerability-reported │
│ - release.revoked │
└─────────────────────────────────────────┘
Downstream consumers verify via Quidnug

Data model

Quids

  • Developer, individual maintainer. HSM/hardware key for signing; co-maintainers as recovery guardians.
  • Project, team-owned artifact namespace.
  • Organization, for corporate OSS (e.g., apache-software- foundation).
  • Release registry, npm, PyPI, crates, Maven Central. Each has a quid for its own signing role.

Domain

developer.signing.npm
developer.signing.pypi
developer.signing.crates
developer.signing.maven-central
developer.signing.github-releases

Release title

{
"type":"TITLE",
"assetId":"webapp-js-2.3.1",
"domain":"developer.signing.npm",
"titleType":"software-release",
"owners":[{"ownerId":"maintainer-alice","percentage":100.0}],
"attributes":{
"packageName":"webapp-js",
"version":"2.3.1",
"artifactHash":"<sha256 of tarball>",
"repository":"github.com/acme/webapp-js",
"commitHash":"abc123...",
"buildEnvironment":"github-actions-ubuntu-22.04",
"buildProvenance":"<sigstore reference>",
"publishedAt":1713400000,
"previousReleaseRef":"webapp-js-2.3.0" /* chain link */
},
"signatures":{"maintainer-alice":"<sig>"}
}

Release lifecycle events

1. release.published
payload: { version, artifactHash, buildLogHash }
signer: maintainer
2. release.sbom-attested
payload: { sbomHash, componentAnalysisHash }
signer: maintainer (or CI system)
3. release.vulnerability-reported (by security researcher)
payload: { cveId, severity, affectedVersions }
signer: reporter-quid
4. release.vulnerability-patched
payload: { cveId, patchCommit, patchedInVersion }
signer: maintainer
5. release.revoked
payload: { reason: "key-compromise", revokedAt }
signer: maintainer (or post-key-recovery, successor)

Multi-maintainer project flow

Project maintained by Alice, Bob, Carol. Each is a quid. The project quid has guardian set:

{
"subjectQuid":"project-webapp-js",
"newSet":{
"guardians":[
{"quid":"alice","weight":1},
{"quid":"bob","weight":1},
{"quid":"carol","weight":1}
],
"threshold":1, /* any one maintainer can publish */
"recoveryDelay":86400000000000, /* 24h */
"requireGuardianRotation":true
}
}

Any of Alice, Bob, or Carol can publish a release (threshold 1). But recovery (changing the maintainer set, rotating to new keys) requires a quorum among the remaining maintainers.

Key loss: Alice loses her HSM

Alice is the lead maintainer; her HSM died. Her co-maintainers initiate guardian recovery to give her a new key:

Terminal window
curl -X POST $NODE/api/v2/guardian/recovery/init -d '{
"subjectQuid":"maintainer-alice",
"fromEpoch":0,
"toEpoch":1,
"newPublicKey":"<Alice'\''s new HSM pub key>",
"minNextNonce":1,
"maxAcceptedOldNonce":0, /* revoke all old-epoch sigs */
"guardianSigs":[
{"guardianQuid":"bob","keyEpoch":0,"signature":"<sig>"},
{"guardianQuid":"carol","keyEpoch":0,"signature":"<sig>"}
],
...
}'

24-hour delay (since this is a high-stakes key). If Alice’s HSM is genuinely dead, no one vetoes. Post-commit, Alice’s epoch advances.

Downstream consumers querying for “alice’s current signing key” see the new one automatically via Quidnug. No downstream config change needed.

Consumer verification

type ArtifactVerifier struct {
client QuidnugClient
selfQuid string
}
func (v *ArtifactVerifier) Verify(ctx context.Context, packageName, version string, artifactBytes []byte) (*VerifyResult, error) {
// Query for the release title
releaseID := packageName + "-" + version
title, err := v.client.GetTitle(ctx, releaseID)
if err != nil {
return nil, err
}
// Verify artifact hash matches
expectedHash := title.Attributes["artifactHash"].(string)
actualHash := sha256sum(artifactBytes)
if expectedHash != actualHash {
return &VerifyResult{Valid: false, Reason: "Artifact hash mismatch"}, nil
}
// Check maintainer trust
maintainer := title.Owners[0].OwnerID
trust, err := v.client.GetTrust(ctx, v.selfQuid, maintainer,
title.Domain, nil)
if err != nil || trust.TrustLevel < 0.5 {
return &VerifyResult{Valid: false, Reason: "Maintainer trust too low"}, nil
}
// Check for revocation
events, _ := v.client.GetSubjectEvents(ctx, releaseID, "TITLE")
for _, ev := range events {
if ev.EventType == "release.revoked" {
return &VerifyResult{Valid: false, Reason: fmt.Sprintf("Release revoked: %s",
ev.Payload["reason"])}, nil
}
}
// Check for unpatched high-severity vulnerabilities
hasUnpatchedHighSev := false
for _, ev := range events {
if ev.EventType == "release.vulnerability-reported" {
severity := ev.Payload["severity"].(string)
cveID := ev.Payload["cveId"].(string)
// Was it patched?
patched := hasPatchEvent(events, cveID)
if severity == "HIGH" && !patched {
hasUnpatchedHighSev = true
}
}
}
return &VerifyResult{
Valid: true,
MaintainerTrust: trust.TrustLevel,
HasUnpatchedIssues: hasUnpatchedHighSev,
}, nil
}

Ecosystem-wide upgrade

NPM ecosystem decides “effective block H, all packages must include a sigstore attestation.” Fork-block:

Terminal window
curl -X POST $NODE/api/v2/fork-block -d '{
"trustDomain":"developer.signing.npm",
"feature":"require_tx_tree_root", /* or similar app-specific feature */
"forkHeight":<future>,
"signatures":[
{"validatorQuid":"npm-foundation","keyEpoch":0,"signature":"<sig>"},
{"validatorQuid":"github","keyEpoch":0,"signature":"<sig>"},
{"validatorQuid":"eslint-maintainer-council","keyEpoch":0,"signature":"<sig>"}
]
}'

At the fork height, every downstream consumer’s verifier automatically enforces the new requirement.

Key Quidnug features

  • Guardian recovery (QDP-0002), maintainer’s co-maintainers are their recovery guardians.
  • Anchor rotation, clean chain from old to new signing key.
  • Event streams, release history + security lifecycle.
  • Relational trust, consumers trust specific maintainers.
  • Push gossip (QDP-0005), revocation propagates in seconds.
  • Fork-block (QDP-0009), ecosystem-wide upgrades.
  • Domain hierarchy, per-registry scoping.

Value delivered

DimensionGPG / sigstoreWith Quidnug
Key loss recoveryFull re-keying by all consumersGuardian recovery; consumers auto-resolve new key
Multi-maintainer signingAd-hoc workaroundsFirst-class guardian set
Revocation propagationTwitter + GitHub issueSeconds via push gossip
Release chain continuityManual “this key replaces X”Anchor rotation chain
Consumer customizationAll-or-nothingRelational trust per maintainer
Ecosystem coordinationNoneFork-block activations
SBOM / vulnerability trackingTool-specificNative events on release stream
Cross-registry identitySeparate keys per registryOne quid, multiple registry mappings

What’s in this folder

Runnable POC

Full end-to-end demo at examples/developer-artifact-signing/:

  • artifact_verify.py, pure verifier logic (hash match, revocation, CVE status, trust threshold).
  • artifact_verify_test.py, 16 pytest cases covering match, mismatch, revocation, severity handling, batch verification.
  • demo.py, ten-step end-to-end flow against a live node: register actors, publish release, verify, report CVE, re-verify (warn), patch, verify superseded version, revoke, tamper-detection sanity check.
Terminal window
cd examples/developer-artifact-signing
python demo.py

Implementation

Concrete API calls, pseudocode, signing shape.

Implementation: Developer Artifact Signing

1. Maintainer + project identity

Terminal window
# Each maintainer is a quid
curl -X POST $NODE/api/identities -d '{
"quidId":"maintainer-alice",
"name":"Alice (maintainer)",
"homeDomain":"developer.signing.npm",
"creator":"maintainer-alice","updateNonce":1
}'
# Alice's personal guardian set (for key recovery)
curl -X POST $NODE/api/v2/guardian/set-update -d '{
"subjectQuid":"maintainer-alice",
"newSet":{
"guardians":[
{"quid":"bob","weight":1,"epoch":0},
{"quid":"carol","weight":1,"epoch":0},
{"quid":"alice-backup-hsm","weight":1,"epoch":0}
],
"threshold":2,
"recoveryDelay":86400000000000, /* 24h */
"requireGuardianRotation":true
},
...
}'
# The project itself
curl -X POST $NODE/api/identities -d '{
"quidId":"project-webapp-js",
"name":"webapp-js (npm package)",
"homeDomain":"developer.signing.npm",
"creator":"maintainer-alice","updateNonce":1,
"attributes":{
"packageName":"webapp-js",
"registry":"npm",
"repository":"github.com/acme/webapp-js"
}
}'
# Project's guardian set = maintainer team
curl -X POST $NODE/api/v2/guardian/set-update -d '{
"subjectQuid":"project-webapp-js",
"newSet":{
"guardians":[
{"quid":"maintainer-alice","weight":1},
{"quid":"maintainer-bob","weight":1},
{"quid":"maintainer-carol","weight":1}
],
"threshold":1,
"recoveryDelay":86400000000000,
"requireGuardianRotation":true
},
...
}'

2. Publish a release

Terminal window
# Alice publishes v2.3.1
curl -X POST $NODE/api/v1/titles -d '{
"assetId":"webapp-js-2.3.1",
"domain":"developer.signing.npm",
"titleType":"software-release",
"owners":[{"ownerId":"project-webapp-js","percentage":100.0}],
"attributes":{
"packageName":"webapp-js",
"version":"2.3.1",
"artifactHash":"<sha256 of .tgz>",
"repository":"github.com/acme/webapp-js",
"commitHash":"abc123...",
"buildEnvironment":"github-actions-ubuntu-22.04",
"buildLogHash":"<sha256>",
"previousReleaseRef":"webapp-js-2.3.0",
"sbomCID":"bafy..."
},
"creator":"maintainer-alice",
"signatures":{"maintainer-alice":"<sig>"}
}'
# Published event
curl -X POST $NODE/api/v1/events -d '{
"subjectId":"webapp-js-2.3.1",
"subjectType":"TITLE",
"eventType":"release.published",
"payload":{
"publisher":"maintainer-alice",
"timestamp":1713400000,
"changelog":"<url>",
"npmTag":"latest"
},
"creator":"maintainer-alice","signature":"<sig>"
}'

3. SBOM attestation

Terminal window
curl -X POST $NODE/api/v1/events -d '{
"subjectId":"webapp-js-2.3.1",
"subjectType":"TITLE",
"eventType":"release.sbom-attested",
"payload":{
"sbomFormat":"CycloneDX-1.5",
"sbomHash":"<sha256>",
"sbomCID":"bafy...",
"dependencyCount":247
},
"creator":"maintainer-alice","signature":"<sig>"
}'

4. Vulnerability reporting

Security researcher files a CVE:

Terminal window
curl -X POST $NODE/api/v1/events -d '{
"subjectId":"webapp-js-2.3.1",
"subjectType":"TITLE",
"eventType":"release.vulnerability-reported",
"payload":{
"reporter":"security-researcher-x",
"cveId":"CVE-2026-1234",
"severity":"HIGH",
"affectedVersions":"^2.3.0",
"proofOfConceptCID":"bafy..."
},
"creator":"security-researcher-x","signature":"<sig>"
}'

Maintainer patches:

Terminal window
curl -X POST $NODE/api/v1/events -d '{
"subjectId":"webapp-js-2.3.1",
"subjectType":"TITLE",
"eventType":"release.vulnerability-patched",
"payload":{
"cveId":"CVE-2026-1234",
"patchCommit":"def456",
"patchedInVersion":"2.3.2"
},
"creator":"maintainer-alice","signature":"<sig>"
}'

5. Key rotation (scheduled)

Alice rotates her signing key every 6 months as policy:

Terminal window
curl -X POST $NODE/api/anchors -d '{
"kind":"rotation",
"signerQuid":"maintainer-alice",
"fromEpoch":0,"toEpoch":1,
"newPublicKey":"<hex>",
"minNextNonce":1,
"maxAcceptedOldNonce":1000, /* grace for in-flight */
"anchorNonce":<next>,
"signature":"<signed with current epoch key>"
}'

6. Key loss + recovery

Alice’s HSM failed. Bob and Carol initiate recovery:

Terminal window
curl -X POST $NODE/api/v2/guardian/recovery/init -d '{
"subjectQuid":"maintainer-alice",
"fromEpoch":1,
"toEpoch":2,
"newPublicKey":"<Alice'\''s new HSM>",
"minNextNonce":1,
"maxAcceptedOldNonce":0,
"guardianSigs":[
{"guardianQuid":"bob","keyEpoch":0,"signature":"<sig>"},
{"guardianQuid":"carol","keyEpoch":0,"signature":"<sig>"}
],
...
}'

24h delay; if no veto, commit. Post-commit, Alice signs new releases with her epoch-2 key.

7. Consumer verification

type PackageVerifier struct {
client QuidnugClient
selfQuid string
}
func (v *PackageVerifier) VerifyInstall(ctx context.Context, packageName, version string, artifactBytes []byte) (*VerifyResult, error) {
releaseID := fmt.Sprintf("%s-%s", packageName, version)
title, err := v.client.GetTitle(ctx, releaseID)
if err != nil {
return nil, err
}
// Hash check
expected := title.Attributes["artifactHash"].(string)
if sha256sum(artifactBytes) != expected {
return &VerifyResult{Valid: false, Reason: "Artifact hash mismatch"}, nil
}
// Trust in project
projectID := title.Owners[0].OwnerID
trust, _ := v.client.GetTrust(ctx, v.selfQuid, projectID,
title.Domain, &GetTrustOptions{MaxDepth: 3})
if trust.TrustLevel < 0.5 {
return &VerifyResult{Valid: false, Reason: "Project trust too low"}, nil
}
// Revocation check
events, _ := v.client.GetSubjectEvents(ctx, releaseID, "TITLE")
for _, ev := range events {
if ev.EventType == "release.revoked" {
return &VerifyResult{Valid: false, Reason: "Release revoked"}, nil
}
}
return &VerifyResult{Valid: true, Trust: trust.TrustLevel}, nil
}

8. npm install integration (sketch)

Terminal window
# Hypothetical npm-quidnug plugin
npm install --with-quidnug-verify webapp-js
# ... plugin queries Quidnug for the release title, verifies
# artifact hash, checks trust, proceeds with install

9. Testing

func TestArtifact_VerifyHashMatch(t *testing.T) {
// Publish release with hash H
// Verify with tampered bytes → fails
// Verify with correct bytes → passes
}
func TestArtifact_GuardianKeyRecovery(t *testing.T) {
// Alice publishes v1.0 with epoch-0 key
// Guardian recovery to epoch-1
// Alice publishes v1.1 with epoch-1 key
// Consumer verifies both (historical v1.0 under old epoch;
// current v1.1 under new epoch)
}
func TestArtifact_RevocationPropagation(t *testing.T) {
// Publish, then revoke
// Consumer sees revoked state within gossip window
}
func TestArtifact_MultiMaintainer(t *testing.T) {
// Alice, Bob, Carol all in guardian set (threshold 1)
// Any can publish a valid release
// Consumer verifies regardless of which maintainer signed
}

Where to go next

Threat model

Adversaries, assumed capabilities, mitigations.

Threat Model: Developer Artifact Signing

Assets

  1. Package authenticity, downstream consumers installing the real artifact the maintainer intended.
  2. Maintainer identity continuity, maintainer’s public key isn’t orphaned by one lost HSM.
  3. Supply-chain integrity, cryptographic chain from code commit through release to consumer install.

Attackers

AttackerCapabilityGoal
Malware injectorCan publish to a registry if credentialsDistribute malware via package
Compromised maintainer keyValid signing keyPublish malicious release
Dependency-confusion attackerRegister similar-named packageTrick consumers into wrong pkg
Registry compromiseRegistry’s signing keyRe-serve altered artifacts
Nation-stateCompel maintainer / registryBackdoor via legitimate channel

Threats and mitigations

T1. Compromised maintainer key

Attack. Attacker has Alice’s HSM. Publishes malicious webapp-js@2.3.1. Mitigation.

  • Guardian recovery rotates Alice’s key. Post-rotation, the compromised epoch is invalid. Bob and Carol notice anomalous releases and initiate recovery.
  • release.revoked event on the malicious release; downstream verifiers see it within seconds.
  • maxAcceptedOldNonce=0 at rotation time invalidates all old-epoch signatures.

Residual risk. Window between compromise and detection. Malware may reach some consumers.

T2. Dependency confusion

Attack. Attacker publishes webapp-js (no dash) with a similar name; downstream require("webapp-js") is ambiguous. Mitigation.

  • Trust in the project quid, consumer trusts project-webapp-js (dash), not a random quid.
  • Registry-level name uniqueness is still needed; Quidnug adds identity on top.

T3. Rogue maintainer (insider)

Attack. One of Alice/Bob/Carol goes rogue and publishes malware. Mitigation.

  • Project guardian set has threshold 1 (any maintainer can publish). Trade-off chosen for velocity.
  • For high-security packages, raise threshold to 2, every release needs cosigning.
  • If rogue detected: GuardianResignation removes them, or guardian-set update replaces the set.

T4. Registry compromise

Attack. NPM registry itself is compromised; attacker replaces artifacts server-side. Mitigation.

  • artifactHash on the release title is the authoritative hash. Consumers compute the hash from downloaded bytes and compare.
  • Registry-served artifact bytes tampering → hash mismatch → verifier rejects.
  • Even with full registry compromise, consumers with Quidnug verification remain safe.

T5. Build environment compromise

Attack. Attacker compromises CI/CD, injects backdoor into built artifact; official build signature is applied. Mitigation.

  • buildProvenance attestation (sigstore style) is an additional field.
  • Reproducible builds + attestation from independent rebuilders’ quids.
  • Protocol supports multiple attestations per release; any mismatch is visible.

Residual risk. Deep supply-chain attacks at build time are a fundamental challenge beyond Quidnug’s scope. Quidnug helps with the “trust the build attestation authority” question via relational trust.

T6. Ecosystem fork-block abuse

Attack. Fork-block transaction passed requiring attestations that attacker controls. Mitigation. 2/3 validator quorum + 24h notice. Npm ecosystem validators are the major players (npm, Github, key maintainer councils); collusion hard.

T7. Revocation propagation latency

Attack. Revocation emitted; attacker races to reach consumers before they see it. Mitigation.

  • Push gossip: seconds propagation.
  • Verifiers query at install time, not at package publish.
  • Cached-verifier systems should periodically re-check.

Not defended against

  1. Build-time injection at the maintainer’s own infra. If Alice’s laptop is compromised, the build the laptop produces and the signature Alice applies are both malicious. Sigstore + independent builders help.
  2. Social engineering of maintainers into signing malicious builds. Protocol can’t prevent; monitoring helps.
  3. Vulnerability delay. A vulnerability exists for some time before it’s discovered and disclosed. Protocol can’t help with unknown unknowns.
  4. Ecosystem governance. Who decides who runs npm registry? That’s not a protocol question.

References