mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-utils.git
synced 2026-11-06 01:01:07 +00:00
Root signing page and a root chain check for replacing the root keys (#81)
- signed_config.py root drafts a later root in docs/root-signing/proposals/, where wallets never look, and keeps the current targets key by default. - docs/root-signing/: a page for the Pezkuwi Wallet DApp browser that shows the proposal and its fingerprint, has the holder's wallet sign <Bytes>payload</Bytes>, checks every signature as the wallet will, collects signature codes and offers root.N.json once two current and two new holders have signed. Crypto bundles are served beside it. - scripts/root/verify_root.mjs checks every published signed/root.N.json the way installed wallets do; Code Quality runs it. Wallet 1.3.0 accepts sr25519 only under the substrate signing context, and a root it cannot verify stops its config refresh, so such a root must never reach master. - Draft root 2: three new holder accounts, threshold 2, five years.
This commit is contained in:
1 parent
7d53e16b76
commit
6864b8c0aa
13 files changed
+27902
-8
No files matched your search
@@ -93,6 +93,18 @@ jobs:
|
||||
- name: Light client checkpoint not stale
|
||||
run: python3 scripts/light_client_specs.py --relay-rpc https://zagros-rpc.pezkuwichain.io --check-age 60
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
# A wallet that finds a root it cannot verify stops refreshing its config, so every published
|
||||
# signed/root.N.json is checked here exactly as installed wallets check it.
|
||||
- name: Root chain verifies as installed wallets check it
|
||||
run: |
|
||||
npm ci --prefix scripts/root --ignore-scripts --no-audit --no-fund
|
||||
node scripts/root/verify_root.mjs docs/root-signing/proposals/*.json
|
||||
|
||||
chain-integrity:
|
||||
name: Chain Config Integrity
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
@@ -78,6 +78,22 @@ Code Quality fails once the checkpoint is more than 60 days old.
|
||||
Code Quality refuses a tree whose files do not match the signed list, and its weekly run fails a month
|
||||
before a signature expires.
|
||||
|
||||
### Replacing the root keys
|
||||
|
||||
A new root is drafted where the wallet never looks, then signed by the holders in their wallets:
|
||||
|
||||
```
|
||||
python3 scripts/signed_config.py root --version N --expires-days 1825 --root-threshold 2 --root ADDR ADDR ADDR
|
||||
```
|
||||
|
||||
This writes `docs/root-signing/proposals/root.N.json`, keeping the current targets key. Each holder of the
|
||||
current root and of the new one opens the root signing page (`docs/root-signing/`, served by GitHub Pages) in
|
||||
the Pezkuwi Wallet DApp browser, compares the fingerprint with the others by phone, signs, and sends the
|
||||
signature code to whoever collects them. Once two current and two new holders have signed, the page offers
|
||||
`root.N.json`; it goes to `signed/root.N.json` in a pull request. Code Quality checks every published root as
|
||||
installed wallets do (wallet 1.3.0 accepts sr25519 only under the `substrate` signing context) and fails
|
||||
before a root they would refuse reaches master: such a root stops their config refresh.
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork this repository
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
<!doctype html>
|
||||
<html lang="ku">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Guhertina mifteya kokê</title>
|
||||
<!--
|
||||
Root signing page for the wallet's signed config. Open it in the Pezkuwi Wallet DApp browser.
|
||||
It shows the drafted root (proposals/root.N.json), has the holder's wallet sign <Bytes>payload</Bytes>,
|
||||
checks each signature the way the wallet will, and assembles signed/root.N.json once a threshold of the
|
||||
previous root's keys and of the new root's keys have signed. Wallet 1.3.0 accepts sr25519 under the
|
||||
"substrate" signing context only, so that is what counts toward the thresholds.
|
||||
-->
|
||||
<script src="vendor/bundle-pezkuwi-util.js"></script>
|
||||
<script src="vendor/bundle-pezkuwi-util-crypto.js"></script>
|
||||
<script src="vendor/bundle-polkadot-util.js"></script>
|
||||
<script src="vendor/bundle-polkadot-util-crypto.js"></script>
|
||||
<style>
|
||||
:root { --bg: #08090E; --panel: rgba(153,158,199,0.10); --line: rgba(153,158,199,0.16); --text: rgba(255,255,255,0.88);
|
||||
--muted: rgba(255,255,255,0.60); --accent: #009639; --ok: #2FC864; --warn: #EBC50A; --bad: #E53450; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: var(--bg); color: var(--text); font: 15px/1.5 system-ui, sans-serif; }
|
||||
main { max-width: 560px; margin: 0 auto; padding: 20px 16px 40px; display: flex; flex-direction: column; gap: 14px; }
|
||||
h1 { margin: 0; font-size: 22px; }
|
||||
p { margin: 0; color: var(--muted); }
|
||||
.panel { background: var(--panel); border-radius: 12px; }
|
||||
.row { display: flex; justify-content: space-between; gap: 12px; padding: 12px; border-bottom: 1px solid var(--line); }
|
||||
.row:last-child { border-bottom: 0; }
|
||||
.row span:first-child { color: var(--muted); }
|
||||
.mono { font-family: ui-monospace, monospace; word-break: break-all; }
|
||||
.fp { font-size: 17px; font-weight: 700; letter-spacing: 1px; }
|
||||
button { min-height: 48px; border-radius: 12px; border: 0; background: var(--accent); color: #fff; font: inherit; font-weight: 600; cursor: pointer; }
|
||||
button.secondary { background: rgba(153,158,199,0.16); color: var(--text); }
|
||||
button:disabled { opacity: 0.45; cursor: default; }
|
||||
textarea { width: 100%; min-height: 80px; border-radius: 10px; border: 1px solid var(--line); background: var(--panel); color: var(--text); padding: 10px; font: 13px ui-monospace, monospace; }
|
||||
.ok { color: var(--ok); } .warn { color: var(--warn); } .bad { color: var(--bad); }
|
||||
.note { padding: 12px; border-radius: 12px; font-size: 14px; }
|
||||
.note.warn { background: rgba(237,206,54,0.14); } .note.bad { background: rgba(229,52,80,0.14); } .note.ok { background: rgba(47,200,100,0.12); }
|
||||
label { display: flex; flex-direction: column; gap: 6px; color: var(--muted); font-size: 13px; }
|
||||
[hidden] { display: none !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Guhertina mifteya kokê</h1>
|
||||
<p id="summary">Tê barkirin…</p>
|
||||
<div id="error" class="note bad" hidden></div>
|
||||
|
||||
<div class="panel" id="facts" hidden>
|
||||
<div class="row"><span>Guherto</span><span id="version"></span></div>
|
||||
<div class="row"><span>Sînor</span><span id="threshold"></span></div>
|
||||
<div class="row"><span>Dema derbasbûnê</span><span id="expires"></span></div>
|
||||
</div>
|
||||
|
||||
<div id="keysBlock" hidden>
|
||||
<p>Mifteyên kokê yên nû (sr25519, hesabên cîzdanê)</p>
|
||||
<div class="panel" id="newKeys"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel row" id="fpBlock" hidden><span>Şopa pêşniyarê</span><span class="mono fp" id="fingerprint"></span></div>
|
||||
<p id="fpHint" hidden>Berî îmzekirinê vê şopê bi telefonê bi xwediyên din re bide ber hev.</p>
|
||||
|
||||
<button id="signButton" hidden>Bi cîzdanê îmze bike</button>
|
||||
<div id="signResult" class="note" hidden></div>
|
||||
<button id="shareButton" class="secondary" hidden>Koda îmzeya min kopî bike</button>
|
||||
|
||||
<div id="collect" hidden>
|
||||
<p id="tally"></p>
|
||||
<div class="panel" id="holders"></div>
|
||||
</div>
|
||||
<label id="pasteBlock" hidden>Koda îmzeyê ya xwediyekî din zêde bike
|
||||
<textarea id="paste" placeholder="Kodê li vir bixe"></textarea>
|
||||
</label>
|
||||
<button id="addButton" class="secondary" hidden>Kodê zêde bike</button>
|
||||
<button id="downloadButton" hidden disabled>root.json daxîne</button>
|
||||
</main>
|
||||
<script>
|
||||
(async () => {
|
||||
const pz = window.pezkuwiUtilCrypto, pzu = window.pezkuwiUtil, pk = window.polkadotUtilCrypto;
|
||||
const RAW = 'https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/signed/';
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const enc = new TextEncoder();
|
||||
const show = (id, on = true) => { $(id).hidden = !on; };
|
||||
const fail = (message) => { $('error').textContent = message; show('error'); $('summary').textContent = ''; };
|
||||
|
||||
const b64 = (u8) => btoa(String.fromCharCode(...u8));
|
||||
const unb64 = (s) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
|
||||
const hex = (u8) => Array.from(u8, (b) => b.toString(16).padStart(2, '0')).join('');
|
||||
const sha256 = async (u8) => new Uint8Array(await crypto.subtle.digest('SHA-256', u8));
|
||||
const wrap = (payload) => pzu.u8aConcat(enc.encode('<Bytes>'), payload, enc.encode('</Bytes>'));
|
||||
|
||||
async function readEnvelope(text) {
|
||||
const envelope = JSON.parse(text);
|
||||
const payload = unb64(envelope.signed);
|
||||
return { payload, root: JSON.parse(new TextDecoder().decode(payload)), signatures: envelope.signatures || [] };
|
||||
}
|
||||
|
||||
try {
|
||||
await pz.cryptoWaitReady();
|
||||
await pk.cryptoWaitReady();
|
||||
|
||||
// The newest published root: root.json, then root.N.json while they exist.
|
||||
let previous = await readEnvelope(await (await fetch(RAW + 'root.json', { cache: 'no-store' })).text());
|
||||
for (;;) {
|
||||
const next = await fetch(RAW + `root.${previous.root.version + 1}.json`, { cache: 'no-store' });
|
||||
if (!next.ok) break;
|
||||
previous = await readEnvelope(await next.text());
|
||||
}
|
||||
const version = previous.root.version + 1;
|
||||
const response = await fetch(`proposals/root.${version}.json`, { cache: 'no-store' });
|
||||
if (!response.ok) { $('summary').textContent = `Ji bo guhertoya ${version} pêşniyar tune.`; return; }
|
||||
const proposalText = await response.text();
|
||||
const proposal = await readEnvelope(proposalText);
|
||||
if (proposal.root.type !== 'root' || proposal.root.version !== version) throw new Error('Pêşniyar ne koka paşîn e.');
|
||||
|
||||
const message = wrap(proposal.payload);
|
||||
const fingerprint = hex(await sha256(proposal.payload)).slice(0, 12).toUpperCase().match(/.{4}/g).join(' · ');
|
||||
|
||||
// keyId -> {address, roles}
|
||||
const holders = new Map();
|
||||
const addHolder = (root, role) => {
|
||||
for (const keyId of root.root.keyids) {
|
||||
const key = root.keys[keyId];
|
||||
if (key.scheme !== 'sr25519') continue;
|
||||
const entry = holders.get(keyId) || { public: unb64(key.public), roles: new Set() };
|
||||
entry.roles.add(role);
|
||||
holders.set(keyId, entry);
|
||||
}
|
||||
};
|
||||
addHolder(previous.root, 'previous');
|
||||
addHolder(proposal.root, 'next');
|
||||
for (const entry of holders.values()) entry.address = pz.encodeAddress(entry.public, 42);
|
||||
|
||||
const signatures = new Map(); // keyId -> {sig, context}
|
||||
function classify(keyId, sig) {
|
||||
const entry = holders.get(keyId);
|
||||
if (!entry) return null;
|
||||
if (pk.sr25519Verify(message, sig, entry.public)) return 'substrate';
|
||||
if (pz.sr25519Verify(message, sig, entry.public)) return 'bizinikiwi';
|
||||
return null;
|
||||
}
|
||||
for (const { keyid, sig } of proposal.signatures) {
|
||||
const bytes = unb64(sig);
|
||||
const context = classify(keyid, bytes);
|
||||
if (context) signatures.set(keyid, { sig: bytes, context });
|
||||
}
|
||||
|
||||
$('summary').textContent = `Pêşniyar: koka config-a îmzekirî ya cîzdanê. Ji ${proposal.root.root.keyids.length} xwediyên nû ${proposal.root.root.threshold} îmze, ji yên heyî ${previous.root.root.threshold} îmze pêwîst in.`;
|
||||
$('version').textContent = `${previous.root.version} → ${version}`;
|
||||
$('threshold').textContent = `${proposal.root.root.threshold} ji ${proposal.root.root.keyids.length}`;
|
||||
$('expires').textContent = new Date(proposal.root.expires * 1000).toLocaleDateString('de-DE');
|
||||
for (const keyId of proposal.root.root.keyids) {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
row.innerHTML = '<span></span><span class="mono"></span>';
|
||||
row.children[0].textContent = holders.get(keyId).address.slice(0, 6) + '…';
|
||||
row.children[1].textContent = holders.get(keyId).address;
|
||||
$('newKeys').appendChild(row);
|
||||
}
|
||||
$('fingerprint').textContent = fingerprint;
|
||||
['facts', 'keysBlock', 'fpBlock', 'fpHint', 'collect', 'pasteBlock', 'addButton', 'downloadButton'].forEach((id) => show(id));
|
||||
|
||||
function render() {
|
||||
$('holders').replaceChildren();
|
||||
const counts = { previous: 0, next: 0 };
|
||||
for (const [keyId, entry] of holders) {
|
||||
const signed = signatures.get(keyId);
|
||||
if (signed && signed.context === 'substrate') entry.roles.forEach((role) => counts[role]++);
|
||||
const row = document.createElement('div');
|
||||
row.className = 'row';
|
||||
row.innerHTML = '<span class="mono"></span><span></span>';
|
||||
const roles = [...entry.roles].map((r) => (r === 'previous' ? 'heyî' : 'nû')).join(' + ');
|
||||
row.children[0].textContent = `${entry.address.slice(0, 6)}…${entry.address.slice(-4)} (${roles})`;
|
||||
row.children[1].textContent = !signed ? 'Li bendê' : signed.context === 'substrate' ? 'Îmzekirî' : 'Çewt: bizinikiwi';
|
||||
row.children[1].className = !signed ? '' : signed.context === 'substrate' ? 'ok' : 'bad';
|
||||
$('holders').appendChild(row);
|
||||
}
|
||||
const need = { previous: previous.root.root.threshold, next: proposal.root.root.threshold };
|
||||
$('tally').textContent = `Îmze: heyî ${counts.previous} ji ${need.previous} · nû ${counts.next} ji ${need.next}`;
|
||||
$('downloadButton').disabled = !(counts.previous >= need.previous && counts.next >= need.next);
|
||||
}
|
||||
render();
|
||||
|
||||
$('downloadButton').onclick = () => {
|
||||
const valid = [...signatures].filter(([, s]) => s.context === 'substrate').map(([keyid, s]) => ({ keyid, sig: b64(s.sig) }));
|
||||
const envelope = JSON.parse(proposalText);
|
||||
envelope.signatures = valid;
|
||||
const blob = new Blob([JSON.stringify(envelope, null, 1) + '\n'], { type: 'application/json' });
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `root.${version}.json`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
$('addButton').onclick = () => {
|
||||
const text = $('paste').value.trim();
|
||||
try {
|
||||
const { keyid, sig } = JSON.parse(atob(text));
|
||||
const bytes = unb64(sig);
|
||||
const context = classify(keyid, bytes);
|
||||
if (!context) throw new Error('bad');
|
||||
signatures.set(keyid, { sig: bytes, context });
|
||||
$('paste').value = '';
|
||||
render();
|
||||
} catch (e) {
|
||||
alert('Ev kod ji bo vê pêşniyarê ne derbasdar e.');
|
||||
}
|
||||
};
|
||||
|
||||
// The wallet's injected provider; the signing page asks for accounts only to find the holder's key.
|
||||
const injected = window.injectedWeb3 && (injectedWeb3['pezkuwi-wallet'] || injectedWeb3['pezkuwi-js'] || injectedWeb3['polkadot-js']);
|
||||
if (!injected) {
|
||||
$('signResult').textContent = 'Ji bo îmzekirinê vê rûpelê di geroka DApp ya Pezkuwi Cîzdan de veke.';
|
||||
$('signResult').className = 'note warn';
|
||||
show('signResult');
|
||||
return;
|
||||
}
|
||||
show('signButton');
|
||||
$('signButton').onclick = async () => {
|
||||
try {
|
||||
const extension = await injected.enable('Pezkuwi root signing');
|
||||
const accounts = await extension.accounts.get();
|
||||
const mine = accounts.map((a) => ({ address: a.address, keyId: null, public: pz.decodeAddress(a.address) }));
|
||||
for (const account of mine) account.keyId = hex(await sha256(account.public));
|
||||
const holder = mine.find((a) => holders.has(a.keyId));
|
||||
if (!holder) throw new Error('Di vê cîzdanê de hesabekî xwediyê kokê tune.');
|
||||
|
||||
const { signature } = await extension.signer.signRaw({ address: holder.address, data: pzu.u8aToHex(message), type: 'bytes' });
|
||||
let sig = pzu.hexToU8a(signature);
|
||||
if (sig.length === 65 && sig[0] === 1) sig = sig.slice(1); // MultiSignature: 0x01 = sr25519
|
||||
const context = classify(holder.keyId, sig);
|
||||
signatures.set(holder.keyId, { sig, context });
|
||||
render();
|
||||
|
||||
const box = $('signResult');
|
||||
if (context === 'substrate') {
|
||||
box.textContent = 'Îmze derbasdar e. Kodê kopî bike û ji xwediyê ku pelê berhev dike re bişîne.';
|
||||
box.className = 'note ok';
|
||||
const code = btoa(JSON.stringify({ keyid: holder.keyId, sig: b64(sig) }));
|
||||
$('shareButton').onclick = () => navigator.clipboard.writeText(code).then(() => alert('Hate kopîkirin'), () => prompt('Kod', code));
|
||||
show('shareButton');
|
||||
} else {
|
||||
box.textContent = context === 'bizinikiwi'
|
||||
? 'Cîzdan bi çarçoveya "bizinikiwi" îmze kir; cîzdanên 1.3.0 vê qebûl nakin. Ji pêşdebiran re bêje.'
|
||||
: 'Îmze nehat piştrastkirin.';
|
||||
box.className = 'note bad';
|
||||
}
|
||||
show('signResult');
|
||||
} catch (e) {
|
||||
$('signResult').textContent = e.message || String(e);
|
||||
$('signResult').className = 'note bad';
|
||||
show('signResult');
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
fail(e.message || String(e));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"signed": "eyJleHBpcmVzIjoxOTUxNTAxMDIwLCJrZXlzIjp7IjYwNjZlNmE4NzI5YzM0MDk4OWU3YzE1MGVmYmQ2M2IxZWZiOWZmMWNjYWExZTFmMzhmYTAxNWMyZjFmOWFiNWEiOnsicHVibGljIjoiWGFPSGYzT1NTWUxxNmxabUVuSTNDRlk5cmR3UGU5eWNsc0R4U1A2NEdXWT0iLCJzY2hlbWUiOiJlZDI1NTE5In0sIjdhMzU1Y2Q1MWNjMDQxNTc5MDM1NzJhNjRkY2ZiYmIyY2Q2NTUzNTA4ODZkNzllMGM5ZDI1Yzc5MWI3MDQ2ZWEiOnsicHVibGljIjoiakhGZ3p2YThEUkJKSWRYa1dXdGxHZ3hBazVLOW1KeXhCYWIvakhPQlMyaz0iLCJzY2hlbWUiOiJzcjI1NTE5In0sIjdlZmQ2MDg4Y2FlOWYzZDMzMGFmZjAwOGM1N2RkNzA5NWE2MjczMjkxMjRhZTYwZTg2Yzg3NmZhMWE1ZTkwY2MiOnsicHVibGljIjoiM0lmZThTRnNVWkRhdFIrb3BsekdSZjdYSU8rQU93RGVmTVJOMFVLcjBRZz0iLCJzY2hlbWUiOiJzcjI1NTE5In0sImZmZjc1NGJiNjcxNmYwNDU1Mjk2NzJkNmRkNWQxYzhlNmUxNzBkODNmZThjZDA3NjUyYWE4N2E0MjdjMGFhZjgiOnsicHVibGljIjoicEsyMi9nUVVtRmZsekNJTXU3czdHNEpiNkhvNFNTR2ZLSHNLUk5hVkkwYz0iLCJzY2hlbWUiOiJzcjI1NTE5In19LCJyb290Ijp7ImtleWlkcyI6WyI3YTM1NWNkNTFjYzA0MTU3OTAzNTcyYTY0ZGNmYmJiMmNkNjU1MzUwODg2ZDc5ZTBjOWQyNWM3OTFiNzA0NmVhIiwiN2VmZDYwODhjYWU5ZjNkMzMwYWZmMDA4YzU3ZGQ3MDk1YTYyNzMyOTEyNGFlNjBlODZjODc2ZmExYTVlOTBjYyIsImZmZjc1NGJiNjcxNmYwNDU1Mjk2NzJkNmRkNWQxYzhlNmUxNzBkODNmZThjZDA3NjUyYWE4N2E0MjdjMGFhZjgiXSwidGhyZXNob2xkIjoyfSwidGFyZ2V0cyI6eyJrZXlpZHMiOlsiNjA2NmU2YTg3MjljMzQwOTg5ZTdjMTUwZWZiZDYzYjFlZmI5ZmYxY2NhYTFlMWYzOGZhMDE1YzJmMWY5YWI1YSJdLCJ0aHJlc2hvbGQiOjF9LCJ0eXBlIjoicm9vdCIsInZlcnNpb24iOjJ9",
|
||||
"signatures": []
|
||||
}
|
||||
+8325
File diff suppressed because it is too large.
Load diff
+4863
File diff suppressed because it is too large.
Load diff
+8865
File diff suppressed because it is too large.
Load diff
+4829
File diff suppressed because it is too large.
Load diff
Generated
+562
@@ -0,0 +1,562 @@
|
||||
{
|
||||
"name": "pezkuwi-wallet-utils-root",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pezkuwi-wallet-utils-root",
|
||||
"dependencies": {
|
||||
"@pezkuwi/util": "14.0.26",
|
||||
"@pezkuwi/util-crypto": "14.0.26",
|
||||
"@polkadot/util": "13.5.9",
|
||||
"@polkadot/util-crypto": "13.5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
|
||||
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.21.3 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/networks": {
|
||||
"version": "14.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/networks/-/networks-14.0.26.tgz",
|
||||
"integrity": "sha512-omDbJzFjDSm1FS32zebyNP1QGBk9/ZayM0YM0mUlOOwiLIP7iDVq3KQe2JdI7hm1dz01Lm68/hjerSoqiWEclA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pezkuwi/util": "14.0.26",
|
||||
"@substrate/ss58-registry": "^1.51.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/scure-sr25519": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/scure-sr25519/-/scure-sr25519-0.2.1.tgz",
|
||||
"integrity": "sha512-KGxCR+fI4p8QyIpwmRNIj8fbq7O+yz0iZZ/O2HJBVE7O9D6ksBKwIILqODJfE6Qvrf3F8DyT6/3cPoR3e/s8iA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/curves": "~1.9.2",
|
||||
"@noble/hashes": "~1.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/util": {
|
||||
"version": "14.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/util/-/util-14.0.26.tgz",
|
||||
"integrity": "sha512-xV7uct4jtIqf+9LixcCoTgYz8JEZ2eK6aosMD92Zu5DRPSW87QcCYBQuq8Phec0LdjGvC/l7DgiloQODhTtHWA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pezkuwi/x-bigint": "14.0.26",
|
||||
"@pezkuwi/x-global": "14.0.26",
|
||||
"@pezkuwi/x-textdecoder": "14.0.26",
|
||||
"@pezkuwi/x-textencoder": "14.0.26",
|
||||
"@types/bn.js": "^5.1.6",
|
||||
"bn.js": "^5.2.1",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/util-crypto": {
|
||||
"version": "14.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/util-crypto/-/util-crypto-14.0.26.tgz",
|
||||
"integrity": "sha512-YR+3N2FwKx3q9NF11q75O0lJ+SKpM9CNKPE7rLEoPvFt7wpHpB/GXOAekYhNtrotkcsI8v/WoyOi4RI9NiwYVQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@noble/curves": "^1.9.7",
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@pezkuwi/networks": "14.0.26",
|
||||
"@pezkuwi/scure-sr25519": "^0.2.1",
|
||||
"@pezkuwi/util": "14.0.26",
|
||||
"@pezkuwi/wasm-crypto": "^7.5.20",
|
||||
"@pezkuwi/wasm-util": "^7.5.20",
|
||||
"@pezkuwi/x-bigint": "14.0.26",
|
||||
"@pezkuwi/x-randomvalues": "14.0.26",
|
||||
"@scure/base": "^1.1.7",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pezkuwi/util": "14.0.26"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/wasm-bridge": {
|
||||
"version": "7.5.21",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/wasm-bridge/-/wasm-bridge-7.5.21.tgz",
|
||||
"integrity": "sha512-KVWLXpmVHpYo1+woN+o8OK03Kk8+VwUtF/Dn5pnsVXbsGbXM22HWbx36ChzdkjpW35a5Ukfk92H0aCy8klfeDg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pezkuwi/wasm-util": "7.5.21",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pezkuwi/util": "*",
|
||||
"@pezkuwi/x-randomvalues": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/wasm-crypto": {
|
||||
"version": "7.5.21",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/wasm-crypto/-/wasm-crypto-7.5.21.tgz",
|
||||
"integrity": "sha512-gL771hQsC0NXPFTX4+yKTPEMfX1svRAM1lzep+LkniQcYqpqI4AAxzUaOGZYj5lQzXKSkgYpcaLuTZ441zPXAg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.3.3",
|
||||
"@pezkuwi/wasm-bridge": "7.5.21",
|
||||
"@pezkuwi/wasm-crypto-asmjs": "7.5.21",
|
||||
"@pezkuwi/wasm-crypto-init": "7.5.21",
|
||||
"@pezkuwi/wasm-crypto-wasm": "7.5.21",
|
||||
"@pezkuwi/wasm-util": "7.5.21",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pezkuwi/util": "*",
|
||||
"@pezkuwi/x-randomvalues": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/wasm-crypto-asmjs": {
|
||||
"version": "7.5.21",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.5.21.tgz",
|
||||
"integrity": "sha512-uqhAR3slp6qbn2Cf7g72v4Ko0hxWm/NuZL2ZDH65QBSGP9jkSnAoBjm5L7AO739quEVA8rFsR8CxN5gvzERspA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pezkuwi/util": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/wasm-crypto-init": {
|
||||
"version": "7.5.21",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/wasm-crypto-init/-/wasm-crypto-init-7.5.21.tgz",
|
||||
"integrity": "sha512-pdgYqhsHIoxTdhwbxS8iacw4sFh3NLN9fJ3odzCAo4+QCr3xTU1FMjBiYJRkx6Q4BpthCjOWJPCwTEZnJ3lSCg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pezkuwi/wasm-bridge": "7.5.21",
|
||||
"@pezkuwi/wasm-crypto-asmjs": "7.5.21",
|
||||
"@pezkuwi/wasm-crypto-wasm": "7.5.21",
|
||||
"@pezkuwi/wasm-util": "7.5.21",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pezkuwi/util": "*",
|
||||
"@pezkuwi/x-randomvalues": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/wasm-crypto-wasm": {
|
||||
"version": "7.5.21",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/wasm-crypto-wasm/-/wasm-crypto-wasm-7.5.21.tgz",
|
||||
"integrity": "sha512-TR8hHkjY6oga4aI9Irlv/y00+KwiCBMt+EyXi3mTUVNn7qKE4DwXbDc/az4rLK8p5Kno4/iyCF6YXuHbjEK4Ag==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pezkuwi/wasm-util": "7.5.21",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pezkuwi/util": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/wasm-util": {
|
||||
"version": "7.5.21",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/wasm-util/-/wasm-util-7.5.21.tgz",
|
||||
"integrity": "sha512-KMiUHCEa6iFNLNWyw9GUf5/K2UP6RjCia2iSGjcazwBNofAQVpP6zfeLb9y5szpextKGkDsODxpcIFenJ3KcFQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pezkuwi/util": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/x-bigint": {
|
||||
"version": "14.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/x-bigint/-/x-bigint-14.0.26.tgz",
|
||||
"integrity": "sha512-TiCUVJiW07KurXot79AuKsqdIKGCtysMVDKLGlUc7mxlDJ+BCNJVmi9D0vvbB/gboxfJqONFQKt8eOXHKFg1iw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pezkuwi/x-global": "14.0.26",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/x-global": {
|
||||
"version": "14.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/x-global/-/x-global-14.0.26.tgz",
|
||||
"integrity": "sha512-3D/dgFUugJfJ59JETey2EPpWL7X7NOrSEgNj1Rt3Vu9zR74QNocc5HEcyapFKj2hNNp5gPPDOOWbrLkkDnbdXw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/x-randomvalues": {
|
||||
"version": "14.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/x-randomvalues/-/x-randomvalues-14.0.26.tgz",
|
||||
"integrity": "sha512-9tQ5+e7b5sC6y7NWFojfkfKZNSmWBpB1dnuyMv5vJMPlBPlsscTYARfk8ln860XWLdemQBYBvYbibmEBtLFfSQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pezkuwi/x-global": "14.0.26",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pezkuwi/util": "14.0.26",
|
||||
"@pezkuwi/wasm-util": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/x-textdecoder": {
|
||||
"version": "14.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/x-textdecoder/-/x-textdecoder-14.0.26.tgz",
|
||||
"integrity": "sha512-O3l/pcT+7q89COMe+h+Gr+HLLym3k0RfWXWwEg6RYGBHxp77GfixwqAR/o6k6rBXNfxlAY60RcYUqu+DGkKSZw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pezkuwi/x-global": "14.0.26",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@pezkuwi/x-textencoder": {
|
||||
"version": "14.0.26",
|
||||
"resolved": "https://registry.npmjs.org/@pezkuwi/x-textencoder/-/x-textencoder-14.0.26.tgz",
|
||||
"integrity": "sha512-aUk+h0g5iO+V3CZV1vdlRy4G9MbE0SbomB5Lw/8IkuQ+wNPK9JQ1FlArMhxVBfXM9JD8DPih/ViZsaNhWfjVVw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pezkuwi/x-global": "14.0.26",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/networks": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-13.5.9.tgz",
|
||||
"integrity": "sha512-nmKUKJjiLgcih0MkdlJNMnhEYdwEml2rv/h59ll2+rAvpsVWMTLCb6Cq6q7UC44+8kiWK2UUJMkFU+3PFFxndA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/util": "13.5.9",
|
||||
"@substrate/ss58-registry": "^1.51.0",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/util": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/util/-/util-13.5.9.tgz",
|
||||
"integrity": "sha512-pIK3XYXo7DKeFRkEBNYhf3GbCHg6dKQisSvdzZwuyzA6m7YxQq4DFw4IE464ve4Z7WsJFt3a6C9uII36hl9EWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/x-bigint": "13.5.9",
|
||||
"@polkadot/x-global": "13.5.9",
|
||||
"@polkadot/x-textdecoder": "13.5.9",
|
||||
"@polkadot/x-textencoder": "13.5.9",
|
||||
"@types/bn.js": "^5.1.6",
|
||||
"bn.js": "^5.2.1",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/util-crypto": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.5.9.tgz",
|
||||
"integrity": "sha512-foUesMhxkTk8CZ0/XEcfvHk6I0O+aICqqVJllhOpyp/ZVnrTBKBf59T6RpsXx2pCtBlMsLRvg/6Mw7RND1HqDg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@noble/curves": "^1.3.0",
|
||||
"@noble/hashes": "^1.3.3",
|
||||
"@polkadot/networks": "13.5.9",
|
||||
"@polkadot/util": "13.5.9",
|
||||
"@polkadot/wasm-crypto": "^7.5.3",
|
||||
"@polkadot/wasm-util": "^7.5.3",
|
||||
"@polkadot/x-bigint": "13.5.9",
|
||||
"@polkadot/x-randomvalues": "13.5.9",
|
||||
"@scure/base": "^1.1.7",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "13.5.9"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/wasm-bridge": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/wasm-bridge/-/wasm-bridge-7.5.4.tgz",
|
||||
"integrity": "sha512-6xaJVvoZbnbgpQYXNw9OHVNWjXmtcoPcWh7hlwx3NpfiLkkjljj99YS+XGZQlq7ks2fVCg7FbfknkNb8PldDaA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/wasm-util": "7.5.4",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*",
|
||||
"@polkadot/x-randomvalues": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/wasm-crypto": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-7.5.4.tgz",
|
||||
"integrity": "sha512-1seyClxa7Jd7kQjfnCzTTTfYhTa/KUTDUaD3DMHBk5Q4ZUN1D1unJgX+v1aUeXSPxmzocdZETPJJRZjhVOqg9g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/wasm-bridge": "7.5.4",
|
||||
"@polkadot/wasm-crypto-asmjs": "7.5.4",
|
||||
"@polkadot/wasm-crypto-init": "7.5.4",
|
||||
"@polkadot/wasm-crypto-wasm": "7.5.4",
|
||||
"@polkadot/wasm-util": "7.5.4",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*",
|
||||
"@polkadot/x-randomvalues": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/wasm-crypto-asmjs": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.5.4.tgz",
|
||||
"integrity": "sha512-ZYwxQHAJ8pPt6kYk9XFmyuFuSS+yirJLonvP+DYbxOrARRUHfN4nzp4zcZNXUuaFhpbDobDSFn6gYzye6BUotA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/wasm-crypto-init": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.5.4.tgz",
|
||||
"integrity": "sha512-U6s4Eo2rHs2n1iR01vTz/sOQ7eOnRPjaCsGWhPV+ZC/20hkVzwPAhiizu/IqMEol4tO2yiSheD4D6bn0KxUJhg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/wasm-bridge": "7.5.4",
|
||||
"@polkadot/wasm-crypto-asmjs": "7.5.4",
|
||||
"@polkadot/wasm-crypto-wasm": "7.5.4",
|
||||
"@polkadot/wasm-util": "7.5.4",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*",
|
||||
"@polkadot/x-randomvalues": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/wasm-crypto-wasm": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.5.4.tgz",
|
||||
"integrity": "sha512-PsHgLsVTu43eprwSvUGnxybtOEuHPES6AbApcs7y5ZbM2PiDMzYbAjNul098xJK/CPtrxZ0ePDFnaQBmIJyTFw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/wasm-util": "7.5.4",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/wasm-util": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/wasm-util/-/wasm-util-7.5.4.tgz",
|
||||
"integrity": "sha512-hqPpfhCpRAqCIn/CYbBluhh0TXmwkJnDRjxrU9Bnqtw9nMNa97D8JuOjdd2pi0rxm+eeLQ/f1rQMp71RMM9t4w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/x-bigint": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-13.5.9.tgz",
|
||||
"integrity": "sha512-JVW6vw3e8fkcRyN9eoc6JIl63MRxNQCP/tuLdHWZts1tcAYao0hpWUzteqJY93AgvmQ91KPsC1Kf3iuuZCi74g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "13.5.9",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/x-global": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-13.5.9.tgz",
|
||||
"integrity": "sha512-zSRWvELHd3Q+bFkkI1h2cWIqLo1ETm+MxkNXLec3lB56iyq/MjWBxfXnAFFYFayvlEVneo7CLHcp+YTFd9aVSA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/x-randomvalues": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-13.5.9.tgz",
|
||||
"integrity": "sha512-Uuuz3oubf1JCCK97fsnVUnHvk4BGp/W91mQWJlgl5TIOUSSTIRr+lb5GurCfl4kgnQq53Zi5fJV+qR9YumbnZw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "13.5.9",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "13.5.9",
|
||||
"@polkadot/wasm-util": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/x-textdecoder": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-13.5.9.tgz",
|
||||
"integrity": "sha512-W2HhVNUbC/tuFdzNMbnXAWsIHSg9SC9QWDNmFD3nXdSzlXNgL8NmuiwN2fkYvCQBtp/XSoy0gDLx0C+Fo19cfw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "13.5.9",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@polkadot/x-textencoder": {
|
||||
"version": "13.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-13.5.9.tgz",
|
||||
"integrity": "sha512-SG0MHnLUgn1ZxFdm0KzMdTHJ47SfqFhdIPMcGA0Mg/jt2rwrfrP3jtEIJMsHfQpHvfsNPfv55XOMmoPWuQnP/Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@polkadot/x-global": "13.5.9",
|
||||
"tslib": "^2.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@scure/base": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
|
||||
"integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@substrate/ss58-registry": {
|
||||
"version": "1.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@substrate/ss58-registry/-/ss58-registry-1.51.0.tgz",
|
||||
"integrity": "sha512-TWDurLiPxndFgKjVavCniytBIw+t4ViOi7TYp9h/D0NMmkEc9klFTo+827eyEJ0lELpqO207Ey7uGxUa+BS1jQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@types/bn.js": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz",
|
||||
"integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.6.tgz",
|
||||
"integrity": "sha512-MzQj+8Kx3XI40OmUAE6Qoh58ZMNstNNMwn2nfd3qWsfe7nDNSqJNzes4njhz6AHwrSHOffSLwXU6yb474Q4aog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.5",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.5.tgz",
|
||||
"integrity": "sha512-Vq886eXykuP5E6HcKSSStP3bJgrE6In5WKxVUvJ8XGpWWYs2xZHWqUwzCtGgEtBcxyd57KBFDPFoUfNzdaHCNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.9.1.tgz",
|
||||
"integrity": "sha512-a4Ux9VKuIMHsOS7ENbEq6hK2ZGYIT27Rtl0E4eD9KqwxhKiQeMZE/9xb3IdYwWaSOyIBQLExhQgXDTYoiCxnWA==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
||||
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "pezkuwi-wallet-utils-root",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Verifies the signed config root chain as the wallet does",
|
||||
"dependencies": {
|
||||
"@pezkuwi/util": "14.0.26",
|
||||
"@pezkuwi/util-crypto": "14.0.26",
|
||||
"@polkadot/util": "13.5.9",
|
||||
"@polkadot/util-crypto": "13.5.9"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// Verifies the published root chain the way the wallet does, so a root that installed wallets would refuse
|
||||
// is never published: a root they cannot verify stops their config refresh.
|
||||
//
|
||||
// signed/root.json is the root the APK carries. Each signed/root.N.json after it must be the next version and
|
||||
// carry valid signatures from a threshold of the previous root's keys and of its own. An sr25519 key is a
|
||||
// wallet account and signs <Bytes>payload</Bytes>. Wallet 1.3.0 checks sr25519 under the "substrate" signing
|
||||
// context only, so that is what counts; a signature valid only under "bizinikiwi" is reported, not counted.
|
||||
//
|
||||
// node scripts/root/verify_root.mjs check the published chain
|
||||
// node scripts/root/verify_root.mjs FILE... also report the signatures on proposals
|
||||
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { cryptoWaitReady as polkadotReady, sr25519Verify as substrateVerify } from '@polkadot/util-crypto';
|
||||
import { cryptoWaitReady as pezkuwiReady, sr25519Verify as bizinikiwiVerify, ed25519Verify } from '@pezkuwi/util-crypto';
|
||||
|
||||
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const SIGNED = join(REPO, 'signed');
|
||||
|
||||
const sha256Hex = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
||||
const wrapBytes = (payload) => Buffer.concat([Buffer.from('<Bytes>'), payload, Buffer.from('</Bytes>')]);
|
||||
|
||||
function readEnvelope(path) {
|
||||
const envelope = JSON.parse(readFileSync(path, 'utf8'));
|
||||
const payload = Buffer.from(envelope.signed, 'base64');
|
||||
return { payload, root: JSON.parse(payload.toString('utf8')), signatures: envelope.signatures };
|
||||
}
|
||||
|
||||
function parseRoot(root) {
|
||||
if (root.type !== 'root') throw new Error(`type is ${root.type}, not root`);
|
||||
const keys = {};
|
||||
for (const [keyId, key] of Object.entries(root.keys)) {
|
||||
const bytes = Buffer.from(key.public, 'base64');
|
||||
if (bytes.length !== 32 || sha256Hex(bytes) !== keyId) throw new Error(`key ${keyId} does not match its id`);
|
||||
if (key.scheme !== 'sr25519' && key.scheme !== 'ed25519') throw new Error(`key ${keyId} has unknown scheme ${key.scheme}`);
|
||||
keys[keyId] = { scheme: key.scheme, bytes };
|
||||
}
|
||||
for (const role of ['root', 'targets']) {
|
||||
const { keyids, threshold } = root[role];
|
||||
if (threshold < 1 || threshold > keyids.length || !keyids.every((id) => id in keys)) throw new Error(`unusable ${role} role`);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// keyId -> "substrate" | "bizinikiwi" | "ed25519" | null for each signature on payload
|
||||
function classify(payload, signatures, keys) {
|
||||
const result = new Map();
|
||||
for (const { keyid, sig } of signatures) {
|
||||
const key = keys[keyid];
|
||||
if (!key) continue;
|
||||
const signature = Buffer.from(sig, 'base64');
|
||||
let kind = null;
|
||||
if (key.scheme === 'ed25519') {
|
||||
if (ed25519Verify(payload, signature, key.bytes)) kind = 'ed25519';
|
||||
} else if (substrateVerify(wrapBytes(payload), signature, key.bytes)) {
|
||||
kind = 'substrate';
|
||||
} else if (bizinikiwiVerify(wrapBytes(payload), signature, key.bytes)) {
|
||||
kind = 'bizinikiwi';
|
||||
}
|
||||
if (!result.get(keyid)) result.set(keyid, kind);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function count(classified, keyIds) {
|
||||
const accepted = keyIds.filter((id) => ['substrate', 'ed25519'].includes(classified.get(id)));
|
||||
const contextOnly = keyIds.filter((id) => classified.get(id) === 'bizinikiwi');
|
||||
return { accepted: accepted.length, contextOnly: contextOnly.length };
|
||||
}
|
||||
|
||||
function checkNext(previous, path) {
|
||||
const next = readEnvelope(path);
|
||||
const nextKeys = parseRoot(next.root);
|
||||
if (next.root.version !== previous.root.version + 1) {
|
||||
throw new Error(`version ${next.root.version} does not follow ${previous.root.version}`);
|
||||
}
|
||||
const lines = [];
|
||||
let ok = true;
|
||||
for (const [label, keys, role] of [
|
||||
['previous root', previous.keys, previous.root.root],
|
||||
['its own root', nextKeys, next.root.root],
|
||||
]) {
|
||||
const { accepted, contextOnly } = count(classify(next.payload, next.signatures, keys), role.keyids);
|
||||
const met = accepted >= role.threshold;
|
||||
ok &&= met;
|
||||
lines.push(` ${label}: ${accepted} of ${role.threshold} required${contextOnly ? `, ${contextOnly} valid only under bizinikiwi (not accepted by wallet 1.3.0)` : ''}${met ? '' : ' <- NOT MET'}`);
|
||||
}
|
||||
return { ok, lines, next: { root: next.root, keys: nextKeys } };
|
||||
}
|
||||
|
||||
await polkadotReady();
|
||||
await pezkuwiReady();
|
||||
|
||||
let failed = false;
|
||||
const first = readEnvelope(join(SIGNED, 'root.json'));
|
||||
let current = { root: first.root, keys: parseRoot(first.root) };
|
||||
console.log(`root ${current.root.version}: carried by the APK`);
|
||||
|
||||
for (let version = current.root.version + 1; existsSync(join(SIGNED, `root.${version}.json`)); version++) {
|
||||
const path = join(SIGNED, `root.${version}.json`);
|
||||
try {
|
||||
const { ok, lines, next } = checkNext(current, path);
|
||||
console.log(`root ${version}: ${ok ? 'accepted' : 'REFUSED'}`);
|
||||
lines.forEach((line) => console.log(line));
|
||||
if (!ok) {
|
||||
console.log(`::error::signed/root.${version}.json would be refused by installed wallets and stop their config refresh`);
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
current = next;
|
||||
} catch (e) {
|
||||
console.log(`::error::signed/root.${version}.json: ${e.message}`);
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const proposal of process.argv.slice(2)) {
|
||||
try {
|
||||
const { ok, lines } = checkNext(current, proposal);
|
||||
console.log(`proposal ${proposal}: ${ok ? 'ready to publish' : 'not ready'}`);
|
||||
lines.forEach((line) => console.log(line));
|
||||
} catch (e) {
|
||||
console.log(`proposal ${proposal}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(failed ? 1 : 0);
|
||||
@@ -9,7 +9,7 @@ Every metadata file is an envelope {"signed": base64(payload), "signatures": [{"
|
||||
hex sha256 of the 32-byte public key. An ed25519 key signs the payload bytes. An sr25519 key is a person's
|
||||
wallet account and signs through the wallet's message signing, which wraps the bytes as <Bytes>payload</Bytes>.
|
||||
|
||||
signed_config.py root --version N --expires-days D --root-threshold T --root ADDR... --targets-pub FILE
|
||||
signed_config.py root --version N --expires-days D --root-threshold T --root ADDR... [--targets-pub FILE]
|
||||
signed_config.py sign --key SEED_FILE [--expires-days D]
|
||||
signed_config.py check [--min-days D]
|
||||
"""
|
||||
@@ -33,10 +33,14 @@ from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
ROOT_FILE = REPO / "signed" / "root.json"
|
||||
TARGETS_FILE = REPO / "signed" / "targets.json"
|
||||
# A root after the first is drafted here, where the wallet never looks, and signed with the root signing page
|
||||
# beside it. Only a root with all its signatures moves to signed/: wallets that find a root they cannot verify
|
||||
# stop refreshing their config.
|
||||
PROPOSALS = REPO / "docs" / "root-signing" / "proposals"
|
||||
|
||||
# Everything the wallet may fetch from this repository is JSON outside these trees; the wallet
|
||||
# refuses any other JSON file from here, so a tree is only left out when nothing reads it.
|
||||
UNSERVED = ("nova-base/", "pezkuwi-overlay/", "docs/", "signed/")
|
||||
UNSERVED = ("nova-base/", "pezkuwi-overlay/", "docs/", "signed/", "scripts/")
|
||||
|
||||
BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
|
||||
DAY = 86400
|
||||
@@ -117,7 +121,13 @@ def cmd_root(args):
|
||||
"public": base64.b64encode(public).decode(),
|
||||
}
|
||||
root_ids.append(key_id(public))
|
||||
targets_public = base64.b64decode(Path(args.targets_pub).read_text().strip())
|
||||
if args.targets_pub:
|
||||
targets_public = base64.b64decode(Path(args.targets_pub).read_text().strip())
|
||||
else:
|
||||
# Rotating the root keys keeps the targets key unless a new one is given.
|
||||
previous = current_root()
|
||||
(targets_id,) = previous["targets"]["keyids"]
|
||||
targets_public = base64.b64decode(previous["keys"][targets_id]["public"])
|
||||
if len(targets_public) != 32:
|
||||
sys.exit("targets public key must be 32 bytes")
|
||||
keys[key_id(targets_public)] = {
|
||||
@@ -135,11 +145,15 @@ def cmd_root(args):
|
||||
"root": {"keyids": sorted(root_ids), "threshold": args.root_threshold},
|
||||
"targets": {"keyids": [key_id(targets_public)], "threshold": 1},
|
||||
}
|
||||
path = root_file(args.version)
|
||||
path.parent.mkdir(exist_ok=True)
|
||||
# The first root is trusted because the APK carries it; a later one needs the signatures of a threshold
|
||||
# of the previous root keys and of its own, collected from the holders' wallets and added to this file
|
||||
# before it is published.
|
||||
# of the previous root keys and of its own, collected with the root signing page, before it is published.
|
||||
if args.version == 1:
|
||||
path = ROOT_FILE
|
||||
else:
|
||||
if args.version != current_root()["version"] + 1:
|
||||
sys.exit(f"the next root is version {current_root()['version'] + 1}")
|
||||
path = PROPOSALS / f"root.{args.version}.json"
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(envelope(canonical(payload), []))
|
||||
print(f"wrote {path.relative_to(REPO)} version {args.version}")
|
||||
|
||||
@@ -241,7 +255,7 @@ def main():
|
||||
root.add_argument("--expires-days", type=int, required=True)
|
||||
root.add_argument("--root-threshold", type=int, required=True)
|
||||
root.add_argument("--root", nargs="+", required=True)
|
||||
root.add_argument("--targets-pub", required=True)
|
||||
root.add_argument("--targets-pub")
|
||||
root.set_defaults(run=cmd_root)
|
||||
|
||||
sign = sub.add_parser("sign")
|
||||
|
||||
Reference in new issue
Block a user