Beginnings of codec and transport.

This commit is contained in:
Tony Garnock-Jones 2011-08-11 00:25:28 -04:00
parent 19774e7bf1
commit 09a45e12a2
2 changed files with 306 additions and 0 deletions

168
ssh-message-types.rkt Normal file
View File

@ -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))

138
ssh-transport.rkt Normal file
View File

@ -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)