-
Notifications
You must be signed in to change notification settings - Fork 703
add mlkem768x25519-sha256 key exchange support #1471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lucasqueiroz23
wants to merge
14
commits into
mscdex:master
Choose a base branch
from
lucasqueiroz23:mlkem
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
7f7c930
feat: checks if mlkem is supported and lints files
lucasqueiroz23 7625b02
feat: adds mlkem class, adds it to factory and adds generateKeys()
lucasqueiroz23 15162a6
feat: adds getPublicKey() and start()
lucasqueiroz23 287389f
fix: changes key pair function used to make it sync
lucasqueiroz23 01f4e2d
feat: adds more context to support check
lucasqueiroz23 a7ec2c6
fix: changes constant name to correct one
lucasqueiroz23 08876dc
feat: removes async logic and finishes implementation
lucasqueiroz23 ddb8f2a
docs: adds mlkem768x25519-sha256 as kex option
lucasqueiroz23 a3eef8d
style: reverts style changes
lucasqueiroz23 a9e0418
fix: hashes key concatenation
lucasqueiroz23 79a9c97
test: adds integration tests for mlkem
lucasqueiroz23 5e0c4ee
refactor: applies suggestions
lucasqueiroz23 722d0f2
refactor: apply suggestions from code review
lucasqueiroz23 c93bccd
refactor: imports in alphabetic order
lucasqueiroz23 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -848,6 +848,7 @@ You can find more examples in the `examples` directory of this repository. | |
|
|
||
| * **kex** - _mixed_ - Key exchange algorithms. | ||
| * Default list (in order from most to least preferable): | ||
| * `mlkem768x25519-sha256` (node v24.7.0+) | ||
| * `curve25519-sha256` (node v14.0.0+) | ||
| * `[email protected]` (node v14.0.0+) | ||
| * `ecdh-sha2-nistp256` | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,29 @@ const curve25519Supported = (typeof crypto.diffieHellman === 'function' | |
| && typeof crypto.generateKeyPairSync === 'function' | ||
| && typeof crypto.createPublicKey === 'function'); | ||
|
|
||
| const mlkemSupported = (() => { | ||
| try { | ||
| if (!curve25519Supported | ||
| || typeof crypto.encapsulate !== 'function' | ||
| || typeof crypto.decapsulate !== 'function') { | ||
| return false; | ||
| } | ||
| const keys = crypto.generateKeyPairSync('ml-kem-768'); | ||
| const { ciphertext } = crypto.encapsulate( | ||
| keys.publicKey | ||
| ); | ||
|
|
||
| crypto.decapsulate( | ||
| keys.privateKey, | ||
| ciphertext | ||
| ); | ||
|
|
||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| })(); | ||
|
|
||
| const DEFAULT_KEX = [ | ||
| // https://tools.ietf.org/html/rfc5656#section-10.1 | ||
| 'ecdh-sha2-nistp256', | ||
|
|
@@ -52,6 +75,11 @@ if (curve25519Supported) { | |
| DEFAULT_KEX.unshift('curve25519-sha256'); | ||
| DEFAULT_KEX.unshift('[email protected]'); | ||
| } | ||
|
|
||
| if (mlkemSupported) | ||
| DEFAULT_KEX.unshift('mlkem768x25519-sha256'); | ||
|
|
||
|
|
||
| const SUPPORTED_KEX = DEFAULT_KEX.concat([ | ||
| // https://tools.ietf.org/html/rfc4419#section-4 | ||
| 'diffie-hellman-group-exchange-sha1', | ||
|
|
@@ -189,6 +217,9 @@ module.exports = { | |
| KEXECDH_INIT: 30, | ||
| KEXECDH_REPLY: 31, | ||
|
|
||
| KEX_HYBRID_INIT: 30, | ||
| KEX_HYBRID_REPLY: 31, | ||
|
|
||
| // User auth protocol -- generic (50-59) | ||
| USERAUTH_REQUEST: 50, | ||
| USERAUTH_FAILURE: 51, | ||
|
|
@@ -349,6 +380,7 @@ module.exports = { | |
|
|
||
| curve25519Supported, | ||
| eddsaSupported, | ||
| mlkemSupported, | ||
| }; | ||
|
|
||
| module.exports.DISCONNECT_REASON_BY_VALUE = | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,13 +9,16 @@ const { | |
| diffieHellman, | ||
| generateKeyPairSync, | ||
| randomFillSync, | ||
| encapsulate, | ||
| decapsulate, | ||
| } = require('crypto'); | ||
|
|
||
| const { Ber } = require('asn1'); | ||
|
|
||
| const { | ||
| COMPAT, | ||
| curve25519Supported, | ||
| mlkemSupported, | ||
lucasqueiroz23 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| DEFAULT_KEX, | ||
| DEFAULT_SERVER_HOST_KEY, | ||
| DEFAULT_CIPHER, | ||
|
|
@@ -1580,13 +1583,210 @@ const createKeyExchange = (() => { | |
| } | ||
| } | ||
|
|
||
| class MLKEMx25519Exchange extends KeyExchange { | ||
| // NOTE: based on the following draft | ||
| // https://datatracker.ietf.org/doc/html/draft-ietf-sshm-mlkem-hybrid-kex-03.html | ||
| constructor(hashName, ...args) { | ||
| super(...args); | ||
|
|
||
| this.type = 'mlkem768x25519'; | ||
| this.hashName = hashName; | ||
|
|
||
| this._x25519Keys = null; | ||
| this._mlkemKeys = null; | ||
|
|
||
| this._S_CT2 = null; // needed for server | ||
| } | ||
|
|
||
| generateKeys() { | ||
| if (!this._x25519Keys) | ||
| this._x25519Keys = generateKeyPairSync('x25519'); | ||
|
|
||
| if (!this._mlkemKeys) | ||
| this._mlkemKeys = generateKeyPairSync('ml-kem-768'); | ||
| } | ||
|
|
||
| convertPublicKey(key) { | ||
| // We need to override this because the key | ||
| // is not an mpint (compare section 2.1 on the draft | ||
| // to section 8 on rfc4253) | ||
| return key; | ||
| } | ||
|
|
||
| getPublicKey() { | ||
| this.generateKeys(); | ||
|
|
||
| const isServer = this._protocol._server; | ||
|
|
||
| if (!isServer) { | ||
| // client | ||
| const C_PK1 = this._x25519Keys.publicKey | ||
| .export({ type: 'spki', format: 'der' }) | ||
| .slice(-32); | ||
|
|
||
| const C_PK2 = this._mlkemKeys.publicKey | ||
| .export({ type: 'spki', format: 'der' }) | ||
| .slice(-1184); | ||
|
|
||
| return Buffer.concat([C_PK2, C_PK1]); | ||
| } | ||
|
|
||
| // server | ||
|
|
||
| // NOTE: S_CT2 should already have been | ||
| // calculated by now, since client is the | ||
| // one that initiates the kex | ||
| const S_PK1 = this._x25519Keys.publicKey | ||
| .export({ type: 'spki', format: 'der' }) | ||
| .slice(-32); | ||
|
|
||
| return Buffer.concat([this._S_CT2, S_PK1]); | ||
| } | ||
|
|
||
| buildX25519SPKI(key) { | ||
| const asnWriter = new Ber.Writer(); | ||
| asnWriter.startSequence(); | ||
| // algorithm | ||
| asnWriter.startSequence(); | ||
| asnWriter.writeOID('1.3.101.110'); // id-X25519 | ||
| asnWriter.endSequence(); | ||
|
|
||
| // PublicKey | ||
| asnWriter.startSequence(Ber.BitString); | ||
| asnWriter.writeByte(0x00); | ||
| // XXX: hack to write a raw buffer without a tag -- yuck | ||
| asnWriter._ensure(key.length); | ||
| key.copy( | ||
| asnWriter._buf, | ||
| asnWriter._offset, | ||
| 0, | ||
| key.length | ||
| ); | ||
| asnWriter._offset += key.length; | ||
| asnWriter.endSequence(); | ||
| asnWriter.endSequence(); | ||
|
|
||
| return asnWriter; | ||
| } | ||
|
|
||
| computeSecret(otherPublicKey) { | ||
| try { | ||
| this.generateKeys(); | ||
| const isServer = this._protocol._server; | ||
| if (isServer && !this._S_CT2) { | ||
| // server | ||
| const C_PK2 = otherPublicKey.slice( | ||
| 0, | ||
| 1184 | ||
| ); | ||
lucasqueiroz23 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const C_PK1 = otherPublicKey.slice(1184); | ||
|
|
||
| if (C_PK1.length !== 32) { | ||
| const got = C_PK1.length; | ||
| throw new Error( | ||
| `Invalid C_PK1 length: expected 32, got ${got}` | ||
| ); | ||
| } | ||
|
|
||
| const x25519SPKI = this.buildX25519SPKI(C_PK1); | ||
|
|
||
| const K_CL = diffieHellman({ | ||
| privateKey: this._x25519Keys.privateKey, | ||
| publicKey: createPublicKey({ | ||
| key: x25519SPKI.buffer, | ||
| type: 'spki', | ||
| format: 'der', | ||
| }), | ||
| }); | ||
|
|
||
| const templateDER = this._mlkemKeys.publicKey | ||
| .export({ type: 'spki', format: 'der' }); | ||
|
|
||
| const C_PK2_DER = Buffer.allocUnsafe(templateDER.length); | ||
|
|
||
| templateDER.copy( | ||
| C_PK2_DER, | ||
| 0, | ||
| 0, | ||
| templateDER.length - 1184 | ||
| ); | ||
|
|
||
| C_PK2.copy( | ||
| C_PK2_DER, | ||
| templateDER.length - 1184 | ||
| ); | ||
lucasqueiroz23 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const { sharedKey: K_PQ, ciphertext: S_CT2 } = encapsulate( | ||
| createPublicKey({ | ||
| key: C_PK2_DER, | ||
| type: 'spki', | ||
| format: 'der', | ||
| }) | ||
| ); | ||
|
|
||
| this._S_CT2 = S_CT2; | ||
|
|
||
| // K = HASH(K_PQ || K_CL) from 2.4 on draft | ||
| const K = createHash('sha256') | ||
| .update(K_PQ) | ||
| .update(K_CL) | ||
| .digest(); | ||
|
|
||
| return K; | ||
| } | ||
| // client | ||
| const S_CT2 = otherPublicKey.slice(0, 1088); | ||
| const S_PK1 = otherPublicKey.slice(1088); | ||
|
|
||
| if (S_PK1.length !== 32) { | ||
| const got = S_PK1.length; | ||
| throw new Error( | ||
| `Invalid S_PK1 length: expected 32, got ${got}` | ||
| ); | ||
| } | ||
|
|
||
| const x25519SPKI = this.buildX25519SPKI(S_PK1); | ||
|
|
||
| const K_CL = diffieHellman({ | ||
| privateKey: this._x25519Keys.privateKey, | ||
| publicKey: createPublicKey({ | ||
| key: x25519SPKI.buffer, | ||
| type: 'spki', | ||
| format: 'der', | ||
| }), | ||
| }); | ||
|
|
||
| const K_PQ = decapsulate( | ||
| this._mlkemKeys.privateKey, | ||
| S_CT2 | ||
| ); | ||
lucasqueiroz23 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| // K = HASH(K_PQ || K_CL) from 2.4 on draft | ||
| const K = createHash('sha256') | ||
| .update(K_PQ) | ||
| .update(K_CL) | ||
| .digest(); | ||
|
|
||
| return K; | ||
|
|
||
| } catch (error) { | ||
| return error; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return (negotiated, ...args) => { | ||
| if (typeof negotiated !== 'object' || negotiated === null) | ||
| throw new Error('Invalid negotiated argument'); | ||
| const kexType = negotiated.kex; | ||
| if (typeof kexType === 'string') { | ||
| args = [negotiated, ...args]; | ||
| switch (kexType) { | ||
| case 'mlkem768x25519-sha256': | ||
| if (!mlkemSupported) | ||
| break; | ||
| return new MLKEMx25519Exchange('sha256', ...args); | ||
|
|
||
| case 'curve25519-sha256': | ||
| case '[email protected]': | ||
| if (!curve25519Supported) | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.