Skip to content

Commit 15d42c5

Browse files
committed
security: close all findings from security audit v5
C2: Remove dead PLACEHOLDER Dilithium key and is_valid_system_signature from genesis_constants.rs C1: Replace silent fallback with hard exit(1) when QNET_BURN_TX_HASH/QNET_BURN_AMOUNT are missing in quantum_crypto.rs C3: Add automatic rollback in cross-shard transaction on notify failure (production_sharding.rs) H1: Remove all activation-related code from browser extension background.js (burnOneDevTokens, burnAndActivateNode, activateNode, spendQNCToPool3, startActivationSync, BURN_1DEV_TOKENS handler) H3: Make QNET_ADMIN_SECRET mandatory for shutdown endpoint - blocked entirely if not configured (rpc.rs + docker-compose.production.yml all 3 services) H4: Replace SkipServerVerification with SelfSignedCertVerifier + post-handshake verify_peer_cert_node_id() binding TLS cert SAN to claimed node_id on both client and server sides (quic_transport.rs) M3: Move blocking reqwest IP detection to dedicated OS thread via std::thread::spawn, update all logs to two-level format [DBG][IP]/[INFO][IP]/[WARN][IP] (qnet-node.rs) N1: Upgrade VRF domain separation to v4 with pk-bound hash_input_keyed() for formal uniqueness guarantee (vrf.rs) N2: Add block hash integrity check in load_block_by_height - recomputes and verifies against stored hash (storage.rs) Made-with: Cursor
1 parent c868720 commit 15d42c5

18 files changed

Lines changed: 414 additions & 1219 deletions

File tree

