From ada59a24c92c117e44e2e888dc2e5fc6d5ca0217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?rich=CE=9Brd?= Date: Wed, 5 Mar 2025 12:16:42 -0400 Subject: [PATCH] feat: picotls integration (#55) --- .github/workflows/test.yml | 25 ++- .gitignore | 3 - quic.nimble | 2 +- quic/api.nim | 75 ++++++++- quic/basics.nim | 2 +- quic/connection.nim | 31 +++- quic/errors.nim | 1 + quic/listener.nim | 15 +- .../ngtcp2/connection/closedstate.nim | 4 - .../ngtcp2/connection/closingstate.nim | 4 - .../ngtcp2/connection/disconnectingstate.nim | 4 - .../transport/ngtcp2/connection/openstate.nim | 62 ++++++-- quic/transport/ngtcp2/native.nim | 9 ++ .../ngtcp2/native/certificateverifier.nim | 7 + .../certificateverifier.nim | 11 ++ .../native/certificateverifier/custom.nim | 70 +++++++++ .../native/certificateverifier/insecure.nim | 17 ++ quic/transport/ngtcp2/native/client.nim | 145 +++++++++--------- quic/transport/ngtcp2/native/connection.nim | 54 +++++-- quic/transport/ngtcp2/native/cryptodata.nim | 13 -- quic/transport/ngtcp2/native/encryption.nim | 64 +------- quic/transport/ngtcp2/native/errors.nim | 2 +- quic/transport/ngtcp2/native/handshake.nim | 2 +- quic/transport/ngtcp2/native/ids.nim | 11 +- quic/transport/ngtcp2/native/keys.nim | 70 --------- quic/transport/ngtcp2/native/params.nim | 2 +- quic/transport/ngtcp2/native/path.nim | 37 +++-- quic/transport/ngtcp2/native/picotls.nim | 110 +++++++++++++ quic/transport/ngtcp2/native/pointers.nim | 2 +- quic/transport/ngtcp2/native/rand.nim | 8 + quic/transport/ngtcp2/native/server.nim | 144 +++++++++-------- quic/transport/ngtcp2/native/settings.nim | 2 +- quic/transport/ngtcp2/native/streams.nim | 33 +++- quic/transport/ngtcp2/stream/closedstate.nim | 6 + quic/transport/quicclientserver.nim | 13 +- quic/transport/stream.nim | 2 +- quic/transport/timeout.nim | 4 + quic/transport/tlsbackend.nim | 45 ++++++ quic/transport/version.nim | 5 +- tests/helpers/certificate.nim | 15 ++ tests/helpers/simulation.nim | 18 ++- tests/helpers/testCertificate.pem | 12 ++ tests/helpers/testPrivateKey.pem | 5 + tests/quic/testApi.nim | 26 ++-- tests/quic/testConnection.nim | 13 +- tests/quic/testExample.nim | 28 +++- tests/quic/testListener.nim | 13 +- tests/quic/testNgtcp2TransportParameters.nim | 27 ++-- tests/quic/testQuicConnection.nim | 44 ++++-- tests/quic/testStreams.nim | 7 - 50 files changed, 864 insertions(+), 460 deletions(-) create mode 100644 quic/transport/ngtcp2/native/certificateverifier.nim create mode 100644 quic/transport/ngtcp2/native/certificateverifier/certificateverifier.nim create mode 100644 quic/transport/ngtcp2/native/certificateverifier/custom.nim create mode 100644 quic/transport/ngtcp2/native/certificateverifier/insecure.nim delete mode 100644 quic/transport/ngtcp2/native/cryptodata.nim delete mode 100644 quic/transport/ngtcp2/native/keys.nim create mode 100644 quic/transport/ngtcp2/native/picotls.nim create mode 100644 quic/transport/ngtcp2/native/rand.nim create mode 100644 quic/transport/tlsbackend.nim create mode 100644 tests/helpers/certificate.nim create mode 100644 tests/helpers/testCertificate.pem create mode 100644 tests/helpers/testPrivateKey.pem diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 61836dc..3203b01 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ on: jobs: test: - timeout-minutes: 30 + timeout-minutes: 90 strategy: fail-fast: false matrix: @@ -23,18 +23,26 @@ jobs: cpu: i386 shell: bash - os: macos - runner: macos-latest + runner: macos-13 cpu: amd64 shell: bash + - os: macos + runner: macos-14 + cpu: arm64 + shell: bash - os: windows runner: windows-latest cpu: amd64 shell: msys2 {0} nim: - - branch: version-1-6 - - branch: version-2-0 + - ref: version-1-6 + - ref: version-2-0 - name: '${{ matrix.platform.os }}-${{ matrix.platform.cpu }} (Nim ${{ matrix.nim.branch }})' + defaults: + run: + shell: ${{ matrix.platform.shell }} + + name: '${{ matrix.platform.os }}-${{ matrix.platform.cpu }} (Nim ${{ matrix.nim.ref }})' runs-on: ${{ matrix.platform.runner }} steps: - name: Checkout @@ -46,7 +54,12 @@ jobs: os: ${{ matrix.platform.os }} cpu: ${{ matrix.platform.cpu }} shell: ${{ matrix.platform.shell }} - nim_branch: ${{ matrix.nim.branch }} + nim_ref: ${{ matrix.nim.ref }} + + - name: Install deps (windows) + if : ${{ matrix.platform.os == 'windows'}} + run: | + pacman -S --noconfirm base-devel gcc mingw-w64-x86_64-openssl - name: Install dependencies run: nimble install -y diff --git a/.gitignore b/.gitignore index becd7f9..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +0,0 @@ -* -!*/ -!*.* diff --git a/quic.nimble b/quic.nimble index 6896077..916a387 100644 --- a/quic.nimble +++ b/quic.nimble @@ -8,6 +8,6 @@ requires "nim >= 1.6.0" requires "stew#head" requires "chronos >= 4.0.3 & < 5.0.0" requires "nimcrypto >= 0.6.0 & < 0.7.0" -requires "ngtcp2#6834f4756b6af58356ac9c4fef3d71db3c3ae5fe" +requires "ngtcp2 >= 0.35.0" requires "unittest2" requires "chronicles >= 0.10.2" diff --git a/quic/api.nim b/quic/api.nim index 9a03cf0..f81f003 100644 --- a/quic/api.nim +++ b/quic/api.nim @@ -1,10 +1,13 @@ -import pkg/chronos +import chronos +import results import ./listener import ./connection import ./udp/datagram import ./errors +import ./transport/tlsbackend export Listener +export accept export Connection export Stream export openStream @@ -18,20 +21,76 @@ export drop export close export waitClosed export errors +export destroy +export CertificateVerifier +export certificateVerifierCB +export CustomCertificateVerifier +export InsecureCertificateVerifier +export init -proc listen*(address: TransportAddress): Listener = - newListener(address) +type TLSConfig* = object + certificate: seq[byte] + key: seq[byte] + certificateVerifier: Opt[CertificateVerifier] -proc accept*(listener: Listener): Future[Connection] {.async.} = - result = await listener.waitForIncoming() +type Quic = ref object of RootObj + tlsConfig: TLSConfig -proc dial*(address: TransportAddress): Future[Connection] {.async.} = +type QuicClient* = object of Quic + +type QuicServer* = object of Quic + +proc init*( + t: typedesc[TLSConfig], + certificate: seq[byte] = @[], + key: seq[byte] = @[], + certificateVerifier: Opt[CertificateVerifier] = Opt.none(CertificateVerifier), +): TLSConfig {.gcsafe, raises: [QuicConfigError].} = + # In a config, certificate and keys are optional, but if using them, both must + # be specified at the same time + if certificate.len != 0 or key.len != 0: + if certificate.len == 0: + raise newException(QuicConfigError, "certificate is required in TLSConfig") + + if key.len == 0: + raise newException(QuicConfigError, "key is required in TLSConfig") + + return TLSConfig( + certificate: certificate, key: key, certificateVerifier: certificateVerifier + ) + +proc init*( + t: typedesc[QuicServer], tlsConfig: TLSConfig +): QuicServer {.raises: [QuicConfigError].} = + if tlsConfig.certificate.len == 0: + raise newException(QuicConfigError, "tlsConfig does not contain a certificate") + + return QuicServer(tlsConfig: tlsConfig) + +proc init*(t: typedesc[QuicClient], tlsConfig: TLSConfig): QuicClient {.raises: [].} = + return QuicClient(tlsConfig: tlsConfig) + +proc listen*( + self: QuicServer, address: TransportAddress +): Listener {.raises: [QuicError, TransportOsError].} = + let tlsBackend = newServerTLSBackend( + self.tlsConfig.certificate, self.tlsConfig.key, self.tlsConfig.certificateVerifier + ) + + return newListener(tlsBackend, address) + +proc dial*( + self: QuicClient, address: TransportAddress +): Future[Connection] {.async: (raises: [QuicError, TransportOsError]).} = + let tlsBackend = newClientTLSBackend( + self.tlsConfig.certificate, self.tlsConfig.key, self.tlsConfig.certificateVerifier + ) var connection: Connection proc onReceive(udp: DatagramTransport, remote: TransportAddress) {.async.} = let datagram = Datagram(data: udp.getMessage()) connection.receive(datagram) let udp = newDatagramTransport(onReceive) - connection = newOutgoingConnection(udp, address) + connection = newOutgoingConnection(tlsBackend, udp, address) connection.startHandshake() - result = connection + return connection diff --git a/quic/basics.nim b/quic/basics.nim index 64c04fc..f288c81 100644 --- a/quic/basics.nim +++ b/quic/basics.nim @@ -1,5 +1,5 @@ import pkg/chronos -import pkg/stew/results +import results import ./udp/datagram import ./errors diff --git a/quic/connection.nim b/quic/connection.nim index 7a6d62a..d4a7dff 100644 --- a/quic/connection.nim +++ b/quic/connection.nim @@ -5,6 +5,7 @@ import ./transport/connectionid import ./transport/stream import ./transport/quicconnection import ./transport/quicclientserver +import ./transport/tlsbackend import ./helpers/asyncloop export Stream, close, read, write @@ -22,7 +23,9 @@ type closed: AsyncEvent IncomingConnection = ref object of Connection + OutgoingConnection = ref object of Connection + tlsBackend: Opt[TLSBackend] proc ids*(connection: Connection): seq[ConnectionId] = connection.quic.ids @@ -43,9 +46,19 @@ proc drop*(connection: Connection) {.async.} = proc close*(connection: Connection) {.async.} = await connection.quic.close() + if connection is OutgoingConnection: + let outConn = OutgoingConnection(connection) + if outConn.tlsBackend.isSome: + outConn.tlsBackend.get().destroy() + outConn.tlsBackend = Opt.none(TLSBackend) proc waitClosed*(connection: Connection) {.async.} = await connection.closed.wait() + if connection is OutgoingConnection: + let outConn = OutgoingConnection(connection) + if outConn.tlsBackend.isSome: + outConn.tlsBackend.get().destroy() + outConn.tlsBackend = Opt.none(TLSBackend) proc startSending(connection: Connection, remote: TransportAddress) = trace "Starting sending loop" @@ -53,9 +66,9 @@ proc startSending(connection: Connection, remote: TransportAddress) = try: trace "Getting datagram" let datagram = await connection.quic.outgoing.get() - trace "Sending datagraom" + trace "Sending datagram" await connection.udp.sendTo(remote, datagram.data) - trace "Sent datagraom" + trace "Sent datagram" except TransportError as e: trace "Failed to send datagram", errorMsg = e.msg trace "Failing connection loop future with error" @@ -94,10 +107,10 @@ proc disconnect(connection: Connection) {.async.} = trace "Fired closed event" proc newIncomingConnection*( - udp: DatagramTransport, remote: TransportAddress + tlsBackend: TLSBackend, udp: DatagramTransport, remote: TransportAddress ): Connection = let datagram = Datagram(data: udp.getMessage()) - let quic = newQuicServerConnection(udp.localAddress, remote, datagram) + let quic = newQuicServerConnection(tlsBackend, udp.localAddress, remote, datagram) let closed = newAsyncEvent() let connection = IncomingConnection(udp: udp, quic: quic, closed: closed) proc onDisconnect() {.async.} = @@ -111,11 +124,13 @@ proc newIncomingConnection*( connection proc newOutgoingConnection*( - udp: DatagramTransport, remote: TransportAddress + tlsBackend: TLSBackend, udp: DatagramTransport, remote: TransportAddress ): Connection = - let quic = newQuicClientConnection(udp.localAddress, remote) + let quic = newQuicClientConnection(tlsBackend, udp.localAddress, remote) let closed = newAsyncEvent() - let connection = OutgoingConnection(udp: udp, quic: quic, closed: closed) + let connection = OutgoingConnection( + udp: udp, quic: quic, closed: closed, tlsBackend: Opt.some(tlsBackend) + ) proc onDisconnect() {.async.} = trace "Calling onDisconnect for newOutgoingConnection" await connection.disconnect() @@ -126,7 +141,7 @@ proc newOutgoingConnection*( connection.startSending(remote) connection -proc startHandshake*(connection: Connection) = +proc startHandshake*(connection: Connection) {.gcsafe.} = connection.quic.send() proc receive*(connection: Connection, datagram: Datagram) = diff --git a/quic/errors.nim b/quic/errors.nim index ba583b9..94da4e1 100644 --- a/quic/errors.nim +++ b/quic/errors.nim @@ -1,6 +1,7 @@ type QuicError* = object of IOError QuicDefect* = object of Defect + QuicConfigError* = object of CatchableError template errorAsDefect*(body): untyped = try: diff --git a/quic/listener.nim b/quic/listener.nim index e20b90a..8402e9f 100644 --- a/quic/listener.nim +++ b/quic/listener.nim @@ -3,8 +3,10 @@ import ./basics import ./connection import ./transport/connectionid import ./transport/parsedatagram +import ./transport/tlsbackend type Listener* = ref object + tlsBackend: TLSBackend udp: DatagramTransport incoming: AsyncQueue[Connection] connections: Table[ConnectionId, Connection] @@ -42,23 +44,30 @@ proc getOrCreateConnection*( var connection: Connection let destination = parseDatagram(udp.getMessage()).destination if not listener.hasConnection(destination): - connection = newIncomingConnection(udp, remote) + connection = newIncomingConnection(listener.tlsBackend, udp, remote) listener.addConnection(connection, destination) else: connection = listener.getConnection(destination) connection -proc newListener*(address: TransportAddress): Listener = +proc newListener*(tlsBackend: TLSBackend, address: TransportAddress): Listener = let listener = Listener(incoming: newAsyncQueue[Connection]()) proc onReceive(udp: DatagramTransport, remote: TransportAddress) {.async.} = let connection = listener.getOrCreateConnection(udp, remote) connection.receive(Datagram(data: udp.getMessage())) + listener.tlsBackend = tlsBackend listener.udp = newDatagramTransport(onReceive, local = address) listener proc waitForIncoming*(listener: Listener): Future[Connection] {.async.} = - result = await listener.incoming.get() + await listener.incoming.get() + +proc accept*(listener: Listener): Future[Connection] {.async.} = + result = await listener.waitForIncoming() proc stop*(listener: Listener) {.async.} = await listener.udp.closeWait() + +proc destroy*(listener: Listener) = + listener.tlsBackend.destroy() diff --git a/quic/transport/ngtcp2/connection/closedstate.nim b/quic/transport/ngtcp2/connection/closedstate.nim index 0eb0509..fdf9b69 100644 --- a/quic/transport/ngtcp2/connection/closedstate.nim +++ b/quic/transport/ngtcp2/connection/closedstate.nim @@ -15,8 +15,6 @@ type proc newClosedConnection*(): ClosedConnection = ClosedConnection() -{.push locks: "unknown".} - method ids(state: ClosedConnection): seq[ConnectionId] = @[] @@ -38,5 +36,3 @@ method drop(state: ClosedConnection) {.async.} = trace "Dropping ClosedConnection state" discard trace "Dropped ClosedConnection state" - -{.pop.} diff --git a/quic/transport/ngtcp2/connection/closingstate.nim b/quic/transport/ngtcp2/connection/closingstate.nim index 0e6a48c..bdb968a 100644 --- a/quic/transport/ngtcp2/connection/closingstate.nim +++ b/quic/transport/ngtcp2/connection/closingstate.nim @@ -21,13 +21,9 @@ proc sendFinalDatagram(state: ClosingConnection) = except AsyncQueueFullError: raise newException(QuicError, "Outgoing queue is full") -{.push locks: "unknown".} - method enter(state: ClosingConnection, connection: QuicConnection) = procCall enter(DrainingConnection(state), connection) state.sendFinalDatagram() method receive(state: ClosingConnection, datagram: Datagram) = state.sendFinalDatagram() - -{.pop.} diff --git a/quic/transport/ngtcp2/connection/disconnectingstate.nim b/quic/transport/ngtcp2/connection/disconnectingstate.nim index 1ea92bd..e699418 100644 --- a/quic/transport/ngtcp2/connection/disconnectingstate.nim +++ b/quic/transport/ngtcp2/connection/disconnectingstate.nim @@ -24,8 +24,6 @@ proc callDisconnect(connection: QuicConnection) {.async.} = await disconnect() trace "Called disconnect proc on QuicConnection" -{.push locks: "unknown".} - method ids*(state: DisconnectingConnection): seq[ConnectionId] = state.ids @@ -68,5 +66,3 @@ method drop(state: DisconnectingConnection) {.async.} = return connection.switch(newClosedConnection()) trace "dropped DisconnectingConnection state" - -{.pop.} diff --git a/quic/transport/ngtcp2/connection/openstate.nim b/quic/transport/ngtcp2/connection/openstate.nim index 42b52a0..a6fb65f 100644 --- a/quic/transport/ngtcp2/connection/openstate.nim +++ b/quic/transport/ngtcp2/connection/openstate.nim @@ -4,10 +4,12 @@ import ../../../basics import ../../quicconnection import ../../connectionid import ../../stream +import ../../tlsbackend import ../native/connection import ../native/streams import ../native/client import ../native/server +import ../native/errors import ./closingstate import ./drainingstate import ./disconnectingstate @@ -24,42 +26,59 @@ type OpenConnection* = ref object of ConnectionState proc newOpenConnection*(ngtcp2Connection: Ngtcp2Connection): OpenConnection = OpenConnection(ngtcp2Connection: ngtcp2Connection, streams: OpenStreams.new) -proc openClientConnection*(local, remote: TransportAddress): OpenConnection = - newOpenConnection(newNgtcp2Client(local, remote)) +proc openClientConnection*( + tlsBackend: TLSBackend, local, remote: TransportAddress +): OpenConnection = + let ngtcp2Conn = newNgtcp2Client(tlsBackend.picoTLS, local, remote) + newOpenConnection(ngtcp2Conn) proc openServerConnection*( - local, remote: TransportAddress, datagram: Datagram + tlsBackend: TLSBackend, local, remote: TransportAddress, datagram: Datagram ): OpenConnection = - newOpenConnection(newNgtcp2Server(local, remote, datagram.data)) + newOpenConnection(newNgtcp2Server(tlsBackend.picoTLS, local, remote, datagram.data)) {.push locks: "unknown".} +method close(state: OpenConnection) {.async.} + method enter(state: OpenConnection, connection: QuicConnection) = trace "Entering OpenConnection state" procCall enter(ConnectionState(state), connection) state.quicConnection = Opt.some(connection) # Workaround weird bug - proc onNewId(id: ConnectionId) = + var onNewId = proc(id: ConnectionId) = if isNil(connection.onNewId): return connection.onNewId(id) - state.ngtcp2Connection.onNewId = Opt.some(onNewId) - - proc onRemoveId(id: ConnectionId) = + var onRemoveId = proc(id: ConnectionId) = if isNil(connection.onRemoveId): return connection.onRemoveId(id) + state.ngtcp2Connection.onNewId = Opt.some(onNewId) state.ngtcp2Connection.onRemoveId = Opt.some(onRemoveId) + state.ngtcp2Connection.onSend = proc(datagram: Datagram) = errorAsDefect: connection.outgoing.putNoWait(datagram) + state.ngtcp2Connection.onIncomingStream = proc(stream: Stream) = state.streams.add(stream) connection.incoming.putNoWait(stream) state.ngtcp2Connection.onHandshakeDone = proc() = connection.handshake.fire() + + state.ngtcp2Connection.onTimeout = proc() {.gcsafe, raises: [].} = + try: + waitFor connection.close() + except QuicError: + # TODO: handle + discard + except CatchableError: + # TODO: handle + discard + trace "Entered OpenConnection state" method leave(state: OpenConnection) = @@ -77,15 +96,24 @@ method send(state: OpenConnection) = state.ngtcp2Connection.send() method receive(state: OpenConnection, datagram: Datagram) = - state.ngtcp2Connection.receive(datagram) - let quicConnection = state.quicConnection.valueOr: - return - if state.ngtcp2Connection.isDraining: - let duration = state.ngtcp2Connection.closingDuration() - let ids = state.ids - let draining = newDrainingConnection(ids, duration) - quicConnection.switch(draining) - asyncSpawn draining.close() + var isDraining = false + try: + state.ngtcp2Connection.receive(datagram) + except Ngtcp2Error as e: + trace "ngtcp2 error on receive", code = $e.msg + isDraining = state.ngtcp2Connection.isDraining + # TODO: + # if not isDraining: + # raise newException(QuicError, "could not receive - code:" & $e.msg) + finally: + let quicConnection = state.quicConnection.valueOr: + return + if isDraining: + let duration = state.ngtcp2Connection.closingDuration() + let ids = state.ids + let draining = newDrainingConnection(ids, duration) + quicConnection.switch(draining) + asyncSpawn draining.close() method openStream( state: OpenConnection, unidirectional: bool diff --git a/quic/transport/ngtcp2/native.nim b/quic/transport/ngtcp2/native.nim index 7cdfdab..649e502 100644 --- a/quic/transport/ngtcp2/native.nim +++ b/quic/transport/ngtcp2/native.nim @@ -4,6 +4,8 @@ import ./native/client import ./native/handshake import ./native/streams import ./native/parsedatagram +import ./native/picotls +import ./native/certificateverifier export parseDatagram export Ngtcp2Connection @@ -15,3 +17,10 @@ export handshake export ids export openStream export destroy + +export PicoTLSContext +export PicoTLSConnection +export init +export destroy +export newConnection +export certificateverifier \ No newline at end of file diff --git a/quic/transport/ngtcp2/native/certificateverifier.nim b/quic/transport/ngtcp2/native/certificateverifier.nim new file mode 100644 index 0000000..a999f5f --- /dev/null +++ b/quic/transport/ngtcp2/native/certificateverifier.nim @@ -0,0 +1,7 @@ +import ./certificateverifier/certificateverifier +import ./certificateverifier/custom +import ./certificateverifier/insecure + +export certificateverifier +export custom +export insecure diff --git a/quic/transport/ngtcp2/native/certificateverifier/certificateverifier.nim b/quic/transport/ngtcp2/native/certificateverifier/certificateverifier.nim new file mode 100644 index 0000000..b4c3e86 --- /dev/null +++ b/quic/transport/ngtcp2/native/certificateverifier/certificateverifier.nim @@ -0,0 +1,11 @@ +import ngtcp2 + +type CertificateVerifier* = ref object of RootObj + +method destroy*(t: CertificateVerifier) {.base, gcsafe.} = + doAssert false, "override this method" + +method getPtlsVerifyCertificateT*( + t: CertificateVerifier +): ptr ptls_verify_certificate_t {.base, gcsafe.} = + doAssert false, "override this method" diff --git a/quic/transport/ngtcp2/native/certificateverifier/custom.nim b/quic/transport/ngtcp2/native/certificateverifier/custom.nim new file mode 100644 index 0000000..8d15b11 --- /dev/null +++ b/quic/transport/ngtcp2/native/certificateverifier/custom.nim @@ -0,0 +1,70 @@ +import ngtcp2 +import sequtils +import ./certificateverifier +import ../pointers +import ../../../../helpers/openarray + +type + certificateVerifierCB* = + proc(derCertificates: seq[seq[byte]]): bool {.gcsafe, noSideEffect.} + + customPTLSVerifyCertificateT = object of ptls_verify_certificate_t + customCertVerifier: certificateVerifierCB + + CustomCertificateVerifier* = ref object of CertificateVerifier + verifier: ptr customPTLSVerifyCertificateT + +proc validateCertificate( + self: ptr ptls_verify_certificate_t, + tls: ptr ptls_t, + server_name: cstring, + verify_sign: proc( + verify_ctx: pointer, algo: uint16, data: ptls_iovec_t, sign: ptls_iovec_t + ): cint {.cdecl.}, + verify_data: ptr pointer, + certs: ptr ptls_iovec_t, + num_certs: csize_t, +): cint {.cdecl.} = + let certVerifier = cast[ptr customPTLSVerifyCertificateT](self) + if certVerifier.customCertVerifier.isNil: + doAssert false, "custom cert verifier was not setup" + + var derCertificates = newSeq[seq[byte]](num_certs) + for i in 0 ..< int(num_certs): + let cert = certs + i + derCertificates[i] = toSeq(toOpenArray(cert.base, cert.len)) + + if certVerifier.customCertVerifier(derCertificates): + return 0 + else: + return PTLS_ALERT_BAD_CERTIFICATE + +proc init*( + t: typedesc[CustomCertificateVerifier], certVerifierCB: certificateVerifierCB +): CustomCertificateVerifier {.gcsafe.} = + let response = CustomCertificateVerifier() + response.verifier = create(customPTLSVerifyCertificateT) + var algos = cast[ptr UncheckedArray[uint16]](alloc(uint16.sizeof * 5)) + algos[0] = PTLS_SIGNATURE_RSA_PSS_RSAE_SHA256 + algos[1] = PTLS_SIGNATURE_ECDSA_SECP256R1_SHA256 + algos[2] = PTLS_SIGNATURE_RSA_PKCS1_SHA256 + algos[3] = PTLS_SIGNATURE_RSA_PKCS1_SHA1 + algos[4] = high(uint16) + response.verifier.cb = validateCertificate + response.verifier.algos = cast[ptr uint16](algos) + response.verifier.customCertVerifier = certVerifierCB + return response + +method destroy*(t: CustomCertificateVerifier) {.gcsafe.} = + if t.verifier.isNil: + return + + let algosPtr = cast[pointer](t.verifier.algos) + dealloc(algosPtr) + dealloc(t.verifier) + t.verifier = nil + +method getPtlsVerifyCertificateT*( + t: CustomCertificateVerifier +): ptr ptls_verify_certificate_t = + return t.verifier diff --git a/quic/transport/ngtcp2/native/certificateverifier/insecure.nim b/quic/transport/ngtcp2/native/certificateverifier/insecure.nim new file mode 100644 index 0000000..53befeb --- /dev/null +++ b/quic/transport/ngtcp2/native/certificateverifier/insecure.nim @@ -0,0 +1,17 @@ +import ngtcp2 +import ./certificateverifier + +type InsecureCertificateVerifier* = ref object of CertificateVerifier + +proc init*(t: typedesc[InsecureCertificateVerifier]): InsecureCertificateVerifier {.gcsafe.} = + return InsecureCertificateVerifier() + +method destroy*(t: InsecureCertificateVerifier) {.gcsafe.} = + discard + +method getPtlsVerifyCertificateT*( + t: InsecureCertificateVerifier +): ptr ptls_verify_certificate_t = + # picotls will check against null to determine whether a certificate verifier + # was setup or not + return nil diff --git a/quic/transport/ngtcp2/native/client.nim b/quic/transport/ngtcp2/native/client.nim index af71a35..3a296a9 100644 --- a/quic/transport/ngtcp2/native/client.nim +++ b/quic/transport/ngtcp2/native/client.nim @@ -1,75 +1,31 @@ -import pkg/ngtcp2 -import pkg/nimcrypto +import ngtcp2 +import ../../../errors import ../../version import ../../../basics -import ../../../helpers/openarray import ../../connectionid import ./ids import ./encryption -import ./keys import ./settings -import ./cryptodata import ./connection import ./path +import ./picotls +import ./rand import ./streams import ./timestamp import ./handshake -proc onClientInitial(connection: ptr ngtcp2_conn, user_data: pointer): cint {.cdecl.} = - connection.install0RttKey() - connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_INITIAL) - -proc onReceiveCryptoData( - connection: ptr ngtcp2_conn, - level: ngtcp2_encryption_level, - offset: uint64, - data: ptr uint8, - datalen: uint, - userData: pointer, -): cint {.cdecl.} = - if level == NGTCP2_ENCRYPTION_LEVEL_INITIAL: - connection.installHandshakeKeys() - if level == NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: - connection.handleCryptoData(toOpenArray(data, datalen)) - connection.install1RttKeys() - connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE) - ngtcp2_conn_tls_handshake_completed(connection) - -proc onReceiveRetry( - connection: ptr ngtcp2_conn, hd: ptr ngtcp2_pkt_hd, userData: pointer -): cint {.cdecl.} = - return 0 - -proc onRand(dest: ptr uint8, destLen: uint, rand_ctx: ptr ngtcp2_rand_ctx) {.cdecl.} = - doAssert destLen.int == randomBytes(dest, destLen.int) - -proc onDeleteCryptoAeadCtx( - conn: ptr ngtcp2_conn, aead_ctx: ptr ngtcp2_crypto_aead_ctx, userData: pointer -) {.cdecl.} = - discard - -proc onDeleteCryptoCipherCtx( - conn: ptr ngtcp2_conn, cipher_ctx: ptr ngtcp2_crypto_cipher_ctx, userData: pointer -) {.cdecl.} = - discard - -proc onGetPathChallengeData( - conn: ptr ngtcp2_conn, data: ptr uint8, userData: pointer -): cint {.cdecl.} = - let bytesWritten = randomBytes(data, NGTCP2_PATH_CHALLENGE_DATALEN) - if bytesWritten != NGTCP2_PATH_CHALLENGE_DATALEN: - return NGTCP2_ERR_CALLBACK_FAILURE - return 0 - -proc newNgtcp2Client*(local, remote: TransportAddress): Ngtcp2Connection = +proc newNgtcp2Client*( + tlsContext: PicoTLSContext, local, remote: TransportAddress +): Ngtcp2Connection = var callbacks: ngtcp2_callbacks - callbacks.client_initial = onClientInitial - callbacks.recv_crypto_data = onReceiveCryptoData - callbacks.recv_retry = onReceiveRetry + callbacks.client_initial = ngtcp2_crypto_client_initial_cb + callbacks.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb + callbacks.recv_retry = ngtcp2_crypto_recv_retry_cb + callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb + callbacks.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb + callbacks.get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb + callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb callbacks.rand = onRand - callbacks.delete_crypto_aead_ctx = onDeleteCryptoAeadCtx - callbacks.delete_crypto_cipher_ctx = onDeleteCryptoCipherCtx - callbacks.get_path_challenge_data = onGetPathChallengeData installConnectionIdCallback(callbacks) installEncryptionCallbacks(callbacks) @@ -83,24 +39,61 @@ proc newNgtcp2Client*(local, remote: TransportAddress): Ngtcp2Connection = let destination = randomConnectionId().toCid let path = newPath(local, remote) - result = newConnection(path) + let nConn = newConnection(path) + var conn: ptr ngtcp2_conn + var ret = ngtcp2_conn_client_new_versioned( + addr conn, + unsafeAddr destination, + unsafeAddr source, + path.toPathPtr, + CurrentQuicVersion, + NGTCP2_CALLBACKS_V1, + addr callbacks, + NGTCP2_SETTINGS_V2, + unsafeAddr settings, + NGTCP2_TRANSPORT_PARAMS_V1, + unsafeAddr transportParams, + nil, + addr nConn[], + ) + if ret != 0: + raise newException(QuicError, "could not create new client versioned conn: " & $ret) - doAssert 0 == - ngtcp2_conn_client_new_versioned( - addr conn, - unsafeAddr destination, - unsafeAddr source, - path.toPathPtr, - CurrentQuicVersion, - NGTCP2_CALLBACKS_V1, - addr callbacks, - NGTCP2_SETTINGS_V2, - unsafeAddr settings, - NGTCP2_TRANSPORT_PARAMS_V1, - unsafeAddr transportParams, - nil, - addr result[], - ) + let cptls: ptr ngtcp2_crypto_picotls_ctx = create(ngtcp2_crypto_picotls_ctx) - result.conn = Opt.some(conn) + ngtcp2_crypto_picotls_ctx_init(cptls) + + var tls = tlsContext.newConnection(false) + cptls.ptls = tls.conn + + var addExtensions = cast[ptr UncheckedArray[ptls_raw_extension_t]](alloc( + ptls_raw_extension_t.sizeof * 2 + )) + addExtensions[0] = ptls_raw_extension_t(type_field: high(uint16)) + addExtensions[1] = ptls_raw_extension_t(type_field: high(uint16)) + cptls.handshake_properties = ptls_handshake_properties_t( + additional_extensions: cast[ptr ptls_raw_extension_t](addExtensions) + ) + + ngtcp2_conn_set_tls_native_handle(conn, cptls) + + var connref = create(ngtcp2_crypto_conn_ref) + connref.user_data = conn + connref.get_conn = proc( + connRef: ptr ngtcp2_crypto_conn_ref + ): ptr ngtcp2_conn {.cdecl.} = + cast[ptr ngtcp2_conn](connRef.user_data) + + var dataPtr = ptls_get_data_ptr(tls.conn) + dataPtr[] = connref + + ret = ngtcp2_crypto_picotls_configure_client_session(cptls, conn) + if ret != 0: + raise newException(QuicError, "could not configure client session: " & $ret) + + nConn.conn = Opt.some(conn) + nConn.tlsConn = tls + nConn.cptls = cptls + nConn.connref = connref + nConn diff --git a/quic/transport/ngtcp2/native/connection.nim b/quic/transport/ngtcp2/native/connection.nim index e3221ea..a23f66d 100644 --- a/quic/transport/ngtcp2/native/connection.nim +++ b/quic/transport/ngtcp2/native/connection.nim @@ -1,5 +1,6 @@ import std/sequtils -import pkg/ngtcp2 +import ngtcp2 +import chronicles import ../../../basics import ../../../udp/congestion import ../../../helpers/openarray @@ -7,18 +8,27 @@ import ../../stream import ../../timeout import ../../connectionid import ./path +import ./picotls import ./errors as ngtcp2errors import ./timestamp import ./pointers +logScope: + topics = "ngtcp2 conn" + type Ngtcp2Connection* = ref object conn*: Opt[ptr ngtcp2_conn] + tlsConn*: PicoTLSConnection + cptls*: ptr ngtcp2_crypto_picotls_ctx + connref*: ptr ngtcp2_crypto_conn_ref + path*: Path buffer*: array[4096, byte] flowing*: AsyncEvent timeout*: Timeout onSend*: proc(datagram: Datagram) {.gcsafe, raises: [].} + onTimeout*: proc() {.raises: [].} onIncomingStream*: proc(stream: Stream) onHandshakeDone*: proc() onNewId*: Opt[proc(id: ConnectionId)] @@ -31,6 +41,14 @@ proc destroy*(connection: Ngtcp2Connection) = return connection.timeout.stop() ngtcp2_conn_del(conn) + ngtcp2_crypto_picotls_deconfigure_session(connection.cptls) + connection.tlsConn.destroy() + dealloc(connection.cptls.handshake_properties.additional_extensions) + dealloc(connection.connref) + dealloc(connection.cptls) + connection.cptls = nil + connection.connref = nil + connection.tlsConn = nil connection.conn = Opt.none(ptr ngtcp2_conn) connection.onSend = nil connection.onIncomingStream = nil @@ -40,6 +58,8 @@ proc destroy*(connection: Ngtcp2Connection) = proc handleTimeout(connection: Ngtcp2Connection) {.gcsafe, raises: [].} +proc executeOnTimeout(connection: Ngtcp2Connection) {.async.} + proc newConnection*(path: Path): Ngtcp2Connection = let connection = Ngtcp2Connection() connection.path = path @@ -49,6 +69,9 @@ proc newConnection*(path: Path): Ngtcp2Connection = connection.handleTimeout() ) connection.flowing.fire() + + asyncSpawn connection.executeOnTimeout() + connection proc ids*(connection: Ngtcp2Connection): seq[ConnectionId] = @@ -63,7 +86,7 @@ proc ids*(connection: Ngtcp2Connection): seq[ConnectionId] = proc updateTimeout*(connection: Ngtcp2Connection) = let conn = connection.conn.valueOr: raise newException(Ngtcp2ConnectionClosed, "connection no longer exists") - + trace "updateTimeout" let expiry = ngtcp2_conn_get_expiry(conn) if expiry != uint64.high: connection.timeout.set(Moment.init(expiry.int64, 1.nanoseconds)) @@ -88,8 +111,8 @@ proc trySend( addr packetInfo, addr connection.buffer[0], connection.buffer.len.uint, - written, - 0, + cast[ptr ngtcp2_ssize](written), + NGTCP2_WRITE_STREAM_FLAG_NONE, streamId, messagePtr, messageLen, @@ -116,6 +139,7 @@ proc send( messagePtr: ptr byte, messageLen: uint, ): Future[int] {.async.} = + let written = addr result var datagram = trySend(connection, streamId, messagePtr, messageLen, written) while datagram.data.len == 0: @@ -154,10 +178,7 @@ proc tryReceive(connection: Ngtcp2Connection, datagram: openArray[byte], ecn: EC proc receive*( connection: Ngtcp2Connection, datagram: openArray[byte], ecn = ecnNonCapable ) = - try: - connection.tryReceive(datagram, ecn) - except Ngtcp2Error: - return + connection.tryReceive(datagram, ecn) connection.send() connection.flowing.fire() @@ -169,13 +190,20 @@ proc handleTimeout(connection: Ngtcp2Connection) = return errorAsDefect: - checkResult ngtcp2_conn_handle_expiry(conn, now()) + let ret = ngtcp2_conn_handle_expiry(conn, now()) + trace "handleExpiry", ret + checkResult ret connection.send() proc close*(connection: Ngtcp2Connection): Datagram = let conn = connection.conn.valueOr: raise newException(Ngtcp2ConnectionClosed, "connection no longer exists") + if ( + ngtcp2_conn_in_closing_period(conn) == 1 or ngtcp2_conn_in_draining_period(conn) == 1 + ): + return + var ccerr: ngtcp2_ccerr ngtcp2_ccerr_default(addr ccerr) @@ -195,6 +223,14 @@ proc close*(connection: Ngtcp2Connection): Datagram = let ecn = ECN(packetInfo.ecn) Datagram(data: data, ecn: ecn) + # TODO: should stop all event loops + +proc executeOnTimeout(connection: Ngtcp2Connection) {.async.} = + trace "Waiting expiration" + await connection.timeout.expired() + trace "Timeout expired" + #TODO should we call connection.onTimeout() + proc closingDuration*(connection: Ngtcp2Connection): Duration = let conn = connection.conn.valueOr: raise newException(Ngtcp2ConnectionClosed, "connection no longer exists") diff --git a/quic/transport/ngtcp2/native/cryptodata.nim b/quic/transport/ngtcp2/native/cryptodata.nim deleted file mode 100644 index 0266897..0000000 --- a/quic/transport/ngtcp2/native/cryptodata.nim +++ /dev/null @@ -1,13 +0,0 @@ -import pkg/ngtcp2 -import ./params - -proc submitCryptoData*(connection: ptr ngtcp2_conn, level: ngtcp2_encryption_level) = - var cryptoData = connection.encodeTransportParameters() - doAssert 0 == - ngtcp2_conn_submit_crypto_data( - connection, level, addr cryptoData[0], cryptoData.len.uint - ) - -proc handleCryptoData*(connection: ptr ngtcp2_conn, data: openArray[byte]) = - let parameters = decodeTransportParameters(data) - connection.setRemoteTransportParameters(parameters) diff --git a/quic/transport/ngtcp2/native/encryption.nim b/quic/transport/ngtcp2/native/encryption.nim index 1afa177..44ae3ff 100644 --- a/quic/transport/ngtcp2/native/encryption.nim +++ b/quic/transport/ngtcp2/native/encryption.nim @@ -1,61 +1,7 @@ -import pkg/ngtcp2 -import ./pointers - -const aeadlen* = 16 - -proc dummyEncrypt( - dest: ptr uint8, - aead: ptr ngtcp2_crypto_aead, - aead_ctx: ptr ngtcp2_crypto_aead_ctx, - plaintext: ptr uint8, - plaintextlen: uint, - nonce: ptr uint8, - noncelen: uint, - ad: ptr uint8, - adlen: uint, -): cint {.cdecl.} = - moveMem(dest, plaintext, plaintextlen) - zeroMem(dest + plaintextlen.int, aeadlen) - -proc dummyDecrypt( - dest: ptr uint8, - aead: ptr ngtcp2_crypto_aead, - aead_ctx: ptr ngtcp2_crypto_aead_ctx, - ciphertext: ptr uint8, - ciphertextlen: uint, - nonce: ptr uint8, - noncelen: uint, - ad: ptr uint8, - adlen: uint, -): cint {.cdecl.} = - moveMem(dest, ciphertext, ciphertextlen - aeadlen) - -proc dummyHpMask( - dest: ptr uint8, - hp: ptr ngtcp2_crypto_cipher, - hpContext: ptr ngtcp2_crypto_cipher_ctx, - sample: ptr uint8, -): cint {.cdecl.} = - var NGTCP2_FAKE_HP_MASK = "\x00\x00\x00\x00\x00" - copyMem(dest, addr NGTCP2_FAKE_HP_MASK[0], NGTCP2_FAKE_HP_MASK.len) - -proc dummyUpdateKey( - conn: ptr ngtcp2_conn, - rx_secret: ptr uint8, - tx_secret: ptr uint8, - rx_aead_ctx: ptr ngtcp2_crypto_aead_ctx, - rx_iv: ptr uint8, - tx_aead_ctx: ptr ngtcp2_crypto_aead_ctx, - tx_iv: ptr uint8, - current_rx_secret: ptr uint8, - current_tx_secret: ptr uint8, - secretlen: uint, - user_data: pointer, -): cint {.cdecl.} = - discard +import ngtcp2 proc installEncryptionCallbacks*(callbacks: var ngtcp2_callbacks) = - callbacks.encrypt = dummyEncrypt - callbacks.decrypt = dummyDecrypt - callbacks.hp_mask = dummyHpMask - callbacks.update_key = dummyUpdateKey + callbacks.encrypt = ngtcp2_crypto_encrypt_cb + callbacks.decrypt = ngtcp2_crypto_decrypt_cb + callbacks.hp_mask = ngtcp2_crypto_hp_mask_cb + callbacks.update_key = ngtcp2_crypto_update_key_cb diff --git a/quic/transport/ngtcp2/native/errors.nim b/quic/transport/ngtcp2/native/errors.nim index 5d25c7e..aa3d16b 100644 --- a/quic/transport/ngtcp2/native/errors.nim +++ b/quic/transport/ngtcp2/native/errors.nim @@ -1,4 +1,4 @@ -import pkg/ngtcp2 +import ngtcp2 import ../../../errors type diff --git a/quic/transport/ngtcp2/native/handshake.nim b/quic/transport/ngtcp2/native/handshake.nim index 46f8130..3fa9a2b 100644 --- a/quic/transport/ngtcp2/native/handshake.nim +++ b/quic/transport/ngtcp2/native/handshake.nim @@ -1,4 +1,4 @@ -import pkg/ngtcp2 +import ngtcp2 import ./connection proc onHandshakeDone(connection: ptr ngtcp2_conn, userData: pointer): cint {.cdecl.} = diff --git a/quic/transport/ngtcp2/native/ids.nim b/quic/transport/ngtcp2/native/ids.nim index cab85b5..153ef58 100644 --- a/quic/transport/ngtcp2/native/ids.nim +++ b/quic/transport/ngtcp2/native/ids.nim @@ -1,4 +1,4 @@ -import pkg/ngtcp2 +import ngtcp2 import ../../../basics import ../../../helpers/openarray import ../../connectionid @@ -18,13 +18,19 @@ proc getNewConnectionId( conn: ptr ngtcp2_conn, id: ptr ngtcp2_cid, token: ptr uint8, - cidlen: uint, + cidlen: csize_t, userData: pointer, ): cint {.cdecl.} = let newId = randomConnectionId(cidlen.int) id[] = newId.toCid zeroMem(token, NGTCP2_STATELESS_RESET_TOKENLEN) + # TODO: should ngtcp2_crypto_generate_stateless_reset_token so + # we can signal the other peer that the connection is no longer valid? + # ngtcp2_crypto_generate_stateless_reset_token( + # token, some_static_secret_data, config.static_secret.size(), cid) != + # 0) + let connection = cast[Ngtcp2Connection](userData) onNewId = connection.onNewId.valueOr: @@ -44,3 +50,4 @@ proc removeConnectionId( proc installConnectionIdCallback*(callbacks: var ngtcp2_callbacks) = callbacks.get_new_connection_id = getNewConnectionId callbacks.remove_connection_id = removeConnectionId + diff --git a/quic/transport/ngtcp2/native/keys.nim b/quic/transport/ngtcp2/native/keys.nim deleted file mode 100644 index ce9b382..0000000 --- a/quic/transport/ngtcp2/native/keys.nim +++ /dev/null @@ -1,70 +0,0 @@ -import pkg/ngtcp2 -import ../../../helpers/openarray -import ./encryption - -type - Secret = seq[byte] - AuthenticatedEncryptionWithAssociatedData = object - context: ngtcp2_crypto_aead_ctx - - HeaderProtection = object - context: ngtcp2_crypto_cipher_ctx - - Key = object - aead: AuthenticatedEncryptionWithAssociatedData - hp: HeaderProtection - iv: seq[byte] - - CryptoContext = ngtcp2_crypto_ctx - -proc dummyCryptoContext(): CryptoContext = - var ctx = CryptoContext() - ctx.max_encryption = 1000 - ctx.aead.max_overhead = aeadlen - return ctx - -proc dummyKey(): Key = - Key(iv: cast[seq[byte]]("dummykey")) - -proc dummySecret(): Secret = - cast[seq[byte]]("dummysecret") - -proc install0RttKey*(connection: ptr ngtcp2_conn) = - let context = dummyCryptoContext() - connection.ngtcp2_conn_set_initial_crypto_ctx(unsafeAddr context) - let key = dummyKey() - doAssert 0 == - connection.ngtcp2_conn_install_initial_key( - key.aead.context.unsafeAddr, key.iv.toUnsafePtr, key.hp.context.unsafeAddr, - key.aead.context.unsafeAddr, key.iv.toUnsafePtr, key.hp.context.unsafeAddr, - key.iv.len.uint, - ) - -proc installHandshakeKeys*(connection: ptr ngtcp2_conn) = - let context = dummyCryptoContext() - connection.ngtcp2_conn_set_crypto_ctx(unsafeAddr context) - let rx, tx = dummyKey() - doAssert 0 == - ngtcp2_conn_install_rx_handshake_key( - connection, rx.aead.context.unsafeAddr, rx.iv.toUnsafePtr, rx.iv.len.uint, - rx.hp.context.unsafeAddr, - ) - doAssert 0 == - ngtcp2_conn_install_tx_handshake_key( - connection, tx.aead.context.unsafeAddr, tx.iv.toUnsafePtr, tx.iv.len.uint, - tx.hp.context.unsafeAddr, - ) - -proc install1RttKeys*(connection: ptr ngtcp2_conn) = - let secret = dummySecret() - let rx, tx = dummyKey() - doAssert 0 == - ngtcp2_conn_install_rx_key( - connection, secret.toUnsafePtr, secret.len.uint, rx.aead.context.unsafeAddr, - rx.iv.toUnsafePtr, rx.iv.len.uint, rx.hp.context.unsafeAddr, - ) - doAssert 0 == - ngtcp2_conn_install_tx_key( - connection, secret.toUnsafePtr, secret.len.uint, tx.aead.context.unsafeAddr, - tx.iv.toUnsafePtr, tx.iv.len.uint, tx.hp.context.unsafeAddr, - ) diff --git a/quic/transport/ngtcp2/native/params.nim b/quic/transport/ngtcp2/native/params.nim index 3e21479..694ccc8 100644 --- a/quic/transport/ngtcp2/native/params.nim +++ b/quic/transport/ngtcp2/native/params.nim @@ -1,4 +1,4 @@ -import pkg/ngtcp2 +import ngtcp2 import ./errors type TransportParameters* = ngtcp2_transport_params diff --git a/quic/transport/ngtcp2/native/path.nim b/quic/transport/ngtcp2/native/path.nim index 2cf9c5a..e69aebb 100644 --- a/quic/transport/ngtcp2/native/path.nim +++ b/quic/transport/ngtcp2/native/path.nim @@ -1,25 +1,30 @@ import std/nativesockets -import pkg/ngtcp2 +import ngtcp2 import ../../../basics type Path* = ref object - storage: ngtcp2_path_storage + path: ngtcp2_path + localAddress: Sockaddr_storage + localAddrLen: SockLen + remoteAddress: Sockaddr_storage + remoteAddrLen: SockLen proc toPathPtr*(path: Path): ptr ngtcp2_path = - addr path.storage.path + addr path.path proc newPath*(local, remote: TransportAddress): Path = - var localAddress, remoteAddress: Sockaddr_storage - var localLength, remoteLength: SockLen - local.toSAddr(localAddress, localLength) - remote.toSAddr(remoteAddress, remoteLength) - var path = Path() - ngtcp2_path_storage_init( - addr path.storage, - cast[ptr SockAddr](addr localAddress), - localLength, - cast[ptr SockAddr](addr remoteAddress), - remoteLength, - nil, + var p = Path() + local.toSAddr(p.localAddress, p.localAddrLen) + remote.toSAddr(p.remoteAddress, p.remoteAddrLen) + p.path = ngtcp2_path( + local: ngtcp2_addr( + addr_field: cast[ptr ngtcp2_sockaddr](p.localAddress.addr), + addr_len: p.localAddrLen, + ), + remote: ngtcp2_addr( + addr_field: cast[ptr ngtcp2_sockaddr](p.remoteAddress.addr), + addr_len: p.remoteAddrLen, + ), + user_data: nil ) - path + p diff --git a/quic/transport/ngtcp2/native/picotls.nim b/quic/transport/ngtcp2/native/picotls.nim new file mode 100644 index 0000000..42e0279 --- /dev/null +++ b/quic/transport/ngtcp2/native/picotls.nim @@ -0,0 +1,110 @@ +import ngtcp2 +import results +import ../../../errors +import tables +import ./certificateverifier + +type + PicoTLSContext* = ref object + context*: ptr ptls_context_t + signCert: ptr ptls_openssl_sign_certificate_t + certVerifier: Opt[CertificateVerifier] + + PicoTLSConnection* = ref object + conn*: ptr ptls_t + +proc loadCertificate(ctx: ptr ptls_context_t, certificate: seq[byte]) = + var buf = create(ptls_cred_buffer_t) + defer: + dealloc(buf) + buf.off = 0 + buf.owns_base = 0 + buf.len = uint(len(certificate)) + buf.base = newString(buf.len).cstring + copyMem(buf.base[0].unsafeAddr, certificate[0].unsafeAddr, buf.len) + + let ret = ptls_load_certificates_from_memory(ctx, buf) + if ret != 0: + raise newException(QuicError, "could not load certificate: " & $ret) + +proc loadPrivateKey(signCert: ptr ptls_openssl_sign_certificate_t, key: seq[byte]) = + let ret = + ptls_openssl_init_sign_certificate_with_mem_key(signCert, key[0].unsafeAddr, key.len.cint) + if ret != 0: + raise newException(QuicError, "could not load private key: " & $ret) + +proc init*( + t: typedesc[PicoTLSContext], + certificate: seq[byte], + key: seq[byte], + certVerifier: Opt[CertificateVerifier], + requiresClientAuthentication: bool, +): PicoTLSContext = + var ctx = create(ptls_context_t) + ctx.random_bytes = ptls_openssl_random_bytes + ctx.get_time = addr ptls_get_time + ctx.key_exchanges = + cast[ptr ptr ptls_key_exchange_algorithm_t](addr ptls_openssl_key_exchanges) + ctx.cipher_suites = cast[ptr ptr ptls_cipher_suite_t](addr ptls_openssl_cipher_suites) + + if certVerifier.isSome: + if requiresClientAuthentication: + ctx.require_client_authentication = 1 + try: + ctx.verify_certificate = certVerifier.get().getPtlsVerifyCertificateT() + except: + doAssert false, "checked with if" + else: + ctx.verify_certificate = nil + + var signCert: ptr ptls_openssl_sign_certificate_t = nil + if len(key) != 0 and len(certificate) != 0: + signCert = create(ptls_openssl_sign_certificate_t) + loadPrivateKey(signCert, key) + loadCertificate(ctx, certificate) + ctx.sign_certificate = addr signCert.super + + return PicoTLSContext(context: ctx, signCert: signCert, certVerifier: certVerifier) + +proc cfree(p: pointer) {.importc: "free", header: "".} + +proc destroy*(p: PicoTLSContext) = + if p.context == nil: + return + + if not p.signCert.isNil: + ptls_openssl_dispose_sign_certificate(p.signCert) + let arr = cast[ptr UncheckedArray[ptls_iovec_t]](p.context.certificates.list) + for i in 0 ..< p.context.certificates.count:# + cfree(arr[i].base) + cfree(p.context.certificates.list) + dealloc(p.signCert) + p.context.certificates.list = nil + p.context.certificates.count = 0 + p.signCert = nil + + if p.certVerifier.isSome: + try: + p.certVerifier.get().destroy() + except: + doAssert false, "checked with if" + p.certVerifier = Opt.none(CertificateVerifier) + + dealloc(p.context) + p.context = nil + +proc newConnection*(p: PicoTLSContext, isServer: bool): PicoTLSConnection = + return PicoTLSConnection( + conn: + if isServer: + ptls_server_new(p.context) + else: + ptls_client_new(p.context) + ) + +proc destroy*(p: PicoTLSConnection) = + if p.conn == nil: + return + + ptls_free(p.conn) + p.conn = nil diff --git a/quic/transport/ngtcp2/native/pointers.nim b/quic/transport/ngtcp2/native/pointers.nim index bc049e7..f608795 100644 --- a/quic/transport/ngtcp2/native/pointers.nim +++ b/quic/transport/ngtcp2/native/pointers.nim @@ -1,2 +1,2 @@ proc `+`*[T](p: ptr T, a: int): ptr T = - cast[ptr T](cast[ByteAddress](p) + ByteAddress(a)) + cast[ptr T](cast[uint](p) + uint(a)) diff --git a/quic/transport/ngtcp2/native/rand.nim b/quic/transport/ngtcp2/native/rand.nim new file mode 100644 index 0000000..ab138e0 --- /dev/null +++ b/quic/transport/ngtcp2/native/rand.nim @@ -0,0 +1,8 @@ +import ngtcp2 +import nimcrypto + +proc onRand*( + dest: ptr uint8, destLen: csize_t, rand_ctx: ptr ngtcp2_rand_ctx +) {.cdecl.} = + # TODO: external source of randomness? + doAssert destLen.int == randomBytes(dest, destLen.int) diff --git a/quic/transport/ngtcp2/native/server.nim b/quic/transport/ngtcp2/native/server.nim index 718ebda..a9a1f91 100644 --- a/quic/transport/ngtcp2/native/server.nim +++ b/quic/transport/ngtcp2/native/server.nim @@ -1,76 +1,33 @@ import pkg/ngtcp2 -import pkg/nimcrypto - import ../../../basics -import ../../../helpers/openarray +import ../../../errors import ../../packets import ../../version import ./encryption import ./ids -import ./keys import ./settings -import ./cryptodata import ./connection import ./path +import ./picotls +import ./rand import ./streams import ./timestamp import ./handshake import ./parsedatagram -proc onReceiveClientInitial( - connection: ptr ngtcp2_conn, dcid: ptr ngtcp2_cid, userData: pointer -): cint {.cdecl.} = - connection.install0RttKey() - -proc onReceiveCryptoData( - connection: ptr ngtcp2_conn, - level: ngtcp2_encryption_level, - offset: uint64, - data: ptr uint8, - datalen: uint, - userData: pointer, -): cint {.cdecl.} = - if level == NGTCP2_ENCRYPTION_LEVEL_INITIAL: - connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_INITIAL) - connection.installHandshakeKeys() - connection.handleCryptoData(toOpenArray(data, datalen)) - connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE) - connection.install1RttKeys() - if level == NGTCP2_ENCRYPTION_LEVEL_HANDSHAKE: - connection.submitCryptoData(NGTCP2_ENCRYPTION_LEVEL_1RTT) - ngtcp2_conn_tls_handshake_completed(connection) - -proc onRand(dest: ptr uint8, destLen: uint, rand_ctx: ptr ngtcp2_rand_ctx) {.cdecl.} = - doAssert destLen.int == randomBytes(dest, destLen.int) - -proc onDeleteCryptoAeadCtx( - conn: ptr ngtcp2_conn, aead_ctx: ptr ngtcp2_crypto_aead_ctx, userData: pointer -) {.cdecl.} = - discard - -proc onDeleteCryptoCipherCtx( - conn: ptr ngtcp2_conn, cipher_ctx: ptr ngtcp2_crypto_cipher_ctx, userData: pointer -) {.cdecl.} = - discard - -proc onGetPathChallengeData( - conn: ptr ngtcp2_conn, data: ptr uint8, userData: pointer -): cint {.cdecl.} = - let bytesWritten = randomBytes(data, NGTCP2_PATH_CHALLENGE_DATALEN) - if bytesWritten != NGTCP2_PATH_CHALLENGE_DATALEN: - return NGTCP2_ERR_CALLBACK_FAILURE - return 0 - proc newNgtcp2Server*( - local, remote: TransportAddress, source, destination: ngtcp2_cid + tlsContext: PicoTLSContext, + local, remote: TransportAddress, + source, destination: ngtcp2_cid, ): Ngtcp2Connection = var callbacks: ngtcp2_callbacks - callbacks.recv_client_initial = onReceiveClientInitial - callbacks.recv_crypto_data = onReceiveCryptoData + callbacks.recv_client_initial = ngtcp2_crypto_recv_client_initial_cb + callbacks.recv_crypto_data = ngtcp2_crypto_recv_crypto_data_cb + callbacks.delete_crypto_aead_ctx = ngtcp2_crypto_delete_crypto_aead_ctx_cb + callbacks.delete_crypto_cipher_ctx = ngtcp2_crypto_delete_crypto_cipher_ctx_cb + callbacks.get_path_challenge_data = ngtcp2_crypto_get_path_challenge_data_cb + callbacks.version_negotiation = ngtcp2_crypto_version_negotiation_cb callbacks.rand = onRand - callbacks.delete_crypto_aead_ctx = onDeleteCryptoAeadCtx - callbacks.delete_crypto_cipher_ctx = onDeleteCryptoCipherCtx - callbacks.get_path_challenge_data = onGetPathChallengeData installConnectionIdCallback(callbacks) installEncryptionCallbacks(callbacks) @@ -86,34 +43,71 @@ proc newNgtcp2Server*( let id = randomConnectionId().toCid let path = newPath(local, remote) - result = newConnection(path) + let nConn = newConnection(path) + var conn: ptr ngtcp2_conn + var ret = ngtcp2_conn_server_new_versioned( + addr conn, + unsafeAddr source, + unsafeAddr id, + path.toPathPtr, + CurrentQuicVersion, + NGTCP2_CALLBACKS_V1, + addr callbacks, + NGTCP2_SETTINGS_V2, + addr settings, + NGTCP2_TRANSPORT_PARAMS_V1, + addr transportParams, + nil, + addr nConn[], + ) + if ret != 0: + raise newException(QuicError, "could not create new server versioned conn: " & $ret) - doAssert 0 == - ngtcp2_conn_server_new_versioned( - addr conn, - unsafeAddr source, - unsafeAddr id, - path.toPathPtr, - CurrentQuicVersion, - NGTCP2_CALLBACKS_V1, - addr callbacks, - NGTCP2_SETTINGS_V2, - addr settings, - NGTCP2_TRANSPORT_PARAMS_V1, - addr transportParams, - nil, - addr result[], - ) + let cptls: ptr ngtcp2_crypto_picotls_ctx = create(ngtcp2_crypto_picotls_ctx) - result.conn = Opt.some(conn) + ngtcp2_crypto_picotls_ctx_init(cptls) + + var tls = tlsContext.newConnection(true) + cptls.ptls = tls.conn + + var addExtensions = cast[ptr UncheckedArray[ptls_raw_extension_t]](alloc( + ptls_raw_extension_t.sizeof * 2 + )) + addExtensions[0] = ptls_raw_extension_t(type_field: high(uint16)) + addExtensions[1] = ptls_raw_extension_t(type_field: high(uint16)) + cptls.handshake_properties = ptls_handshake_properties_t( + additional_extensions: cast[ptr ptls_raw_extension_t](addExtensions) + ) + + ngtcp2_conn_set_tls_native_handle(conn, cptls) + + var connref = create(ngtcp2_crypto_conn_ref) + connref.user_data = conn + connref.get_conn = proc( + connRef: ptr ngtcp2_crypto_conn_ref + ): ptr ngtcp2_conn {.cdecl.} = + cast[ptr ngtcp2_conn](connRef.user_data) + + var dataPtr = ptls_get_data_ptr(tls.conn) + dataPtr[] = connref + + ret = ngtcp2_crypto_picotls_configure_server_session(cptls) + if ret != 0: + raise newException(QuicError, "could not configure server session: " & $ret) + + nConn.conn = Opt.some(conn) + nConn.tlsConn = tls + nConn.cptls = cptls + nConn.connref = connref + nConn proc extractIds(datagram: openArray[byte]): tuple[source, dest: ngtcp2_cid] = let info = parseDatagram(datagram) (source: info.source.toCid, dest: info.destination.toCid) proc newNgtcp2Server*( - local, remote: TransportAddress, datagram: openArray[byte] + tlsContext: PicoTLSContext, local, remote: TransportAddress, datagram: openArray[byte] ): Ngtcp2Connection = let (source, destination) = extractIds(datagram) - newNgtcp2Server(local, remote, source, destination) + newNgtcp2Server(tlsContext, local, remote, source, destination) diff --git a/quic/transport/ngtcp2/native/settings.nim b/quic/transport/ngtcp2/native/settings.nim index 3b81e5e..5880ec9 100644 --- a/quic/transport/ngtcp2/native/settings.nim +++ b/quic/transport/ngtcp2/native/settings.nim @@ -1,4 +1,4 @@ -import pkg/ngtcp2 +import ngtcp2 proc defaultSettings*(): ngtcp2_settings = ngtcp2_settings_default_versioned(NGTCP2_SETTINGS_V2, addr result) diff --git a/quic/transport/ngtcp2/native/streams.nim b/quic/transport/ngtcp2/native/streams.nim index 574f20b..bcfb92f 100644 --- a/quic/transport/ngtcp2/native/streams.nim +++ b/quic/transport/ngtcp2/native/streams.nim @@ -1,8 +1,9 @@ -import pkg/ngtcp2 +import ngtcp2 import ../../../helpers/openarray import ../../stream import ../stream/openstate import ./connection +import chronicles proc newStream(connection: Ngtcp2Connection, id: int64): Stream = newStream(id, newOpenStream(connection)) @@ -29,6 +30,7 @@ proc onStreamClose( user_data: pointer, stream_user_data: pointer, ): cint {.cdecl.} = + trace "onStreamClose" let openStream = cast[OpenStream](stream_user_data) if openStream != nil: openStream.onClose() @@ -39,16 +41,43 @@ proc onReceiveStreamData( stream_id: int64, offset: uint64, data: ptr uint8, - datalen: uint, + datalen: csize_t, user_data: pointer, stream_user_data: pointer, ): cint {.cdecl.} = + trace "onReceiveStreamData" let state = cast[OpenStream](stream_user_data) var bytes = newSeqUninitialized[byte](datalen) copyMem(bytes.toUnsafePtr, data, datalen) state.receive(bytes) +proc onStreamReset( + connection: ptr ngtcp2_conn, + stream_id: int64, + final_size: uint64, + app_error_code: uint64, + user_data: pointer, + stream_user_data: pointer, +): cint {.cdecl.} = + trace "onStreamReset" + let openStream = cast[OpenStream](stream_user_data) + if openStream != nil: + openStream.onClose() + return 0 + +proc onStreamStopSending( + conn: ptr ngtcp2_conn, + stream_id: int64, + app_error_code: uint64, + user_data: pointer, + stream_user_data: pointer, +): cint {.cdecl.} = + trace "onStreamStopSending" + return 0 + proc installStreamCallbacks*(callbacks: var ngtcp2_callbacks) = callbacks.stream_open = onStreamOpen callbacks.stream_close = onStreamClose callbacks.recv_stream_data = onReceiveStreamData + callbacks.stream_reset = onStreamReset + callbacks.stream_stop_sending = onStreamStopSending diff --git a/quic/transport/ngtcp2/stream/closedstate.nim b/quic/transport/ngtcp2/stream/closedstate.nim index 3dcb65c..e2a6c68 100644 --- a/quic/transport/ngtcp2/stream/closedstate.nim +++ b/quic/transport/ngtcp2/stream/closedstate.nim @@ -1,5 +1,9 @@ import ../../../basics import ../../stream +import chronicles + +logScope: + topics = "closed state" type ClosedStream* = ref object of StreamState @@ -15,9 +19,11 @@ method enter*(state: ClosedStream, stream: Stream) = stream.closed.fire() method read*(state: ClosedStream): Future[seq[byte]] {.async.} = + trace "cant read, stream is closed" raise newException(ClosedStreamError, "stream is closed") method write*(state: ClosedStream, bytes: seq[byte]) {.async.} = + trace "cant write, stream is closed" raise newException(ClosedStreamError, "stream is closed") method close*(state: ClosedStream) {.async.} = diff --git a/quic/transport/quicclientserver.nim b/quic/transport/quicclientserver.nim index 1050d51..b8272c4 100644 --- a/quic/transport/quicclientserver.nim +++ b/quic/transport/quicclientserver.nim @@ -1,11 +1,16 @@ import ../basics +import ./tlsbackend import ./quicconnection import ./ngtcp2/connection/openstate -proc newQuicClientConnection*(local, remote: TransportAddress): QuicConnection = - newQuicConnection(openClientConnection(local, remote)) +proc newQuicClientConnection*( + tlsBackend: TLSBackend, local, remote: TransportAddress +): QuicConnection = + let openConn = openClientConnection(tlsBackend, local, remote) + newQuicConnection(openConn) proc newQuicServerConnection*( - local, remote: TransportAddress, datagram: Datagram + tlsBackend: TLSBackend, local, remote: TransportAddress, datagram: Datagram ): QuicConnection = - newQuicConnection(openServerConnection(local, remote, datagram)) + let openConn = openServerConnection(tlsBackend, local, remote, datagram) + newQuicConnection(openConn) diff --git a/quic/transport/stream.nim b/quic/transport/stream.nim index 04d7935..ec6c63c 100644 --- a/quic/transport/stream.nim +++ b/quic/transport/stream.nim @@ -12,7 +12,7 @@ type StreamError* = object of QuicError -{.push locks: "unknown", raises: [QuicError].} +{.push raises: [QuicError].} method enter*(state: StreamState, stream: Stream) {.base.} = doAssert not state.entered # states are not reentrant diff --git a/quic/transport/timeout.nim b/quic/transport/timeout.nim index 5261d26..c51e8fd 100644 --- a/quic/transport/timeout.nim +++ b/quic/transport/timeout.nim @@ -1,4 +1,5 @@ import ../basics +import chronicles type Timeout* = ref object timer: Opt[TimerCallback] @@ -6,6 +7,7 @@ type Timeout* = ref object expired: AsyncEvent proc setTimer(timeout: Timeout, moment: Moment) = + trace "setTimer" proc onTimeout(_: pointer) = timeout.expired.fire() timeout.onExpiry() @@ -16,6 +18,7 @@ const skip = proc() = discard proc newTimeout*(onExpiry: proc() {.gcsafe, raises: [].} = skip): Timeout = + trace "newTimeout" Timeout(onExpiry: onExpiry, expired: newAsyncEvent()) proc stop*(timeout: Timeout) = @@ -32,3 +35,4 @@ proc set*(timeout: Timeout, duration: Duration) = proc expired*(timeout: Timeout) {.async.} = await timeout.expired.wait() + trace "expired" diff --git a/quic/transport/tlsbackend.nim b/quic/transport/tlsbackend.nim new file mode 100644 index 0000000..a6abc88 --- /dev/null +++ b/quic/transport/tlsbackend.nim @@ -0,0 +1,45 @@ +import results +import ngtcp2 +import ./ngtcp2/native +import ../errors + +export CertificateVerifier +export certificateVerifierCB +export CustomCertificateVerifier +export InsecureCertificateVerifier +export init +export destroy + +type TLSBackend* = ref object + picoTLS*: PicoTLSContext + +proc newServerTLSBackend*( + certificate: seq[byte], + key: seq[byte], + certificateVerifier: Opt[CertificateVerifier], +): TLSBackend {.raises: [QuicError].} = + let picotlsCtx = PicoTLSContext.init( + certificate, key, certificateVerifier, certificateVerifier.isSome + ) + let ret = ngtcp2_crypto_picotls_configure_server_context(picotlsCtx.context) + if ret != 0: + raise newException(QuicError, "could not configure server context: " & $ret) + return TLSBackend(picoTLS: picotlsCtx) + +proc newClientTLSBackend*( + certificate: seq[byte], + key: seq[byte], + certificateVerifier: Opt[CertificateVerifier], +): TLSBackend {.raises: [QuicError].} = + let picotlsCtx = PicoTLSContext.init(certificate, key, certificateVerifier, false) + let ret = ngtcp2_crypto_picotls_configure_client_context(picotlsCtx.context) + if ret != 0: + raise newException(QuicError, "could not configure client context: " & $ret) + return TLSBackend(picoTLS: picotlsCtx) + +proc destroy*(self: TLSBackend) = + if self.picoTLS.isNil: + return + + self.picoTLS.destroy() + self.picoTLS = nil diff --git a/quic/transport/version.nim b/quic/transport/version.nim index 9fc18de..07ff028 100644 --- a/quic/transport/version.nim +++ b/quic/transport/version.nim @@ -1,4 +1,3 @@ -from pkg/ngtcp2 import NGTCP2_PROTO_VER_V2 +#from pkg/ngtcp2 import NGTCP2_PROTO_VER_V2 -const DraftVersion29 = 0xFF00001D'u32 -const CurrentQuicVersion* = cast[uint32](NGTCP2_PROTO_VER_V2) +const CurrentQuicVersion* = cast[uint32](0x6b3343cfu) # TODO: figure out how to export NGTCP2_PROTO_VER_V2 with futhark diff --git a/tests/helpers/certificate.nim b/tests/helpers/certificate.nim new file mode 100644 index 0000000..e011e14 --- /dev/null +++ b/tests/helpers/certificate.nim @@ -0,0 +1,15 @@ +import sequtils +import os + +const certificateStr = + staticRead(parentDir(currentSourcePath()) / "testCertificate.pem") +const privateKeyStr = staticRead(parentDir(currentSourcePath()) / "testPrivateKey.pem") + +proc strToSeq(val: string): seq[byte] = + toSeq(val.toOpenArrayByte(0, val.high)) + +proc testCertificate*(): seq[byte] = + strToSeq(certificateStr) + +proc testPrivateKey*(): seq[byte] = + strToSeq(privateKeyStr) diff --git a/tests/helpers/simulation.nim b/tests/helpers/simulation.nim index 327c00e..cbd2cd8 100644 --- a/tests/helpers/simulation.nim +++ b/tests/helpers/simulation.nim @@ -1,8 +1,8 @@ import std/random import pkg/chronos -import pkg/quic/transport/quicconnection -import pkg/quic/transport/quicclientserver +import pkg/quic/transport/[quicconnection, quicclientserver, tlsbackend] import pkg/quic/helpers/asyncloop +import ./certificate import ./addresses proc networkLoop*(source, destination: QuicConnection) {.async.} = @@ -37,11 +37,17 @@ proc simulateLossyNetwork*(a, b: QuicConnection) {.async.} = await allFutures(loop1.cancelAndWait(), loop2.cancelAndWait()) proc setupConnection*(): Future[tuple[client, server: QuicConnection]] {.async.} = - let client = newQuicClientConnection(zeroAddress, zeroAddress) - client.send() + let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + let client = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress) + + client.send() # Start Handshake let datagram = await client.outgoing.get() - let server = newQuicServerConnection(zeroAddress, zeroAddress, datagram) - server.receive(datagram) + let serverTLSBackend = newServerTLSBackend( + testCertificate(), testPrivateKey(), Opt.none(CertificateVerifier) + ) + let server = + newQuicServerConnection(serverTLSBackend, zeroAddress, zeroAddress, datagram) + result = (client, server) proc performHandshake*(): Future[tuple[client, server: QuicConnection]] {.async.} = diff --git a/tests/helpers/testCertificate.pem b/tests/helpers/testCertificate.pem new file mode 100644 index 0000000..40bf0d0 --- /dev/null +++ b/tests/helpers/testCertificate.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIB0jCCAXegAwIBAgIVAJUFhCQrlzzkFZbZkyXNRwCXSvYWMAwGCCqGSM49BAMC +BQAwFDESMBAGA1UEAwwJbGlicDJwLmlvMCAXDTc1MDEwMTAwMDAwMFoYDzQwOTYw +MTAxMDAwMDAwWjAUMRIwEAYDVQQDDAlsaWJwMnAuaW8wWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAAThQpOzDszrSDCf9wqR1+0WOt6BHVwv3Q3R+8quylRwLbR2D8ov +PoXfWCzoACju9j2wBj4WHgb69Cpp9FiW7qx0o4GhMIGeMIGABgorBgEEAYOiWgEB +BHIwbwQlCAISIQIz55rVQB21ks44HQ9G/0YJgvDB5v5/6Bl+MkQq5k3A4gRGMEQC +IBLxYkAwI5H5GMzWPpjhOt3pJQ9gi7ICqCejEIFSuvf1AiA2kZZ+twfvyIYV7AAv +InM3WOWZlgY4a7TDRm8C974pHwAwCQYDVR0TBAIwADAOBgNVHQ8BAf8EBAMCBaAw +DAYIKoZIzj0EAwIFAANHADBEAiBV/KbtQ5SIAu373/8j/PzLnewCHUzXWTd3zjN9 +nzkGIgIgd1IJKDqnotzvCKw4WYrAmgTMkINJwlaQf278ranRaEY= +-----END CERTIFICATE----- diff --git a/tests/helpers/testPrivateKey.pem b/tests/helpers/testPrivateKey.pem new file mode 100644 index 0000000..b432f5d --- /dev/null +++ b/tests/helpers/testPrivateKey.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJTCwOljI3BRUBE3VXVy4sN16sh62IliV7X9y3uEPh4FoAoGCCqGSM49 +AwEHoUQDQgAE4UKTsw7M60gwn/cKkdftFjregR1cL90N0fvKrspUcC20dg/KLz6F +31gs6AAo7vY9sAY+Fh4G+vQqafRYlu6sdA== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/tests/quic/testApi.nim b/tests/quic/testApi.nim index 561ea93..9183998 100644 --- a/tests/quic/testApi.nim +++ b/tests/quic/testApi.nim @@ -1,17 +1,23 @@ -import pkg/chronos -import pkg/chronos/unittest2/asynctests -import pkg/quic +import chronos +import chronos/unittest2/asynctests +import quic +import quic/transport/tlsbackend +import ../helpers/certificate suite "api": setup: - var listener = listen(initTAddress("127.0.0.1:0")) + let serverTLSConfig = TLSConfig.init(testCertificate(), testPrivateKey()) + var server = QuicServer.init(serverTLSConfig) + let clientTLSConfig = TLSConfig.init() + var client = QuicClient.init(clientTLSConfig) + var listener = server.listen(initTAddress("127.0.0.1:0")) let address = listener.localAddress teardown: waitFor listener.stop() asyncTest "opens and drops connections": - let dialing = dial(address) + let dialing = client.dial(address) let accepting = listener.accept() let outgoing = await dialing @@ -26,7 +32,7 @@ suite "api": await incoming.drop() asyncTest "opens and closes streams": - let dialing = dial(address) + let dialing = client.dial(address) let accepting = listener.accept() let outgoing = await dialing @@ -42,7 +48,7 @@ suite "api": await incoming.drop() asyncTest "waits until peer closes connection": - let dialing = dial(address) + let dialing = client.dial(address) let accepting = listener.accept() let outgoing = await dialing @@ -53,11 +59,11 @@ suite "api": asyncTest "accepts multiple incoming connections": let accepting1 = listener.accept() - let outgoing1 = await dial(address) + let outgoing1 = await client.dial(address) let incoming1 = await accepting1 let accepting2 = listener.accept() - let outgoing2 = await dial(address) + let outgoing2 = await client.dial(address) let incoming2 = await accepting2 check incoming1 != incoming2 @@ -70,7 +76,7 @@ suite "api": asyncTest "writes to and reads from streams": let message = @[1'u8, 2'u8, 3'u8] - let outgoing = await dial(address) + let outgoing = await client.dial(address) defer: await outgoing.drop() diff --git a/tests/quic/testConnection.nim b/tests/quic/testConnection.nim index 2e73b3a..b681eee 100644 --- a/tests/quic/testConnection.nim +++ b/tests/quic/testConnection.nim @@ -1,6 +1,9 @@ -import pkg/chronos -import pkg/chronos/unittest2/asynctests -import pkg/quic/connection +import chronos +import chronos/unittest2/asynctests +import results + +import quic/connection +import quic/transport/tlsbackend import ../helpers/udp suite "connections": @@ -9,8 +12,8 @@ suite "connections": asyncTest "handles error when writing to udp transport by closing connection": let udp = newDatagramTransport() - let connection = newOutgoingConnection(udp, address) - + let tlsBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + let connection = newOutgoingConnection(tlsBackend, udp, address) await udp.closeWait() connection.startHandshake() diff --git a/tests/quic/testExample.nim b/tests/quic/testExample.nim index 9bfc14c..fd84a8a 100644 --- a/tests/quic/testExample.nim +++ b/tests/quic/testExample.nim @@ -1,27 +1,39 @@ +import results import pkg/unittest2 import ../helpers/async import pkg/quic import pkg/chronos +import ../helpers/certificate suite "examples from Readme": test "outgoing and incoming connections": + let message = cast[seq[byte]]("some message") proc outgoing() {.async.} = - let connection = await dial(initTAddress("127.0.0.1:12345")) + let cb = proc(derCertificates: seq[seq[byte]]): bool {.gcsafe.} = + # TODO: implement custom certificate validation + return derCertificates.len > 0 + + let customCertVerif: CertificateVerifier = CustomCertificateVerifier.init(cb) + let tlsConfig = TLSConfig.init(certificateVerifier = Opt.some(customCertVerif)) + let client = QuicClient.init(tlsConfig) + let connection = await client.dial(initTAddress("127.0.0.1:12345")) let stream = await connection.openStream() - let message = cast[seq[byte]]("some message") await stream.write(message) await stream.close() - await connection.waitClosed() + await connection.close() proc incoming() {.async.} = - let listener = listen(initTAddress("127.0.0.1:12345")) + let tlsConfig = TLSConfig.init(testCertificate(), testPrivateKey()) + let server = QuicServer.init(tlsConfig) + let listener = server.listen(initTAddress("127.0.0.1:12345")) + let connection = await listener.accept() let stream = await connection.incomingStream() - let message = await stream.read() + let readMessage = await stream.read() await stream.close() - await connection.close() + await connection.waitClosed() await listener.stop() - - check message == cast[seq[byte]]("some message") + listener.destroy() + check readMessage == message waitFor allSucceeded(incoming(), outgoing()) diff --git a/tests/quic/testListener.nim b/tests/quic/testListener.nim index e60ca65..95d6453 100644 --- a/tests/quic/testListener.nim +++ b/tests/quic/testListener.nim @@ -1,12 +1,15 @@ -import pkg/chronos -import pkg/chronos/unittest2/asynctests -import pkg/quic -import pkg/quic/listener +import chronos +import chronos/unittest2/asynctests +import quic +import quic/listener +import quic/transport/tlsbackend import ../helpers/udp +import ../helpers/certificate suite "listener": setup: - var listener = newListener(initTAddress("127.0.0.1:0")) + let tlsBackend = newServerTLSBackend(testCertificate(), testPrivateKey(), Opt.none(CertificateVerifier)) + var listener = newListener(tlsBackend, initTAddress("127.0.0.1:0")) let address = listener.localAddress check address.port != Port(0) diff --git a/tests/quic/testNgtcp2TransportParameters.nim b/tests/quic/testNgtcp2TransportParameters.nim index c5cc4b7..24b894b 100644 --- a/tests/quic/testNgtcp2TransportParameters.nim +++ b/tests/quic/testNgtcp2TransportParameters.nim @@ -1,11 +1,9 @@ -import pkg/unittest2 -import pkg/ngtcp2 -import pkg/stew/results -import pkg/quic/errors -import pkg/quic/transport/ngtcp2/native/connection -import pkg/quic/transport/ngtcp2/native/client -import pkg/quic/transport/ngtcp2/native/params -import pkg/quic/transport/ngtcp2/native/settings +import unittest2 +import ngtcp2 +import results +import quic/errors +import quic/transport/tlsbackend +import quic/transport/ngtcp2/native/[connection, client, params, settings] import ../helpers/addresses suite "ngtcp2 transport parameters": @@ -17,7 +15,15 @@ suite "ngtcp2 transport parameters": test "encoding and decoding": let encoded = encodeTransportParameters(transport_params) let decoded = decodeTransportParameters(encoded) - check decoded == transport_params + check: + transport_params.initial_max_streams_uni == decoded.initial_max_streams_uni + transport_params.initial_max_stream_data_uni == decoded.initial_max_stream_data_uni + transport_params.initial_max_streams_bidi == decoded.initial_max_streams_bidi + transport_params.initial_max_stream_data_bidi_local == + decoded.initial_max_stream_data_bidi_local + transport_params.initial_max_stream_data_bidi_remote == + decoded.initial_max_stream_data_bidi_remote + transport_params.initial_max_data == decoded.initial_max_data test "raises when decoding fails": var encoded = encodeTransportParameters(transport_params) @@ -27,7 +33,8 @@ suite "ngtcp2 transport parameters": discard decodeTransportParameters(encoded) test "raises when setting remote parameters fails": - let connection = newNgtcp2Client(zeroAddress, zeroAddress) + let tlsBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + let connection = newNgtcp2Client(tlsBackend.picoTLS, zeroAddress, zeroAddress) defer: connection.destroy() transport_params.active_connection_id_limit = 0 diff --git a/tests/quic/testQuicConnection.nim b/tests/quic/testQuicConnection.nim index 5440e27..6dfb791 100644 --- a/tests/quic/testQuicConnection.nim +++ b/tests/quic/testQuicConnection.nim @@ -1,16 +1,17 @@ -import pkg/chronos -import pkg/chronos/unittest2/asynctests -import pkg/quic/errors -import pkg/quic/transport/quicconnection -import pkg/quic/transport/quicclientserver -import pkg/quic/udp/datagram -import pkg/quic/transport/connectionid +import chronos +import chronos/unittest2/asynctests +import quic/errors +import quic/transport/[quicconnection, quicclientserver, tlsbackend] +import quic/udp/datagram +import quic/transport/connectionid import ../helpers/simulation import ../helpers/addresses +import ../helpers/certificate suite "quic connection": asyncTest "sends outgoing datagrams": - let client = newQuicClientConnection(zeroAddress, zeroAddress) + let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + let client = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress) defer: await client.drop() client.send() @@ -18,14 +19,17 @@ suite "quic connection": check datagram.len > 0 asyncTest "processes received datagrams": - let client = newQuicClientConnection(zeroAddress, zeroAddress) + let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + let client = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress) defer: await client.drop() client.send() let datagram = await client.outgoing.get() - let server = newQuicServerConnection(zeroAddress, zeroAddress, datagram) + let serverTLSBackend = newServerTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + let server = + newQuicServerConnection(serverTLSBackend, zeroAddress, zeroAddress, datagram) defer: await server.drop() @@ -35,7 +39,10 @@ suite "quic connection": let invalid = Datagram(data: @[0'u8]) expect QuicError: - discard newQuicServerConnection(zeroAddress, zeroAddress, invalid) + let serverTLSBackend = + newServerTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + discard + newQuicServerConnection(serverTLSBackend, zeroAddress, zeroAddress, invalid) asyncTest "performs handshake": let (client, server) = await performHandshake() @@ -60,11 +67,16 @@ suite "quic connection": check server.ids != client.ids asyncTest "notifies about id changes": - let client = newQuicClientConnection(zeroAddress, zeroAddress) + let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + let client = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress) client.send() let datagram = await client.outgoing.get() - let server = newQuicServerConnection(zeroAddress, zeroAddress, datagram) + let serverTLSBackend = newServerTLSBackend( + testCertificate(), testPrivateKey(), Opt.none(CertificateVerifier) + ) + let server = + newQuicServerConnection(serverTLSBackend, zeroAddress, zeroAddress, datagram) var newId: ConnectionId server.onNewId = proc(id: ConnectionId) = newId = id @@ -80,7 +92,8 @@ suite "quic connection": await server.drop asyncTest "raises ConnectionError when closed": - let connection = newQuicClientConnection(zeroAddress, zeroAddress) + let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + let connection = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress) await connection.drop() expect ConnectionError: @@ -93,7 +106,8 @@ suite "quic connection": discard await connection.openStream() asyncTest "has empty list of ids when closed": - let connection = newQuicClientConnection(zeroAddress, zeroAddress) + let clientTLSBackend = newClientTLSBackend(@[], @[], Opt.none(CertificateVerifier)) + let connection = newQuicClientConnection(clientTLSBackend, zeroAddress, zeroAddress) await connection.drop() check connection.ids.len == 0 diff --git a/tests/quic/testStreams.nim b/tests/quic/testStreams.nim index ca86c7f..a281b91 100644 --- a/tests/quic/testStreams.nim +++ b/tests/quic/testStreams.nim @@ -33,13 +33,6 @@ suite "streams": let stream = await client.openStream() await stream.close() - asyncTest "writes to stream": - let stream = await client.openStream() - let message = @[1'u8, 2'u8, 3'u8] - await stream.write(message) - - check client.outgoing.anyIt(it.data.contains(message)) - asyncTest "writes zero-length message": let stream = await client.openStream() await stream.write(@[])