(Re)keying. Oakley DH groups. AES CTR mode.
This commit is contained in:
parent
6f7e6ea573
commit
6c860ed727
|
@ -0,0 +1,64 @@
|
||||||
|
#lang racket/base
|
||||||
|
|
||||||
|
;; Provide AES CTR mode, since OpenSSL's EVP support for AES CTR mode
|
||||||
|
;; is still ifdef'd out.
|
||||||
|
|
||||||
|
(provide start-aes-ctr
|
||||||
|
aes-ctr-process!)
|
||||||
|
|
||||||
|
(require ffi/unsafe)
|
||||||
|
(require ffi/unsafe/define)
|
||||||
|
(require openssl/libcrypto)
|
||||||
|
|
||||||
|
(define _AES_KEY-pointer _pointer)
|
||||||
|
|
||||||
|
(define AES_BLOCK_SIZE 16)
|
||||||
|
(define sizeof-AES_KEY 244) ;; TODO: figure out a good way to get this
|
||||||
|
;; from the header file or the library
|
||||||
|
;; itself
|
||||||
|
|
||||||
|
(define-ffi-definer define-crypto libcrypto
|
||||||
|
#:default-make-fail make-not-available)
|
||||||
|
|
||||||
|
(define-crypto AES_set_encrypt_key (_fun _pointer _int _AES_KEY-pointer -> _int))
|
||||||
|
;;(define-crypto AES_set_decrypt_key (_fun _pointer _int _AES_KEY-pointer -> _int))
|
||||||
|
|
||||||
|
(define-crypto AES_ctr128_encrypt
|
||||||
|
(_fun _pointer ;; in
|
||||||
|
_pointer ;; out
|
||||||
|
_long ;; length
|
||||||
|
_AES_KEY-pointer ;; key
|
||||||
|
_pointer ;; ivec, AES_BLOCK_SIZE bytes
|
||||||
|
_pointer ;; ecount_buf, AES_BLOCK_SIZE bytes
|
||||||
|
_pointer ;; int pointer, the "num" field of the ongoing state (??)
|
||||||
|
-> _void))
|
||||||
|
|
||||||
|
(struct aes-ctr-state (key ivec ecount num) #:transparent)
|
||||||
|
|
||||||
|
(define (start-aes-ctr key initialization-vector)
|
||||||
|
(let ((key-buffer (malloc sizeof-AES_KEY))
|
||||||
|
(ivec (make-bytes AES_BLOCK_SIZE))
|
||||||
|
(ecount (make-bytes AES_BLOCK_SIZE))
|
||||||
|
(num (make-bytes (ctype-sizeof _int))))
|
||||||
|
(AES_set_encrypt_key key
|
||||||
|
(* 8 (bytes-length key)) ;; measured in bits
|
||||||
|
key-buffer)
|
||||||
|
(bytes-copy! ivec 0 initialization-vector 0 AES_BLOCK_SIZE)
|
||||||
|
(bytes-fill! ecount 0)
|
||||||
|
(bytes-fill! num 0)
|
||||||
|
(aes-ctr-state key-buffer
|
||||||
|
ivec
|
||||||
|
ecount
|
||||||
|
num)))
|
||||||
|
|
||||||
|
(define (aes-ctr-process! state input-block)
|
||||||
|
(define block-length (bytes-length input-block))
|
||||||
|
(define output-block (make-bytes block-length))
|
||||||
|
(AES_ctr128_encrypt input-block
|
||||||
|
output-block
|
||||||
|
block-length
|
||||||
|
(aes-ctr-state-key state)
|
||||||
|
(aes-ctr-state-ivec state)
|
||||||
|
(aes-ctr-state-ecount state)
|
||||||
|
(aes-ctr-state-num state))
|
||||||
|
output-block)
|
|
@ -0,0 +1,7 @@
|
||||||
|
-----BEGIN DH PARAMETERS-----
|
||||||
|
MIIBCAKCAQEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxObIlFKCHmO
|
||||||
|
NATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjftawv/XLb0Brft7jhr
|
||||||
|
+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXTmmkWP6j9JM9fg2VdI9yjrZYc
|
||||||
|
YvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhghfDKQXkYuNs474553LBgOhgObJ4Oi7Aei
|
||||||
|
j7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq5RXSJhiY+gUQFXKOWoqsqmj//////////wIBAg==
|
||||||
|
-----END DH PARAMETERS-----
|
|
@ -0,0 +1,5 @@
|
||||||
|
-----BEGIN DH PARAMETERS-----
|
||||||
|
MIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE
|
||||||
|
3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/ta
|
||||||
|
iZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgEC
|
||||||
|
-----END DH PARAMETERS-----
|
|
@ -0,0 +1,28 @@
|
||||||
|
#lang racket/base
|
||||||
|
|
||||||
|
;; Construct Oakley MODP Diffie-Hellman groups from RFCs 2409 and 3526.
|
||||||
|
|
||||||
|
(provide dh:oakley-group-2
|
||||||
|
dh:oakley-group-14)
|
||||||
|
|
||||||
|
;;(require (planet vyzo/crypto))
|
||||||
|
(require (planet vyzo/crypto/dh))
|
||||||
|
(require (only-in net/base64 base64-decode))
|
||||||
|
|
||||||
|
(define dh:oakley-group-2
|
||||||
|
(make-!dh
|
||||||
|
1024
|
||||||
|
(base64-decode
|
||||||
|
#"MIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE
|
||||||
|
3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/ta
|
||||||
|
iZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgEC")))
|
||||||
|
|
||||||
|
(define dh:oakley-group-14
|
||||||
|
(make-!dh
|
||||||
|
2048
|
||||||
|
(base64-decode
|
||||||
|
#"MIIBCAKCAQEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxObIlFKCHmO
|
||||||
|
NATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjftawv/XLb0Brft7jhr
|
||||||
|
+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXTmmkWP6j9JM9fg2VdI9yjrZYc
|
||||||
|
YvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhghfDKQXkYuNs474553LBgOhgObJ4Oi7Aei
|
||||||
|
j7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq5RXSJhiY+gUQFXKOWoqsqmj//////////wIBAg==")))
|
|
@ -14,7 +14,19 @@
|
||||||
(provide ssh-message-decode
|
(provide ssh-message-decode
|
||||||
ssh-message-encode)
|
ssh-message-encode)
|
||||||
|
|
||||||
(provide (struct-out ssh-msg-kexinit))
|
(provide t:boolean
|
||||||
|
t:string
|
||||||
|
t:mpint
|
||||||
|
mpint-width
|
||||||
|
t:name-list)
|
||||||
|
|
||||||
|
(provide (struct-out ssh-msg-kexinit)
|
||||||
|
(struct-out ssh-msg-kexdh-init)
|
||||||
|
(struct-out ssh-msg-kexdh-reply)
|
||||||
|
(struct-out ssh-msg-disconnect)
|
||||||
|
(struct-out ssh-msg-unimplemented)
|
||||||
|
(struct-out ssh-msg-newkeys)
|
||||||
|
(struct-out ssh-msg-ignore))
|
||||||
|
|
||||||
(define decoder-map (make-hasheqv))
|
(define decoder-map (make-hasheqv))
|
||||||
|
|
||||||
|
@ -22,13 +34,11 @@
|
||||||
(make-struct-type-property 'ssh-message-encoder))
|
(make-struct-type-property 'ssh-message-encoder))
|
||||||
|
|
||||||
(define (ssh-message-decode packet)
|
(define (ssh-message-decode packet)
|
||||||
(let ((type-code (bytes-ref packet 0)))
|
(define type-code (bytes-ref packet 0))
|
||||||
((hash-ref decoder-map
|
(define decoder (hash-ref decoder-map type-code #f))
|
||||||
type-code
|
(if decoder
|
||||||
(lambda () (error 'ssh-message-decode
|
(decoder packet)
|
||||||
"Unknown message packet type number ~v"
|
#f))
|
||||||
type-code)))
|
|
||||||
packet)))
|
|
||||||
|
|
||||||
(define (ssh-message-encode m)
|
(define (ssh-message-encode m)
|
||||||
(bit-string->bytes ((ssh-message-encoder m) m)))
|
(bit-string->bytes ((ssh-message-encoder m) m)))
|
||||||
|
@ -187,3 +197,24 @@
|
||||||
(name-list languages_server_to_client)
|
(name-list languages_server_to_client)
|
||||||
(boolean first_kex_packet_follows)
|
(boolean first_kex_packet_follows)
|
||||||
(uint32 reserved))
|
(uint32 reserved))
|
||||||
|
|
||||||
|
(define-ssh-message-type ssh-msg-kexdh-init SSH_MSG_KEXDH_INIT
|
||||||
|
(mpint e))
|
||||||
|
|
||||||
|
(define-ssh-message-type ssh-msg-kexdh-reply SSH_MSG_KEXDH_REPLY
|
||||||
|
(string host-key)
|
||||||
|
(mpint f)
|
||||||
|
(string h-signature))
|
||||||
|
|
||||||
|
(define-ssh-message-type ssh-msg-disconnect SSH_MSG_DISCONNECT
|
||||||
|
(uint32 reason-code)
|
||||||
|
(string description)
|
||||||
|
(string language-tag))
|
||||||
|
|
||||||
|
(define-ssh-message-type ssh-msg-unimplemented SSH_MSG_UNIMPLEMENTED
|
||||||
|
(uint32 sequence-number))
|
||||||
|
|
||||||
|
(define-ssh-message-type ssh-msg-newkeys SSH_MSG_NEWKEYS)
|
||||||
|
|
||||||
|
(define-ssh-message-type ssh-msg-ignore SSH_MSG_IGNORE
|
||||||
|
(string data))
|
||||||
|
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
(require "mapping.rkt")
|
(require "mapping.rkt")
|
||||||
|
|
||||||
|
(provide (all-defined-out)) ;; I know, I know
|
||||||
|
|
||||||
;; Assigned numbers, from RFCs 4250 and 4344.
|
;; Assigned numbers, from RFCs 4250 and 4344.
|
||||||
|
|
||||||
;; Protocol packets have message numbers in the range 1 to 255. These
|
;; Protocol packets have message numbers in the range 1 to 255. These
|
||||||
|
@ -34,7 +36,21 @@
|
||||||
;;
|
;;
|
||||||
;; 192 to 255 Local extensions
|
;; 192 to 255 Local extensions
|
||||||
|
|
||||||
(provide (all-defined-out)) ;; I know, I know
|
(define (ssh-msg-type-transport-layer? msg-type) (>= 49 msg-type 1))
|
||||||
|
(define (ssh-msg-type-transport-layer-generic? msg-type) (>= 19 msg-type 1))
|
||||||
|
(define (ssh-msg-type-transport-layer-algorithm-negotiation? msg-type) (>= 29 msg-type 20))
|
||||||
|
(define (ssh-msg-type-transport-layer-key-exchange? msg-type) (>= 49 msg-type 30))
|
||||||
|
|
||||||
|
(define (ssh-msg-type-authentication? msg-type) (>= 79 msg-type 50))
|
||||||
|
(define (ssh-msg-type-authentication-generic? msg-type) (>= 59 msg-type 50))
|
||||||
|
(define (ssh-msg-type-authentication-specific? msg-type) (>= 79 msg-type 60))
|
||||||
|
|
||||||
|
(define (ssh-msg-type-connection? msg-type) (>= 127 msg-type 80))
|
||||||
|
(define (ssh-msg-type-connection-generic? msg-type) (>= 89 msg-type 80))
|
||||||
|
(define (ssh-msg-type-connection-channel? msg-type) (>= 127 msg-type 90))
|
||||||
|
|
||||||
|
(define (ssh-msg-type-client? msg-type) (>= 191 msg-type 128))
|
||||||
|
(define (ssh-msg-type-local? msg-type) (>= 255 msg-type 192))
|
||||||
|
|
||||||
;;; SSH message type IDs.
|
;;; SSH message type IDs.
|
||||||
;;
|
;;
|
||||||
|
@ -48,6 +64,8 @@
|
||||||
(define SSH_MSG_SERVICE_ACCEPT 6) ;[SSH-TRANS]
|
(define SSH_MSG_SERVICE_ACCEPT 6) ;[SSH-TRANS]
|
||||||
(define SSH_MSG_KEXINIT 20) ;[SSH-TRANS]
|
(define SSH_MSG_KEXINIT 20) ;[SSH-TRANS]
|
||||||
(define SSH_MSG_NEWKEYS 21) ;[SSH-TRANS]
|
(define SSH_MSG_NEWKEYS 21) ;[SSH-TRANS]
|
||||||
|
(define SSH_MSG_KEXDH_INIT 30) ;RFC 4253 errata
|
||||||
|
(define SSH_MSG_KEXDH_REPLY 31) ;RFC 4253 errata
|
||||||
(define SSH_MSG_USERAUTH_REQUEST 50) ;[SSH-USERAUTH]
|
(define SSH_MSG_USERAUTH_REQUEST 50) ;[SSH-USERAUTH]
|
||||||
(define SSH_MSG_USERAUTH_FAILURE 51) ;[SSH-USERAUTH]
|
(define SSH_MSG_USERAUTH_FAILURE 51) ;[SSH-USERAUTH]
|
||||||
(define SSH_MSG_USERAUTH_SUCCESS 52) ;[SSH-USERAUTH]
|
(define SSH_MSG_USERAUTH_SUCCESS 52) ;[SSH-USERAUTH]
|
||||||
|
|
|
@ -2,22 +2,80 @@
|
||||||
|
|
||||||
(require (planet tonyg/bitsyntax))
|
(require (planet tonyg/bitsyntax))
|
||||||
(require (planet vyzo/crypto:2:3))
|
(require (planet vyzo/crypto:2:3))
|
||||||
|
(require (planet vyzo/crypto/util)) ;; hex, unhex
|
||||||
|
(require racket/port)
|
||||||
|
(require racket/bool)
|
||||||
|
|
||||||
(require rackunit)
|
(require rackunit)
|
||||||
|
(require "aes-ctr.rkt")
|
||||||
(require "safe-io.rkt")
|
(require "safe-io.rkt")
|
||||||
(require "ssh-numbers.rkt")
|
(require "ssh-numbers.rkt")
|
||||||
(require "ssh-message-types.rkt")
|
(require "ssh-message-types.rkt")
|
||||||
|
(require "oakley-groups.rkt")
|
||||||
|
|
||||||
|
;; An exn:fail:contract:protocol, when thrown by the transport (TODO:
|
||||||
|
;; clarify scope of this behaviour) will cause a SSH_MSG_DISCONNECT to
|
||||||
|
;; be sent to the remote party with the included reason code, using
|
||||||
|
;; the exn-message as the description. The local-info field is useful
|
||||||
|
;; information for diagnosing problems known to the local stack that
|
||||||
|
;; should not be transmitted to the remote party. For example, upon
|
||||||
|
;; detection of a MAC failure, it might be useful to know the expected
|
||||||
|
;; and actual MACs for debugging, but they should not be sent over the
|
||||||
|
;; wire because we could be experiencing some kind of attack.
|
||||||
|
(struct exn:fail:contract:protocol exn:fail:contract
|
||||||
|
(reason-code local-info originated-at-peer?)
|
||||||
|
#:transparent)
|
||||||
|
|
||||||
|
;; A RekeyState is one of
|
||||||
|
;; - a (rekey-wait Number Number), representing a time or
|
||||||
|
;; transfer-amount by which rekeying should be started
|
||||||
|
;; - a (rekey-local SshMsgKexinit), when we've sent our local
|
||||||
|
;; algorithm list and are waiting for the other party to send theirs
|
||||||
|
;; - a (rekey-in-progress KeyExchangeState), when both our local
|
||||||
|
;; algorithm list has been sent and the remote one has arrived and the
|
||||||
|
;; actual key exchange has begun
|
||||||
|
(struct rekey-wait (deadline threshold-bytes) #:transparent)
|
||||||
|
(struct rekey-local (local-algorithms) #:transparent)
|
||||||
|
(struct rekey-in-progress (state) #:transparent)
|
||||||
|
|
||||||
|
(struct stream-state (port
|
||||||
|
cipher
|
||||||
|
cipher-description
|
||||||
|
sequence-number
|
||||||
|
bytes-transferred
|
||||||
|
hmac
|
||||||
|
hmac-description
|
||||||
|
packet-size-limit)
|
||||||
|
#:transparent)
|
||||||
|
|
||||||
|
;; Generic inputs into the exchange-hash part of key
|
||||||
|
;; exchange. Diffie-Hellman uses these fields along with the host key,
|
||||||
|
;; the exchange values, and the shared secret to get the final hash.
|
||||||
|
(struct exchange-hash-info (client-id
|
||||||
|
server-id
|
||||||
|
client-kexinit-bytes
|
||||||
|
server-kexinit-bytes)
|
||||||
|
#: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)
|
||||||
|
|
||||||
(define identification-recogniser #rx"^SSH-")
|
(define identification-recogniser #rx"^SSH-")
|
||||||
(define (identification-line? str)
|
(define (identification-line? str)
|
||||||
(regexp-match identification-recogniser str))
|
(regexp-match identification-recogniser str))
|
||||||
|
|
||||||
(define required-server-identification-regex (make-parameter #rx"^SSH-2\\.0-.*"))
|
(define required-peer-identification-regex (make-parameter #rx"^SSH-2\\.0-.*"))
|
||||||
|
|
||||||
(define client-preamble-lines (make-parameter '()))
|
(define client-preamble-lines (make-parameter '()))
|
||||||
(define client-identification-string (make-parameter "SSH-2.0-RacketSSH_0.0"))
|
(define client-identification-string (make-parameter "SSH-2.0-RacketSSH_0.0"))
|
||||||
|
|
||||||
(define (send-preamble-and-identification! out)
|
(define (send-preamble-and-identification! out)
|
||||||
|
(let ((my-id (client-identification-string)))
|
||||||
(for-each (lambda (line)
|
(for-each (lambda (line)
|
||||||
(when (identification-line? line)
|
(when (identification-line? line)
|
||||||
(error 'ssh-session
|
(error 'ssh-session
|
||||||
|
@ -26,9 +84,10 @@
|
||||||
(display line out)
|
(display line out)
|
||||||
(display "\r\n" out))
|
(display "\r\n" out))
|
||||||
(client-preamble-lines))
|
(client-preamble-lines))
|
||||||
(display (client-identification-string) out)
|
(display my-id out)
|
||||||
(display "\r\n" out)
|
(display "\r\n" out)
|
||||||
(flush-output out))
|
(flush-output out)
|
||||||
|
my-id))
|
||||||
|
|
||||||
(define (read-preamble-and-identification! in)
|
(define (read-preamble-and-identification! in)
|
||||||
(let ((line (read-line-limited in 253))) ;; 255 incl CRLF
|
(let ((line (read-line-limited in 253))) ;; 255 incl CRLF
|
||||||
|
@ -38,102 +97,679 @@
|
||||||
line
|
line
|
||||||
(read-preamble-and-identification! in))))
|
(read-preamble-and-identification! in))))
|
||||||
|
|
||||||
|
(define (disconnect-with-error reason-code format-string . args)
|
||||||
|
(apply disconnect-with-error/local-info '() reason-code format-string args))
|
||||||
|
|
||||||
|
(define (disconnect-with-error/local-info local-info reason-code format-string . args)
|
||||||
|
(let ((message (apply format format-string args)))
|
||||||
|
(raise (exn:fail:contract:protocol message
|
||||||
|
(current-continuation-marks)
|
||||||
|
reason-code
|
||||||
|
local-info
|
||||||
|
#f))))
|
||||||
|
|
||||||
(define (check-packet-length! actual-length limit block-size)
|
(define (check-packet-length! actual-length limit block-size)
|
||||||
(when (> actual-length limit)
|
(when (> actual-length limit)
|
||||||
(error 'check-packet-length!
|
(disconnect-with-error 0 ;; TODO: better reason code?
|
||||||
"Packet of length ~v is longer than packet limit ~v"
|
"Packet of length ~v is longer than packet limit ~v"
|
||||||
actual-length
|
actual-length
|
||||||
limit))
|
limit))
|
||||||
(when (not (zero? (modulo actual-length block-size)))
|
(when (not (zero? (modulo (+ actual-length 4) block-size)))
|
||||||
(error 'check-packet-length!
|
;; 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 SSH_DISCONNECT_PROTOCOL_ERROR
|
||||||
"Packet of length ~v is not a multiple of block size ~v"
|
"Packet of length ~v is not a multiple of block size ~v"
|
||||||
actual-length
|
actual-length
|
||||||
block-size)))
|
block-size)))
|
||||||
|
|
||||||
(define (read-bytes/decrypt count in decryptor)
|
(define (read-bytes/timeout count in timeout)
|
||||||
(let ((encrypted (read-bytes count in)))
|
(sync/timeout timeout (read-bytes-evt count in)))
|
||||||
(if (eof-object? encrypted)
|
|
||||||
encrypted
|
(define (read-bytes/decrypt count in timeout decryptor)
|
||||||
(decryptor encrypted))))
|
(let ((encrypted (read-bytes/timeout count in timeout)))
|
||||||
|
(cond
|
||||||
|
((false? encrypted) #f)
|
||||||
|
((eof-object? encrypted) eof)
|
||||||
|
(else (decryptor encrypted)))))
|
||||||
|
|
||||||
|
(define default-packet-limit (make-parameter 65536))
|
||||||
|
(define rekey-interval (make-parameter 5)) ;;3600))
|
||||||
|
(define rekey-volume (make-parameter 1000000000))
|
||||||
|
(define inter-packet-timeout (make-parameter 1)) ;;300))
|
||||||
|
(define intra-packet-timeout (make-parameter 1)) ;;300))
|
||||||
|
|
||||||
|
(define (make-evp-cipher-entry name cipher)
|
||||||
|
(list name
|
||||||
|
(supported-cipher name
|
||||||
|
(lambda (enc? key iv)
|
||||||
|
(let ((state ((if enc? cipher-encrypt cipher-decrypt)
|
||||||
|
cipher key iv #:padding #f)))
|
||||||
|
(lambda (block)
|
||||||
|
(cipher-update! state block))))
|
||||||
|
(cipher-key-length cipher)
|
||||||
|
(cipher-block-size cipher)
|
||||||
|
(cipher-iv-length cipher))))
|
||||||
|
|
||||||
|
(define (aes-ctr-cipher-factory enc? key iv)
|
||||||
|
(let ((state (start-aes-ctr key iv)))
|
||||||
|
(lambda (block)
|
||||||
|
(aes-ctr-process! state block))))
|
||||||
|
|
||||||
|
(define (make-aes-ctr-entry name key-length)
|
||||||
|
(list name
|
||||||
|
(supported-cipher name
|
||||||
|
aes-ctr-cipher-factory
|
||||||
|
key-length
|
||||||
|
16
|
||||||
|
16)))
|
||||||
|
|
||||||
|
(define supported-crypto-algorithms
|
||||||
|
(list
|
||||||
|
(make-aes-ctr-entry 'aes128-ctr 16)
|
||||||
|
(make-aes-ctr-entry 'aes192-ctr 24)
|
||||||
|
(make-aes-ctr-entry 'aes256-ctr 32)
|
||||||
|
(make-evp-cipher-entry 'aes128-cbc cipher:aes-128-cbc)
|
||||||
|
(make-evp-cipher-entry 'aes192-cbc cipher:aes-192-cbc)
|
||||||
|
(make-evp-cipher-entry 'aes256-cbc cipher:aes-256-cbc)
|
||||||
|
(make-evp-cipher-entry '3des-cbc cipher:des-ede3)
|
||||||
|
)) ;; TODO: actually test these!
|
||||||
|
|
||||||
|
(define (make-hmac-entry name digest key-length-or-false)
|
||||||
|
(let* ((digest-length (digest-size digest))
|
||||||
|
(key-length (if (false? key-length-or-false)
|
||||||
|
digest-length
|
||||||
|
key-length-or-false)))
|
||||||
|
(list name
|
||||||
|
(supported-hmac name
|
||||||
|
(lambda (key)
|
||||||
|
(lambda (blob)
|
||||||
|
(hmac digest key blob)))
|
||||||
|
digest-length
|
||||||
|
key-length))))
|
||||||
|
|
||||||
|
(define supported-hmac-algorithms
|
||||||
|
(list (make-hmac-entry 'hmac-md5 digest:md5 #f)
|
||||||
|
(make-hmac-entry 'hmac-sha1 digest: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 (random-bytes 16)
|
||||||
|
'(diffie-hellman-group14-sha1
|
||||||
|
diffie-hellman-group1-sha1)
|
||||||
|
'(ssh-rsa ssh-dss)
|
||||||
|
crypto-names
|
||||||
|
crypto-names
|
||||||
|
mac-names
|
||||||
|
mac-names
|
||||||
|
supported-compression-algorithms
|
||||||
|
supported-compression-algorithms
|
||||||
|
'()
|
||||||
|
'()
|
||||||
|
#f
|
||||||
|
0)))))
|
||||||
|
|
||||||
|
(define (bump-sequence-number! state byte-count)
|
||||||
|
(struct-copy stream-state state
|
||||||
|
[sequence-number
|
||||||
|
(+ 1 (stream-state-sequence-number state))]
|
||||||
|
[bytes-transferred
|
||||||
|
(+ byte-count (stream-state-bytes-transferred state))]))
|
||||||
|
|
||||||
|
;; Read and decode a transport message from in-state. If it can't be
|
||||||
|
;; decoded (we don't support that message type), complain with a
|
||||||
|
;; SSH_MSG_UNIMPLEMENTED packet. Finally, return a quadruple of the
|
||||||
|
;; packet, the decoded message, the updated input state, and the
|
||||||
|
;; updated output state. May return eof or #f for end-of-file or
|
||||||
|
;; timeout, respectively, depending on error-on-eof-or-timeout.
|
||||||
|
(define (read-message in-state out-state [error-on-eof-or-timeout #t])
|
||||||
|
(let-values (((packet in-state) (read-packet in-state error-on-eof-or-timeout)))
|
||||||
|
(if (not (bytes? packet))
|
||||||
|
(values packet packet in-state out-state)
|
||||||
|
(let ((message (ssh-message-decode packet)))
|
||||||
|
(write `(received ,message)) (newline) (flush-output)
|
||||||
|
(if message
|
||||||
|
(values packet message in-state out-state)
|
||||||
|
(let ((bad-seq-num (- (stream-state-sequence-number in-state) 1)))
|
||||||
|
;; TODO: remove this debug output
|
||||||
|
(display "BAD PACKET ")
|
||||||
|
(display (hex packet))
|
||||||
|
(newline)
|
||||||
|
(flush-output)
|
||||||
|
(read-message in-state
|
||||||
|
(write-message! (ssh-msg-unimplemented bad-seq-num)
|
||||||
|
out-state)
|
||||||
|
error-on-eof-or-timeout)))))))
|
||||||
|
|
||||||
;; uint32 packet_length
|
;; uint32 packet_length
|
||||||
;; byte padding_length
|
;; byte padding_length
|
||||||
;; byte[n1] payload; n1 = packet_length - padding_length - 1
|
;; byte[n1] payload; n1 = packet_length - padding_length - 1
|
||||||
;; byte[n2] random padding; n2 = padding_length
|
;; byte[n2] random padding; n2 = padding_length
|
||||||
;; byte[m] mac (Message Authentication Code - MAC); m = mac_length
|
;; byte[m] mac (Message Authentication Code - MAC); m = mac_length
|
||||||
(define (read-packet in
|
(define (read-packet in-state error-on-eof-or-timeout)
|
||||||
cipher
|
(define cipher (stream-state-cipher in-state))
|
||||||
sequence-number
|
(define block-size (supported-cipher-block-size (stream-state-cipher-description in-state)))
|
||||||
check-mac!
|
(define in (stream-state-port in-state))
|
||||||
packet-size-limit)
|
(define first-block-size block-size)
|
||||||
(define first-block-size (if cipher (cipher-block-size cipher) 8))
|
(define subsequent-block-size (if cipher block-size 1))
|
||||||
(define subsequent-block-size (if cipher first-block-size 1))
|
(define decryptor (if cipher cipher values))
|
||||||
(define decryptor (if cipher (lambda (blocks) (cipher-update! cipher blocks)) values))
|
(define first-block (read-bytes/decrypt first-block-size in
|
||||||
(define first-block (read-bytes/decrypt first-block-size in decryptor))
|
(inter-packet-timeout) decryptor))
|
||||||
(if (eof-object? first-block)
|
(cond
|
||||||
first-block
|
((false? first-block)
|
||||||
|
(if error-on-eof-or-timeout
|
||||||
|
(disconnect-with-error SSH_DISCONNECT_CONNECTION_LOST
|
||||||
|
"Timeout waiting for a packet")
|
||||||
|
(values #f in-state)))
|
||||||
|
((eof-object? first-block)
|
||||||
|
(if error-on-eof-or-timeout
|
||||||
|
(error 'read-packet "End-of-file at the start of a packet")
|
||||||
|
(values first-block in-state)))
|
||||||
|
(else
|
||||||
(let* ((packet-length (integer-bytes->integer first-block #f #t 0 4)))
|
(let* ((packet-length (integer-bytes->integer first-block #f #t 0 4)))
|
||||||
(check-packet-length! packet-length packet-size-limit subsequent-block-size)
|
(check-packet-length! packet-length
|
||||||
|
(stream-state-packet-size-limit in-state)
|
||||||
|
subsequent-block-size)
|
||||||
(let* ((padding-length (bytes-ref first-block 4))
|
(let* ((padding-length (bytes-ref first-block 4))
|
||||||
(payload-length (- packet-length padding-length 1))
|
(payload-length (- packet-length padding-length 1))
|
||||||
(remaining-to-read (- packet-length (bytes-length first-block))))
|
(amount-of-packet-in-first-block (- (bytes-length first-block) 4)) ;; not incl length
|
||||||
|
(remaining-to-read (- packet-length amount-of-packet-in-first-block)))
|
||||||
(define (read-packet-trailer packet)
|
(define (read-packet-trailer packet)
|
||||||
(check-mac! sequence-number packet in)
|
(let ((bytes-read (+ (check-hmac! (apply-hmac (stream-state-hmac in-state)
|
||||||
(subbytes packet 5 (+ 5 payload-length)))
|
(stream-state-sequence-number in-state)
|
||||||
|
packet)
|
||||||
|
in)
|
||||||
|
packet-length)))
|
||||||
|
(values (subbytes packet 5 (+ 5 payload-length))
|
||||||
|
(bump-sequence-number! in-state bytes-read))))
|
||||||
(if (positive? remaining-to-read)
|
(if (positive? remaining-to-read)
|
||||||
(let ((trailing-blocks (read-bytes/decrypt remaining-to-read in decryptor)))
|
(let ((trailing-blocks (read-bytes/decrypt remaining-to-read in
|
||||||
(if (eof-object? trailing-blocks)
|
(intra-packet-timeout) decryptor)))
|
||||||
trailing-blocks
|
(cond
|
||||||
(read-packet-trailer (bytes-append first-block trailing-blocks))))
|
((false? trailing-blocks)
|
||||||
(read-packet-trailer first-block))))))
|
(disconnect-with-error SSH_DISCONNECT_CONNECTION_LOST
|
||||||
|
"Timeout partway through reading a packet"))
|
||||||
|
((eof-object? trailing-blocks)
|
||||||
|
(if error-on-eof-or-timeout
|
||||||
|
(error 'read-packet "End-of-file during a packet")
|
||||||
|
(values trailing-blocks in-state)))
|
||||||
|
(else
|
||||||
|
(read-packet-trailer (bytes-append first-block trailing-blocks)))))
|
||||||
|
(read-packet-trailer first-block)))))))
|
||||||
|
|
||||||
(define (null-mac-checker sequence-number packet in)
|
(define (round-up what to)
|
||||||
'ok)
|
(* to (quotient (+ what (- to 1)) to)))
|
||||||
|
|
||||||
(define (ssh-session in out seed packet-handler)
|
(check-equal? (round-up 0 8) 0)
|
||||||
(send-preamble-and-identification! out)
|
(check-equal? (round-up 1 8) 8)
|
||||||
(let ((server-identification-string (read-preamble-and-identification! in)))
|
(check-equal? (round-up 7 8) 8)
|
||||||
|
(check-equal? (round-up 8 8) 8)
|
||||||
|
(check-equal? (round-up 9 8) 16)
|
||||||
|
|
||||||
|
(define (write-message! message out-state [flush #f])
|
||||||
|
(write `(sending ,message)) (newline) (flush-output)
|
||||||
|
(write-packet! (ssh-message-encode message) out-state flush))
|
||||||
|
|
||||||
|
(define (write-packet! payload out-state flush)
|
||||||
|
(define cipher (stream-state-cipher out-state))
|
||||||
|
(define pad-block-size (supported-cipher-block-size (stream-state-cipher-description out-state)))
|
||||||
|
(define out (stream-state-port out-state))
|
||||||
|
(define encryptor (if cipher cipher values))
|
||||||
|
;; 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 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)
|
||||||
|
((random-bytes padding-length) :: binary))))
|
||||||
|
(define encrypted-packet (encryptor packet))
|
||||||
|
(define computed-hmac-bytes (apply-hmac (stream-state-hmac out-state)
|
||||||
|
(stream-state-sequence-number out-state)
|
||||||
|
packet))
|
||||||
|
(define mac-byte-count (bytes-length computed-hmac-bytes))
|
||||||
|
(write-bytes encrypted-packet out)
|
||||||
|
(when (positive? mac-byte-count)
|
||||||
|
(write-bytes computed-hmac-bytes out))
|
||||||
|
(when flush
|
||||||
|
(flush-output out))
|
||||||
|
(bump-sequence-number! out-state (+ (bytes-length encrypted-packet) mac-byte-count)))
|
||||||
|
|
||||||
|
(define (apply-hmac mac sequence-number packet)
|
||||||
|
(mac (bit-string->bytes (bit-string-append (integer->bit-string sequence-number 32 #t) packet))))
|
||||||
|
|
||||||
|
(define (check-hmac! computed-hmac-bytes in)
|
||||||
|
(define mac-byte-count (bytes-length computed-hmac-bytes))
|
||||||
|
(when (positive? mac-byte-count)
|
||||||
|
(let ((received-hmac-bytes (read-bytes/timeout mac-byte-count in
|
||||||
|
(intra-packet-timeout))))
|
||||||
|
(cond
|
||||||
|
((false? received-hmac-bytes)
|
||||||
|
(disconnect-with-error SSH_DISCONNECT_CONNECTION_LOST
|
||||||
|
"Timeout reading MAC"))
|
||||||
|
((eof-object? received-hmac-bytes)
|
||||||
|
(disconnect-with-error SSH_DISCONNECT_CONNECTION_LOST
|
||||||
|
"EOF instead of MAC"))
|
||||||
|
(else
|
||||||
|
(when (not (equal? computed-hmac-bytes received-hmac-bytes))
|
||||||
|
(disconnect-with-error/local-info `((expected-hmac ,computed-hmac-bytes)
|
||||||
|
(actual-hmac ,received-hmac-bytes))
|
||||||
|
SSH_DISCONNECT_MAC_ERROR
|
||||||
|
"Corrupt MAC"))))))
|
||||||
|
mac-byte-count)
|
||||||
|
|
||||||
|
(define null-cipher-description
|
||||||
|
(supported-cipher 'none
|
||||||
|
(lambda (enc? key iv)
|
||||||
|
(error 'null-cipher-description
|
||||||
|
"Cannot construct null cipher instance"))
|
||||||
|
0
|
||||||
|
8 ;; pseudo-block-size for packet I/O
|
||||||
|
0))
|
||||||
|
|
||||||
|
(define (null-hmac blob)
|
||||||
|
#"")
|
||||||
|
|
||||||
|
(define null-hmac-description
|
||||||
|
(supported-hmac 'none
|
||||||
|
(lambda (key)
|
||||||
|
(error 'null-hmac-description
|
||||||
|
"Cannot construct null hmac instance"))
|
||||||
|
0
|
||||||
|
0))
|
||||||
|
|
||||||
|
(define (encoded-packet-msg-type encoded-packet)
|
||||||
|
(bytes-ref encoded-packet 0))
|
||||||
|
|
||||||
|
(define (key-exchange-init? encoded-packet)
|
||||||
|
(= (encoded-packet-msg-type encoded-packet) SSH_MSG_KEXINIT))
|
||||||
|
|
||||||
|
(define (acceptable-during-key-exchange? encoded-packet)
|
||||||
|
;; See end of RFC 4253 section 7.1.
|
||||||
|
(let ((msg-type (encoded-packet-msg-type encoded-packet)))
|
||||||
|
(and (ssh-msg-type-transport-layer? msg-type)
|
||||||
|
(not (memv msg-type (list SSH_MSG_SERVICE_REQUEST
|
||||||
|
SSH_MSG_SERVICE_ACCEPT
|
||||||
|
SSH_MSG_KEXINIT))))))
|
||||||
|
|
||||||
|
(define (default-stream-state port)
|
||||||
|
(stream-state port
|
||||||
|
#f ;; cipher
|
||||||
|
null-cipher-description
|
||||||
|
0
|
||||||
|
0
|
||||||
|
null-hmac
|
||||||
|
null-hmac-description
|
||||||
|
(default-packet-limit)))
|
||||||
|
|
||||||
|
(define (ssh-session role in out seed message-handler)
|
||||||
|
(define local-identification-string (send-preamble-and-identification! out))
|
||||||
|
(with-handlers
|
||||||
|
((exn:fail? (lambda (e)
|
||||||
|
(close-input-port in)
|
||||||
|
(close-output-port out)
|
||||||
|
(raise e))))
|
||||||
|
(let ((peer-identification-string (read-preamble-and-identification! in)))
|
||||||
;; Each identification string is both a cleartext indicator that
|
;; Each identification string is both a cleartext indicator that
|
||||||
;; we've reached some notion of the right place and also input to
|
;; we've reached some notion of the right place and also input to
|
||||||
;; the hash function used during D-H key exchange.
|
;; the hash function used during D-H key exchange.
|
||||||
(when (not (regexp-match (required-server-identification-regex)
|
(when (not (regexp-match (required-peer-identification-regex)
|
||||||
server-identification-string))
|
peer-identification-string))
|
||||||
(error 'ssh-session "Received invalid identification ~v from peer"
|
(display "Invalid identification\r\n" out)
|
||||||
server-identification-string))
|
(flush-output out)
|
||||||
|
(error 'ssh-session
|
||||||
(define (write-packet! . args)
|
"Invalid peer identification string ~v"
|
||||||
(error 'write-packet "Unimplemented"))
|
peer-identification-string))
|
||||||
|
|
||||||
(define result
|
(define result
|
||||||
(let loop ((seed seed)
|
(let ((orig-in-state (default-stream-state in))
|
||||||
(sequence-number 0)
|
(orig-out-state (default-stream-state out)))
|
||||||
(cipher #f)
|
(ssh-session-loop role
|
||||||
(check-mac! null-mac-checker))
|
local-identification-string
|
||||||
(let ((packet (read-packet in cipher sequence-number check-mac! 65536)))
|
peer-identification-string
|
||||||
(if (eof-object? packet)
|
message-handler
|
||||||
|
#f
|
||||||
seed
|
seed
|
||||||
(packet-handler seed
|
(rekey-in-seconds-or-bytes -1 -1 orig-in-state orig-out-state)
|
||||||
packet
|
orig-in-state
|
||||||
write-packet!
|
orig-out-state)))
|
||||||
(lambda (new-seed) (loop new-seed
|
|
||||||
(bitwise-and (+ sequence-number 1)
|
|
||||||
#xffffffff)
|
|
||||||
cipher
|
|
||||||
check-mac!))
|
|
||||||
(lambda args
|
|
||||||
(error 'rekey "Unimplemented")))))))
|
|
||||||
|
|
||||||
(close-input-port in)
|
(close-input-port in)
|
||||||
(close-output-port out)
|
(close-output-port out)
|
||||||
result))
|
result)))
|
||||||
|
|
||||||
|
(define (rekey-in-seconds-or-bytes delta-seconds delta-bytes in-state out-state)
|
||||||
|
(rekey-wait (+ (current-seconds) delta-seconds)
|
||||||
|
(+ (stream-state-bytes-transferred in-state)
|
||||||
|
(stream-state-bytes-transferred out-state)
|
||||||
|
delta-bytes)))
|
||||||
|
|
||||||
|
(define (time-to-rekey? rekey in-state out-state)
|
||||||
|
(and (rekey-wait? rekey)
|
||||||
|
(or (>= (current-seconds) (rekey-wait-deadline rekey))
|
||||||
|
(>= (+ (stream-state-bytes-transferred in-state)
|
||||||
|
(stream-state-bytes-transferred out-state))
|
||||||
|
(rekey-wait-threshold-bytes rekey)))))
|
||||||
|
|
||||||
|
(define (maybe-send-disconnect-message! e out-state)
|
||||||
|
(if (exn:fail:contract:protocol-originated-at-peer? e)
|
||||||
|
out-state
|
||||||
|
(write-message! (ssh-msg-disconnect (exn:fail:contract:protocol-reason-code e)
|
||||||
|
(string->bytes/utf-8 (exn-message e))
|
||||||
|
#"")
|
||||||
|
out-state
|
||||||
|
#t)))
|
||||||
|
|
||||||
|
(define (write-messages! outbound-messages out-state)
|
||||||
|
(let ((final-state (foldl write-message! out-state outbound-messages)))
|
||||||
|
(flush-output (stream-state-port final-state))
|
||||||
|
final-state))
|
||||||
|
|
||||||
|
(define (ssh-session-loop role local-id remote-id message-handler
|
||||||
|
session-id
|
||||||
|
seed rekey in-state out-state)
|
||||||
|
(let loop ((seed seed)
|
||||||
|
(rekey rekey)
|
||||||
|
(in-state in-state)
|
||||||
|
(out-state out-state))
|
||||||
|
(with-handlers
|
||||||
|
((exn:fail:contract:protocol? (lambda (e)
|
||||||
|
(maybe-send-disconnect-message! e out-state)
|
||||||
|
(raise e))))
|
||||||
|
(if (time-to-rekey? rekey in-state out-state)
|
||||||
|
(let ((algs ((local-algorithm-list))))
|
||||||
|
(loop seed
|
||||||
|
(rekey-local algs)
|
||||||
|
in-state
|
||||||
|
(write-message! algs out-state #t)))
|
||||||
|
(let-values (((packet message in-state out-state)
|
||||||
|
(read-message in-state out-state #f)))
|
||||||
|
(cond
|
||||||
|
((eof-object? packet) seed)
|
||||||
|
((false? packet)
|
||||||
|
;; Timeout waiting for a message.
|
||||||
|
(loop seed rekey in-state out-state))
|
||||||
|
((key-exchange-init? packet)
|
||||||
|
(if (rekey-in-progress? rekey)
|
||||||
|
(disconnect-with-error SSH_DISCONNECT_PROTOCOL_ERROR
|
||||||
|
"Received SSH_MSG_KEXINIT during ongoing key exchange")
|
||||||
|
(let* ((algs (if (rekey-local? rekey)
|
||||||
|
(rekey-local-local-algorithms rekey)
|
||||||
|
((local-algorithm-list))))
|
||||||
|
(encoded-algs (ssh-message-encode algs))
|
||||||
|
(out-state (if (rekey-wait? rekey)
|
||||||
|
(write-packet! encoded-algs out-state #t)
|
||||||
|
out-state)))
|
||||||
|
(start-key-exchange session-id ;; may be #f, in which case will change below
|
||||||
|
role
|
||||||
|
local-id
|
||||||
|
encoded-algs
|
||||||
|
algs
|
||||||
|
remote-id
|
||||||
|
packet
|
||||||
|
message
|
||||||
|
in-state
|
||||||
|
out-state
|
||||||
|
(lambda (session-id in-state out-state)
|
||||||
|
(ssh-session-loop
|
||||||
|
role
|
||||||
|
local-id
|
||||||
|
remote-id
|
||||||
|
message-handler
|
||||||
|
session-id ;; just in case it changed
|
||||||
|
seed
|
||||||
|
(rekey-in-seconds-or-bytes (rekey-interval)
|
||||||
|
(rekey-volume)
|
||||||
|
in-state
|
||||||
|
out-state)
|
||||||
|
in-state
|
||||||
|
(write-message! (ssh-msg-ignore #"hello world")
|
||||||
|
out-state #t)))))))
|
||||||
|
((and (rekey-in-progress? rekey)
|
||||||
|
(not (acceptable-during-key-exchange? packet)))
|
||||||
|
(disconnect-with-error SSH_DISCONNECT_PROTOCOL_ERROR
|
||||||
|
"Unacceptable packet type ~v"
|
||||||
|
(encoded-packet-msg-type packet)))
|
||||||
|
(else
|
||||||
|
(message-handler seed
|
||||||
|
message
|
||||||
|
(lambda (outbound-messages new-seed)
|
||||||
|
(loop new-seed
|
||||||
|
rekey
|
||||||
|
in-state
|
||||||
|
(write-messages! outbound-messages out-state)))))))))))
|
||||||
|
|
||||||
|
(define (best-result getter client-algs server-algs)
|
||||||
|
(define client-list0 (getter client-algs))
|
||||||
|
(define server-list (getter server-algs))
|
||||||
|
(let loop ((client-list client-list0))
|
||||||
|
(cond
|
||||||
|
((null? client-list) (disconnect-with-error/local-info
|
||||||
|
`((client-list ,client-list0)
|
||||||
|
(server-list ,server-list))
|
||||||
|
SSH_DISCONNECT_KEY_EXCHANGE_FAILED
|
||||||
|
"Could not agree on a suitable algorithm for ~v"
|
||||||
|
getter))
|
||||||
|
((memq (car client-list) server-list) (car client-list))
|
||||||
|
(else (loop (cdr client-list))))))
|
||||||
|
|
||||||
|
(define (apply-negotiated-options state role outbound derive-key
|
||||||
|
c2s-enc s2c-enc
|
||||||
|
c2s-mac s2c-mac
|
||||||
|
c2s-zip s2c-zip)
|
||||||
|
;; TODO: zip
|
||||||
|
;; TODO: make this less ugly. Compute all the keys, select just the ones we need afterward?
|
||||||
|
(define c2s (case role
|
||||||
|
((client) outbound)
|
||||||
|
((server) (not 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))
|
||||||
|
(struct-copy stream-state state
|
||||||
|
[cipher (cond
|
||||||
|
((assq enc supported-crypto-algorithms) =>
|
||||||
|
(lambda (entry)
|
||||||
|
(define c (cadr entry))
|
||||||
|
(define key (derive-key (if c2s #"C" #"D")
|
||||||
|
(supported-cipher-key-length c)))
|
||||||
|
(define iv (derive-key (if c2s #"A" #"B")
|
||||||
|
(supported-cipher-iv-length c)))
|
||||||
|
(define factory (supported-cipher-factory c))
|
||||||
|
;;(pretty-print `(,role ,(if c2s 'c2s 's2c) (key ,(hex key)) (iv ,(hex iv))))
|
||||||
|
(factory outbound key iv)))
|
||||||
|
(else (disconnect-with-error SSH_DISCONNECT_KEY_EXCHANGE_FAILED
|
||||||
|
"Could not find driver for encryption algorithm ~v"
|
||||||
|
enc)))]
|
||||||
|
[hmac (cond
|
||||||
|
((assq mac supported-hmac-algorithms) =>
|
||||||
|
(lambda (entry)
|
||||||
|
(define h (cadr entry))
|
||||||
|
(define factory (supported-hmac-factory h))
|
||||||
|
(define key (derive-key (if c2s #"E" #"F")
|
||||||
|
(supported-hmac-key-length h)))
|
||||||
|
;;(pretty-print `(,role ,(if c2s 'c2s 's2c) (key ,(hex key))))
|
||||||
|
(factory key)))
|
||||||
|
(else (disconnect-with-error SSH_DISCONNECT_KEY_EXCHANGE_FAILED
|
||||||
|
"Could not find driver for HMAC algorithm ~v"
|
||||||
|
mac)))]))
|
||||||
|
|
||||||
|
(define (start-key-exchange old-session-id ;; bytes or #f
|
||||||
|
role ;; 'client or 'server
|
||||||
|
local-id ;; string
|
||||||
|
encoded-local-algs ;; bytes, an encoded ssh-msg-kexinit
|
||||||
|
local-algs ;; ssh-msg-kexinit
|
||||||
|
remote-id ;; string
|
||||||
|
encoded-remote-algs ;; bytes, an encoded ssh-msg-kexinit
|
||||||
|
remote-algs ;; ssh-msg-kexinit
|
||||||
|
in-state
|
||||||
|
out-state
|
||||||
|
finish-key-exchange)
|
||||||
|
(when (not (memq role '(client server)))
|
||||||
|
(error 'start-key-exchange "Illegal role ~v, must be either 'client or 'server" role))
|
||||||
|
|
||||||
|
(define c (case role ((client) local-algs) ((server) remote-algs)))
|
||||||
|
(define s (case role ((client) remote-algs) ((server) local-algs)))
|
||||||
|
|
||||||
|
(define kex-alg (best-result ssh-msg-kexinit-kex_algorithms c s))
|
||||||
|
(define host-key-alg (best-result ssh-msg-kexinit-server_host_key_algorithms c s))
|
||||||
|
(define c2s-enc (best-result ssh-msg-kexinit-encryption_algorithms_client_to_server c s))
|
||||||
|
(define s2c-enc (best-result ssh-msg-kexinit-encryption_algorithms_server_to_client c s))
|
||||||
|
(define c2s-mac (best-result ssh-msg-kexinit-mac_algorithms_client_to_server c s))
|
||||||
|
(define s2c-mac (best-result ssh-msg-kexinit-mac_algorithms_server_to_client c s))
|
||||||
|
(define c2s-zip (best-result ssh-msg-kexinit-compression_algorithms_client_to_server c s))
|
||||||
|
(define s2c-zip (best-result ssh-msg-kexinit-compression_algorithms_server_to_client c s))
|
||||||
|
;; Ignore languages.
|
||||||
|
;; Don't check the reserved field here, either. TODO: should we?
|
||||||
|
|
||||||
|
(define (guess-matches? chosen-value getter)
|
||||||
|
(let ((remote-choices (getter remote-algs)))
|
||||||
|
(and (pair? remote-choices) ;; not strictly necessary because of
|
||||||
|
;; the error behaviour of
|
||||||
|
;; best-result.
|
||||||
|
(eq? (car remote-choices) ;; the remote peer's guess for this parameter
|
||||||
|
chosen-value))))
|
||||||
|
|
||||||
|
(define should-discard-first-kex-packet
|
||||||
|
(and (ssh-msg-kexinit-first_kex_packet_follows remote-algs)
|
||||||
|
;; They've already transmitted their guess. Does their guess match
|
||||||
|
;; what we've actually selected?
|
||||||
|
(not (and
|
||||||
|
(guess-matches? kex-alg ssh-msg-kexinit-kex_algorithms)
|
||||||
|
(guess-matches? host-key-alg ssh-msg-kexinit-server_host_key_algorithms)
|
||||||
|
(guess-matches? c2s-enc ssh-msg-kexinit-encryption_algorithms_client_to_server)
|
||||||
|
(guess-matches? s2c-enc ssh-msg-kexinit-encryption_algorithms_server_to_client)
|
||||||
|
(guess-matches? c2s-mac ssh-msg-kexinit-mac_algorithms_client_to_server)
|
||||||
|
(guess-matches? s2c-mac ssh-msg-kexinit-mac_algorithms_server_to_client)
|
||||||
|
(guess-matches? c2s-zip ssh-msg-kexinit-compression_algorithms_client_to_server)
|
||||||
|
(guess-matches? s2c-zip ssh-msg-kexinit-compression_algorithms_server_to_client)))))
|
||||||
|
|
||||||
|
(define (continue-after-discard in-state out-state)
|
||||||
|
(case role
|
||||||
|
((client) (perform-client-key-exchange (exchange-hash-info local-id
|
||||||
|
remote-id
|
||||||
|
encoded-local-algs
|
||||||
|
encoded-remote-algs)
|
||||||
|
kex-alg host-key-alg in-state out-state
|
||||||
|
continue-after-key-exchange))
|
||||||
|
((server) (error 'start-key-exchange "Server role unimplemented"))))
|
||||||
|
|
||||||
|
(define (continue-after-key-exchange shared-secret exchange-hash hash-alg in-state out-state)
|
||||||
|
(let ((session-id (if old-session-id
|
||||||
|
old-session-id ;; don't overwrite existing ID
|
||||||
|
exchange-hash))
|
||||||
|
(k-h-prefix (bit-string ((bit-string->integer shared-secret #t #f) :: (t:mpint))
|
||||||
|
(exchange-hash :: binary))))
|
||||||
|
(define (derive-key kind needed-bytes-or-false)
|
||||||
|
(let extend ((key (hash-alg (bit-string->bytes
|
||||||
|
(bit-string (k-h-prefix :: binary)
|
||||||
|
(kind :: binary)
|
||||||
|
(session-id :: binary))))))
|
||||||
|
(cond
|
||||||
|
((false? needed-bytes-or-false) key)
|
||||||
|
((>= (bytes-length key) needed-bytes-or-false) (subbytes key 0 needed-bytes-or-false))
|
||||||
|
(else
|
||||||
|
(extend (bytes-append key (hash-alg (bit-string->bytes
|
||||||
|
(bit-string (k-h-prefix :: binary)
|
||||||
|
(key :: binary))))))))))
|
||||||
|
(let-values (((newkeys-packet newkeys-message in-state out-state)
|
||||||
|
(read-message in-state out-state)))
|
||||||
|
(when (not (ssh-msg-newkeys? newkeys-message))
|
||||||
|
(disconnect-with-error/local-info `((message ,newkeys-message))
|
||||||
|
SSH_DISCONNECT_PROTOCOL_ERROR
|
||||||
|
"Expected SSH_MSG_NEWKEYS"))
|
||||||
|
(let ((out-state (write-message! (ssh-msg-newkeys) out-state)))
|
||||||
|
(finish-key-exchange session-id
|
||||||
|
(apply-negotiated-options in-state role #f derive-key
|
||||||
|
c2s-enc s2c-enc
|
||||||
|
c2s-mac s2c-mac
|
||||||
|
c2s-zip s2c-zip)
|
||||||
|
(apply-negotiated-options out-state role #t derive-key
|
||||||
|
c2s-enc s2c-enc
|
||||||
|
c2s-mac s2c-mac
|
||||||
|
c2s-zip s2c-zip))))))
|
||||||
|
|
||||||
|
(if should-discard-first-kex-packet
|
||||||
|
(let-values (((discarded-packet discarded-message in-state out-state)
|
||||||
|
(read-message in-state out-state)))
|
||||||
|
(continue-after-discard in-state out-state))
|
||||||
|
(continue-after-discard in-state out-state)))
|
||||||
|
|
||||||
|
(define (check-host-key! host-key)
|
||||||
|
(write `(TODO check-host-key! ,(hex (bit-string->bytes host-key)))) (newline) (flush-output)
|
||||||
|
(void))
|
||||||
|
|
||||||
|
(define (perform-client-key-exchange hash-info kex-alg host-key-alg in-state out-state finish)
|
||||||
|
(case kex-alg
|
||||||
|
((diffie-hellman-group14-sha1 diffie-hellman-group1-sha1)
|
||||||
|
(let ((group (if (eq? kex-alg 'diffie-hellman-group14-sha1)
|
||||||
|
dh:oakley-group-14
|
||||||
|
dh:oakley-group-2))) ;; yes, SSH's group1 == Oakley/RFC2409 group 2
|
||||||
|
(let*-values (((private-key public-key) (generate-key group))
|
||||||
|
((out-state)
|
||||||
|
(write-message! (ssh-msg-kexdh-init (bit-string->integer public-key #t #f))
|
||||||
|
out-state
|
||||||
|
#t))
|
||||||
|
((packet message in-state out-state)
|
||||||
|
(read-message in-state out-state)))
|
||||||
|
(if (not (ssh-msg-kexdh-reply? message))
|
||||||
|
(disconnect-with-error/local-info `((message ,message))
|
||||||
|
SSH_DISCONNECT_KEY_EXCHANGE_FAILED
|
||||||
|
"Unexpected packet type")
|
||||||
|
(let* ((f (ssh-msg-kexdh-reply-f message))
|
||||||
|
(f-width (mpint-width f))
|
||||||
|
(f-as-bytes (integer->bit-string f (* 8 f-width) #t))
|
||||||
|
(shared-secret (compute-key private-key f-as-bytes))
|
||||||
|
(hash-alg sha1)
|
||||||
|
(exchange-hash (dh-exchange-hash hash-info
|
||||||
|
(ssh-msg-kexdh-reply-host-key message)
|
||||||
|
(bit-string->integer public-key #t #f)
|
||||||
|
f
|
||||||
|
(bit-string->integer shared-secret #t #f))))
|
||||||
|
;; (pretty-print `((public-key ,(hex public-key))
|
||||||
|
;; (f-as-bytes ,(hex f-as-bytes))
|
||||||
|
;; (shared-secret ,(hex shared-secret))
|
||||||
|
;; (exchange-hash ,(hex exchange-hash))))
|
||||||
|
(check-host-key! (ssh-msg-kexdh-reply-host-key message))
|
||||||
|
(finish shared-secret exchange-hash hash-alg in-state out-state))))))
|
||||||
|
(else (disconnect-with-error SSH_DISCONNECT_KEY_EXCHANGE_FAILED
|
||||||
|
"Bad key-exchange algorithm ~v" kex-alg))))
|
||||||
|
|
||||||
|
(define (dh-exchange-hash hash-info host-key e f k)
|
||||||
|
(let ((block-to-hash
|
||||||
|
(bit-string->bytes
|
||||||
|
(bit-string ((string->bytes/utf-8 (exchange-hash-info-client-id hash-info)) :: (t:string))
|
||||||
|
((string->bytes/utf-8 (exchange-hash-info-server-id hash-info)) :: (t:string))
|
||||||
|
((exchange-hash-info-client-kexinit-bytes hash-info) :: (t:string))
|
||||||
|
((exchange-hash-info-server-kexinit-bytes hash-info) :: (t:string))
|
||||||
|
(host-key :: (t:string))
|
||||||
|
(e :: (t:mpint))
|
||||||
|
(f :: (t:mpint))
|
||||||
|
(k :: (t:mpint))))))
|
||||||
|
;;(pretty-print `((block-to-hash ,(hex block-to-hash))))
|
||||||
|
(sha1 block-to-hash)))
|
||||||
|
|
||||||
(require racket/tcp)
|
(require racket/tcp)
|
||||||
(require racket/pretty)
|
(require racket/pretty)
|
||||||
(define (t)
|
(define (t)
|
||||||
(let-values (((i o) (tcp-connect "localhost" 22)))
|
(let-values (((i o) (tcp-connect "localhost"
|
||||||
(ssh-session i o #f
|
2323
|
||||||
(lambda (seed packet write-packet! continue-reading rekey)
|
;;22
|
||||||
(pretty-print packet)
|
)))
|
||||||
(pretty-print (ssh-message-decode packet))
|
(ssh-session 'client
|
||||||
(continue-reading seed)))))
|
i o #f
|
||||||
|
(lambda (seed message continue-reading)
|
||||||
|
(pretty-print message)
|
||||||
|
(continue-reading (list) seed)))))
|
||||||
(t)
|
(t)
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
#lang racket/base
|
||||||
|
|
||||||
|
(require "aes-ctr.rkt")
|
||||||
|
(require rackunit)
|
||||||
|
(require (planet tonyg/bitsyntax))
|
||||||
|
|
||||||
|
(check-equal? (let ((x (start-aes-ctr (make-bytes 16 97) (make-bytes 16 0))))
|
||||||
|
(aes-ctr-process! x #"abcdefghijklmnop"))
|
||||||
|
#"\275XO-\317^<d\16(\262\257Fv}e" ;; = bd584f2dcf5e3c640e28b2af46767d65
|
||||||
|
)
|
||||||
|
|
||||||
|
(check-equal? (let ((x (start-aes-ctr (make-bytes 16 97) (make-bytes 16 0))))
|
||||||
|
(let* ((b1 (aes-ctr-process! x #"abcdef"))
|
||||||
|
(b2 (aes-ctr-process! x #"ghijklmnop")))
|
||||||
|
(list b1 b2)))
|
||||||
|
(list #"\275XO-\317^"
|
||||||
|
#"<d\16(\262\257Fv}e"))
|
||||||
|
|
||||||
|
(check-equal? (let ((x (start-aes-ctr (make-bytes 16 97) (make-bytes 16 0))))
|
||||||
|
(aes-ctr-process! x #"\275XO-\317^<d\16(\262\257Fv}e"))
|
||||||
|
#"abcdefghijklmnop")
|
||||||
|
|
||||||
|
(check-equal? (let ((x (start-aes-ctr (make-bytes 16 97) (make-bytes 16 0))))
|
||||||
|
(let* ((b1 (aes-ctr-process! x #"\275XO-\317^"))
|
||||||
|
(b2 (aes-ctr-process! x #"<d\16(\262\257Fv}e")))
|
||||||
|
(list b1 b2)))
|
||||||
|
(list #"abcdef"
|
||||||
|
#"ghijklmnop"))
|
||||||
|
|
||||||
|
(check-equal? (let ((x (start-aes-ctr (make-bytes 16 97) (make-bytes 16 0))))
|
||||||
|
(aes-ctr-process! x #"abcdefghijklmnopabcdefghijklmnop"))
|
||||||
|
#"\275XO-\317^<d\16(\262\257Fv}e!v\301\23\6@\330\305\314c\27\374\276\330 \342")
|
||||||
|
|
||||||
|
(check-equal? (let ((x (start-aes-ctr (make-bytes 16 97) (make-bytes 16 0))))
|
||||||
|
(aes-ctr-process! x
|
||||||
|
#"\275XO-\317^<d\16(\262\257Fv}e!v\301\23\6@\330\305\314c\27\374\276\330 \342"))
|
||||||
|
#"abcdefghijklmnopabcdefghijklmnop")
|
||||||
|
|
||||||
|
;; Test vectors from http://tools.ietf.org/html/draft-ietf-ipsec-ciph-aes-ctr-05
|
||||||
|
|
||||||
|
(define (hex-string->bytes str) ;; grumble
|
||||||
|
(define cleaned (regexp-replace* #rx"[^0-9a-fA-F]+" str ""))
|
||||||
|
(define bits (* (string-length cleaned) 4))
|
||||||
|
(define n (string->number cleaned 16))
|
||||||
|
(integer->bit-string n bits #t))
|
||||||
|
|
||||||
|
(define (test-enc description key ivec plaintext ciphertext)
|
||||||
|
(let ((state (start-aes-ctr (hex-string->bytes key)
|
||||||
|
(hex-string->bytes ivec))))
|
||||||
|
(check-equal? (aes-ctr-process! state (hex-string->bytes plaintext))
|
||||||
|
(hex-string->bytes ciphertext)
|
||||||
|
(format "test-enc ~v" description))))
|
||||||
|
|
||||||
|
;; Test Vector #1: Encrypting 16 octets using AES-CTR with 128-bit key
|
||||||
|
(test-enc 1
|
||||||
|
"AE 68 52 F8 12 10 67 CC 4B F7 A5 76 55 77 F3 9E"
|
||||||
|
"00 00 00 30 00 00 00 00 00 00 00 00 00 00 00 01"
|
||||||
|
"53 69 6E 67 6C 65 20 62 6C 6F 63 6B 20 6D 73 67"
|
||||||
|
"E4 09 5D 4F B7 A7 B3 79 2D 61 75 A3 26 13 11 B8")
|
||||||
|
|
||||||
|
;; Test Vector #2: Encrypting 32 octets using AES-CTR with 128-bit key
|
||||||
|
(test-enc 2
|
||||||
|
"7E 24 06 78 17 FA E0 D7 43 D6 CE 1F 32 53 91 63"
|
||||||
|
"00 6C B6 DB C0 54 3B 59 DA 48 D9 0B 00 00 00 01"
|
||||||
|
(string-append "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
|
||||||
|
"10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F")
|
||||||
|
(string-append "51 04 A1 06 16 8A 72 D9 79 0D 41 EE 8E DA D3 88"
|
||||||
|
"EB 2E 1E FC 46 DA 57 C8 FC E6 30 DF 91 41 BE 28"))
|
||||||
|
|
||||||
|
;; Test Vector #3: Encrypting 36 octets using AES-CTR with 128-bit key
|
||||||
|
(test-enc 3
|
||||||
|
"76 91 BE 03 5E 50 20 A8 AC 6E 61 85 29 F9 A0 DC"
|
||||||
|
"00 E0 01 7B 27 77 7F 3F 4A 17 86 F0 00 00 00 01"
|
||||||
|
(string-append "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
|
||||||
|
"10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
|
||||||
|
"20 21 22 23")
|
||||||
|
(string-append "C1 CF 48 A8 9F 2F FD D9 CF 46 52 E9 EF DB 72 D7"
|
||||||
|
"45 40 A4 2B DE 6D 78 36 D5 9A 5C EA AE F3 10 53"
|
||||||
|
"25 B2 07 2F"))
|
||||||
|
|
||||||
|
;; Test Vector #4: Encrypting 16 octets using AES-CTR with 192-bit key
|
||||||
|
(test-enc 4
|
||||||
|
"16 AF 5B 14 5F C9 F5 79 C1 75 F9 3E 3B FB 0E ED 86 3D 06 CC FD B7 85 15"
|
||||||
|
"00 00 00 48 36 73 3C 14 7D 6D 93 CB 00 00 00 01"
|
||||||
|
"53 69 6E 67 6C 65 20 62 6C 6F 63 6B 20 6D 73 67"
|
||||||
|
"4B 55 38 4F E2 59 C9 C8 4E 79 35 A0 03 CB E9 28")
|
||||||
|
|
||||||
|
;; Test Vector #5: Encrypting 32 octets using AES-CTR with 192-bit key
|
||||||
|
(test-enc 5
|
||||||
|
"7C 5C B2 40 1B 3D C3 3C 19 E7 34 08 19 E0 F6 9C 67 8C 3D B8 E6 F6 A9 1A"
|
||||||
|
"00 96 B0 3B 02 0C 6E AD C2 CB 50 0D 00 00 00 01"
|
||||||
|
(string-append "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
|
||||||
|
"10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F")
|
||||||
|
(string-append "45 32 43 FC 60 9B 23 32 7E DF AA FA 71 31 CD 9F"
|
||||||
|
"84 90 70 1C 5A D4 A7 9C FC 1F E0 FF 42 F4 FB 00"))
|
||||||
|
|
||||||
|
;; Test Vector #6: Encrypting 36 octets using AES-CTR with 192-bit key
|
||||||
|
(test-enc 6
|
||||||
|
"02 BF 39 1E E8 EC B1 59 B9 59 61 7B 09 65 27 9B F5 9B 60 A7 86 D3 E0 FE"
|
||||||
|
"00 07 BD FD 5C BD 60 27 8D CC 09 12 00 00 00 01"
|
||||||
|
(string-append "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
|
||||||
|
"10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
|
||||||
|
"20 21 22 23")
|
||||||
|
(string-append "96 89 3F C5 5E 5C 72 2F 54 0B 7D D1 DD F7 E7 58"
|
||||||
|
"D2 88 BC 95 C6 91 65 88 45 36 C8 11 66 2F 21 88"
|
||||||
|
"AB EE 09 35"))
|
||||||
|
|
||||||
|
;; Test Vector #7: Encrypting 16 octets using AES-CTR with 256-bit key
|
||||||
|
(test-enc 7
|
||||||
|
(string-append "77 6B EF F2 85 1D B0 6F 4C 8A 05 42 C8 69 6F 6C"
|
||||||
|
"6A 81 AF 1E EC 96 B4 D3 7F C1 D6 89 E6 C1 C1 04")
|
||||||
|
"00 00 00 60 DB 56 72 C9 7A A8 F0 B2 00 00 00 01"
|
||||||
|
"53 69 6E 67 6C 65 20 62 6C 6F 63 6B 20 6D 73 67"
|
||||||
|
"14 5A D0 1D BF 82 4E C7 56 08 63 DC 71 E3 E0 C0")
|
||||||
|
|
||||||
|
;; Test Vector #8: Encrypting 32 octets using AES-CTR with 256-bit key
|
||||||
|
(test-enc 8
|
||||||
|
(string-append "F6 D6 6D 6B D5 2D 59 BB 07 96 36 58 79 EF F8 86"
|
||||||
|
"C6 6D D5 1A 5B 6A 99 74 4B 50 59 0C 87 A2 38 84")
|
||||||
|
"00 FA AC 24 C1 58 5E F1 5A 43 D8 75 00 00 00 01"
|
||||||
|
(string-append "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
|
||||||
|
"10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F")
|
||||||
|
(string-append "F0 5E 23 1B 38 94 61 2C 49 EE 00 0B 80 4E B2 A9"
|
||||||
|
"B8 30 6B 50 8F 83 9D 6A 55 30 83 1D 93 44 AF 1C"))
|
||||||
|
|
||||||
|
;; Test Vector #9: Encrypting 36 octets using AES-CTR with 256-bit key
|
||||||
|
(test-enc 9
|
||||||
|
(string-append "FF 7A 61 7C E6 91 48 E4 F1 72 6E 2F 43 58 1D E2"
|
||||||
|
"AA 62 D9 F8 05 53 2E DF F1 EE D6 87 FB 54 15 3D")
|
||||||
|
"00 1C C5 B7 51 A5 1D 70 A1 C1 11 48 00 00 00 01"
|
||||||
|
(string-append "00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F"
|
||||||
|
"10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"
|
||||||
|
"20 21 22 23")
|
||||||
|
(string-append "EB 6C 52 82 1D 0B BB F7 CE 75 94 46 2A CA 4F AA"
|
||||||
|
"B4 07 DF 86 65 69 FD 07 F4 8C C0 B5 83 D6 07 1F"
|
||||||
|
"1E C0 E6 B8"))
|
Loading…
Reference in New Issue