applications/qnet-mobile/android/app/src/main/cpp/dilithium3/poly.c

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -473,16 +473,18 @@ void PQCLEAN_DILITHIUM3_CLEAN_poly_uniform_gamma1(poly *a,
473473
* SHAKE256(seed).
474474
*
475475
* Arguments: - poly *c: pointer to output polynomial
476-
* - const uint8_t mu[]: byte array containing seed of length SEEDBYTES
476+
* - const uint8_t mu[]: byte array containing seed of length CTILDEBYTES
477+
* NOTE: ML-DSA-65 (FIPS 204) absorbs CTILDEBYTES (48) not SEEDBYTES (32).
478+
* Both are passed in, only CTILDEBYTES bytes are used for the challenge hash.
477479
**************************************************/
478-
void PQCLEAN_DILITHIUM3_CLEAN_poly_challenge(poly *c, const uint8_t seed[SEEDBYTES]) {
480+
void PQCLEAN_DILITHIUM3_CLEAN_poly_challenge(poly *c, const uint8_t seed[CTILDEBYTES]) {
479481
unsigned int i, b, pos;
480482
uint64_t signs;
481483
uint8_t buf[SHAKE256_RATE];
482484
shake256incctx state;
483485

484486
shake256_inc_init(&state);
485-
shake256_inc_absorb(&state, seed, SEEDBYTES);
487+
shake256_inc_absorb(&state, seed, CTILDEBYTES);
486488
shake256_inc_finalize(&state);
487489
shake256_inc_squeeze(buf, sizeof buf, &state);
488490

applications/qnet-mobile/android/app/src/main/cpp/dilithium3/poly.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ void PQCLEAN_DILITHIUM3_CLEAN_poly_uniform_eta(poly *a,
3333
void PQCLEAN_DILITHIUM3_CLEAN_poly_uniform_gamma1(poly *a,
3434
const uint8_t seed[CRHBYTES],
3535
uint16_t nonce);
36-
void PQCLEAN_DILITHIUM3_CLEAN_poly_challenge(poly *c, const uint8_t seed[SEEDBYTES]);
36+
void PQCLEAN_DILITHIUM3_CLEAN_poly_challenge(poly *c, const uint8_t seed[CTILDEBYTES]);
3737

3838
void PQCLEAN_DILITHIUM3_CLEAN_polyeta_pack(uint8_t *r, const poly *a);
3939
void PQCLEAN_DILITHIUM3_CLEAN_polyeta_unpack(poly *r, const uint8_t *a);

applications/qnet-mobile/android/app/src/main/cpp/dilithium3/sign.c

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,17 @@ int PQCLEAN_DILITHIUM3_CLEAN_crypto_sign_signature(uint8_t *sig,
9999
rhoprime = mu + CRHBYTES;
100100
PQCLEAN_DILITHIUM3_CLEAN_unpack_sk(rho, tr, key, &t0, &s1, &s2, sk);
101101

102-
/* Compute mu = CRH(tr, msg) */
103-
shake256_inc_init(&state);
104-
shake256_inc_absorb(&state, tr, TRBYTES);
105-
shake256_inc_absorb(&state, m, mlen);
106-
shake256_inc_finalize(&state);
107-
shake256_inc_squeeze(mu, CRHBYTES, &state);
108-
shake256_inc_ctx_release(&state);
102+
/* Compute mu = CRH(tr, 0x00 || 0x00 || msg) — FIPS 204 ML-DSA pure-mode domain */
103+
{
104+
const uint8_t ml_dsa_prefix[2] = {0x00, 0x00};
105+
shake256_inc_init(&state);
106+
shake256_inc_absorb(&state, tr, TRBYTES);
107+
shake256_inc_absorb(&state, ml_dsa_prefix, 2);
108+
shake256_inc_absorb(&state, m, mlen);
109+
shake256_inc_finalize(&state);
110+
shake256_inc_squeeze(mu, CRHBYTES, &state);
111+
shake256_inc_ctx_release(&state);
112+
}
109113

110114
for (n = 0; n < RNDBYTES; n++) {
111115
rnd[n] = 0;
@@ -140,7 +144,7 @@ int PQCLEAN_DILITHIUM3_CLEAN_crypto_sign_signature(uint8_t *sig,
140144
shake256_inc_finalize(&state);
141145
shake256_inc_squeeze(sig, CTILDEBYTES, &state);
142146
shake256_inc_ctx_release(&state);
143-
PQCLEAN_DILITHIUM3_CLEAN_poly_challenge(&cp, sig); /* uses only the first SEEDBYTES bytes of sig */
147+
PQCLEAN_DILITHIUM3_CLEAN_poly_challenge(&cp, sig); /* ML-DSA-65: uses all CTILDEBYTES (48) bytes */
144148
PQCLEAN_DILITHIUM3_CLEAN_poly_ntt(&cp);
145149

146150
/* Compute z, reject if it reveals secret */
@@ -254,17 +258,21 @@ int PQCLEAN_DILITHIUM3_CLEAN_crypto_sign_verify(const uint8_t *sig,
254258
return -1;
255259
}
256260

257-
/* Compute CRH(H(rho, t1), msg) */
261+
/* Compute mu = CRH(H(pk), 0x00 || 0x00 || msg) — FIPS 204 ML-DSA pure-mode domain */
258262
shake256(mu, CRHBYTES, pk, PQCLEAN_DILITHIUM3_CLEAN_CRYPTO_PUBLICKEYBYTES);
259-
shake256_inc_init(&state);
260-
shake256_inc_absorb(&state, mu, CRHBYTES);
261-
shake256_inc_absorb(&state, m, mlen);
262-
shake256_inc_finalize(&state);
263-
shake256_inc_squeeze(mu, CRHBYTES, &state);
264-
shake256_inc_ctx_release(&state);
263+
{
264+
const uint8_t ml_dsa_prefix[2] = {0x00, 0x00};
265+
shake256_inc_init(&state);
266+
shake256_inc_absorb(&state, mu, CRHBYTES);
267+
shake256_inc_absorb(&state, ml_dsa_prefix, 2);
268+
shake256_inc_absorb(&state, m, mlen);
269+
shake256_inc_finalize(&state);
270+
shake256_inc_squeeze(mu, CRHBYTES, &state);
271+
shake256_inc_ctx_release(&state);
272+
}
265273

266274
/* Matrix-vector multiplication; compute Az - c2^dt1 */
267-
PQCLEAN_DILITHIUM3_CLEAN_poly_challenge(&cp, c); /* uses only the first SEEDBYTES bytes of c */
275+
PQCLEAN_DILITHIUM3_CLEAN_poly_challenge(&cp, c); /* ML-DSA-65: uses all CTILDEBYTES (48) bytes */
268276
PQCLEAN_DILITHIUM3_CLEAN_polyvec_matrix_expand(mat, rho);
269277

270278
PQCLEAN_DILITHIUM3_CLEAN_polyvecl_ntt(&z);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt
2+
--- a/android/CMakeLists.txt
3+
+++ b/android/CMakeLists.txt
4+
@@ -119,6 +119,12 @@ target_link_libraries(
5+
)
6+
7+
+# Suppress third-party library warnings (OpenSSL 3.x deprecations, virtual function hiding)
8+
+target_compile_options(${PACKAGE_NAME} PRIVATE
9+
+ -Wno-deprecated-declarations
10+
+ -Wno-overloaded-virtual
11+
+)
12+
+
13+
if(SODIUM_ENABLED)
14+
add_definitions(-DBLSALLOC_SODIUM)
15+
find_package(sodium REQUIRED CONFIG)

applications/qnet-mobile/src/components/WalletManager.js

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3543,18 +3543,18 @@ export class WalletManager {
35433543

35443544
// Hash address (same as Rust: b"QNET_ADDR:" + address.as_bytes())
35453545
const addrHashHex = sha3_256(this.concatBytes(
3546-
new TextEncoder().encode('QNET_ADDR:'),
3547-
new TextEncoder().encode(address)
3546+
Buffer.from('QNET_ADDR:', 'utf8'),
3547+
Buffer.from(address, 'utf8')
35483548
));
35493549

35503550
// Hash account data (same as Rust: b"QNET_ACCOUNT:" + balance(8) + nonce(8) + pending_rewards(8) + address)
35513551
// CRITICAL: Use raw bytes, not hex strings!
35523552
const accountDataBytes = this.concatBytes(
3553-
new TextEncoder().encode('QNET_ACCOUNT:'),
3553+
Buffer.from('QNET_ACCOUNT:', 'utf8'),
35543554
this.uint64ToBytes(balance), // 8 bytes little-endian
35553555
this.uint64ToBytes(nonce), // 8 bytes little-endian
35563556
this.uint64ToBytes(0), // pending_rewards = 0 for basic proof
3557-
new TextEncoder().encode(address) // address string bytes
3557+
Buffer.from(address, 'utf8') // address string bytes
35583558
);
35593559
let currentHash = sha3_256(accountDataBytes);
35603560

@@ -3617,18 +3617,17 @@ export class WalletManager {
36173617
);
36183618

36193619
// Build hash (same as Rust: b"QNET_VALIDATOR_SET:" + epoch + validators)
3620-
const encoder = new TextEncoder();
36213620
let dataToHash = this.concatBytes(
3622-
encoder.encode('QNET_VALIDATOR_SET:'),
3621+
Buffer.from('QNET_VALIDATOR_SET:', 'utf8'),
36233622
this.uint64ToBytes(epoch)
36243623
);
36253624

36263625
for (const v of sorted) {
36273626
dataToHash = this.concatBytes(
36283627
dataToHash,
3629-
encoder.encode(v.node_id || ''),
3630-
encoder.encode(v.address || ''),
3631-
encoder.encode(v.node_type || ''),
3628+
Buffer.from(v.node_id || '', 'utf8'),
3629+
Buffer.from(v.address || '', 'utf8'),
3630+
Buffer.from(v.node_type || '', 'utf8'),
36323631
this.float64ToBytes(v.reputation || 0),
36333632
this.uint64ToBytes(v.last_seen || 0),
36343633
new Uint8Array([v.is_active ? 1 : 0])
@@ -5289,7 +5288,7 @@ export class WalletManager {
52895288
// v4.7: Include Ed25519 signature + burn_wallet for wallet ownership proof
52905289
const sigTimestamp = Math.floor(Date.now() / 1000);
52915290
const ed25519Message = `qnet_register:${activationCode}:${sigTimestamp}`;
5292-
const ed25519MsgBytes = new TextEncoder().encode(ed25519Message);
5291+
const ed25519MsgBytes = Buffer.from(ed25519Message, 'utf8');
52935292
const ed25519Sig = nacl.sign.detached(ed25519MsgBytes, new Uint8Array(walletData.secretKey));
52945293
const ed25519SigHex = Array.from(ed25519Sig).map(b => b.toString(16).padStart(2, '0')).join('');
52955294

@@ -5498,8 +5497,7 @@ export class WalletManager {
54985497
// blake3::hash("LIGHT_NODE_PRIVACY_{wallet}") → first 8 hex chars
54995498
const { blake3 } = require('@noble/hashes/blake3.js');
55005499
const input = `LIGHT_NODE_PRIVACY_${walletAddress}`;
5501-
const inputBytes = new TextEncoder().encode(input);
5502-
const hashBytes = blake3(inputBytes);
5500+
const hashBytes = blake3(Buffer.from(input, 'utf8'));
55035501
// First 4 bytes → 8 hex chars (matches Rust: &pseudonym_hash.to_hex()[..8])
55045502
const hexHash = Array.from(hashBytes.slice(0, 4))
55055503
.map(b => b.toString(16).padStart(2, '0'))
@@ -5528,7 +5526,7 @@ export class WalletManager {
55285526

55295527
const timestamp = Math.floor(Date.now() / 1000);
55305528
const message = `client_node_reg:${nodeId}:${walletAddress}:${registrationProof}:${timestamp}`;
5531-
const messageBytes = new TextEncoder().encode(message);
5529+
const messageBytes = Buffer.from(message, 'utf8');
55325530

55335531
const ed25519Sig = nacl.sign.detached(messageBytes, fullSecretKey);
55345532
const signature = Buffer.from(ed25519Sig).toString('hex');
@@ -5648,7 +5646,7 @@ export class WalletManager {
56485646
ed25519GossipPubkey = Buffer.from(kp.publicKey).toString('hex');
56495647
const fullSk = new Uint8Array([...privateKeyBytes, ...kp.publicKey]);
56505648
const gossipMsg = `light_node_gossip:${systemPseudonym}:${walletAddress}`;
5651-
const sig = nacl.sign.detached(new TextEncoder().encode(gossipMsg), fullSk);
5649+
const sig = nacl.sign.detached(Buffer.from(gossipMsg, 'utf8'), fullSk);
56525650
ed25519GossipSignature = Buffer.from(sig).toString('hex');
56535651
console.log('[Registration] Ed25519 gossip signature created (hybrid v6.1)');
56545652

@@ -5784,7 +5782,7 @@ export class WalletManager {
57845782
if (wd && wd.secretKey) {
57855783
signatureTimestamp = Math.floor(Date.now() / 1000);
57865784
const message = `qnet_register:${activationCode}:${signatureTimestamp}`;
5787-
const messageBytes = new TextEncoder().encode(message);
5785+
const messageBytes = Buffer.from(message, 'utf8');
57885786
const secretKeyBytes = new Uint8Array(wd.secretKey);
57895787
const sig = nacl.sign.detached(messageBytes, secretKeyBytes);
57905788
ed25519Signature = Array.from(sig).map(b => b.toString(16).padStart(2, '0')).join('');
@@ -5846,6 +5844,7 @@ export class WalletManager {
58465844
// Re-throw server/network errors as-is so WalletScreen shows proper error messages.
58475845
// Only wrap actual Dilithium/Ed25519 crypto errors.
58485846
const msg = dilithiumError.message || '';
5847+
console.error('[Registration] Error in quantum registration block:', msg);
58495848
const isCryptoError = msg.includes('DilithiumModule') ||
58505849
msg.includes('Dilithium3') ||
58515850
msg.includes('dilithium') ||
@@ -6114,7 +6113,7 @@ export class WalletManager {
61146113
// Hybrid signature: Ed25519 (ownership) + Dilithium3 (quantum-safe)
61156114
// Format matches validator's create_client_signing_message: "claim_rewards:from:to"
61166115
const message = `claim_rewards:${nodeId}:${walletAddress}`;
6117-
const messageBytes = new TextEncoder().encode(message);
6116+
const messageBytes = Buffer.from(message, 'utf8');
61186117

61196118
// Ed25519 signature (nacl needs 64-byte secret key = privateKey + publicKey)
61206119
const fullSecretKey = privateKeyBytes.length === 64
@@ -6311,7 +6310,7 @@ export class WalletManager {
63116310
// CRITICAL: nonce in TX = account.nonce + 1 (like Ethereum)
63126311
const txNonce = currentNonce + 1;
63136312
const message = `transfer:${fromAddress}:${toAddress}:${amountSmallest}:${txNonce}:${gasPrice}:${gasLimit}`;
6314-
const messageBytes = new TextEncoder().encode(message);
6313+
const messageBytes = Buffer.from(message, 'utf8');
63156314

63166315
// Sign with Ed25519 (nacl needs 64-byte secret key = privateKey + publicKey)
63176316
const fullSecretKey = privateKeyBytes.length === 64

applications/qnet-mobile/src/screens/WalletScreen.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1843,9 +1843,12 @@ const WalletScreen = () => {
18431843
'Could not connect to the QNet network.\n\nPlease check your internet connection and try again.'
18441844
);
18451845
} else if (msg.includes('dilithium') || msg.includes('quantum signature') || msg.includes('signature')) {
1846+
const detail = error.message || '';
18461847
showAlert(
18471848
'Signature Error',
1848-
'Failed to create quantum-secure signature for node registration.\n\nPlease try again. If the problem persists, restart the app.'
1849+
'Failed to create quantum-secure signature for node registration.\n\n' +
1850+
(detail ? `Details: ${detail}\n\n` : '') +
1851+
'Please try again. If the problem persists, restart the app.'
18491852
);
18501853
} else {
18511854
showAlert(

applications/qnet-mobile/src/services/PushService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ export async function refreshFcmTokenOnServer(nodeId, ed25519SecretKey64) {
532532
const nacl = require('tweetnacl');
533533
const timestamp = now;
534534
const message = `token_refresh:${nodeId}:${timestamp}`;
535-
const messageBytes = new TextEncoder().encode(message);
535+
const messageBytes = Buffer.from(message, 'utf8');
536536
const sig = nacl.sign.detached(messageBytes, new Uint8Array(ed25519SecretKey64));
537537
const signatureHex = Buffer.from(sig).toString('hex');
538538

0 commit comments

Comments
 (0)