marketplace-ssh-2014/syndicate-ssh/keys/ssh-keys.rkt

111 lines
4.6 KiB
Racket

#lang racket/base
;;; SPDX-License-Identifier: LGPL-3.0-or-later
;;; SPDX-FileCopyrightText: Copyright © 2018-2021 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
(provide file->ssh-private-key
bytes->ssh-private-key)
(require racket/match)
(require (only-in racket/file file->bytes))
(require (only-in racket/port call-with-input-bytes))
(require (only-in racket/string string-join))
(require net/base64)
(require bitsyntax)
;; (require blowfish/bcrypt-hash)
(require crypto)
(require "../ssh-message-types.rkt")
(define (file->ssh-private-key filename [passphrase-bytes #f])
(bytes->ssh-private-key (file->bytes filename) passphrase-bytes))
(define (bytes->ssh-private-key bs [passphrase-bytes #f])
(call-with-input-bytes
bs
(lambda (p)
(and (equal? (read-line p) "-----BEGIN OPENSSH PRIVATE KEY-----")
(let ((blob (let collect ((acc '()))
(match (read-line p)
["-----END OPENSSH PRIVATE KEY-----"
(base64-decode (string->bytes/latin-1 (string-join (reverse acc))))]
[line
(collect (cons line acc))]))))
(bit-string-case blob
([ (= #"openssh-key-v1\0" :: binary bytes 15)
(ciphername :: (t:string #:pack))
(kdfname :: (t:string #:pack))
(kdfoptions :: (t:string #:pack))
(= 1 :: bits 32) ;; OpenSSH only supports one key
(public-keys :: (t:repeat 1 (t:string #:pack)))
(private-keys :: (t:string #:pack)) ]
(decode-private-keys passphrase-bytes
ciphername
kdfname
kdfoptions
(car public-keys)
private-keys))
(else #f)))))))
(define (decode-private-keys passphrase-bytes
ciphername
kdfname
kdfoptions
public-key
private-keys)
(define pk-bytes
(bit-string-case public-key
([ (= #"ssh-ed25519" :: (t:string #:pack))
(bs :: (t:string #:pack)) ]
bs)))
(define (decode-decrypted blob)
;; Oddly, this only partially lines up with the spec at
;; https://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?annotate=1.1
;;
;; Specifically, contra spec, after the checkints, we seem to have
;; the public key again followed by the private key bytes and then
;; a comment string.
;;
;; The code (sshkey.c) does this:
;; - retrieve a key type string
;; - dispatch on it
;; - for ed25519, read a string with the PK, then a string with the SK
;; - checks the sizes to ensure they are correct for ed25519
;;
;; This is what the PROTOCOL.key documentation says to do. It's
;; not what actually needs to be done.
;;
;; [ (checkint1 :: bits 32) (= checkint1 :: bits 32) ;; must be the same
;; (keys :: (t:repeat 1 (t:repeat 2 (t:string #:pack))))
;; (= 'padding-ok :: (t:padding)) ]
;;
(bit-string-case blob
([ (checkint1 :: bits 32) (= checkint1 :: bits 32) ;; must be the same
(= #"ssh-ed25519" :: (t:string #:pack))
(pk-bytes-in-sk :: (t:string #:pack))
(sk :: (t:string #:pack))
(comment :: (t:string #:pack))
(= 'padding-ok :: (t:padding))
]
(and (equal? pk-bytes pk-bytes-in-sk)
(list pk-bytes sk (bytes->string/utf-8 comment))))
(else #f)))
(match* (ciphername kdfname)
[(#"none" #"none")
(decode-decrypted private-keys)]
;; This stanza works, I just don't want to depend on bcrypt just yet:
#;[(#"aes256-ctr" #"bcrypt")
(define keylen (/ 256 8)) ;; aes256 = 256 bit key length
(define ivlen (/ 128 8)) ;; fixed block size of 128 bits
(bit-string-case kdfoptions
([ (salt :: (t:string #:pack))
(rounds :: bits 32) ]
(when (not passphrase-bytes) (error 'read-ssh-private-key "Passphrase required"))
(bit-string-case (bcrypt-pbkdf passphrase-bytes salt (+ keylen ivlen) rounds)
([ (key :: binary bytes keylen) (iv :: binary bytes ivlen) ]
(decode-decrypted
(parameterize ((crypto-factories (list libcrypto-factory)))
(decrypt '(aes ctr) (bit-string->bytes key) (bit-string->bytes iv) private-keys)))))))]
[(_ _)
(error 'read-ssh-private-key "Unsupported private-key cipher/kdf")]))