diff --git a/ssh-message-types.rkt b/ssh-message-types.rkt new file mode 100644 index 0000000..ff67532 --- /dev/null +++ b/ssh-message-types.rkt @@ -0,0 +1,168 @@ +#lang racket/base + +(require "ssh-numbers.rkt") + +(require (for-syntax racket/base)) +(require (for-syntax (only-in racket/list append*))) +(require (for-syntax (only-in srfi/1 iota))) + +(require (planet tonyg/bitsyntax)) +(require racket/bytes) + +(require rackunit) + +(provide ssh-message-decode + ssh-message-encode) + +(define decoder-map (make-hasheqv)) + +(define-values (prop:ssh-message-encoder ssh-message-encoder? ssh-message-encoder) + (make-struct-type-property 'ssh-message-encoder)) + +(define (ssh-message-decode packet) + (let ((type-code (bytes-ref packet 0))) + ((hash-ref decoder-map + type-code + (lambda () (error 'ssh-message-decode + "Unknown message packet type number ~v" + type-code))) + packet))) + +(define (ssh-message-encode m) + ((ssh-message-encoder m) m)) + +(define-syntax define-ssh-message-type + (syntax-rules () + ((_ name type-byte-value (field-type field-name) ...) + (begin + (provide (struct-out name)) + (struct name (field-name ...) + #:transparent + #:property prop:ssh-message-encoder + (compute-ssh-message-encoder type-byte-value field-type ...)) + (hash-set! decoder-map type-byte-value + (compute-ssh-message-decoder name type-byte-value field-type ...)))))) + +(define-syntax compute-ssh-message-encoder + (lambda (stx) + (define (encoder-field index vec field-type) + (syntax-case field-type (byte boolean uint32 uint64 string mpint name-list) + (byte + #`(vector-ref #,vec #,index)) + ((byte n) + #`((vector-ref #,vec #,index) :: binary bytes n)) + (boolean + #`(if (vector-ref #,vec #,index) 1 0)) + (uint32 + #`((vector-ref #,vec #,index) :: integer bits 32)) + (uint64 + #`((vector-ref #,vec #,index) :: integer bits 64)) + (string + #`((let ((v (vector-ref #,vec #,index))) + (bit-string ((bytes-length v) :: integer bits 32) + (v :: binary))) :: binary)) + (mpint + #`((let* ((v (vector-ref #,vec #,index)) + (width (mpint-width v)) + (buf (integer->bit-string v (* 8 width) #t))) + (bit-string (width :: integer bits 32) + (buf :: binary))) :: binary)) + (name-list + #`((let ((v (symbols->name-list (vector-ref #,vec #,index)))) + (bit-string ((quotient (bit-string-length v) 8) :: integer bits 32) + (v :: binary))) :: binary)))) + (syntax-case stx () + ((_ type-byte-value field-type ...) + #`(lambda (message) + (let ((vec (struct->vector message))) + #,(with-syntax (((field-spec ...) + (let ((type-list (syntax->list #'(field-type ...)))) + (map (lambda (index type) (encoder-field index #'vec type)) + (iota (length type-list) 1) + type-list)))) + #'(bit-string (type-byte-value :: integer bytes 1) + field-spec ...)))))))) + +(define-syntax compute-ssh-message-decoder + (lambda (stx) + (define (field-extractor temp-name field-type) + (syntax->list + (syntax-case field-type (byte boolean uint32 uint64 string mpint name-list) + (byte + #`(#,temp-name)) + ((byte n) + #`((#,temp-name :: binary bytes n))) + (boolean + #`(#,temp-name)) + (uint32 + #`((#,temp-name :: integer bits 32))) + (uint64 + #`((#,temp-name :: integer bits 64))) + (string + (let ((length-name (car (generate-temporaries (list temp-name))))) + #`((#,length-name :: integer bits 32) + (#,temp-name :: binary bytes #,length-name)))) + (mpint + (let ((length-name (car (generate-temporaries (list temp-name))))) + #`((#,@length-name :: integer bits 32) + (#,temp-name :: binary bytes #,length-name)))) + (name-list + (let ((length-name (car (generate-temporaries (list temp-name))))) + #`((#,length-name :: integer bits 32) + (#,temp-name :: binary bytes #,length-name))))))) + (define (field-transformer temp-name field-type) + (syntax-case field-type (byte boolean uint32 uint64 string mpint name-list) + ((byte n) #`(bit-string->bytes #,temp-name)) + (boolean #`(not (zero? #,temp-name))) + (string #`(bit-string->bytes #,temp-name)) + (mpint #`(bit-string->integer #,temp-name)) + (name-list #`(name-list->symbols #,temp-name)) + (else temp-name))) + (syntax-case stx () + ((_ struct-name type-byte-value field-type ...) + (let* ((field-types (syntax->list #'(field-type ...))) + (temp-names (generate-temporaries field-types))) + #`(lambda (packet) + (bit-string-case packet + (( type-byte-value + #,@(append* (map field-extractor temp-names field-types))) + (struct-name #,@(map field-transformer temp-names field-types)))))))))) + +(define (mpint-width n) + (if (zero? n) + 0 + (+ 1 (quotient (integer-length n) 8)))) + +(check-eqv? (mpint-width 0) 0) +(check-eqv? (mpint-width #x9a378f9b2e332a7) 8) +(check-eqv? (mpint-width #x7f) 1) +(check-eqv? (mpint-width #x80) 2) +(check-eqv? (mpint-width #x81) 2) +(check-eqv? (mpint-width #xff) 2) +(check-eqv? (mpint-width #x100) 2) +(check-eqv? (mpint-width #x101) 2) +(check-eqv? (mpint-width #x-1234) 2) +(check-eqv? (mpint-width #x-deadbeef) 5) + +(define (symbols->name-list syms) + (bytes-join (map (lambda (s) (string->bytes/utf-8 (symbol->string s))) syms) #",")) + +(define (name-list->symbols bs) + (if (zero? (bit-string-length bs)) + '() + (map string->symbol (regexp-split #rx"," (bytes->string/utf-8 (bit-string->bytes bs)))))) + +(define-ssh-message-type ssh-msg-kexinit SSH_MSG_KEXINIT + ((byte 16) cookie) + (name-list kex_algorithms) + (name-list server_host_key_algorithms) + (name-list encryption_algorithms_client_to_server) + (name-list encryption_algorithms_server_to_client) + (name-list mac_algorithms_client_to_server) + (name-list mac_algorithms_server_to_client) + (name-list compression_algorithms_client_to_server) + (name-list compression_algorithms_server_to_client) + (name-list languages_client_to_server) + (name-list languages_server_to_client) + (boolean first_kex_packet_follows) + (uint32 reserved)) diff --git a/ssh-transport.rkt b/ssh-transport.rkt new file mode 100644 index 0000000..d507033 --- /dev/null +++ b/ssh-transport.rkt @@ -0,0 +1,138 @@ +#lang racket/base + +(require (planet tonyg/bitsyntax)) +(require (planet vyzo/crypto:2:3)) + +(require rackunit) +(require "safe-io.rkt") +(require "ssh-numbers.rkt") +(require "ssh-message-types.rkt") + +(define identification-recogniser #rx"^SSH-") +(define (identification-line? str) + (regexp-match identification-recogniser str)) + +(define required-server-identification-regex (make-parameter #rx"^SSH-2\\.0-.*")) + +(define client-preamble-lines (make-parameter '())) +(define client-identification-string (make-parameter "SSH-2.0-RacketSSH_0.0")) + +(define (send-preamble-and-identification! out) + (for-each (lambda (line) + (when (identification-line? line) + (error 'ssh-session + "Client preamble includes forbidden line ~v" + line)) + (display line out) + (display "\r\n" out)) + (client-preamble-lines)) + (display (client-identification-string) out) + (display "\r\n" out) + (flush-output out)) + +(define (read-preamble-and-identification! in) + (let ((line (read-line-limited in 253))) ;; 255 incl CRLF + (when (eof-object? line) + (error 'ssh-session "EOF while reading connection preamble")) + (if (identification-line? line) + line + (read-preamble-and-identification! in)))) + +(define (check-packet-length! actual-length limit block-size) + (when (> actual-length limit) + (error 'check-packet-length! + "Packet of length ~v is longer than packet limit ~v" + actual-length + limit)) + (when (not (zero? (modulo actual-length block-size))) + (error 'check-packet-length! + "Packet of length ~v is not a multiple of block size ~v" + actual-length + block-size))) + +(define (read-bytes/decrypt count in decryptor) + (let ((encrypted (read-bytes count in))) + (if (eof-object? encrypted) + encrypted + (decryptor encrypted)))) + +;; uint32 packet_length +;; byte padding_length +;; byte[n1] payload; n1 = packet_length - padding_length - 1 +;; byte[n2] random padding; n2 = padding_length +;; byte[m] mac (Message Authentication Code - MAC); m = mac_length +(define (read-packet in + cipher + sequence-number + check-mac! + packet-size-limit) + (define first-block-size (if cipher (cipher-block-size cipher) 8)) + (define subsequent-block-size (if cipher first-block-size 1)) + (define decryptor (if cipher (lambda (blocks) (cipher-update! cipher blocks)) values)) + (define first-block (read-bytes/decrypt first-block-size in decryptor)) + (if (eof-object? first-block) + first-block + (let* ((packet-length (integer-bytes->integer first-block #f #t 0 4))) + (check-packet-length! packet-length packet-size-limit subsequent-block-size) + (let* ((padding-length (bytes-ref first-block 4)) + (payload-length (- packet-length padding-length 1)) + (remaining-to-read (- packet-length (bytes-length first-block)))) + (define (read-packet-trailer packet) + (check-mac! sequence-number packet in) + (subbytes packet 5 (+ 5 payload-length))) + (if (positive? remaining-to-read) + (let ((trailing-blocks (read-bytes/decrypt remaining-to-read in decryptor))) + (if (eof-object? trailing-blocks) + trailing-blocks + (read-packet-trailer (bytes-append first-block trailing-blocks)))) + (read-packet-trailer first-block)))))) + +(define (null-mac-checker sequence-number packet in) + 'ok) + +(define (ssh-session in out seed packet-handler) + (send-preamble-and-identification! out) + (let ((server-identification-string (read-preamble-and-identification! in))) + ;; Each identification string is both a cleartext indicator that + ;; we've reached some notion of the right place and also input to + ;; the hash function used during D-H key exchange. + (when (not (regexp-match (required-server-identification-regex) + server-identification-string)) + (error 'ssh-session "Received invalid identification ~v from peer" + server-identification-string)) + + (define (write-packet! . args) + (error 'write-packet "Unimplemented")) + + (define result + (let loop ((seed seed) + (sequence-number 0) + (cipher #f) + (check-mac! null-mac-checker)) + (let ((packet (read-packet in cipher sequence-number check-mac! 65536))) + (if (eof-object? packet) + seed + (packet-handler seed + packet + write-packet! + (lambda (new-seed) (loop new-seed + (+ sequence-number 1) + cipher + check-mac!)) + (lambda args + (error 'rekey "Unimplemented"))))))) + + (close-input-port in) + (close-output-port out) + result)) + +(require racket/tcp) +(require racket/pretty) +(define (t) + (let-values (((i o) (tcp-connect "localhost" 22))) + (ssh-session i o #f + (lambda (seed packet write-packet! continue-reading rekey) + (pretty-print packet) + (pretty-print (ssh-message-decode packet)) + (continue-reading seed))))) +(t)