mirror of
https://github.com/cryptpad/cryptpad.git
synced 2026-09-14 11:05:41 +05:00
Successful Hybrid Signature for Login operation.
This commit is contained in:
parent
0a3335b0fc
commit
70e7f1823e
@ -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, () => {});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
};
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user