-
Notifications
You must be signed in to change notification settings - Fork 4
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
Add preliminary TLS1.3 PSK-mode implementation. #24
Conversation
Ok we got it to work, it was a problem with psk |
@vladikoff @eoger thanks for the fixes; heads-up that I'm going to port some of them to master and squash the others with my latest work. |
Could we rebase this on |
Rebased. |
705de41
to
7fa784c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since I was looking, I spent a little time on this. Hopefully some of my suggestions result in more tests being added :)
src/utils.js
Outdated
|
||
seek(pos) { | ||
this._pos = pos; | ||
assert(this._pos <= this.length(), 'do not seek past end of buffer'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't you assert on < 0 as well? And why not check the input before applying it?
src/utils.js
Outdated
this._pos = 0; | ||
} | ||
|
||
resize(size) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you can't shrink, then grow() might be the name you want.
src/utils.js
Outdated
return slice; | ||
} | ||
|
||
readRemainingBytes() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In TLS, this is probably a hazard. You should always know how many bytes you are reading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 ; from a quick grep it looks like this is actually a leftover that is no longer used, so I guess I refactored away whatever hazard was using it...
src/utils.js
Outdated
_readVector(length, cb) { | ||
const limit = this.tell() + length; | ||
// Keep calling the callback until we've consumed the expected number of bytes. | ||
// It can return `true` to indicate we should stop iterating and skip the remainder. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would disable this feature. The callback should be run once for each item in the list until it is done.
One thing I would do with the callback is pass it a different instance of this object that offers a view onto the same buffer, but with a the shorter length. Then they can't read past "length" by design and you don't have to worry about inner reads going outside of the space that is available.
src/utils.js
Outdated
// | ||
|
||
_readVector(length, cb) { | ||
const limit = this.tell() + length; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
assert that limit <= this.length()
src/utils.js
Outdated
for (let i = 0; i < v1.length; i++) { | ||
mismatch &= v1[i] !== v2[i]; | ||
mismatch |= v1[i] ^ v2[i]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As we discussed, crypto.subtle.verify might be what you want here.
// XXX TODO: feed it the pre-generated ServerHello. | ||
}); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some tests to consider, though I'm guessing that you have some of these already:
- test reading and writing more thoroughly, including short reads and long reads
- skipping records fails
- damage the ciphertext and the recipient explodes
- skipping handshake messages fails
- missing extensions means failure
- extra extensions (in ClientHello are ignored, in ServerHello generate alerts)
- test that writing blocks/fails prior to the handshake being complete (you could write at the server in 0.5-RTT, but please disable that and test that it is disabled)
- test that you ignore ChangeCipherSpec (or that you choke cleanly) and limit it to during the handshake if you do accept it, if you do ignore it, the server should probably emit one if the client sends a non-zero session ID
- test that you are actually encrypting (yep, that's somewhat tricky, but it's pretty important)
- test without a mocked RNG and see that you get different ClientHello/ServerHello when you run the handshake multiple times
- check that reordered extensions still work fine
- add dummy versions, ciphersuites, and PSK modes to extensions and see that these are ignored
- add a non-zero compression mode and the server should explode
- validate that the version in the record header is correct (TLS 1.0 I think is right: 0x0301)
- validate the version in the ServerHello is TLS 1.2 (0x0303)
- handle padded records properly
- abort if a record only contains zeros (i.e., it is over-padded)
- abort if a handshake record is empty (i.e., it is properly padded with a handshake type, but no data)
- abort if an alert of any kind is received
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you could write at the server in 0.5-RTT
To clarify, do you mean the server sending application data after it has sent its Finished
, but before it has received Finished
from the client in return?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right. In TLS 1.3, the server can send before it receives the Finished from the client, but that is generally considered risky in mutually authenticated modes. For your purposes, you want to wait until you have verified the peer's Finished, in both directions.
e3d28a2
to
9d7f09b
Compare
The latest push includes both (tlslite-ng => fxa-pairing-channel) and (fxa-pairing-channel => tlslite-ng) test vectors and they appear to be passing \o/
I can honestly say I looked, but also am not short on testcases to add as evidenced by comment above... |
src/states.js
Outdated
async sendApplicationData(bytes) { | ||
assert(false, 'uninitialized state'); | ||
} | ||
async sendAlertMessage(err) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this sendAlertMessage
used? I see _sendAlertMessage
elsewhere?
src/messages.js
Outdated
} | ||
|
||
static _read(buf) { | ||
// The legacy_version field may indiciate an earlier version of TLS |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: indiciate
-> indicate
src/messages.js
Outdated
break; | ||
case EXTENSION_TYPE.PRE_SHARED_KEY: | ||
// https://tools.ietf.org/html/rfc8446#section-4.2.11 | ||
// The extension data contains an `OfferredPsks` struct: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo, per spec OfferredPsks
--> OfferedPsks
?
src/states.js
Outdated
throw this.error; | ||
} | ||
async handleError(err) { | ||
// We've alreaded errored out the connection, don't try to do it again. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
already
?
src/states.js
Outdated
alertIf(! bytesAreEqual(msg.verifyData, this._expectedServerFinishedMAC), ALERT_DESCRIPTION.DECRYPT_ERROR); | ||
// Send our own Finished message in return. | ||
// This must be encrypted with the handshake traffic key, | ||
// but must not appear in the transscript used to calculate the application keys. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
transcript
this.extBinderKey = await this.deriveSecret('ext binder', EMPTY); | ||
} | ||
|
||
async addECDHE(ecdhe) { |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer to make the caller pass in an explicit "I don't have an ECDHE output" indicator, seems safer to me. I think I'll make this a more explicit check for null
rather than just falsiness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good!
src/messages.js
Outdated
} | ||
|
||
static fromBytes(bytes) { | ||
// Each handshake messages has a type and length prefix, per |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
messages
-> message
?
src/messages.js
Outdated
} | ||
|
||
write(buf) { | ||
// Each handshake messages has a type and length prefix, per |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
messages
-> message
?
src/recordlayer.js
Outdated
// * xor with the provided iv | ||
const nonce = new Uint8Array(IV_LENGTH); | ||
// Our sequence numbers are always less than 2^24, so fit in a Uint32. | ||
(new DataView(nonce.buffer)).setUint32(IV_LENGTH - 4, this.seqnum); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new DataView(nonce.buffer).setUint32(IV_LENGTH - 4, this.seqnum);
? remove extra (
?
// The main RecordLayer class. | ||
|
||
export class RecordLayer { | ||
constructor(sendCallback) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you want to assert that sendCallback
is set?
By way of a brief update here, I'm working my way through the recommended testcases and expect to have them wrapped by in the next day or two. I also managed to get the test runner to emit coverage reports and run them automatically on CircleCI, latest one from the build output here: This does a pretty good job of highlighting the error cases that are not yet covered by tests! @vladikoff, whenever you have a few moments, I'd appreciate your feedback on this commit which adds the test coverage reporting. I had to add a third build configuration with |
Ugh, this one test "FxAccountsPairingChannel a Client and Server connected together should communicate successfully if they have the same PSK FAILED" is perma-failing due to a timeout, I wonder if there's some timing issue in setting up the websocket listeners or something, because it passes reliably on my machine... |
// Using `value instanceof Uint8Array` seems to fail in Firefox chrome code | ||
// for inscrutable reasons, so we do a less direct check. | ||
assert(ArrayBuffer.isView(value), msg); | ||
assert(value.BYTES_PER_ELEMENT === 1, msg); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@vladikoff do either or both of these pass in Firefox chrome code, or do we need to keep looking for a suitable approach here?
@martinthomson I've pushed a bunch of tests for the client side of the handshake, and I'm pretty happy with the level of test coverage overall; if you're interested you can view the built coverage report here: I don't have anything else I plan to add here in response to your first round of comments. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to try to review all the TLS stuff here, and not a lot more, but I do have a few comments outside of that.
Running npm install;npm test
produces changes to checked-in code. That's not cool. I don't pretend to know how to manage this stuff, but I find the fact that you have build output in the tree to be unhygienic (for this exact reason).
The changes to package-lock.json seem to be the addition of an optional
key to most of the dev dependencies, which might be a cleanup performed by npm 6.5.0 (if you are running an earlier version, that is).
This is part1. I need to continue this later. I haven't really started looking at the tests, so it might take a few more hours.
src/alerts.js
Outdated
switch (description) { | ||
case ALERT_DESCRIPTION.CLOSE_NOTIFY: | ||
case ALERT_DESCRIPTION.USER_CANCELLED: | ||
this.level = ALERT_LEVEL.WARNING; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you use the user_cancelled alert? Because if you don't, then it might pay not to have the case here. TLS 1.3 treats all alerts as fatal other than close_notify, so this might not work out anyway.
Put differently: if you aren't testing this, remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not using it, and only included it because it was mentioned alongside close_notify
in [1]. I'll be very happy to remove it.
src/crypto.js
Outdated
export async function decrypt(key, iv, ciphertext, additionalData) { | ||
let plaintext; | ||
try { | ||
plaintext = await crypto.subtle.decrypt({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you put the return inside the try, would that mean you have to deal with warnings about the function not returning?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope, I think I might have just had it outside for some console.log
debugging at some point
export async function hkdfExpand(prk, info, length) { | ||
// Ref https://tools.ietf.org/html/rfc5869#section-2.3 | ||
const N = Math.ceil(length / HASH_LENGTH); | ||
if (N >= 255) { |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
@@ -72,7 +72,7 @@ export class InsecurePairingChannel extends EventTarget { | |||
const pskId = utf8ToBytes(channelId); | |||
const connection = await ConnectionClass.create(psk, pskId, data => { | |||
// To send data over the websocket, it needs to be encoded as a safe string. | |||
socket.send(bytesToHex(data)); | |||
socket.send(bytesToBase64url(data)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about this assertion regarding strings. They take ABV: https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-send
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an implementation detail of the server we're talking to, rather than of websockets in general. (The comment could be worded more clearly in that regard).
const STAGE_HANDSHAKE_SECRET = 2; | ||
const STAGE_MASTER_SECRET = 3; | ||
|
||
export class KeySchedule { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to have a comment explaining the state machine: UNINITIALIZED --addPSK--> EARLY_SECRET --addECDHE--> HANDSHAKE_SECRET --finalize--> MASTER_SECRET.
src/utils.js
Outdated
} | ||
|
||
writeVector8(cb) { | ||
return this._writeVector(Math.pow(2, 8), this.writeUint8.bind(this), cb); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is len => this.writeUint8(len)
better than using bind()?
test/FxAccountsPairingChannel.js
Outdated
const client = await FxAccountsPairingChannel.InsecurePairingChannel.connect(CHANNEL_SERVER, channelId, channelKey); | ||
client.addEventListener('message', ({detail: {data}}) => { | ||
CLIENT_RECV.push(data.msg); | ||
const client = await FxAccountsPairingChannel.PairingChannel.connect(CHANNEL_SERVER, channelId, channelKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You run against a network-based service in test? That seems brittle. Can you run this server locally? Or at least a cut-down version of the same.
const tampered = bytes.slice(); | ||
const i = Math.floor(Math.random() * (tampered.byteLength - start) + start); | ||
tampered[i] += 1 + Math.floor(Math.random() * 255); | ||
return tampered; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with this function is that if it does trigger an error, then you won't ever know what conditions produced that error. If you are going to fuzz, record the way in which your tampering was applied so that you can reproduce problems easily.
const buf = new BufferWriter(); | ||
buf.writeUint8(1); | ||
buf.writeVector24(buf => { | ||
buf.writeUint16(typeof opts.version !== 'undefined' ? opts.version : 0x0303); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You use typeof opts.x !== 'undefined' ? opts.x : blah
here, but the simpler opts.x || blah
above. Any particular reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC the simpler opts.x || default
form is used when the default is falsy, and hence there's no reason for the caller to want to pass in an explicit falsy value. Probably cleaner to just use typeof opts.x !== 'undefined'
consistently throughout.
test/helpers.js
Outdated
signClientHelloMessage: async function (clientHello, psk) { | ||
const keyschedule = new KeySchedule(); | ||
await keyschedule.addPSK(psk); | ||
const binder = await keyschedule.calculateFinishedMAC(keyschedule.extBinderKey, clientHello.slice(0, -35)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-35 = magic number
src/states.js
Outdated
this.conn._closeForRecv(); | ||
break; | ||
default: | ||
return await this.handleError(err); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self: this won't actually throw the error, but it should!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you should name the function error
when you have it throw.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep; in my head it's something like handleAndRethrowError
to make it super clear what will happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good.
It's tedious going through all these tests, but I'm fairly happy with the coverage you are getting. I see some unreachable code that is not tested, but the rest seems fairly thorough.
I have a few suggestions for more tests, a couple of minor problems, and probably a need for another round before I'm completely happy with this. But this is a very competent effort. A good example of how to do this right.
src/messages.js
Outdated
// We should not receive any encrypted extensions, | ||
// since we do not advertize any in the ClientHello. | ||
buf.readVector16(buf => { | ||
if (buf.length() !== 0) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The coverage report here seems to suggest that this test always passes - as in, the callback doesn't get called if the buffer is empty. You can remove the check in that case.
@@ -0,0 +1,374 @@ | |||
# |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't review this bit.
assert.ok(! bytesAreEqual(arrayToBytes([1, 2, 3]), arrayToBytes([1, 2, 4]))); | ||
assert.ok(! bytesAreEqual(arrayToBytes([1, 2, 3]), arrayToBytes([1, 2, 3, 4]))); | ||
assert.ok(! bytesAreEqual(arrayToBytes([1, 2, 3, 4]), arrayToBytes([1, 2, 3]))); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about null or invalid inputs?
test/utils.js
Outdated
assert.equal(contentsBuf.length(), 2); | ||
contentsBuf.readUint24(); | ||
}); | ||
}, TLSError, /DECODE_ERROR/); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is a regex necessary? Can this be a more specific test?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is mostly just using the existing convenience API of assert.throws
; I could instead check for a specific numeric value of the alert description, if that seems better?
test/recordlayer.js
Outdated
|
||
it('prevents wrapping of the sequence number', async () => { | ||
await cs.setKey(zeros(32), ['encrypt', 'decrypt']); | ||
cs.seqnum = Math.pow(2, 24) - 1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Magic number. Make a constant or share the one you have.
}, TLSError, 'CLOSE_NOTIFY'); | ||
}); | ||
|
||
describe('is able to send an explicit close in return, and then', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't seem to have a separate case for server.close() without having first received a close.
}, TLSError, 'CLOSE_NOTIFY'); | ||
}); | ||
|
||
describe('is able to send an explicit close in return, and then', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you test for sending the close without having received the close first?
content: await testHelpers.makeServerHelloMessage({ | ||
extensions: [ | ||
testHelpers.makePreSharedKeyExtension(1) | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add version: 0x0304 to this, maybe as a supplementary test, so that you catch the case where the version field in the SH is used to drive version negotiation. (Not that your code is at all vulnerable to this currently, but it's a good paranoia check.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be clear, the handshake should fail if SH.version
is 0x0304
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. Fail.
}, TLSError, 'DECODE_ERROR'); | ||
}); | ||
|
||
it('errors if ServerHello and EncryptedExtensions appear in the same record', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have a test for a partial record being present after the ServerHello or Finished? I see that you have the code for checking this, but I can't find the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I don't believe I do - perhaps that check is being exercised accidentally by some other test. I'll add one.
server._keyschedule.serverApplicationTrafficSecret, | ||
client._keyschedule.serverApplicationTrafficSecret | ||
)); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add tests here for the promise I talked about.
Also, can you add a test that includes calls to send(). It would be good if you could also test that you can close in one direction then continue sending and receiving in the other direction.
In my experience, at this point |
src/extensions.js
Outdated
// | ||
// Extension parsing. | ||
// | ||
// This file contains some helpers reading/writing the various kinds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some helpers reading/writing
-> some helpers for reading/writing
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also this comment didn't make it to the final bundle, need to regenerate the build
Thanks for the detailed feedback @martinthomson, I've updated based on your review with the following key points:
The diff from the version you reviewed is here, and the latest coverage report is here for completeness. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got into a Error: TLS Alert: DECODE_ERROR (50)
with bce404a545165a791ebcd7d1ca984bd9861120ee
, trying to it track down, probably incompat with the Firefox build.
.circleci/config.yml
Outdated
@@ -21,6 +21,9 @@ jobs: | |||
npm install |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's try to use npm ci
here
src/extensions.js
Outdated
// | ||
// Extension parsing. | ||
// | ||
// This file contains some helpers reading/writing the various kinds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also this comment didn't make it to the final bundle, need to regenerate the build
@@ -8,19 +8,15 @@ It will be used by the Firefox Accounts pairing flow, with one side | |||
of the channel being web content from https://accounts.firefox.com and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's add a doc about running npm ci
and npm run build
to ensure the build is consistent and no new sub-deps show up. Maybe we can do it as a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be a pre-commit hook or something? (TBH it kind of weirds me out checking in the build output on every commit, and I wonder if we can only do it on releases or something; but if we're going to do it we should be able to automate it)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pre-commit hooks are local only, you want something in CI ideally
src/states.js
Outdated
async sendApplicationData(bytes) { | ||
throw this.error; | ||
} | ||
async sendAlertMessage(err) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this still used , needed ? I think we use _sendAlertMessage
in other places
src/index.js
Outdated
if (! this._peerClosed) { | ||
this.dispatchEvent(new CustomEvent('error', { | ||
detail: { | ||
error: new Error('WebSocket unexepectedly closed'), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unexepectedly
--> unexpectedly
test/recordlayer.js
Outdated
assert.equal(rl._recvDecryptState, null); | ||
}); | ||
|
||
it('will send encypted handshake records', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
encrypted
test/recordlayer.js
Outdated
assert.equal(type, 22); | ||
}); | ||
|
||
it('will send encypted application data records', async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
encrypted
src/recordlayer.js
Outdated
if (this._recvError !== null) { | ||
throw this._recvError; | ||
} | ||
// For simplicity, we assume that the given data conatins exactly one record. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
contains
?
src/states.js
Outdated
const keyschedule = this.conn._keyschedule; | ||
const transcript = keyschedule.getTranscript(); | ||
// Calculate size occupied by the PSK binders. | ||
let pskBindersSize = 2; // Vector16 represntation overhead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
representation
?
test/tlsconnection.js
Outdated
// keyshares, optional extensions, and other exciting features that we do not support. | ||
const data = await server.recv(TEST_VECTORS.EXTENDED_CLIENT_HELLO); | ||
assert.equal(data, null); | ||
// It sends ServerHello, ChangeCipherSpec, EncyptedExtensions+Finished in response. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EncryptedExtensions ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm done with this.
Thanks for catching a few extra things with the new code.
// | | ||
// | addECDHE() | ||
// v | ||
// HANDSHAKE_SECRET |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes me think. Can you null-out the old secrets as you go, so that we get something like post-compromise security?
const buf = this._pendingRecordBuf; | ||
const type = this._pendingRecordType; | ||
let type = this._pendingRecordType; | ||
if (! type) { | ||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might pay to assert that buf.byteLength === 0 here too.
src/recordlayer.js
Outdated
// | ||
// The additional data for the decryption is the `TLSCiphertext` record | ||
// header, which is a fixed size and at the start of the buffer. | ||
buf.seek(0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This breaks encapsulation a little, but I think that it's OK.
this.conn._closeForRecv(); | ||
break; | ||
this.conn._closeForRecv(alert); | ||
throw alert; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not so sure about throwing when a close alert is received. It's not an error condition. Is the problem here that you don't have an alternative means of signaling a close from the peer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically, yes. I think a better approach is probably to shuffle the API to use an EventEmitter
style, so it can emit a close
event in this case, which is basically what the higher-level PairingChannel
abstraction does anyway. But it was a bigger refactor than I wanted to take on inline in this PR.
} | ||
|
||
// A base class for states that occur in the middle of the handshake | ||
// (that is, between ClientHello and Finished). These states may receive |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CCS appears between ClientHello and Finished. CCS also happens between CCS and EncryptedExtensions, if you request - as you do - compatibility mode.
I wonder if you shouldn't have an CLIENT_WAIT_CCS state immediately after handling the ServerHello then.
test/tlsconnection.js
Outdated
const CLIENT_SENT = [], SERVER_SENT = []; | ||
const server = await ServerConnection.create(TEST_VECTORS.PSK, TEST_VECTORS.PSK_ID, data => SERVER_SENT.push(data)); | ||
const client = await ClientConnection.create(TEST_VECTORS.PSK, TEST_VECTORS.PSK_ID, data => CLIENT_SENT.push(data)); | ||
await server.recv(CLIENT_SENT[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// CH
test/tlsconnection.js
Outdated
const server = await ServerConnection.create(TEST_VECTORS.PSK, TEST_VECTORS.PSK_ID, data => SERVER_SENT.push(data)); | ||
const client = await ClientConnection.create(TEST_VECTORS.PSK, TEST_VECTORS.PSK_ID, data => CLIENT_SENT.push(data)); | ||
await server.recv(CLIENT_SENT[0]); | ||
await client.recv(SERVER_SENT[0]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// SH
test/tlsconnection.js
Outdated
const client = await ClientConnection.create(TEST_VECTORS.PSK, TEST_VECTORS.PSK_ID, data => CLIENT_SENT.push(data)); | ||
await server.recv(CLIENT_SENT[0]); | ||
await client.recv(SERVER_SENT[0]); | ||
await client.recv(SERVER_SENT[1]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// EE+Finished
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add comments, but for completeness, this is ChangeCipherSpec...
test/tlsconnection.js
Outdated
await server.recv(CLIENT_SENT[0]); | ||
await client.recv(SERVER_SENT[0]); | ||
await client.recv(SERVER_SENT[1]); | ||
await client.recv(SERVER_SENT[2]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
?? Finished?
NSS bundles everything together, so I'm unused to multiple messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...and this is EE+Finished.
test/tlsconnection.js
Outdated
await client.recv(SERVER_SENT[0]); | ||
await client.recv(SERVER_SENT[1]); | ||
await client.recv(SERVER_SENT[2]); | ||
await server.recv(CLIENT_SENT[1]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Client Finished
Thanks so much @martinthomson! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix leftover review comments from #24
Fixes #24
(This currently targets #23 but I will rebase against master once that's merged)
This adds actually-appears-to-be-functioning code to implement the TLS1.3 PSK mode. It needs a lot of tests, but I have confirmed that it interoperates successfully with an existing TLS library (tlslite-ng) so I have some base level of confidence that it's doing the right thing in the happy case.
@vladikoff @eoger please try building against this branch and check if things work OK. I will rebase this branch as I add additional testcases and cleanups.
Things still to do:
Look in the NSS codebase for specific testcases to duplicate here.