fractal-pqc 0.5.0 → 0.5.1
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/package.json +1 -1
- package/src/claims-registry.mjs +21 -4
- package/src/index.mjs +1 -1
- package/src/mutations.mjs +20 -0
- package/src/primacy.mjs +57 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fractal-pqc",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Runnable reference for quantum-safe migration of a Bitcoin-style key: bind secp256k1/Taproot to ML-DSA-65 (FIPS-204), derive P2TR addresses, build+sign BIP-341 key-path spends (official-vector-verified), and broadcast on testnet. Real primitives, honest scope.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/claims-registry.mjs
CHANGED
|
@@ -181,10 +181,11 @@ export const CLAIMS = [
|
|
|
181
181
|
{
|
|
182
182
|
id: "R6-exposure-relative-primacy",
|
|
183
183
|
module: "primacy.mjs",
|
|
184
|
-
statement: "A commitment anchored in Bitcoin strictly BEFORE the block
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
184
|
+
statement: "A commitment anchored in Bitcoin strictly BEFORE the block in which the " +
|
|
185
|
+
"CHAIN first revealed the holder's public key needs no Q-day estimate: at " +
|
|
186
|
+
"that moment breaking secp256k1 was not sufficient to produce it. Anchored " +
|
|
187
|
+
"after exposure — or with the exposure height merely ABSENT — the strong " +
|
|
188
|
+
"claim is refused, and the refusal says so in `valid`, not only in a new field.",
|
|
188
189
|
proof: () => {
|
|
189
190
|
const r = P.proveExposureRelativePrimacy({
|
|
190
191
|
subjectClassicalPub: cert.classicalPub, entries: ENTRIES, sth: STH,
|
|
@@ -201,6 +202,16 @@ export const CLAIMS = [
|
|
|
201
202
|
// coins should be moved rather than quietly degrading to the guessed cutoff.
|
|
202
203
|
const late = P.proveExposureRelativePrimacy({ ...base, exposureHeight: ANCHOR_HEIGHT - 1 });
|
|
203
204
|
if (late.exposureRelative !== false || late.reason !== "anchored-after-exposure") return false;
|
|
205
|
+
// ROUND 6: the refusal must be visible to a consumer that checks `.valid` — which is
|
|
206
|
+
// what every other gate in this package does. It used to come back green.
|
|
207
|
+
if (late.valid !== false || late.quantumPropertyHolds !== false) return false;
|
|
208
|
+
// ROUND 6: an ABSENT field is nobody's assertion. It used to yield "never-exposed",
|
|
209
|
+
// the strongest claim the module makes.
|
|
210
|
+
const omitted = P.proveExposureRelativePrimacy(base);
|
|
211
|
+
if (omitted.exposureRelative !== false || omitted.reason !== "exposure-height-not-supplied") return false;
|
|
212
|
+
// ROUND 6: the internal sentinel must never be accepted as a real exposure height.
|
|
213
|
+
if (P.proveExposureRelativePrimacy({ ...base, exposureHeight: Number.MAX_SAFE_INTEGER })
|
|
214
|
+
.exposureRelative !== false) return false;
|
|
204
215
|
// Exposed at EXACTLY the anchor height is not "before" — the boundary must be strict.
|
|
205
216
|
const same = P.proveExposureRelativePrimacy({ ...base, exposureHeight: ANCHOR_HEIGHT });
|
|
206
217
|
if (same.exposureRelative !== false) return false;
|
|
@@ -466,6 +477,12 @@ export const UNCLAIMED_GUARDS = Object.freeze([
|
|
|
466
477
|
"blocks) — again stronger than a claim, again not in the ledger" },
|
|
467
478
|
{ where: "policy.mjs", guard: "the ORDER of the refusal guards",
|
|
468
479
|
why: "each guard is claimed individually; nothing claims that reordering them is safe" },
|
|
480
|
+
{ where: "primacy.mjs", guard: "exposure-relative primacy is not an authorisation gate",
|
|
481
|
+
why: "policy.mjs does not consume it. It is a verifiable proof primitive; nothing in the " +
|
|
482
|
+
"signing path requires it, and we do not imply otherwise" },
|
|
483
|
+
{ where: "primacy.mjs", guard: "'never exposed' is the CALLER's assertion",
|
|
484
|
+
why: "the kit cannot verify non-exposure. It can only refuse when the field is absent, " +
|
|
485
|
+
"which it now does. Off-chain publication of a key is outside what we measure" },
|
|
469
486
|
{ where: "primacy.mjs", guard: "key rotation / revocation",
|
|
470
487
|
why: "NOT IMPLEMENTED. A holder who loses their ML-DSA secret is locked out permanently " +
|
|
471
488
|
"and the old certificate keeps authorising. This is a known gap, not a covered one." },
|
package/src/index.mjs
CHANGED
|
@@ -56,7 +56,7 @@ export {
|
|
|
56
56
|
parsePsbt, signPsbtTaprootKeyPath, finalizePsbt,
|
|
57
57
|
} from "./psbt.mjs";
|
|
58
58
|
|
|
59
|
-
export const VERSION = "0.5.
|
|
59
|
+
export const VERSION = "0.5.1";
|
|
60
60
|
|
|
61
61
|
// The security layer, actually exported. Round 2 of our own siege found that primacy,
|
|
62
62
|
// policy and transparency existed in the repo and were unreachable from the published
|
package/src/mutations.mjs
CHANGED
|
@@ -109,6 +109,26 @@ export const MUTATIONS = [
|
|
|
109
109
|
to: ` if (false) {`,
|
|
110
110
|
mustKill: ["R6-exposure-relative-primacy"],
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
id: "M-R6-absent-height-is-assertion",
|
|
114
|
+
describes: "R6: an ABSENT exposure height is treated as the 'never exposed' assertion again",
|
|
115
|
+
file: "src/primacy.mjs",
|
|
116
|
+
from: ` if (!args || !Object.hasOwn(args, "exposureHeight")) {`,
|
|
117
|
+
to: ` if (false) {`,
|
|
118
|
+
mustKill: ["R6-exposure-relative-primacy"],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "M-R6-refusal-stays-green",
|
|
122
|
+
describes: "R6: a refused exposure result comes back valid:true, as it did before",
|
|
123
|
+
file: "src/primacy.mjs",
|
|
124
|
+
from: ` ...base, valid: false, quantumPropertyHolds: false,
|
|
125
|
+
exposureRelative: false, anchoredAtHeight: anchoredAt, exposureHeight,
|
|
126
|
+
reason: "anchored-after-exposure",`,
|
|
127
|
+
to: ` ...base,
|
|
128
|
+
exposureRelative: false, anchoredAtHeight: anchoredAt, exposureHeight,
|
|
129
|
+
reason: "anchored-after-exposure",`,
|
|
130
|
+
mustKill: ["R6-exposure-relative-primacy"],
|
|
131
|
+
},
|
|
112
132
|
{
|
|
113
133
|
id: "M-R4-cutoff-unvalidated",
|
|
114
134
|
describes: "R4: a malformed cutoff is coerced again — `height > NaN` passes silently",
|
package/src/primacy.mjs
CHANGED
|
@@ -546,10 +546,27 @@ export const PRIMACY_SCOPE = Object.freeze({
|
|
|
546
546
|
* checkable in Bitcoin itself. Pass `null` to assert the key has never been exposed —
|
|
547
547
|
* which the verifier must also confirm themselves.
|
|
548
548
|
*/
|
|
549
|
-
export function proveExposureRelativePrimacy({
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
549
|
+
export function proveExposureRelativePrimacy(args) {
|
|
550
|
+
const {
|
|
551
|
+
subjectClassicalPub, entries, sth, expectedLogId,
|
|
552
|
+
exposureHeight, otsHex, blockMerkleRoots,
|
|
553
|
+
} = args || {};
|
|
554
|
+
|
|
555
|
+
// FAIL-OPEN BY OMISSION (round 6 of our own siege). The guard below used `== null`, so
|
|
556
|
+
// an ABSENT field, an `undefined`, or a typo produced `strength: "never-exposed"` — the
|
|
557
|
+
// strongest claim this module makes — byte-identical to the deliberate `null` the
|
|
558
|
+
// docstring documents as a verifier ASSERTION. Absence is nobody's assertion. Worse, the
|
|
559
|
+
// sibling function in this same file uses the identical sentinel with the OPPOSITE
|
|
560
|
+
// polarity: it refuses. Presence is now required, and the comparison is strict.
|
|
561
|
+
if (!args || !Object.hasOwn(args, "exposureHeight")) {
|
|
562
|
+
return {
|
|
563
|
+
valid: false, exposureRelative: false, quantumPropertyHolds: false,
|
|
564
|
+
reason: "exposure-height-not-supplied",
|
|
565
|
+
note: "Non-exposure is an ASSERTION the verifier makes from their own node. A field " +
|
|
566
|
+
"that is simply absent does not make it. Pass an explicit height, or an " +
|
|
567
|
+
"explicit null to assert the key has never appeared on chain.",
|
|
568
|
+
};
|
|
569
|
+
}
|
|
553
570
|
// Establish the anchor first, WITHOUT letting the exposure height act as the cutoff.
|
|
554
571
|
// Passing the exposure as `cutoffBlockHeight` would make the base check fire first and
|
|
555
572
|
// report a generic "anchored-after-cutoff", losing the specific — and far more useful —
|
|
@@ -565,7 +582,10 @@ export function proveExposureRelativePrimacy({
|
|
|
565
582
|
|
|
566
583
|
if (!base.quantumPropertyHolds) {
|
|
567
584
|
return {
|
|
568
|
-
|
|
585
|
+
// A refusal must say NO in the field consumers read. This kept `valid: true` from
|
|
586
|
+
// the spread, so the case this function exists to reject came back green to every
|
|
587
|
+
// caller that checks `.valid` — which is what the rest of this package does.
|
|
588
|
+
...base, valid: false, quantumPropertyHolds: false, exposureRelative: false,
|
|
569
589
|
reason: "no-confirmed-anchor",
|
|
570
590
|
note: "Without a header-confirmed Bitcoin anchor there is no height to compare " +
|
|
571
591
|
"against the exposure, so the stronger claim cannot be made.",
|
|
@@ -573,8 +593,16 @@ export function proveExposureRelativePrimacy({
|
|
|
573
593
|
}
|
|
574
594
|
|
|
575
595
|
const anchoredAt = base.temporalFrontier.height;
|
|
596
|
+
// The permissive cutoff passed internally is a SENTINEL, not a verifier's choice. It
|
|
597
|
+
// must never travel in the result as `cutoffBlockHeight`, where a reader could cite it
|
|
598
|
+
// as an established frontier.
|
|
599
|
+
base.temporalFrontier = {
|
|
600
|
+
...base.temporalFrontier,
|
|
601
|
+
cutoffBlockHeight: undefined,
|
|
602
|
+
cutoffNotApplied: "none — exposure-relative mode; the comparison is against the key's exposure block",
|
|
603
|
+
};
|
|
576
604
|
|
|
577
|
-
if (exposureHeight
|
|
605
|
+
if (exposureHeight === null) {
|
|
578
606
|
return {
|
|
579
607
|
...base, exposureRelative: true, anchoredAtHeight: anchoredAt, exposureHeight: null,
|
|
580
608
|
strength: "never-exposed",
|
|
@@ -585,13 +613,21 @@ export function proveExposureRelativePrimacy({
|
|
|
585
613
|
};
|
|
586
614
|
}
|
|
587
615
|
|
|
588
|
-
|
|
589
|
-
|
|
616
|
+
// An implausible height is not a height. MAX_SAFE_INTEGER especially: it is the internal
|
|
617
|
+
// sentinel, and accepting it as a real exposure would make every anchor look early.
|
|
618
|
+
if (!Number.isSafeInteger(exposureHeight) || exposureHeight < 0 || exposureHeight > 10_000_000) {
|
|
619
|
+
return {
|
|
620
|
+
...base, valid: false, quantumPropertyHolds: false, exposureRelative: false,
|
|
621
|
+
reason: "malformed-exposure-height",
|
|
622
|
+
note: "Expected a plausible Bitcoin block height (0..10,000,000). Refused rather than " +
|
|
623
|
+
"coerced.",
|
|
624
|
+
};
|
|
590
625
|
}
|
|
591
626
|
|
|
592
627
|
if (anchoredAt >= exposureHeight) {
|
|
593
628
|
return {
|
|
594
|
-
...base,
|
|
629
|
+
...base, valid: false, quantumPropertyHolds: false,
|
|
630
|
+
exposureRelative: false, anchoredAtHeight: anchoredAt, exposureHeight,
|
|
595
631
|
reason: "anchored-after-exposure",
|
|
596
632
|
strength: "cutoff-only",
|
|
597
633
|
note: `Anchored at block ${anchoredAt}, but the key was already public at block ` +
|
|
@@ -607,8 +643,8 @@ export function proveExposureRelativePrimacy({
|
|
|
607
643
|
blocksBefore: exposureHeight - anchoredAt,
|
|
608
644
|
note: `Anchored at block ${anchoredAt}, ${exposureHeight - anchoredAt} blocks BEFORE ` +
|
|
609
645
|
`the key was first exposed at ${exposureHeight}. At the moment of anchoring the ` +
|
|
610
|
-
`
|
|
611
|
-
`to
|
|
646
|
+
`chain had NOT yet revealed the public key, so breaking secp256k1 was not sufficient ` +
|
|
647
|
+
`to produce this commitment. This claim does not depend on any Q-day estimate.`,
|
|
612
648
|
};
|
|
613
649
|
}
|
|
614
650
|
|
|
@@ -626,6 +662,16 @@ export const EXPOSURE_SCOPE = Object.freeze({
|
|
|
626
662
|
cannotSave:
|
|
627
663
|
"keys already exposed by an earlier spend. You cannot anchor into the past, and no " +
|
|
628
664
|
"evidence scheme changes that — those coins must be moved.",
|
|
665
|
+
alsoRequires:
|
|
666
|
+
"that the holder published the key through NO OTHER CHANNEL. We measure on-chain " +
|
|
667
|
+
"appearance only. A key revealed in a mempool broadcast that never confirmed, in a " +
|
|
668
|
+
"shared xpub, or anywhere off-chain was derivable earlier than this proof suggests. " +
|
|
669
|
+
"The adversary's window opens at min(any publication of the key, on-chain exposure); " +
|
|
670
|
+
"this kit measures the second.",
|
|
671
|
+
notAnAuthorisationGate:
|
|
672
|
+
"policy.mjs does not consume this. It is an independently verifiable proof primitive, " +
|
|
673
|
+
"not yet a mandatory gate on the signing path. Saying otherwise would be the sixth " +
|
|
674
|
+
"incarnation of the mistake this package documents.",
|
|
629
675
|
exposureHeightIsNotOurs:
|
|
630
676
|
"the verifier supplies it from their own node. An exposure height we asserted would be " +
|
|
631
677
|
"one more thing to trust us about, and the point is that it is independently checkable.",
|