Basics of publickey userauth

This commit is contained in:
Tony Garnock-Jones 2021-06-19 23:56:22 +02:00
parent 4df961db1f
commit 32595e718f
5 changed files with 168 additions and 90 deletions

View File

@ -21,22 +21,33 @@
(require "ssh-channel.rkt")
(require "ssh-message-types.rkt")
(require "ssh-exceptions.rkt")
(require "ssh-keys.rkt")
(require "schemas/gen/channel.rkt")
(require "schemas/gen/auth.rkt")
(module+ main
(standard-actor-system (ds)
(with-services [syndicate/drivers/racket-event]
(define spec (TcpLocal "0.0.0.0" 29418))
(at ds
(stop-on (asserted (StreamListenerError spec $message)))
(during/spawn (StreamConnection $source $sink spec)
#:name (list 'ssh source)
(session ds source sink))))))
(define spec (TcpLocal "0.0.0.0" 29418))
(at ds
(stop-on (asserted (StreamListenerError spec $message)))
(during/spawn (StreamConnection $source $sink spec)
#:name (list 'ssh source)
(session ds source sink (load-private-key "test-host-keys/ssh_host_ed25519_key"))))
))
;; (define host-key-ed25519-public (pk-key->public-only-key host-key-ed25519-private))
(define test-user-private (load-private-key "test-user-key"))
(define test-user-public (load-public-key "test-user-key.pub"))
(define (user-public-key-accepted? user-name public-key)
(match* (user-name public-key)
[(#"tonyg" (== (public-key->pieces test-user-public))) #t]
[(_ _) #f]))
;;---------------------------------------------------------------------------
(define (session ground-ds source sink)
(define (session ground-ds source sink host-private-key)
(on-stop (log-info "Session VM for ~a closed" source))
(actor-group
#:link? #t
@ -57,7 +68,13 @@
(send! (inbound-credit 1))
(spawn
#:name 'session
(ssh-session conn-ds ground-ds local-identification remote-identification 'server))]
(ssh-session conn-ds
ground-ds
local-identification
remote-identification
'server
host-private-key
user-public-key-accepted?))]
[else
(log-error "Invalid peer identification string ~v" remote-identification)
(stop-actor-system)])])

View File

@ -2,29 +2,32 @@
;;; SPDX-License-Identifier: LGPL-3.0-or-later
;;; SPDX-FileCopyrightText: Copyright © 2011-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
(require racket/match)
(require racket/port)
(require net/base64)
(require bitsyntax)
(require "crypto.rkt")
(require "keys/ssh-keys.rkt")
(require "ssh-message-types.rkt")
(module+ test (require rackunit))
(provide (struct-out ed25519-private-key)
(struct-out ed25519-public-key)
public-key->pieces
pieces->public-key
host-key-algorithm->keys
host-key-signature
verify-host-key-signature!
make-key-signature
verify-key-signature!
pieces->ssh-host-key
ssh-host-key->pieces)
pieces->ssh-key
ssh-key->pieces
load-private-key
load-public-key)
(require racket/match)
(require racket/port)
(require net/base64)
(require (only-in racket/file file->bytes file->string))
(require bitsyntax)
(require "crypto.rkt")
(require "keys/ssh-keys.rkt")
(require "ssh-message-types.rkt")
(module+ test (require rackunit))
(struct ed25519-private-key (q d) #:transparent)
(struct ed25519-public-key (q) #:transparent)
@ -49,52 +52,45 @@
[(ed25519-public-key q)
(datum->pk-key (list 'eddsa 'public 'ed25519 q) 'rkt-public)]))
(define (host-key-algorithm->keys host-key-alg)
(case host-key-alg
((ssh-ed25519) (values host-key-ed25519-private host-key-ed25519-public))
(else (error 'host-key-algorithm->keys "Unsupported host-key-alg ~v" host-key-alg))))
(define (host-key-signature private-key host-key-alg exchange-hash)
(case host-key-alg
(define (make-key-signature private-key key-alg exchange-hash)
(case key-alg
[(ssh-ed25519)
(define signature (pk-sign private-key exchange-hash))
(bit-string (#"ssh-ed25519" :: (t:string))
(signature :: (t:string)))]))
(define (verify-host-key-signature! public-key host-key-alg exchange-hash h-signature)
(define (verify-key-signature! public-key key-alg exchange-hash h-signature)
;; TODO: If we are *re*keying, worth checking here that the key hasn't *changed* either.
(write `(TODO check-host-key!)) (newline) (flush-output) ;; actually check the identity TOFU/POP
(case host-key-alg
(write `(TODO check-key!)) (newline) (flush-output) ;; actually check the identity TOFU/POP
(case key-alg
[(ssh-ed25519)
(define signature (bit-string-case h-signature
([ (= #"ssh-ed25519" :: (t:string #:pack))
(sig :: (t:string #:pack)) ]
sig)))
(when (not (pk-verify public-key exchange-hash signature))
(error 'verify-host-key-signature! "Signature mismatch"))]))
(error 'verify-key-signature! "Signature mismatch"))]))
(define (pieces->ssh-host-key pieces)
(define (pieces->ssh-key pieces)
(match pieces
[(ed25519-public-key q)
(bit-string (#"ssh-ed25519" :: (t:string))
(q :: (t:string)))]))
(define (ssh-host-key->pieces blob)
(define (ssh-key->pieces blob)
(bit-string-case blob
([ (= #"ssh-ed25519" :: (t:string #:pack))
(q :: (t:string #:pack)) ]
(ed25519-public-key q))))
(ed25519-public-key q))
(else #f)))
;; TODO: proper store for keys
(define (load-private-key filename)
(local-require (only-in racket/file file->bytes))
(pieces->private-key (bytes->private-key-pieces (file->bytes filename))))
(define host-key-ed25519-private (load-private-key "test-host-keys/ssh_host_ed25519_key"))
(define host-key-ed25519-public (pk-key->public-only-key host-key-ed25519-private))
(module+ test
(check-equal? (pk-key->datum (pieces->public-key (public-key->pieces host-key-ed25519-public))
'SubjectPublicKeyInfo)
(pk-key->datum host-key-ed25519-private 'SubjectPublicKeyInfo)))
(define (load-public-key filename)
(match (file->string filename)
[(pregexp #px"ssh-ed25519 +(\\S+) +([^\n]*)\n$" (list _ data-base64 _comment))
(pieces->public-key (ssh-key->pieces (base64-decode (string->bytes/utf-8 data-base64))))]
[_ (error 'load-public-key "Cannot load key in file ~s" filename)]))

View File

@ -28,6 +28,7 @@
(struct-out ssh-msg-userauth-failure)
(struct-out ssh-msg-userauth-success)
(struct-out ssh-msg-userauth-banner)
(struct-out ssh-msg-userauth-pk-ok)
(struct-out ssh-msg-global-request)
(struct-out ssh-msg-request-success)
(struct-out ssh-msg-request-failure)
@ -323,6 +324,10 @@
(string message)
(string language))
(define-ssh-message-type ssh-msg-userauth-pk-ok SSH_MSG_USERAUTH_PK_OK
(string algorithm-name)
(string pk-blob))
(define-ssh-message-type ssh-msg-global-request SSH_MSG_GLOBAL_REQUEST
(string request-name)
(boolean want-reply?)

View File

@ -72,6 +72,7 @@
(define SSH_MSG_USERAUTH_FAILURE 51) ;[SSH-USERAUTH]
(define SSH_MSG_USERAUTH_SUCCESS 52) ;[SSH-USERAUTH]
(define SSH_MSG_USERAUTH_BANNER 53) ;[SSH-USERAUTH]
(define SSH_MSG_USERAUTH_PK_OK 60) ;RFC 4252 section 7
(define SSH_MSG_GLOBAL_REQUEST 80) ;[SSH-CONNECT]
(define SSH_MSG_REQUEST_SUCCESS 81) ;[SSH-CONNECT]
(define SSH_MSG_REQUEST_FAILURE 82) ;[SSH-CONNECT]

View File

@ -9,7 +9,7 @@
(require "crypto.rkt")
(require "oakley-groups.rkt")
(require "ssh-host-key.rkt")
(require "ssh-keys.rkt")
(require "ssh-numbers.rkt")
(require "ssh-message-types.rkt")
(require "ssh-exceptions.rkt")
@ -145,7 +145,7 @@
;; DS ExchangeHashInfo Symbol Symbol (Bytes Bytes Symbol -> Void) -> Void
;; Performs the server's half of the Diffie-Hellman key exchange protocol.
(define (perform-server-key-exchange conn-ds hash-info kex-alg host-key-alg finish)
(define (perform-server-key-exchange conn-ds hash-info kex-alg host-key-private host-key-alg finish)
(match kex-alg
['diffie-hellman-group14-sha256
(define group dh:oakley-group-14)
@ -157,15 +157,15 @@
(define peer-key (datum->pk-key (list 'dh 'public p g e) 'rkt-public))
(define shared-secret (pk-derive-secret private-key peer-key))
(define hash-alg sha256)
(define-values (host-key-private host-key-public) (host-key-algorithm->keys host-key-alg))
(define host-key-bytes (pieces->ssh-host-key (public-key->pieces host-key-public)))
(define host-key-public (pk-key->public-only-key host-key-private))
(define host-key-bytes (pieces->ssh-key (public-key->pieces host-key-public)))
(define exchange-hash (dh-exchange-hash hash-alg
hash-info
host-key-bytes
e
public-key-as-integer
(bit-string->integer shared-secret #t #f)))
(define h-signature (host-key-signature host-key-private host-key-alg exchange-hash))
(define h-signature (make-key-signature host-key-private host-key-alg exchange-hash))
(send! (outbound-packet
(ssh-msg-kexdh-reply (bit-string->bytes host-key-bytes)
public-key-as-integer
@ -176,7 +176,7 @@
;; DS ExchangeHashInfo Symbol Symbol (Bytes Bytes Symbol -> Void) -> Void
;; Performs the client's half of the Diffie-Hellman key exchange protocol.
(define (perform-client-key-exchange conn-ds hash-info kex-alg host-key-alg finish)
(define (perform-client-key-exchange conn-ds hash-info kex-alg host-key-public host-key-alg finish)
(match kex-alg
['diffie-hellman-group14-sha256
(define group dh:oakley-group-14)
@ -190,14 +190,16 @@
(define peer-key (datum->pk-key (list 'dh 'public p g f) 'rkt-public))
(define shared-secret (pk-derive-secret private-key peer-key))
(define hash-alg sha256)
(define host-public-key (pieces->public-key (ssh-host-key->pieces host-key-bytes)))
(when (not (equal? (ssh-key->pieces host-key-bytes) (public-key->pieces host-key-public)))
(disconnect-with-error conn-ds SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE
"Unexpected host key! ~v" host-key-bytes))
(define exchange-hash (dh-exchange-hash hash-alg
hash-info
host-key-bytes
public-key-as-integer
f
(bit-string->integer shared-secret #t #f)))
(verify-host-key-signature! host-public-key host-key-alg exchange-hash h-signature)
(verify-key-signature! host-key-public host-key-alg exchange-hash h-signature)
(finish shared-secret exchange-hash hash-alg)))]
[_ (disconnect-with-error conn-ds SSH_DISCONNECT_KEY_EXCHANGE_FAILED
"Bad key-exchange algorithm ~v" kex-alg)]))
@ -208,6 +210,7 @@
#:message message
#:rekey-state rekey-state
#:is-server? is-server?
#:supplied-host-key supplied-host-key
#:local-id local-id
#:remote-id remote-id
#:session-id session-id
@ -273,6 +276,7 @@
(exchange-hash-info remote-id local-id encoded-remote-algs encoded-local-algs)
(exchange-hash-info local-id remote-id encoded-local-algs encoded-remote-algs))
kex-alg
supplied-host-key
host-key-alg
(lambda (shared-secret exchange-hash hash-alg)
(when (not (session-id)) (session-id exchange-hash)) ;; don't overwrite existing ID
@ -315,48 +319,99 @@
;; Service request manager and user authentication
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(define (service-request-handler conn-ds)
(define (service-request-handler conn-ds user-public-key-accepted? session-id)
(define-field authentication-state #f)
(begin/dataflow (log-info "authentication-state ~s" (authentication-state)))
(define (acceptable-ed25519-pk key-alg user-name pk-bytes)
(and (equal? key-alg #"ssh-ed25519")
(let ((pk (ssh-key->pieces pk-bytes)))
(and pk
(user-public-key-accepted? user-name pk)
(pieces->public-key pk)))))
(define (run-userauth)
(define expected-service #"ssh-connection")
(define remaining-methods '(publickey password))
(define (give-up-on method)
(set! remaining-methods (remq method remaining-methods)))
(at conn-ds
(send! (outbound-packet (ssh-msg-userauth-banner #"Welcome to Racket SSH!\r\n" #"")))
(react
(define (auth-ok new-authentication-state)
(stop-current-facet
(send! (outbound-packet (ssh-msg-userauth-success)))
(authentication-state new-authentication-state)
;; RFC4252 section 5.1 page 6:
(react (with-incoming-task (SSH_MSG_USERAUTH_REQUEST _ _)))
(spawn #:name 'channel-manager (run-channel-manager conn-ds))))
(with-incoming-task
(SSH_MSG_USERAUTH_REQUEST
_ (ssh-msg-userauth-request $user-name expected-service #"none" _))
;; This is a request for available authentication methods.
(send! (outbound-packet (ssh-msg-userauth-failure remaining-methods #f))))
(with-incoming-task
(SSH_MSG_USERAUTH_REQUEST
_ (ssh-msg-userauth-request $user-name expected-service #"password" $extension))
(log-info "SSH-MSG-USERAUTH-REQUEST ~s ~s ~s ~s" user-name expected-service #"password" extension)
(bit-string-case extension
([ (= #f :: (t:boolean))
(password-bytes :: (t:string #:pack)) ]
(log-info "User supplied password ~s" password-bytes)
(auth-ok (SshAuthenticatedUser user-name expected-service)))
(else
(give-up-on 'password)
(send! (outbound-packet (ssh-msg-userauth-failure remaining-methods #f))))))
(with-incoming-task
(SSH_MSG_USERAUTH_REQUEST
_ (ssh-msg-userauth-request $user-name expected-service #"publickey" $extension))
(log-info "SSH-MSG-USERAUTH-REQUEST ~s ~s ~s ~s" user-name expected-service #"publickey" extension)
(bit-string-case extension
([ (= #f :: (t:boolean))
(key-alg :: (t:string #:pack))
(pk-blob :: (t:string #:pack)) ]
(if (acceptable-ed25519-pk key-alg user-name pk-blob)
(send! (outbound-packet (ssh-msg-userauth-pk-ok key-alg pk-blob)))
(send! (outbound-packet (ssh-msg-userauth-failure remaining-methods #f)))))
([ (= #t :: (t:boolean))
(key-alg :: (t:string #:pack))
(pk-bytes :: (t:string #:pack))
(signature :: (t:string #:pack)) ]
(let ((pk (acceptable-ed25519-pk key-alg user-name pk-bytes)))
(if (not pk)
(send! (outbound-packet (ssh-msg-userauth-failure remaining-methods #f)))
(let* ((exchange-hash
(bit-string->bytes
(bit-string ((session-id) :: (t:string))
SSH_MSG_USERAUTH_REQUEST
(user-name :: (t:string))
(expected-service :: (t:string))
(#"publickey" :: (t:string))
(#t :: (t:boolean))
(key-alg :: (t:string))
(pk-bytes :: (t:string))))))
(verify-key-signature! pk 'ssh-ed25519 exchange-hash signature)
(auth-ok (SshAuthenticatedUser user-name expected-service))))))
(else
(give-up-on 'publickey)
(send! (outbound-packet (ssh-msg-userauth-failure remaining-methods #f)))))))))
(at conn-ds
(assert #:when (authentication-state) (authentication-state))
(with-incoming-task (SSH_MSG_SERVICE_REQUEST _ (ssh-msg-service-request $service))
(match service
[#"ssh-userauth"
(cond
[(authentication-state)
(disconnect-with-error SSH_DISCONNECT_SERVICE_NOT_AVAILABLE
"Repeated authentication is not permitted")]
[else
(at conn-ds
(send! (outbound-packet (ssh-msg-service-accept service)))
(with-incoming-task/react
(SSH_MSG_USERAUTH_REQUEST _ (ssh-msg-userauth-request $user-name
$service-name
$method-name
$extension))
(log-info "SSH-MSG-USERAUTH-REQUEST ~s ~s ~s ~s"
user-name
service-name
method-name
extension)
(send! (outbound-packet
(ssh-msg-userauth-banner #"Welcome to Racket SSH!\r\n" #"")))
(cond
[(and (positive? (bytes-length user-name))
(equal? service-name #"ssh-connection"))
;; TODO: Actually implement client authentication
(send! (outbound-packet (ssh-msg-userauth-success)))
(authentication-state (SshAuthenticatedUser user-name service-name))
(react
(with-incoming-task (SSH_MSG_USERAUTH_REQUEST _ _)
;; RFC4252 section 5.1 page 6
))
(spawn #:name 'channel-manager (run-channel-manager conn-ds))]
[else
(send! (outbound-packet (ssh-msg-userauth-failure '(none) #f)))])))])]
(if (authentication-state)
(disconnect-with-error SSH_DISCONNECT_SERVICE_NOT_AVAILABLE
"Repeated authentication is not permitted")
(begin (send! (outbound-packet (ssh-msg-service-accept service)))
(run-userauth)))]
[_
(disconnect-with-error SSH_DISCONNECT_SERVICE_NOT_AVAILABLE
"Service ~v not supported"
@ -590,7 +645,9 @@
ground-ds
local-identification-string
peer-identification-string
session-role)
session-role
supplied-host-key
user-public-key-accepted?)
(define-field rekey-state (rekey-in-seconds-or-bytes -1 -1 0))
(define-field session-id #f)
(define-field total-transferred 0)
@ -631,6 +688,7 @@
#:message message
#:rekey-state rekey-state
#:is-server? (case session-role ((client) #f) ((server) #t))
#:supplied-host-key supplied-host-key
#:local-id local-identification-string
#:remote-id peer-identification-string
#:session-id session-id
@ -640,7 +698,8 @@
(react
(at conn-ds
(stop-on (message 'enable-service-request-handler)
(spawn #:name 'service-request-handler (service-request-handler conn-ds)))))
(spawn #:name 'service-request-handler
(service-request-handler conn-ds user-public-key-accepted? session-id)))))
(define (maybe-rekey)
(match (rekey-state)