Successful Hybrid Signature for Login operation.

This commit is contained in:
ITScutaru 2025-06-27 10:33:07 +03:00
parent 0a3335b0fc
commit 70e7f1823e
3 changed files with 230 additions and 25 deletions

View File

@ -4,6 +4,8 @@
const Block = module.exports;
const Nacl = require("tweetnacl/nacl-fast");
const ml_kem = require("@noble/post-quantum/ml-kem");
const ml_dsa = require("@noble/post-quantum/ml-dsa");
const nThen = require("nthen");
const Util = require("../common-util");
const BlockStore = require("../storage/block");
@ -68,10 +70,53 @@ Block.validateLoginBlock = function (Env, publicKey, signature, block, _cb) {
// take its hash
var hash = Nacl.hash(u8_block);
// validate the signature against the hash of the content
var verified = Nacl.sign.detached.verify(hash, u8_signature, u8_public_key);
// Check signature type - first byte indicates the format
var sigType = u8_signature[0];
var verified = false;
// existing authentication ensures that users cannot replay old blocks
if (sigType === 1 && u8_signature.length > 65) {
// Hybrid signature - check the classical part first
const classicalSig = u8_signature.subarray(1, 1 + 64); // Ed25519 signature is 64 bytes
verified = Nacl.sign.detached.verify(hash, classicalSig, u8_public_key);
// Check for PQ signature as well, but don't require it to pass
if (verified && ml_kem && ml_dsa) {
try {
// Get PQ signature length from the 4 bytes after classical signature
const pqSigLenView = new DataView(u8_signature.buffer, u8_signature.byteOffset + 65);
const pqSigLen = pqSigLenView.getUint32(0, false); // big-endian
// Extract PQ signature
const pqSig = u8_signature.subarray(1 + 64 + 4, 1 + 64 + 4 + pqSigLen);
// If PQ public key is provided, verify the PQ signature
if (Env.blockInfo && Env.blockInfo[publicKey] && Env.blockInfo[publicKey].pqPublicKey) {
const pqPublicKey = Util.decodeBase64(Env.blockInfo[publicKey].pqPublicKey);
const pqVerified = ml_dsa.ml_dsa44.internal.verify(
pqPublicKey,
hash,
pqSig
);
Env.Log.info('BLOCK_PQ_VERIFICATION_RESULT', {
blockId: publicKey,
result: pqVerified
});
}
} catch (err) {
Env.Log.error('BLOCK_PQ_VERIFICATION_ERROR', {
error: err.message,
blockId: publicKey
});
}
}
} else {
// Classical signature or unrecognized format - use normal verification
// For type 0 we need to skip the first byte
const classicalSig = sigType === 0 ? u8_signature.subarray(1) : u8_signature;
verified = Nacl.sign.detached.verify(hash, classicalSig, u8_public_key);
}
// call back with (err) if unsuccessful
if (!verified) { return void cb("E_COULD_NOT_VERIFY"); }
@ -81,7 +126,7 @@ Block.validateLoginBlock = function (Env, publicKey, signature, block, _cb) {
Block.validateAncestorProof = function (Env, proof, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
/* prove that you own an existing block by signing for its publicKey */
/* prove that you own an existing block by signing for its publicKey */
try {
var parsed = JSON.parse(proof);
var pub = parsed[0];
@ -89,8 +134,55 @@ Block.validateAncestorProof = function (Env, proof, _cb) {
var sig = parsed[1];
var u8_sig = Util.decodeBase64(sig);
var valid = false;
nThen(function (w) {
valid = Nacl.sign.detached.verify(u8_pub, u8_sig, u8_pub);
// Check signature type - first byte indicates the format
var sigType = u8_sig[0];
if (sigType === 1 && u8_sig.length > 65) {
// Hybrid signature - check the classical part first
const classicalSig = u8_sig.subarray(1, 1 + 64); // Ed25519 signature is 64 bytes
valid = Nacl.sign.detached.verify(u8_pub, classicalSig, u8_pub);
// Check for PQ signature as well, but don't require it to pass
if (valid && ml_kem && ml_dsa && Env.blockInfo && Env.blockInfo[pub]) {
try {
// Get PQ signature length from the 4 bytes after classical signature
const pqSigLenView = new DataView(u8_sig.buffer, u8_sig.byteOffset + 65);
const pqSigLen = pqSigLenView.getUint32(0, false); // big-endian
// Extract PQ signature
const pqSig = u8_sig.subarray(1 + 64 + 4, 1 + 64 + 4 + pqSigLen);
// If PQ public key is provided, verify the PQ signature
if (Env.blockInfo[pub].pqPublicKey) {
const pqPublicKey = Util.decodeBase64(Env.blockInfo[pub].pqPublicKey);
const pqVerified = ml_dsa.ml_dsa44.internal.verify(
pqPublicKey,
u8_pub,
pqSig
);
Env.Log.info('ANCESTOR_PQ_VERIFICATION_RESULT', {
blockId: pub,
result: pqVerified
});
}
} catch (err) {
Env.Log.error('ANCESTOR_PQ_VERIFICATION_ERROR', {
error: err.message,
blockId: pub
});
}
}
} else {
// Classical signature or unrecognized format - use normal verification
// For type 0 we need to skip the first byte
const classicalSig = sigType === 0 ? u8_sig.subarray(1) : u8_sig;
valid = Nacl.sign.detached.verify(u8_pub, classicalSig, u8_pub);
}
if (!valid) {
w.abort();
return void cb('E_INVALID_ANCESTOR_PROOF');
@ -109,7 +201,7 @@ Block.validateAncestorProof = function (Env, proof, _cb) {
Block.writeLoginBlock = function (Env, msg, _cb) {
var cb = Util.once(Util.mkAsync(_cb));
const { publicKey, signature, ciphertext, registrationProof, userData, inviteToken, isSSO } = msg;
const { publicKey, signature, ciphertext, registrationProof, userData, inviteToken, isSSO, pqPublicKey } = msg;
var previousKey;
var validatedBlock, path;
@ -170,6 +262,20 @@ Block.writeLoginBlock = function (Env, msg, _cb) {
previousKey: previousKey,
path: path,
});
// Store PQ public key in blockInfo if provided
if (!err && pqPublicKey && typeof pqPublicKey === 'string') {
// Initialize blockInfo if it doesn't exist
if (!Env.blockInfo) { Env.blockInfo = {}; }
if (!Env.blockInfo[publicKey]) { Env.blockInfo[publicKey] = {}; }
// Store PQ public key for later verification
Env.blockInfo[publicKey].pqPublicKey = pqPublicKey;
Env.Log.info('BLOCK_PQ_KEY_STORED', {
blockId: publicKey
});
}
cb(err);
if (!err && registrationProof) {
Users.checkUpdate(Env, userData, publicKey, (err) => {
@ -255,4 +361,3 @@ Block.removeLoginBlock = function (Env, publicKey, reason, edPublic, _cb) {
SSOUtils.deleteBlock(Env, publicKey, () => {});
}
};

View File

@ -39,19 +39,55 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
var symmetric = seed.subarray(Crypto.Random.signSeedLength(),
Crypto.Random.signSeedLength() + Crypto.Random.secretboxKeyLength());
// Generate standard keys using the existing method
var sign = Crypto.Random.signKeyPairFromSeed(signSeed);
// Store the post-quantum keys separately for future use (no server validation issues)
var pqKeypair = null;
var hybridCapable = !!Crypto.PQC && !!Crypto.PQC.ml_dsa;
if (hybridCapable) {
try {
// Use separate seed for PQ keys derived from original seed
// ML-DSA requires a 32-byte seed, so we hash the seed to get a consistent 32-byte value
var pqSeed = Nacl.hash(seed).subarray(0, 32);
// Generate post-quantum keypair using the 32-byte hash
pqKeypair = Crypto.PQC.ml_dsa.ml_dsa44.internal.keygen(pqSeed);
} catch (err) {
console.error("Failed to generate post-quantum keys:", err);
pqKeypair = null;
}
}
return {
sign: Crypto.Random.signKeyPairFromSeed(signSeed), // 32 bytes
symmetric: symmetric, // 32 bytes ...
sign: sign,
pqKeypair: pqKeypair,
symmetric: symmetric,
// Store whether we have post-quantum capability
hasPQ: pqKeypair !== null
};
};
Block.keysToRPCFormat = function (keys) {
try {
var sign = keys.sign;
return {
// Basic format with classical keys
var result = {
edPrivate: Util.encodeBase64(sign.secretKey),
edPublic: Util.encodeBase64(sign.publicKey),
};
// Add post-quantum keys if present
if (keys.pqKeypair) {
result.pqKeys = {
publicKey: Util.encodeBase64(keys.pqKeypair.publicKey),
secretKey: Util.encodeBase64(keys.pqKeypair.secretKey)
};
}
return result;
} catch (err) {
console.error(err);
return;
@ -86,7 +122,43 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
// (Uint8Array block) => signature
Block.sign = function (ciphertext, keys) {
return Crypto.Random.signDetached(Crypto.Random.createHash(ciphertext), keys.sign.secretKey);
var hash = Crypto.Random.createHash(ciphertext);
// Generate hybrid signature if post-quantum capabilities are available
if (keys.hasPQ && keys.pqKeypair) {
try {
// Generate classical signature (always required for server compatibility)
var classicalSig = Crypto.Random.signDetached(hash, keys.sign.secretKey);
// Generate post-quantum signature
var pqSig = Crypto.PQC.ml_dsa.ml_dsa44.internal.sign(keys.pqKeypair.secretKey, hash);
console.log("Hybrid signature created successfully:");
// Create a hybrid signature with format:
// [1-byte type][64-byte classical sig][4-byte PQ sig length][PQ sig bytes]
var hybridSig = new Uint8Array(1 + classicalSig.length + 4 + pqSig.length);
hybridSig[0] = 1; // Type 1 indicates hybrid signature
hybridSig.set(classicalSig, 1);
// Set PQ signature length as 4-byte integer (big endian)
var view = new DataView(hybridSig.buffer);
view.setUint32(1 + classicalSig.length, pqSig.length, false);
// Add the PQ signature
hybridSig.set(pqSig, 1 + classicalSig.length + 4);
return hybridSig;
} catch (e) {
console.error("PQ signing failed, falling back to classical:", e);
}
}
// Classical signature with a type marker
classicalSig = Crypto.Random.signDetached(hash, keys.sign.secretKey);
var taggedSig = new Uint8Array(classicalSig.length + 1);
taggedSig[0] = 0; // Type 0 indicates classical only
taggedSig.set(classicalSig, 1);
return taggedSig;
};
Block.serialize = function (content, keys) {
@ -97,21 +169,28 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
var sig = Block.sign(ciphertext, keys);
// serialize {publickey, sig, ciphertext}
return {
var result = {
publicKey: Util.encodeBase64(keys.sign.publicKey),
signature: Util.encodeBase64(sig),
ciphertext: Util.encodeBase64(ciphertext),
};
// Add post-quantum public key separately if available
if (keys.hasPQ && keys.pqKeypair) {
result.pqPublicKey = Util.encodeBase64(keys.pqKeypair.publicKey);
}
return result;
};
Block.proveAncestor = function (O /* oldBlockKeys, N, newBlockKeys */) {
var u8_pub = Util.find(O, ['sign', 'publicKey']);
var u8_secret = Util.find(O, ['sign', 'secretKey']);
try {
// sign your old publicKey with your old privateKey
var u8_sig = Crypto.Random.signDetached(u8_pub, u8_secret);
// return an array with the sig and the pubkey
return JSON.stringify([u8_pub, u8_sig].map(Util.encodeBase64));
// Use Block.sign to create a hybrid signature if available
var hybridSig = Block.sign(u8_pub, O);
// Return an array with the signature and the pubkey
return JSON.stringify([u8_pub, hybridSig].map(Util.encodeBase64));
} catch (err) {
return void console.error(err);
}
@ -122,6 +201,7 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
};
Block.getBlockUrl = function (keys) {
// Use classical keys for backward compatibility with URL structure
var publicKey = urlSafeB64(keys.sign.publicKey);
// 'block/' here is hardcoded because it's hardcoded on the server
// if we want to make CryptPad work in server subfolders, we'll need
@ -172,9 +252,10 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
ServerCommand(blockKeys.sign, {
command: command,
auth: auth && auth.data
auth: auth && auth.data,
}, cb);
};
Block.writeLoginBlock = function (data, cb) {
const { content, blockKeys, oldBlockKeys, auth, pw, session, token, userData } = data;
@ -191,9 +272,10 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
ServerCommand(blockKeys.sign, {
command: command,
content: block,
session: session // sso session
session: session, // sso session
}, cb);
};
Block.removeLoginBlock = function (data, cb) {
const { reason, blockKeys, auth, edPublic } = data;
@ -204,7 +286,7 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
command: command,
auth: auth && auth.data,
edPublic: edPublic,
reason: reason
reason: reason,
}, cb);
};
@ -214,7 +296,7 @@ const factory = (Util, ApiConfig = {}, ServerCommand, Nacl, Crypto) => {
ServerCommand(blockKeys.sign, {
command: 'SSO_UPDATE_BLOCK',
ancestorProof: oldProof
ancestorProof: oldProof,
}, cb);
};

View File

@ -27,7 +27,8 @@ define([
//var Nacl = window.nacl;
var Exports = {
requiredBytes: 192,
// Increased required bytes to accommodate post-quantum keys
requiredBytes: 256, // Increased from original to accommodate post-quantum keys
};
var allocateBytes = Exports.allocateBytes = function (bytes) {
@ -49,10 +50,21 @@ define([
// 32 more for a signing key
var edSeed = opt.edSeed = dispense(32);
// 64 more bytes to seed an additional signing key
var blockKeys = opt.blockKeys = Block.genkeys(new Uint8Array(dispense(64)));
// Allocate more bytes for block keys seed to accommodate PQ keys
var blockKeysSeed = dispense(96); // Increased from original to fit PQ keys
// Generate block keys
var blockKeys = opt.blockKeys = Block.genkeys(new Uint8Array(blockKeysSeed));
opt.blockHash = Block.getBlockHash(blockKeys);
// Store post-quantum keys if they're available
if (blockKeys.hasPQ && blockKeys.pqKeypair) {
opt.pqKeys = {
publicKey: Util.encodeBase64(blockKeys.pqKeypair.publicKey),
secretKey: Util.encodeBase64(blockKeys.pqKeypair.secretKey)
};
}
// derive a private key from the ed seed
var signingKeypair = Crypto.Random.signKeyPairFromSeed(new Uint8Array(edSeed));
@ -86,6 +98,12 @@ define([
opt.channelHex = parsed.channel;
opt.keys = parsed.keys;
opt.edPublic = blockInfo.edPublic;
// Include post-quantum keys if available in the block info
if (blockInfo.pqKeys) {
opt.pqKeys = blockInfo.pqKeys;
}
return opt;
};
@ -245,7 +263,7 @@ define([
return void cb('NO_SUCH_USER');
}
if (!isProxyEmpty(rt.proxy) && res.auth_token && res.auth_token.bearer) {
if (!isRegister && !isProxyEmpty(rt.proxy) && res.auth_token && res.auth_token.bearer) {
LocalStore.setSessionToken(res.auth_token.bearer);
}