G56-07: fail-closed verifiers (verifyNostrEvent, parseOtsFile never throw on malformed input), add MAX_OTS_PROOF_SIZE/MAX_EVENT_JSON_SIZE size guards, 23 fuzz/property tests (random JSON, random bytes, truncation, oversized inputs)
This commit is contained in:
@@ -9,10 +9,10 @@
|
||||
| Severity | Total | Fixed | In Progress | Not Started |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Critical | 2 | 2 | 0 | 0 |
|
||||
| High | 6 | 1 | 0 | 5 |
|
||||
| High | 6 | 2 | 0 | 4 |
|
||||
| Medium | 8 | 2 | 0 | 6 |
|
||||
| Low / Info | 4 | 0 | 0 | 4 |
|
||||
| **Total** | **20** | **5** | **0** | **15** |
|
||||
| **Total** | **20** | **6** | **0** | **14** |
|
||||
|
||||
## Findings status
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
| G56-04 | High | Bitcoin verification trusts public API JSON, doesn't validate headers/PoW | ❌ Not started | — | Needs trust-mode labeling or real header validation |
|
||||
| G56-05 | High | Signer output reconstructed without validating returned ID/sig/pubkey/fields | ❌ Not started | — | Needs signer-validation helper |
|
||||
| G56-06 | High | New seed continuity asserted, not proven — no successor signature, no identity match | ❌ Not started | — | Protocol design decision needed |
|
||||
| G56-07 | High | Verifier throws on malformed relay/paste input | ⚠️ Partially fixed | v0.0.12 | G56-01 fix wrapped base64/hex decoding in try/catch and made verifyNIPQRContent fail closed. Broader call-site wrapping in verify.html also done. Remaining: fuzz testing, all export functions |
|
||||
| G56-07 | High | Verifier throws on malformed relay/paste input | ✅ Fixed | v0.0.12, v0.0.19 | v0.0.12 wrapped base64/hex in try/catch + verifyNIPQRContent fail closed + verify.html call-site wrapping. v0.0.19 added structural validation + try/catch to `verifyNostrEvent()` (returns false on malformed input), wrapped `parseOtsFile()` body in try/catch (returns null), added `MAX_OTS_PROOF_SIZE` (1 MiB) and `MAX_EVENT_JSON_SIZE` (64 KiB) size guards, 23 fuzz/property tests (random JSON, random bytes, truncation, oversized inputs, wrong-typed objects) |
|
||||
| G56-08 | High | Browser origin security insufficient for seed handling | ❌ Not started | — | Needs CSP hardening, header fixes, asset pinning |
|
||||
| G56-09 | Medium | 12-word seed default, warning removed | ✅ Fixed | v0.0.17 | Defaulted `generateSeedPhrase()` and `generateSeedPhraseWithEntropy()` to 24 words (256-bit); added 12/24-word selector UI; restored entropy-strength warning; removed "quantum-safe root of trust" / "Make your Nostr identity quantum-resistant" claims from index.html, README.md, nip_proposal.md; 4 new tests |
|
||||
| G56-10 | Medium | Secret material not zeroized, retained in globals/DOM/clipboard | ❌ Not started | — | Minimize secret lifetime, overwrite typed arrays |
|
||||
@@ -92,6 +92,14 @@
|
||||
- Added research-prototype / design-only banners to explanation.md, laans_explanation.md, why_what_how.md, and all 5 research/ docs
|
||||
- No new tests (documentation-only change)
|
||||
|
||||
### v0.0.19 — G56-07 fix (fuzz testing + fail-closed verifiers)
|
||||
- `verifyNostrEvent()`: added structural validation (type-checks all required fields) + try/catch; returns `false` on any malformed input instead of throwing
|
||||
- `parseOtsFile()`: wrapped parser body in try/catch; returns `null` on any malformed/truncated input
|
||||
- Added `MAX_OTS_PROOF_SIZE` (1 MiB) and `MAX_EVENT_JSON_SIZE` (64 KiB) size guards; oversized inputs rejected before any parsing/decoding work
|
||||
- `verifyNIPQRContent()`: added content-size guard at entry
|
||||
- verify.html relay path already had try/catch (G56-01); now defense-in-depth with internal fail-closed
|
||||
- 23 new fuzz/property tests: random JSON shapes to `verifyNIPQRContent` (1000 iterations), random bytes to `parseOtsFile` (2000 iterations), random objects to `verifyNostrEvent` (500 iterations), truncated valid-prefix bytes, oversized inputs, wrong-typed objects, `isOtsConfirmed` fuzz
|
||||
|
||||
## Test count
|
||||
|
||||
- **Before remediation:** 38 tests
|
||||
@@ -99,4 +107,5 @@
|
||||
- **After G56-02+03:** 61 tests
|
||||
- **After G56-09:** 65 tests
|
||||
- **After G56-16:** 65 tests (docs-only)
|
||||
- **After G56-07:** 88 tests
|
||||
- **All passing:** ✅
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "post_quantum_nostr",
|
||||
"version": "0.0.18",
|
||||
"version": "0.0.19",
|
||||
"description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -719,3 +719,137 @@ describe('G56-03: identity binding (verifyNIPQRContent)', () => {
|
||||
assert.equal(result.identityBound, true, 'without options, identity binding should pass');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// G56-07: Fuzz / property tests — untrusted input must never throw
|
||||
// ============================================================================
|
||||
|
||||
describe('G56-07: verifyNostrEvent never throws on malformed input', () => {
|
||||
const malformedInputs = [
|
||||
null,
|
||||
undefined,
|
||||
'string',
|
||||
42,
|
||||
true,
|
||||
[],
|
||||
{},
|
||||
{ id: 'abc' },
|
||||
{ pubkey: 'x', sig: 'y', created_at: 'not-a-number', kind: 1, tags: [], content: '' },
|
||||
{ pubkey: 'x', sig: 'y', created_at: 1, kind: 'not-a-number', tags: [], content: '' },
|
||||
{ pubkey: 'x', sig: 'y', created_at: 1, kind: 1, tags: 'not-an-array', content: '' },
|
||||
{ pubkey: 'x', sig: 'y', created_at: 1, kind: 1, tags: [], content: 123 },
|
||||
{ pubkey: 123, sig: 'y', created_at: 1, kind: 1, tags: [], content: '' },
|
||||
{ pubkey: 'x', sig: 123, created_at: 1, kind: 1, tags: [], content: '' },
|
||||
];
|
||||
|
||||
for (const input of malformedInputs) {
|
||||
test(`verifyNostrEvent returns false for ${JSON.stringify(input)?.substring(0, 50)}`, () => {
|
||||
assert.equal(m.verifyNostrEvent(input), false);
|
||||
});
|
||||
}
|
||||
|
||||
test('verifyNostrEvent returns false for random-shaped objects (fuzz)', () => {
|
||||
const types = [null, true, false, 0, 1, 'x', '', [], {}, { id: 1 }, { tags: 'x' }];
|
||||
for (let i = 0; i < 500; i++) {
|
||||
const obj = {};
|
||||
const keys = ['id', 'pubkey', 'sig', 'created_at', 'kind', 'tags', 'content', 'extra'];
|
||||
for (const k of keys) {
|
||||
if (Math.random() < 0.6) obj[k] = types[Math.floor(Math.random() * types.length)];
|
||||
}
|
||||
assert.equal(m.verifyNostrEvent(obj), false, `threw or passed for iteration ${i}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('G56-07: verifyNIPQRContent never throws on arbitrary input', () => {
|
||||
function randomJson() {
|
||||
const types = [null, true, false, 0, 'x', [], {}, { id: 1 }, { tags: 'not-array' }];
|
||||
return types[Math.floor(Math.random() * types.length)];
|
||||
}
|
||||
|
||||
test('random content + tags never throw and never pass (fuzz)', () => {
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const content = Math.random() < 0.5
|
||||
? JSON.stringify(randomJson())
|
||||
: 'not-json-at-' + i;
|
||||
const tags = Math.random() < 0.5
|
||||
? randomJson()
|
||||
: (Math.random() < 0.5 ? [['algorithm', 'x']] : 'not-an-array');
|
||||
let result;
|
||||
assert.doesNotThrow(() => {
|
||||
result = m.verifyNIPQRContent(content, tags);
|
||||
}, `threw on iteration ${i}`);
|
||||
assert.equal(result.validForMigration, false, `passed on iteration ${i}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('null / undefined / wrong-typed content returns structured failure', () => {
|
||||
for (const content of [null, undefined, 42, true, {}]) {
|
||||
let result;
|
||||
assert.doesNotThrow(() => {
|
||||
result = m.verifyNIPQRContent(content, []);
|
||||
});
|
||||
assert.equal(result.validForMigration, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('oversized content is rejected before parsing', () => {
|
||||
const huge = 'x'.repeat(70 * 1024); // > MAX_EVENT_JSON_SIZE (64 KiB)
|
||||
const result = m.verifyNIPQRContent(huge, []);
|
||||
assert.equal(result.validForMigration, false);
|
||||
assert.ok(result.errors.some(e => e.includes('maximum size')), 'should report size error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('G56-07: parseOtsFile never throws on random/truncated/oversized bytes', () => {
|
||||
test('random bytes never throw (fuzz)', () => {
|
||||
for (let i = 0; i < 2000; i++) {
|
||||
const len = Math.floor(Math.random() * 2048);
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let j = 0; j < len; j++) bytes[j] = Math.floor(Math.random() * 256);
|
||||
let result;
|
||||
assert.doesNotThrow(() => { result = m.parseOtsFile(bytes); }, `threw on iteration ${i}, len=${len}`);
|
||||
// result is either null or a valid parse object — never throws
|
||||
if (result !== null) {
|
||||
assert.ok(typeof result === 'object' && 'fileHashOp' in result, 'should be null or valid object');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('non-Uint8Array inputs return null without throwing', () => {
|
||||
for (const input of [null, undefined, 'string', 42, true, [], {}, new Int8Array(10)]) {
|
||||
assert.equal(m.parseOtsFile(input), null);
|
||||
}
|
||||
});
|
||||
|
||||
test('oversized input returns null without parsing', () => {
|
||||
const huge = new Uint8Array(2 * 1024 * 1024); // 2 MiB > MAX_OTS_PROOF_SIZE (1 MiB)
|
||||
assert.equal(m.parseOtsFile(huge), null);
|
||||
});
|
||||
|
||||
test('truncated valid-prefix bytes never throw', () => {
|
||||
// OTS magic header as a starting point
|
||||
const magic = m.hexToBytes('004f70656e54696d657374616d7073000050726f6f6600bf89e2e884e89294');
|
||||
for (let cut = 0; cut <= magic.length; cut++) {
|
||||
const truncated = magic.slice(0, cut);
|
||||
let result;
|
||||
assert.doesNotThrow(() => { result = m.parseOtsFile(truncated); });
|
||||
// short truncations return null (too short); longer ones may parse partially or return null
|
||||
assert.ok(result === null || typeof result === 'object');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('G56-07: isOtsConfirmed never throws on bad input', () => {
|
||||
test('non-Uint8Array and random bytes never throw', () => {
|
||||
for (const input of [null, undefined, 'string', 42, [], {}]) {
|
||||
assert.doesNotThrow(() => m.isOtsConfirmed(input));
|
||||
}
|
||||
for (let i = 0; i < 200; i++) {
|
||||
const len = Math.floor(Math.random() * 512);
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let j = 0; j < len; j++) bytes[j] = Math.floor(Math.random() * 256);
|
||||
assert.doesNotThrow(() => m.isOtsConfirmed(bytes), `threw on iteration ${i}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+79
-38
@@ -412,28 +412,46 @@ export function hexToNpub(hexPubkey) {
|
||||
/**
|
||||
* Verify a Nostr event's secp256k1 Schnorr signature (NIP-01).
|
||||
* Checks both the signature validity AND that event.id matches the computed hash.
|
||||
*
|
||||
* G56-07: This function accepts untrusted input (pasted JSON, relay events) and
|
||||
* must never throw on malformed data. It returns `false` for any structurally
|
||||
* invalid event.
|
||||
*
|
||||
* @param {object} event - Nostr event with id, pubkey, created_at, kind, tags, content, sig
|
||||
* @returns {boolean} true if signature is valid and id matches
|
||||
* @returns {boolean} true if signature is valid and id matches; false otherwise (never throws)
|
||||
*/
|
||||
export function verifyNostrEvent(event) {
|
||||
// Build the event hash (NIP-01 serialization)
|
||||
const serialized = JSON.stringify([
|
||||
0,
|
||||
event.pubkey,
|
||||
event.created_at,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content
|
||||
]);
|
||||
const hash = sha256(new TextEncoder().encode(serialized));
|
||||
const computedId = bytesToHex(hash);
|
||||
// F-H2: Verify event.id matches the computed hash
|
||||
if (event.id && event.id.toLowerCase() !== computedId.toLowerCase()) {
|
||||
try {
|
||||
// Structural validation — fail closed on wrong shape
|
||||
if (!event || typeof event !== 'object') return false;
|
||||
if (typeof event.pubkey !== 'string' || typeof event.sig !== 'string') return false;
|
||||
if (typeof event.created_at !== 'number') return false;
|
||||
if (typeof event.kind !== 'number') return false;
|
||||
if (!Array.isArray(event.tags)) return false;
|
||||
if (typeof event.content !== 'string') return false;
|
||||
|
||||
// Build the event hash (NIP-01 serialization)
|
||||
const serialized = JSON.stringify([
|
||||
0,
|
||||
event.pubkey,
|
||||
event.created_at,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content
|
||||
]);
|
||||
const hash = sha256(new TextEncoder().encode(serialized));
|
||||
const computedId = bytesToHex(hash);
|
||||
// F-H2: Verify event.id matches the computed hash
|
||||
if (event.id && event.id.toLowerCase() !== computedId.toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
const sig = hexToBytes(event.sig);
|
||||
const pubkey = hexToBytes(event.pubkey);
|
||||
return schnorr.verify(sig, hash, pubkey);
|
||||
} catch (e) {
|
||||
// G56-07: never throw on untrusted malformed input
|
||||
return false;
|
||||
}
|
||||
const sig = hexToBytes(event.sig);
|
||||
const pubkey = hexToBytes(event.pubkey);
|
||||
return schnorr.verify(sig, hash, pubkey);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -695,6 +713,16 @@ export function verifyNIPQRContent(kind11112Content, kind11112Tags, options = {}
|
||||
const errors = [];
|
||||
const { expectedAuthor, outerPubkey } = options;
|
||||
|
||||
// G56-07: reject oversized content before any parsing/decoding work
|
||||
if (typeof kind11112Content === 'string' && kind11112Content.length > MAX_EVENT_JSON_SIZE) {
|
||||
return {
|
||||
results: [{ algorithm: 'event', valid: false, note: 'content exceeds maximum size' }],
|
||||
kind1Event: null, fullHash: null, sha256Valid: false, eTagValid: false,
|
||||
pqProofsValid: false, policySufficient: false, identityBound: false,
|
||||
validForMigration: false, errors: ['content exceeds maximum size']
|
||||
};
|
||||
}
|
||||
|
||||
// --- Helper: push a result entry and track failures ---
|
||||
function pushResult(algorithm, valid, note) {
|
||||
results.push({ algorithm, valid, note });
|
||||
@@ -1519,6 +1547,11 @@ class OtsReader {
|
||||
// OTS magic header (31 bytes)
|
||||
const OTS_MAGIC = hexToBytes('004f70656e54696d657374616d7073000050726f6f6600bf89e2e884e89294');
|
||||
|
||||
// G56-07: Maximum sizes for untrusted inputs. Reject larger inputs before any
|
||||
// parsing or cryptographic work to prevent denial-of-service via oversized data.
|
||||
const MAX_OTS_PROOF_SIZE = 1 << 20; // 1 MiB — generous for heavily upgraded proofs
|
||||
const MAX_EVENT_JSON_SIZE = 1 << 16; // 64 KiB — NIP-QR events are typically 30-50 KiB
|
||||
|
||||
// Attestation tags
|
||||
const BITCOIN_ATTESTATION_TAG = hexToBytes('0588960d73d71901');
|
||||
const LITECOIN_ATTESTATION_TAG = hexToBytes('06869a0d73d71b45');
|
||||
@@ -1540,35 +1573,43 @@ function bytesEqual(a, b) {
|
||||
* that the attestation covers (after walking the op tree from the target).
|
||||
*/
|
||||
export function parseOtsFile(otsBytes) {
|
||||
// G56-07: fail closed on untrusted input — never throw
|
||||
if (!(otsBytes instanceof Uint8Array) || otsBytes.length < OTS_MAGIC.length) return null;
|
||||
// G56-07: reject oversized proofs before any parsing work
|
||||
if (otsBytes.length > MAX_OTS_PROOF_SIZE) return null;
|
||||
|
||||
const reader = new OtsReader(otsBytes);
|
||||
try {
|
||||
const reader = new OtsReader(otsBytes);
|
||||
|
||||
// 1. Magic header
|
||||
const magic = reader.readBytes(OTS_MAGIC.length);
|
||||
if (!bytesEqual(magic, OTS_MAGIC)) return null;
|
||||
// 1. Magic header
|
||||
const magic = reader.readBytes(OTS_MAGIC.length);
|
||||
if (!bytesEqual(magic, OTS_MAGIC)) return null;
|
||||
|
||||
// 2. Version (varuint) — we support major version 1
|
||||
const version = reader.readVaruint();
|
||||
if (version !== 1) return null;
|
||||
// 2. Version (varuint) — we support major version 1
|
||||
const version = reader.readVaruint();
|
||||
if (version !== 1) return null;
|
||||
|
||||
// 3. File hash operation tag (1 byte)
|
||||
const hashOpTag = reader.readByte();
|
||||
let fileHashOp = 'unknown';
|
||||
let digestLen = 32;
|
||||
if (hashOpTag === 0x08) { fileHashOp = 'sha256'; digestLen = 32; }
|
||||
else if (hashOpTag === 0x02) { fileHashOp = 'sha1'; digestLen = 20; }
|
||||
else if (hashOpTag === 0x03) { fileHashOp = 'ripemd160'; digestLen = 20; }
|
||||
else { return null; } // unsupported hash op
|
||||
// 3. File hash operation tag (1 byte)
|
||||
const hashOpTag = reader.readByte();
|
||||
let fileHashOp = 'unknown';
|
||||
let digestLen = 32;
|
||||
if (hashOpTag === 0x08) { fileHashOp = 'sha256'; digestLen = 32; }
|
||||
else if (hashOpTag === 0x02) { fileHashOp = 'sha1'; digestLen = 20; }
|
||||
else if (hashOpTag === 0x03) { fileHashOp = 'ripemd160'; digestLen = 20; }
|
||||
else { return null; } // unsupported hash op
|
||||
|
||||
// 4. File digest (the target — what was timestamped)
|
||||
const targetDigest = reader.readBytes(digestLen);
|
||||
// 4. File digest (the target — what was timestamped)
|
||||
const targetDigest = reader.readBytes(digestLen);
|
||||
|
||||
// 5. Timestamp tree
|
||||
const attestations = [];
|
||||
_parseTimestamp(reader, targetDigest, attestations, 0);
|
||||
// 5. Timestamp tree
|
||||
const attestations = [];
|
||||
_parseTimestamp(reader, targetDigest, attestations, 0);
|
||||
|
||||
return { fileHashOp, targetDigest, attestations };
|
||||
return { fileHashOp, targetDigest, attestations };
|
||||
} catch (e) {
|
||||
// G56-07: never throw on malformed/truncated untrusted input
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.0.18",
|
||||
"VERSION_NUMBER": "0.0.18",
|
||||
"BUILD_DATE": "2026-07-24T13:09:51.134Z"
|
||||
"VERSION": "v0.0.19",
|
||||
"VERSION_NUMBER": "0.0.19",
|
||||
"BUILD_DATE": "2026-07-24T13:25:51.884Z"
|
||||
}
|
||||
|
||||
+67
-36
@@ -9761,22 +9761,32 @@ function hexToNpub(hexPubkey) {
|
||||
return bech32.encode("npub", bech32.toWords(bytes));
|
||||
}
|
||||
function verifyNostrEvent(event) {
|
||||
const serialized = JSON.stringify([
|
||||
0,
|
||||
event.pubkey,
|
||||
event.created_at,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content
|
||||
]);
|
||||
const hash = sha256(new TextEncoder().encode(serialized));
|
||||
const computedId = bytesToHex3(hash);
|
||||
if (event.id && event.id.toLowerCase() !== computedId.toLowerCase()) {
|
||||
try {
|
||||
if (!event || typeof event !== "object") return false;
|
||||
if (typeof event.pubkey !== "string" || typeof event.sig !== "string") return false;
|
||||
if (typeof event.created_at !== "number") return false;
|
||||
if (typeof event.kind !== "number") return false;
|
||||
if (!Array.isArray(event.tags)) return false;
|
||||
if (typeof event.content !== "string") return false;
|
||||
const serialized = JSON.stringify([
|
||||
0,
|
||||
event.pubkey,
|
||||
event.created_at,
|
||||
event.kind,
|
||||
event.tags,
|
||||
event.content
|
||||
]);
|
||||
const hash = sha256(new TextEncoder().encode(serialized));
|
||||
const computedId = bytesToHex3(hash);
|
||||
if (event.id && event.id.toLowerCase() !== computedId.toLowerCase()) {
|
||||
return false;
|
||||
}
|
||||
const sig = hexToBytes3(event.sig);
|
||||
const pubkey = hexToBytes3(event.pubkey);
|
||||
return schnorr.verify(sig, hash, pubkey);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
const sig = hexToBytes3(event.sig);
|
||||
const pubkey = hexToBytes3(event.pubkey);
|
||||
return schnorr.verify(sig, hash, pubkey);
|
||||
}
|
||||
var NIP_QR_KIND = 9999;
|
||||
function buildKind1Announcement(hexPubkey, blockHeight, pqKeys) {
|
||||
@@ -9886,6 +9896,20 @@ function verifyNIPQRContent(kind11112Content, kind11112Tags, options = {}) {
|
||||
const results = [];
|
||||
const errors = [];
|
||||
const { expectedAuthor, outerPubkey } = options;
|
||||
if (typeof kind11112Content === "string" && kind11112Content.length > MAX_EVENT_JSON_SIZE) {
|
||||
return {
|
||||
results: [{ algorithm: "event", valid: false, note: "content exceeds maximum size" }],
|
||||
kind1Event: null,
|
||||
fullHash: null,
|
||||
sha256Valid: false,
|
||||
eTagValid: false,
|
||||
pqProofsValid: false,
|
||||
policySufficient: false,
|
||||
identityBound: false,
|
||||
validForMigration: false,
|
||||
errors: ["content exceeds maximum size"]
|
||||
};
|
||||
}
|
||||
function pushResult(algorithm, valid, note) {
|
||||
results.push({ algorithm, valid, note });
|
||||
if (!valid) errors.push(`${algorithm}: ${note || "INVALID"}`);
|
||||
@@ -10446,6 +10470,8 @@ var OtsReader = class {
|
||||
}
|
||||
};
|
||||
var OTS_MAGIC = hexToBytes3("004f70656e54696d657374616d7073000050726f6f6600bf89e2e884e89294");
|
||||
var MAX_OTS_PROOF_SIZE = 1 << 20;
|
||||
var MAX_EVENT_JSON_SIZE = 1 << 16;
|
||||
var BITCOIN_ATTESTATION_TAG = hexToBytes3("0588960d73d71901");
|
||||
var LITECOIN_ATTESTATION_TAG = hexToBytes3("06869a0d73d71b45");
|
||||
var PENDING_ATTESTATION_TAG = hexToBytes3("83dfe30d2ef90c8e");
|
||||
@@ -10456,30 +10482,35 @@ function bytesEqual(a, b) {
|
||||
}
|
||||
function parseOtsFile(otsBytes) {
|
||||
if (!(otsBytes instanceof Uint8Array) || otsBytes.length < OTS_MAGIC.length) return null;
|
||||
const reader = new OtsReader(otsBytes);
|
||||
const magic = reader.readBytes(OTS_MAGIC.length);
|
||||
if (!bytesEqual(magic, OTS_MAGIC)) return null;
|
||||
const version = reader.readVaruint();
|
||||
if (version !== 1) return null;
|
||||
const hashOpTag = reader.readByte();
|
||||
let fileHashOp = "unknown";
|
||||
let digestLen = 32;
|
||||
if (hashOpTag === 8) {
|
||||
fileHashOp = "sha256";
|
||||
digestLen = 32;
|
||||
} else if (hashOpTag === 2) {
|
||||
fileHashOp = "sha1";
|
||||
digestLen = 20;
|
||||
} else if (hashOpTag === 3) {
|
||||
fileHashOp = "ripemd160";
|
||||
digestLen = 20;
|
||||
} else {
|
||||
if (otsBytes.length > MAX_OTS_PROOF_SIZE) return null;
|
||||
try {
|
||||
const reader = new OtsReader(otsBytes);
|
||||
const magic = reader.readBytes(OTS_MAGIC.length);
|
||||
if (!bytesEqual(magic, OTS_MAGIC)) return null;
|
||||
const version = reader.readVaruint();
|
||||
if (version !== 1) return null;
|
||||
const hashOpTag = reader.readByte();
|
||||
let fileHashOp = "unknown";
|
||||
let digestLen = 32;
|
||||
if (hashOpTag === 8) {
|
||||
fileHashOp = "sha256";
|
||||
digestLen = 32;
|
||||
} else if (hashOpTag === 2) {
|
||||
fileHashOp = "sha1";
|
||||
digestLen = 20;
|
||||
} else if (hashOpTag === 3) {
|
||||
fileHashOp = "ripemd160";
|
||||
digestLen = 20;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
const targetDigest = reader.readBytes(digestLen);
|
||||
const attestations = [];
|
||||
_parseTimestamp(reader, targetDigest, attestations, 0);
|
||||
return { fileHashOp, targetDigest, attestations };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
const targetDigest = reader.readBytes(digestLen);
|
||||
const attestations = [];
|
||||
_parseTimestamp(reader, targetDigest, attestations, 0);
|
||||
return { fileHashOp, targetDigest, attestations };
|
||||
}
|
||||
function _parseTimestamp(reader, msg, attestations, depth) {
|
||||
if (depth > 512) throw new Error("OTS: recursion limit exceeded");
|
||||
|
||||
Reference in New Issue
Block a user