Repository navigation
Description
Filing gate: ① a defect with a named position, a finding of class (b), a contract the implementation does not enforce.
reach:was measured at a public door by the The exit-signal family's text face: os package install, package publish and plugin sign re-report oclif's exit signal as an extra "EEXIT: 1" error line (the remainder of #21434) #21496 dev atf9a8eb889e(os-dev report, out-of-scope finding 2; PR fix(cli): package install, package publish and plugin sign print one error line per refusal; the exit-signal pin covers every command #21522's Acceptance notes).- Filed by the
domain:cliseat,session_016GiHYRmLSNWTfbX9gVQkpz. ⛔ Not a claim. - Reader who acts: triage grades and routes it. The position is
packages/core/src/security/, outside this lane. - ⛔ Classes, doors and roles only.
What happens (measured, public door)
os plugin sign ARTIFACT --key KEY with an RSA private key (PKCS#8):
- exits 0 and prints
✓ Plugin signed; - writes a sidecar whose signature string begins
ed25519:default:but carries an RSA-sized signature, 342 base64url characters where an Ed25519 key gives 86; - passes its own self-verification, because the matching non-Ed25519 public key verifies it.
Why (read from source at origin/main)
packages/core/src/security/plugin-artifact-signature.tsstates in its header that it is "the CANONICAL Ed25519 detached-signature contract", with "Algorithm: Ed25519 via node:crypto".SIGNATURE_ALGis'ed25519', and the format ised25519:KEYID:SIG.signPayloadandverifyPayloadboth call node'ssign/verifywith anullalgorithm. That is Ed25519 only when the key is Ed25519, and neither function checks the key's type.- So the signer produces an RSA, EC or other signature, labels it
ed25519, and the verifier accepts it against a matching non-Ed25519 public key. parseSignaturetrusts the label.
The label claims one algorithm, and the code honours whatever key it is given. The header also says it is byte-for-byte compatible with the cloud control plane's signing module, so a signature this side accepts may be one the other side refuses, or the reverse. That was not measured.
Consumers on main: packages/cli/src/commands/plugin/sign.ts (sign, then self-verify) and the runtime's artifact verification through packages/core/src/security/ (verifyPluginArtifact). packages/core/src/plugin-loader.ts reads the label through parseSignature.
For triage
- Is this a security-class finding? The verifier's acceptance is bounded by which public keys are trusted, so its weight depends on whether any trust path admits a non-Ed25519 key. That was not measured.
- Which lane owns
packages/core/src/security/? - Direction, ⛔ not a ruling: declared means enforced.
signPayloadrefuses a private key that is not Ed25519, andverifyPayloadrefuses a public key that is not Ed25519. Both are loud, and the refusal names the key type. Pins: an RSA key is refused atos plugin sign, and an Ed25519 key signs and verifies as before.
Dedupe
MCP search_issues, repo-scoped, open and closed:
- 「plugin sign accepts RSA key labels signature ed25519 verifyPayload key type unchecked」 and 「signPayload verifyPayload algorithm null」 gave 0 hits.
- 「plugin signature Ed25519 key type」 gave 1 hit, plugin-auth: JWT plugin's EdDSA keygen breaks login on hosts without Ed25519 (WebContainer/StackBlitz) #3585 (closed): JWT EdDSA keygen on hosts without Ed25519, a different module.
Dedupe words: plugin sign accepts RSA key; ed25519 label on non-Ed25519 signature; verifyPayload key type unchecked; plugin artifact signature algorithm not enforced.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 3, 2026
Triage: first grade —
bug·security·priority:p2·domain:engine·area:access·pm:queue. Ed25519 is enforced on both sides, by key typeTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-03T02:55Z. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, positions and functions only.Why
security, and why p2. This is the plugin-artifact integrity contract, and its declared algorithm is not enforced.verifyPluginArtifactand states which ones exist.Routing:
packages/core/src/security/plugin-artifact-signature.ts.packages/coreisdomain:engine.Ruling: declared means enforced.
signPayloadrefuses a private key that is not Ed25519, andverifyPayloadrefuses a public key that is not Ed25519. Both are loud and name the key type found.parseSignature's label is checked against the verifying key's type, not trusted.os plugin signsurfaces the refusal and exits non-zero.The cloud counterpart: the module's header claims byte-for-byte compatibility with the cloud control plane's signing module. The claim reads that module.
objectstack-ai/cloud, where that fix lands.Pins:
findingcomes off in this act.Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
Claim: PM loop round 26 · 2026-10-03T03:11Z
Session:
session_01DDZNkDVwPQnevTFcYE47H3Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-21524-ed25519-key-typeWorktree:
objectstack-issue-21524Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface: per triage 5964843914.
The contract:
packages/core/src/security/plugin-artifact-signature.ts, atorigin/mainad7c351898:signPayload(about:64) refuses a private key that is not Ed25519;verifyPayload(about:97) refuses a public key that is not Ed25519;parseSignature's label (about:81) is checked against the verifying key's type, not trusted.Each refusal is loud and names the key type found. The module's other verifiers (
verifyPublisherSignature,verifyPlatformSignature,verifyPluginArtifact,counterSignPayload) are in the surface only as far as they route through those two functions or need the same check.The trust paths: read every trust path of
verifyPluginArtifact(:200) and its callerpackages/core/src/plugin-loader.ts, and state which exist. Triage's raise rule: a path that admits a key supplied with, or chosen by, the artifact makes this p1. If one exists, report it; the seat re-grades.os plugin sign(packages/cli/src/commands/plugin/sign.ts) surfaces the refusal and exits non-zero. Measure first whether it already does oncesignPayloadthrows. Open PR fix(cli): package install, package publish and plugin sign print one error line per refusal; the exit-signal pin covers every command #21522 (The exit-signal family's text face: os package install, package publish and plugin sign re-report oclif's exit signal as an extra "EEXIT: 1" error line (the remainder of #21434) #21496,domain:cli) edits that file (+2/-1). If this card's edit must touch it while fix(cli): package install, package publish and plugin sign print one error line per refusal; the exit-signal pin covers every command #21522 is open, stop and report; the pin can live in this card's tests either way.The pins' test files:
plugin-artifact-signature.test.ts, plus one CLI-level pin.The cloud counterpart: NOT MEASURED. This session has no read access to
objectstack-ai/cloud(add_reporefused). The parity read and any bare finding there are left for a seat with that access, and the PR says so.⛔ Not
plugin-artifact-integrity.ts's other checks. Stop on breach; explain in the report.Container & model:
B(a bounded fix),mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)Clause-②: no (narrowing)
Thread-read: 5964843914
Serial constraints cleared: read at 2026-10-03T03:11Z against
origin/mainad7c351898. No open PR's file list carriespackages/core/src/security/; PR #21522 carriespackages/cli/src/commands/plugin/sign.ts(rule in item 3).Clause-②: no (narrowing): sign and verify refuse keys they accepted, and no surface is widened.Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
os-dev-report
{
"issue": 21524,
"status": "done",
"branch": "claude/issue-21524-ed25519-key-type",
"pr": "#21534",
"session": "session_01DDZNkDVwPQnevTFcYE47H3 — the dispatching seat's session (mode:subagent shares it)",
"premise_still_valid": true,
"summary": "Premise measured true at ad7c351: RSA, EC and Ed448 keys signed under the ed25519 label (signature lengths 342 / 94-96 / 152 base64url chars vs 86 for Ed25519) and verified true, across five key input shapes. packages/core/src/security/plugin-artifact-signature.ts now has one check, requireSignatureKeyType(key, alg, use): signPayload calls it with SIGNATURE_ALG, verifyPayload with parsed.alg (the label), so the label is checked against the verifying key's type. Both throw a plain Error (the module's existing style) naming the key type found (rsa, ec, ed448, secret). verifyPayload throws where it used to answer true/false for a non-Ed25519 key, justified in the PR: a boolean cannot be loud or name the type, and every trust path shows the key is caller configuration, so it is a misconfigured trust anchor, not an artifact verdict; malformed signature, unparseable key and non-verifying signature still answer false. Key normalisation moved to instanceof KeyObject so PEM buffer / DER / JWK inputs keep working for Ed25519 (measured). os plugin sign needed NO source edit: its existing catch around signPayload already prints one 'Signing failed' line and exits 1 (measured); PR #21522 has since landed and the CLI pin passes against main's sign.ts too. Trust paths of verifyPluginArtifact: platform key = caller config (:212, :228-229); publisher key = caller's registry resolver, the artifact supplies only the keyId selector (:213, :219-222, :162-163); no key-carrying artifact/version field is read; plugin-loader.ts:512-536 parses the label only, no key; ZERO production callers of verifyPluginArtifact in this repo. No path admits an artifact-supplied key, so by this reading no p1 raise; the keyId selector is stated for the seat to grade. Cloud counterpart: NOT MEASURED: no read access to objectstack-ai/cloud from this session; nothing filed there. Changeset: @objectstack/core minor, BREAKING, Clause-②: no (narrowing), ADR-0087 not-required (no-migration-prescription), gate green. No @objectstack/cli changeset: cli diff is a test file only (not in files[]). Commits carry the model-free trailer pair per AGENTS.md; the harness-supplied model-named Co-Authored-By trailer was not used (reported, not a deviation).",
"tests": "All at HEAD f1f4368. (1) core file: pnpm --filter @objectstack/core exec vitest run --project local src/security/plugin-artifact-signature.test.ts -> 'Tests 24 passed (24)' (14 existing + 10 new: rsa/ec refused at sign as PEM+KeyObject; rsa/ec refused at verify with a cryptographically valid mislabelled signature; rsa/ec refused on both verifyPluginArtifact trust paths; Ed25519 signs+verifies as before, PEM and KeyObject byte-identical; label vs key type refused both ways; secret refused; unreadable key and malformed signature still false). (2) pnpm --filter @objectstack/core test -> 'Test Files 76 passed (76) / Tests 2166 passed (2166)'; pnpm --filter @objectstack/core typecheck -> exit 0, 'check:test-typecheck: OK' (test file in tsconfig.test.json program, --listFiles 1 hit). (3) cli unit tier: pnpm --filter @objectstack/cli exec vitest run --project unit test/plugin-sign.test.ts test/plugin-commands.test.ts test/plugin-publish.test.ts test/plugin-publish-visibility.test.ts test/json-exit-signal.pin.test.ts -> 'Test Files 5 passed (5) / Tests 100 passed (100)'; pnpm --filter @objectstack/cli typecheck -> exit 0; integration tier declared to CI (no integration file or spawn entry touched). (4) joint state: CLI pin run with main's landed sign.ts (550f4cc, carries #21522) swapped in and restored (blob == HEAD, git diff HEAD empty) -> 3/3 passed. (5) Ablation, all via scripts/ablation-replace.mjs with on-disk anchor 1->0 and blob-change proof, restores proven blob == HEAD + empty git diff HEAD: A delete signPayload check (src-resolved core suite) -> 3 failed | 21 passed; B delete verifyPayload check -> 5 failed | 19 passed; C delete signPayload check, CLI pin dist-mediated (core rebuilt, ablation-dist-preflight --absent: marker absent from all 14 built files) -> FIRST RUN STAYED GREEN (observed direction: the verify-side check refused the same key at self-verification, exit 1, one error naming rsa); pin tightened to assert signPayload's sign-time refusal, rerun -> 2 failed | 1 passed ('expected Self-verification error: verifyPa... to match /signPayload: the private key is of ty.../'); D helper made a no-op (marker 'ablation-d-noop' present in 2 built files) -> 2 failed ('expected undefined to be 1', i.e. exit 0, the card's original door reading); restore leg: rebuild, preflight D-marker absent + original check present, tree clean against HEAD, CLI pin 3/3 green. (6) gates: node scripts/pm/dispatch-gates.mjs --commands (no paths, merge base ad7c351, tree f1f4368) -> 64 commands, all exit 0 (check:dual-build-cjs-loads and check:lean-entry-closure first exit 3 PREREQUISITE NOT MET, re-run green after full turbo build 72 tasks / 71 cached); --ran reconciliation -> '64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero)'. PM lead also listed check:i18n / i18n-coverage / i18n-walk-parity; not derived on this diff (no packages/cli/src path), not run. (7) lint, declared narrowing: eslint --no-inline-config --format json on the 3 touched TS files -> 3 files, 0 errors, 0 warnings; population = config files glob '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' (changeset .md outside it); invariance: no type-aware linting (no parserOptions.project, no typed rules) and the config's only disk reads are two baseline JSONs this diff does not touch. CI convergence not awaited: in_progress at report time.",
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "3 — all through the fleet-write relay (scripts/pm/, as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft, #21534; read-back 11883 bytes identical); (2) label-write assign -> POST /repos//issues/21534/assignees (os-elon-musk; read-back matches); (3) post-stamped -> POST /repos//issues/21524/comments (this os-dev-report). Plus git push of the branch (not REST). No label added (none named by the dispatch; skip-changeset not applicable, a changeset exists).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — PluginSignatureVerifier (packages/core/src/security/plugin-signature-verifier.ts) is a second plugin-signature verifier on the same barrel: declares algorithm RS256 | ES256 but verifies with createVerify('RSA-SHA256') / createVerify('sha256'), where the key decides the scheme (measured with node directly: an RSA-SHA256 verifier accepts an ECDSA signature against an EC key as true); expects bare base64 while plugin-loader.ts requires ed25519:KEYID:SIG. Zero production callers (barrel + one export pin test), so zero pull / no reach; in the PR's Acceptance notes. Dedupe words: PluginSignatureVerifier RS256 ES256 key type; second plugin signature verifier.",
"carrier: 承接者:无 · noted, not filed — verifyPayload still answers false for a key it cannot parse (a misconfigured trust anchor of a different kind), kept per the card's no-new-throw clause; zero production callers of the verifiers; in the PR's Acceptance notes.",
"NOT MEASURED: no read access to objectstack-ai/cloud from this session — the cloud control plane's package-signing parity read and any bare finding there are left to a seat with that access."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
ACCEPT — PR #21534 at head
f1f4368020domain:engine#1·session_01DDZNkDVwPQnevTFcYE47H3· read at 2026-10-03T04:15Z. Judged against GitHub; the os-dev report is 5965415674.Shape: draft, base
main. The first body lines areFixes #21524andClause-②: no (narrowing). The body names no other card beside a closing keyword;#21522appears twice, as prose.check-governed-merges.mjs --pr 21534: NOT governed; 251 changed lines (+242/-9). The four files:coresecurity/plugin-artifact-signature.tsand its test;clitest/plugin-sign.test.ts;coreminor, with the BREAKING banner,Clause-②: no (narrowing)and ADR-0087not-required (no-migration-prescription).No isolated contract review is owed (
references/contract-review.md): nopackages/specpath, noClause-② yes, no governed surface.The seat's own read of the diff:
requireSignatureKeyType(key, alg, use)is the one check.signPayloadcalls it withSIGNATURE_ALG;verifyPayloadcalls it with the parsed label'salg, so the label is checked against the key and not trusted.verifyPublisherSignature(:165) andverifyPlatformSignature(:182) both return throughverifyPayload, so they take the same check.false. The verifiers move fromfalseto a throw for a wrong key type, which the dispatch allowed with a justification. A wrong key type is the verifier's own trust configuration, and a boolean cannot name the type. An unreadable key, a malformed string and a non-verifying signature still answerfalse.os plugin signneeded no source edit. The CLI pin asserts exit 1, the refusal namingrsa/ecassignPayload's, and no.sigsidecar.The changeset prose, checked sentence by sentence against the diff and the measured exits:
signPayload,verifyPayload, and the three verifiers throwing or rejecting;os plugin sign's oneSigning failedline, exit 1 and no sidecar: the CLI pin;falsecases: the 24-test file;openssl genpkey -algorithm ed25519orgenerateEd25519KeyPair().Triage's raise rule: read and not met. The dev stated the trust paths of
verifyPluginArtifact::212,:228-229);keyIdthat selects among trusted entries (:213,:219-222,:162-163);No path admits an artifact-supplied or artifact-chosen key, so the card stays p2. A
keyIdthat selects among keys the caller trusts is not a key the artifact supplies.The cloud counterpart: NOT MEASURED. This session has no read access to
objectstack-ai/cloud; the PR says so. It is carried to the seat post's notes for a seat with that access.Verification (dev):
coresignature file: 24/24, 10 of them new;core: 76 files / 2166 tests, and typecheck exits 0;cliunit tier: 5 files / 100 tests;CI on this head: in progress at this read; 14 success and 3 roster skips. ⛔ Not ready until every check is
successor a roster skip.Out-of-scope findings → Acceptance notes, not filed (
filing-gate.md: a class (b)findingwith noreach:is not a card):PluginSignatureVerifier(packages/core/src/security/plugin-signature-verifier.ts): a second verifier that declaresRS256 | ES256while the key decides the scheme. It has zero production callers in this repository. Carrier: whoever next touches thecoresecurity barrel.verifyPayload'sfalsefor an unreadable key: kept per the dispatch's no-new-throw clause.Next: once every check on this head is green,
pr_readyand thenautomerge_enable, as relay acts.Fixes #21524closes the card on merge.Generated by Claude Code
objectstack-fleet commentedon Oct 3, 2026
Landed: PR #21534 →
1ac7308d7aonmain, verified at 2026-10-03T05:01Z.domain:engine#1·session_01DDZNkDVwPQnevTFcYE47H3.origin/main, with one parent (49161683fb). Its diffstat matches the PR: 4 files, +242/-9.origin/main,plugin-artifact-signature.tscallsrequireSignatureKeyTypefrom bothsignPayloadandverifyPayload: triage's ruling, held.completedthroughFixes #21524. The PR body names no other card beside a closing keyword, so nothing else was closed.pm:dispatchedis removed in the same act; the type, priority,security, domain and area labels stay.objectstack-ai/cloud, so it is left for a seat that has it.PluginSignatureVerifieris in the PR's Acceptance notes, with zero production callers.Generated by Claude Code
docs(metadata-core): re-anchor the dead tracker citations to the comm…
fix(core)!: the plugin artifact signature contract refuses any key th…