syndicate-ssh/syndicate-ssh/ssh-transport.rkt

391 lines
15 KiB
Racket

#lang syndicate
;;; SPDX-License-Identifier: LGPL-3.0-or-later
;;; SPDX-FileCopyrightText: Copyright © 2011-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
(provide (struct-out inbound-packet)
(struct-out inbound-credit)
(struct-out outbound-packet)
(struct-out outbound-byte-credit)
(struct-out new-keys)
default-packet-limit
local-algorithm-list
ssh-reader
ssh-writer)
(require bitsyntax)
(require syndicate/drivers/tcp)
(require "crypto.rkt")
(require "ssh-numbers.rkt")
(require "ssh-message-types.rkt")
(require "ssh-exceptions.rkt")
(module+ test (require rackunit))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data definitions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; A DecodedPacket is one of the packet structures defined in
;; ssh-message-types.rkt.
;; An InboundPacket is an (inbound-packet Number Bytes
;; Maybe<DecodedPacket> Number) representing a packet read from the
;; socket, its sequence number, and the total number of bytes involved
;; in its reception.
(struct inbound-packet (sequence-number payload message transfer-size) #:prefab)
(struct inbound-credit (amount) #:prefab)
(struct outbound-packet (message) #:prefab)
(struct outbound-byte-credit (amount) #:prefab)
(struct new-keys (is-server?
derive-key
c2s-enc s2c-enc
c2s-mac s2c-mac
c2s-zip s2c-zip)
#:prefab)
(struct crypto-configuration (cipher
cipher-description
hmac
hmac-description)
#:transparent)
;; Description of a supported cipher.
(struct supported-cipher (name factory key-length block-size iv-length)
#:transparent)
;; Description of a supported hmac algorithm.
(struct supported-hmac (name factory digest-length key-length)
#:transparent)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Parameters
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define default-packet-limit (make-parameter 65536))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Encryption, MAC, and Compression algorithm descriptions and parameters
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; "none" cipher description.
(define null-cipher-description
(supported-cipher 'none
(lambda (enc? key iv)
(lambda (block)
block))
0
8 ;; pseudo-block-size for packet I/O
0))
;; "none" HMAC function.
(define (null-hmac blob)
#"")
;; "none" HMAC description.
(define null-hmac-description
(supported-hmac 'none
(lambda (key)
(error 'null-hmac-description
"Cannot construct null hmac instance"))
0
0))
(define (make-cipher-entry name cipher-spec key-length
#:block-size [block-size (cipher-block-size cipher-spec)])
(list name
(supported-cipher name
(lambda (enc? key iv)
(let ((ctx ((if enc? make-encrypt-ctx make-decrypt-ctx)
cipher-spec key iv #:pad #f)))
(lambda (input)
(cipher-update ctx input))))
key-length
block-size
(cipher-iv-size cipher-spec))))
(define supported-crypto-algorithms
(list
(make-cipher-entry 'aes128-ctr '(aes ctr) 16 #:block-size 16)
(make-cipher-entry 'aes192-ctr '(aes ctr) 24 #:block-size 16)
(make-cipher-entry 'aes256-ctr '(aes ctr) 32 #:block-size 16)
(make-cipher-entry 'aes128-cbc '(aes cbc) 16)
(make-cipher-entry 'aes192-cbc '(aes cbc) 24)
(make-cipher-entry 'aes256-cbc '(aes cbc) 32)
(make-cipher-entry '3des-cbc '(des-ede3 cbc) 24)
)) ;; TODO: actually test these!
(define (make-hmac-entry name digest-spec key-length-or-false)
(let* ((digest-length (digest-size digest-spec))
(key-length (or key-length-or-false digest-length)))
(list name
(supported-hmac name
(lambda (key)
(lambda (blob)
(hmac digest-spec key blob)))
digest-length
key-length))))
(define supported-hmac-algorithms
(list (make-hmac-entry 'hmac-sha1 'sha1 #f)))
(define supported-compression-algorithms '(none)) ;; TODO: zlib, and zlib delayed
(define local-algorithm-list
(let ((crypto-names (map car supported-crypto-algorithms))
(mac-names (map car supported-hmac-algorithms)))
(make-parameter
(lambda ()
(ssh-msg-kexinit (crypto-random-bytes 16)
'(diffie-hellman-group14-sha256)
'(ssh-ed25519)
crypto-names
crypto-names
mac-names
mac-names
supported-compression-algorithms
supported-compression-algorithms
'()
'()
#f
0)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Cryptographic stream configuration
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define initial-crypto-configuration
(crypto-configuration #f
null-cipher-description
null-hmac
null-hmac-description))
(define (apply-negotiated-options conn-ds nk is-outbound?)
(match-define (new-keys is-server?
(embedded derive-key)
c2s-enc s2c-enc
c2s-mac s2c-mac
c2s-zip s2c-zip) nk)
;; TODO: zip
;; TODO: make this less ugly. Compute all the keys, select just the ones we need afterward?
(define c2s
;; c2s true iff stream is serverward
(if is-server? (not is-outbound?) is-outbound?))
(define enc (if c2s c2s-enc s2c-enc))
(define mac (if c2s c2s-mac s2c-mac))
(define zip (if c2s c2s-zip s2c-zip))
(define cipher-description
(cond
((assq enc supported-crypto-algorithms) => cadr)
(else (disconnect-with-error conn-ds
SSH_DISCONNECT_KEY_EXCHANGE_FAILED
"Could not find driver for encryption algorithm ~v"
enc))))
(define cipher
((supported-cipher-factory cipher-description)
is-outbound?
(derive-key (if c2s #"C" #"D") (supported-cipher-key-length cipher-description))
(derive-key (if c2s #"A" #"B") (supported-cipher-iv-length cipher-description))))
(define hmac-description
(cond
((assq mac supported-hmac-algorithms) => cadr)
(else (disconnect-with-error conn-ds
SSH_DISCONNECT_KEY_EXCHANGE_FAILED
"Could not find driver for HMAC algorithm ~v"
mac))))
(define hmac
((supported-hmac-factory hmac-description)
(derive-key (if c2s #"E" #"F") (supported-hmac-key-length hmac-description))))
(crypto-configuration cipher cipher-description
hmac hmac-description))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Transport utilities
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MacFunction Natural Bytes -> Bytes
;; Computes the HMAC trailer for a given blob at the given sequence number.
(define (apply-hmac mac sequence-number packet)
(mac (bit-string->bytes (bit-string-append (integer->bit-string sequence-number 32 #t)
packet))))
(define (check-packet-length! conn-ds actual-length limit block-size)
(when (> actual-length limit)
(log-warning (format "Packet of length ~v exceeded our limit of ~v"
actual-length
limit)))
(when (> actual-length (* 2 limit))
;; TODO: For some reason, OpenSSH seems to occasionally slightly
;; exceed the packet size limit! (For example, sending a packet of
;; length 65564 when I'm expecting a max of 65536.) So we actually
;; enforce twice our actual limit.
(disconnect-with-error conn-ds
0 ;; TODO: better reason code?
"Packet of length ~v is longer than packet limit ~v"
actual-length
limit))
(when (not (zero? (modulo (+ actual-length 4) block-size)))
;; the +4 is because the length sent on the wire doesn't include
;; the length-of-length, but the requirements for transmitted
;; chunks of data are that they be block-size multiples
;; *including* the length-of-length
(disconnect-with-error conn-ds
SSH_DISCONNECT_PROTOCOL_ERROR
"Packet of length ~v is not a multiple of block size ~v"
actual-length
block-size)))
;; Integer PositiveInteger -> Integer
;; Rounds "what" up to the nearest multiple of "to".
(define (round-up what to)
(* to (quotient (+ what (- to 1)) to)))
(module+ test
(check-equal? (round-up 0 8) 0)
(check-equal? (round-up 1 8) 8)
(check-equal? (round-up 7 8) 8)
(check-equal? (round-up 8 8) 8)
(check-equal? (round-up 9 8) 16))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Encrypted Packet Input
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (ssh-reader conn-ds source transfer-control)
(define input-handler #f)
(define (update-input-handler #:on-data proc) (set! input-handler proc))
(make-sink #:initial-source source
#:name 'ssh-in
#:on-data (lambda (data mode) (input-handler data mode)))
(send! transfer-control 'transfer-control)
(define packet-size-limit (default-packet-limit))
(define sequence-number 0)
(define remaining-credit 0)
(define config initial-crypto-configuration)
(define (current-cipher) (crypto-configuration-cipher config))
(define (block-size)
(supported-cipher-block-size (crypto-configuration-cipher-description config)))
(define (decrypt-chunk chunk) ((or (current-cipher) values) chunk))
(define (subsequent-block-size) (if (current-cipher) (block-size) 1))
(define (hmac) (crypto-configuration-hmac config))
(define (issue-credit)
(when (positive? remaining-credit)
(send-packet-credit source (block-size))))
(define (handle-packet-header encrypted-packet _mode)
(define first-block (decrypt-chunk encrypted-packet))
(define packet-length (integer-bytes->integer first-block #f #t 0 4))
(check-packet-length! conn-ds packet-length packet-size-limit (subsequent-block-size))
(define amount-of-packet-in-first-block (- (bytes-length first-block) 4)) ;; not incl length
(define remaining-to-read (- packet-length amount-of-packet-in-first-block))
(if (positive? remaining-to-read)
(begin
(send-packet-credit source remaining-to-read)
(update-input-handler
#:on-data (lambda (encrypted-packet _mode)
(define subsequent-chunk (decrypt-chunk encrypted-packet))
(check-hmac (bytes-append first-block subsequent-chunk) packet-length))))
(check-hmac first-block packet-length)))
(define (check-hmac packet packet-length)
(define payload-length (let ((padding-length (bytes-ref packet 4)))
(- packet-length padding-length 1)))
(define computed-hmac-bytes (apply-hmac (hmac) sequence-number packet))
(define mac-byte-count (bytes-length computed-hmac-bytes))
(if (positive? mac-byte-count)
(begin
(send-packet-credit source mac-byte-count)
(update-input-handler
#:on-data (lambda (received-hmac-bytes _mode)
(if (equal? computed-hmac-bytes received-hmac-bytes)
(finish-packet mac-byte-count packet-length payload-length packet)
(disconnect-with-error/local-info conn-ds
`((expected-hmac ,computed-hmac-bytes)
(actual-hmac ,received-hmac-bytes))
SSH_DISCONNECT_MAC_ERROR
"Corrupt MAC")))))
(finish-packet 0 packet-length payload-length packet)))
(define (finish-packet mac-byte-count packet-length payload-length packet)
(define bytes-read (+ packet-length mac-byte-count))
(define payload (subbytes packet 5 (+ 5 payload-length)))
(update-input-handler #:on-data handle-packet-header)
(send! conn-ds (inbound-packet sequence-number
payload
(ssh-message-decode payload)
bytes-read))
(set! sequence-number (+ sequence-number 1))
(set! remaining-credit (- remaining-credit 1))
(issue-credit))
(update-input-handler #:on-data handle-packet-header)
(at conn-ds
(on (message (inbound-credit $amount))
(set! remaining-credit (+ remaining-credit amount))
(issue-credit))
(on (message ($ nk (new-keys _ _ _ _ _ _ _ _)))
(set! config (apply-negotiated-options conn-ds nk #f)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Encrypted Packet Output
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (ssh-writer conn-ds sink transfer-control)
(define config initial-crypto-configuration)
(define sequence-number 0)
(make-source #:initial-sink sink
#:name 'ssh-out)
(send! transfer-control 'transfer-control)
(define (block-size)
(supported-cipher-block-size (crypto-configuration-cipher-description config)))
(define (encrypt-chunk chunk) ((or (crypto-configuration-cipher config) values) chunk))
(define (hmac) (crypto-configuration-hmac config))
(at conn-ds
(on (message (outbound-packet $message))
(define pad-block-size (block-size))
(define payload (ssh-message-encode message))
;; There must be at least 4 bytes of padding, and padding needs to
;; make the packet length a multiple of pad-block-size.
(define unpadded-length (+ 4 ;; length of length
1 ;; length of length-of-padding indicator
(bit-string-byte-count payload)))
(define min-padded-length (+ unpadded-length 4))
(define padded-length (round-up min-padded-length pad-block-size))
(define padding-length (- padded-length unpadded-length))
(define packet-length (- padded-length 4))
;; ^^ the packet length does *not* include itself!
(define packet (bit-string->bytes
(bit-string (packet-length :: integer bits 32)
(padding-length :: integer bits 8)
(payload :: binary)
((crypto-random-bytes padding-length) :: binary))))
(define encrypted-packet (encrypt-chunk packet))
(define computed-hmac-bytes (apply-hmac (hmac) sequence-number packet))
(define mac-byte-count (bytes-length computed-hmac-bytes))
(send-data sink encrypted-packet)
(send-data sink computed-hmac-bytes)
(send! (outbound-byte-credit (+ (bytes-length encrypted-packet)
(bytes-length computed-hmac-bytes))))
(set! sequence-number (+ sequence-number 1)))
(on (message ($ nk (new-keys _ _ _ _ _ _ _ _)))
(set! config (apply-negotiated-options conn-ds nk #t)))))