Axone
Axone17mo ago

Bad implementation of X25519?

I can't generate a shared secret with the current implementation of X25519
const alice = await crypto.subtle.generateKey({ name: "X25519" }, true, [
"deriveKey",
"deriveBits",
]) as CryptoKeyPair;

const bob = await crypto.subtle.generateKey({ name: "X25519" }, true, [
"deriveKey",
"deriveBits",
]) as CryptoKeyPair;

const secret = await crypto.subtle.deriveBits(
{ name: "X25519", public: alice.publicKey },
bob.privateKey,
256,
);

console.log(secret);
const alice = await crypto.subtle.generateKey({ name: "X25519" }, true, [
"deriveKey",
"deriveBits",
]) as CryptoKeyPair;

const bob = await crypto.subtle.generateKey({ name: "X25519" }, true, [
"deriveKey",
"deriveBits",
]) as CryptoKeyPair;

const secret = await crypto.subtle.deriveBits(
{ name: "X25519", public: alice.publicKey },
bob.privateKey,
256,
);

console.log(secret);
3 Replies
Axone
AxoneOP17mo ago
error: Uncaught OperationError: Invalid key
const secret = await crypto.subtle.deriveBits(
^
at deriveBits (ext:deno_crypto/00_crypto.js:4483:15)
at SubtleCrypto.deriveBits (ext:deno_crypto/00_crypto.js:1123:26)
at file:///D:/Codes/deno/axon/x25519.ts:11:36
error: Uncaught OperationError: Invalid key
const secret = await crypto.subtle.deriveBits(
^
at deriveBits (ext:deno_crypto/00_crypto.js:4483:15)
at SubtleCrypto.deriveBits (ext:deno_crypto/00_crypto.js:1123:26)
at file:///D:/Codes/deno/axon/x25519.ts:11:36
If we look at the implementation we can see the following code: (https://github.com/denoland/deno/blob/8ae706293149fb6e3d40af3ac80a8661fa379111/ext/crypto/00_crypto.js#L4346)
case "X25519": {
// 1.
if (baseKey[_type] !== "private") {
throw new DOMException("Invalid key type", "InvalidAccessError");
}
// 2.
const publicKey = normalizedAlgorithm.public;
// 3.
if (publicKey[_type] !== "public") {
throw new DOMException("Invalid key type", "InvalidAccessError");
}
// 4.
if (publicKey[_algorithm].name !== baseKey[_algorithm].name) {
throw new DOMException(
"Algorithm mismatch",
"InvalidAccessError",
);
}

// 5.
const kHandle = baseKey[_handle];
const k = WeakMapPrototypeGet(KEY_STORE, kHandle);

const uHandle = publicKey[_handle];
const u = WeakMapPrototypeGet(KEY_STORE, uHandle);

const secret = new Uint8Array(32);
const isIdentity = ops.op_crypto_derive_bits_x25519(k, u, secret);

// 6.
if (isIdentity) {
throw new DOMException("Invalid key", "OperationError");
}
case "X25519": {
// 1.
if (baseKey[_type] !== "private") {
throw new DOMException("Invalid key type", "InvalidAccessError");
}
// 2.
const publicKey = normalizedAlgorithm.public;
// 3.
if (publicKey[_type] !== "public") {
throw new DOMException("Invalid key type", "InvalidAccessError");
}
// 4.
if (publicKey[_algorithm].name !== baseKey[_algorithm].name) {
throw new DOMException(
"Algorithm mismatch",
"InvalidAccessError",
);
}

// 5.
const kHandle = baseKey[_handle];
const k = WeakMapPrototypeGet(KEY_STORE, kHandle);

const uHandle = publicKey[_handle];
const u = WeakMapPrototypeGet(KEY_STORE, uHandle);

const secret = new Uint8Array(32);
const isIdentity = ops.op_crypto_derive_bits_x25519(k, u, secret);

// 6.
if (isIdentity) {
throw new DOMException("Invalid key", "OperationError");
}
Axone
AxoneOP17mo ago
After some research (https://vnhacker.blogspot.com/2015/09/why-not-validating-curve25519-public.html) I found what the isIdentity test was for
Why not validate Curve25519 public keys could be harmful
Update: see this post for a real world protocol that is broken if Curve25519 public keys are not validated. Update: comments on Twitter ....
Axone
AxoneOP17mo ago
And I try to read some rust (https://github.com/denoland/deno/blob/main/ext/crypto/x25519.rs#L35)
#[op(fast)]
pub fn op_crypto_derive_bits_x25519(
k: &[u8],
u: &[u8],
secret: &mut [u8],
) -> bool {
let k: [u8; 32] = k.try_into().expect("Expected byteLength 32");
let u: [u8; 32] = u.try_into().expect("Expected byteLength 32");
let sh_sec = x25519_dalek::x25519(k, u);
let point = MontgomeryPoint(sh_sec);
if point.ct_eq(&MONTGOMERY_IDENTITY).unwrap_u8() == 1 {
return false;
}
secret.copy_from_slice(&sh_sec);
true
}
#[op(fast)]
pub fn op_crypto_derive_bits_x25519(
k: &[u8],
u: &[u8],
secret: &mut [u8],
) -> bool {
let k: [u8; 32] = k.try_into().expect("Expected byteLength 32");
let u: [u8; 32] = u.try_into().expect("Expected byteLength 32");
let sh_sec = x25519_dalek::x25519(k, u);
let point = MontgomeryPoint(sh_sec);
if point.ct_eq(&MONTGOMERY_IDENTITY).unwrap_u8() == 1 {
return false;
}
secret.copy_from_slice(&sh_sec);
true
}
And it seems that the function does not return if it is the identity point, but if the generated secret successfully passes the test So in the javascript, you should just check if the value is true, correct me if I'm wrong