123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- ;;; GNU Guix --- Functional package management for GNU
- ;;; Copyright © 2020-2022 Ludovic Courtès <ludo@gnu.org>
- ;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
- ;;;
- ;;; This file is part of GNU Guix.
- ;;;
- ;;; GNU Guix is free software; you can redistribute it and/or modify it
- ;;; under the terms of the GNU General Public License as published by
- ;;; the Free Software Foundation; either version 3 of the License, or (at
- ;;; your option) any later version.
- ;;;
- ;;; GNU Guix is distributed in the hope that it will be useful, but
- ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
- ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- ;;; GNU General Public License for more details.
- ;;;
- ;;; You should have received a copy of the GNU General Public License
- ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
- (define-module (gnu build secret-service)
- #:use-module (guix build utils)
- #:use-module (srfi srfi-26)
- #:use-module (rnrs bytevectors)
- #:use-module (ice-9 binary-ports)
- #:use-module (ice-9 match)
- #:use-module (ice-9 rdelim)
- #:export (secret-service-receive-secrets
- secret-service-send-secrets))
- ;;; Commentary:
- ;;;
- ;;; Utility procedures for copying secrets into a VM.
- ;;;
- ;;; Code:
- (define-syntax log
- (lambda (s)
- "Log the given message."
- (syntax-case s ()
- ((_ fmt args ...)
- (with-syntax ((fmt (string-append "secret service: "
- (syntax->datum #'fmt))))
- ;; Log to the current output port. That way, when
- ;; 'secret-service-send-secrets' is called from shepherd, output goes
- ;; to syslog.
- #'(format (current-output-port) fmt args ...))))))
- (define-syntax with-modules
- (syntax-rules ()
- "Dynamically load the given MODULEs at run time, making the chosen
- bindings available within the lexical scope of BODY."
- ((_ ((module #:select (bindings ...)) rest ...) body ...)
- (let* ((iface (resolve-interface 'module))
- (bindings (module-ref iface 'bindings))
- ...)
- (with-modules (rest ...) body ...)))
- ((_ () body ...)
- (begin body ...))))
- (define (wait-for-readable-fd port timeout)
- "Wait until PORT has data available for reading or TIMEOUT has expired.
- Return #t in the former case and #f in the latter case."
- (match (resolve-module '(fibers) #f #:ensure #f) ;using Fibers?
- (#f
- (log "blocking on socket...~%")
- (match (select (list port) '() '() timeout)
- (((_) () ()) #t)
- ((() () ()) #f)))
- (fibers
- ;; We're running on the Shepherd 0.9+ with Fibers. Arrange to make a
- ;; non-blocking wait so that other fibers can be scheduled in while we
- ;; wait for PORT.
- (with-modules (((fibers) #:select (spawn-fiber sleep))
- ((fibers channels)
- #:select (make-channel put-message get-message)))
- ;; Make PORT non-blocking.
- (let ((flags (fcntl port F_GETFL)))
- (fcntl port F_SETFL (logior O_NONBLOCK flags)))
- (let ((channel (make-channel)))
- (spawn-fiber
- (lambda ()
- (sleep timeout) ;suspends the fiber
- (put-message channel 'timeout)))
- (spawn-fiber
- (lambda ()
- (lookahead-u8 port) ;suspends the fiber
- (put-message channel 'readable)))
- (log "suspending fiber on socket...~%")
- (match (get-message channel)
- ('readable #t)
- ('timeout #f)))))))
- (define* (secret-service-send-secrets port secret-root
- #:key (retry 60)
- (handshake-timeout 120))
- "Copy all files under SECRET-ROOT using TCP to secret-service listening at
- local PORT. If connect fails, sleep 1s and retry RETRY times; once connected,
- wait for at most HANDSHAKE-TIMEOUT seconds for handshake to complete. Return
- #f on failure."
- (define (file->file+size+mode file-name)
- (let ((stat (stat file-name))
- (target (substring file-name (string-length secret-root))))
- (list target (stat:size stat) (stat:mode stat))))
- (define (send-files sock)
- (let* ((files (if secret-root (find-files secret-root) '()))
- (files-sizes-modes (map file->file+size+mode files))
- (secrets `(secrets
- (version 0)
- (files ,files-sizes-modes))))
- (write secrets sock)
- (for-each (lambda (file)
- (call-with-input-file file
- (lambda (input)
- (dump-port input sock))))
- files)))
- (log "sending secrets to ~a~%" port)
- (let ((sock (socket AF_INET (logior SOCK_CLOEXEC SOCK_STREAM) 0))
- (addr (make-socket-address AF_INET INADDR_LOOPBACK port))
- (sleep (if (resolve-module '(fibers) #f)
- (module-ref (resolve-interface '(fibers)) 'sleep)
- sleep)))
- ;; Connect to QEMU on the forwarded port. The 'connect' call succeeds as
- ;; soon as QEMU is ready, even if there's no server listening on the
- ;; forward port inside the guest.
- (let loop ((retry retry))
- (catch 'system-error
- (cute connect sock addr)
- (lambda (key . args)
- (when (zero? retry)
- (apply throw key args))
- (log "retrying connection [~a attempts left]~%"
- (- retry 1))
- (sleep 1)
- (loop (1- retry)))))
- (log "connected; waiting for handshake...~%")
- ;; Wait for "hello" message from the server. This is the only way to know
- ;; that we're really connected to the server inside the guest.
- (if (wait-for-readable-fd sock handshake-timeout)
- (match (read sock)
- (('secret-service-server ('version version ...))
- (log "sending files from ~s...~%" secret-root)
- (send-files sock)
- (log "done sending files to port ~a~%" port)
- (close-port sock)
- secret-root)
- (x
- (log "invalid handshake ~s~%" x)
- (close-port sock)
- #f))
- (begin ;timeout
- (log "timeout while sending files to ~a~%" port)
- (close-port sock)
- #f))))
- (define (delete-file* file)
- "Ensure FILE does not exist."
- (catch 'system-error
- (lambda ()
- (delete-file file))
- (lambda args
- (unless (= ENOENT (system-error-errno args))
- (apply throw args)))))
- (define (secret-service-receive-secrets port)
- "Listen to local PORT and wait for a secret service client to send secrets.
- Write them to the file system. Return the list of files installed on success,
- and #f otherwise."
- (define (wait-for-client port)
- ;; Wait for a TCP connection on PORT. Note: We cannot use the
- ;; virtio-serial ports, which would be safer, because they are
- ;; (presumably) unsupported on GNU/Hurd.
- (let ((sock (socket AF_INET (logior SOCK_CLOEXEC SOCK_STREAM) 0)))
- (bind sock AF_INET INADDR_ANY port)
- (listen sock 1)
- (log "waiting for secrets on port ~a...~%" port)
- (match (select (list sock) '() '() 60)
- (((_) () ())
- (match (accept sock)
- ((client . address)
- (log "client connection from ~a~%"
- (inet-ntop (sockaddr:fam address)
- (sockaddr:addr address)))
- ;; Send a "hello" message. This allows the client running on the
- ;; host to know that it's now actually connected to server running
- ;; in the guest.
- (write '(secret-service-server (version 0)) client)
- (force-output client)
- (close-port sock)
- client)))
- ((() () ())
- (log "did not receive any secrets; time out~%")
- (close-port sock)
- #f))))
- ;; TODO: Remove when (@ (guix build utils) dump-port) has a 'size'
- ;; parameter.
- (define (dump in out size)
- ;; Copy SIZE bytes from IN to OUT.
- (define buf-size 65536)
- (define buf (make-bytevector buf-size))
- (let loop ((left size))
- (if (<= left 0)
- 0
- (let ((read (get-bytevector-n! in buf 0 (min left buf-size))))
- (if (eof-object? read)
- left
- (begin
- (put-bytevector out buf 0 read)
- (loop (- left read))))))))
- (define (read-secrets port)
- ;; Read secret files from PORT and install them.
- (match (false-if-exception (read port))
- (('secrets ('version 0)
- ('files ((files sizes modes) ...)))
- (for-each (lambda (file size mode)
- (log "installing file '~a' (~a bytes)...~%"
- file size)
- (mkdir-p (dirname file))
- ;; It could be that FILE already exists, for instance
- ;; because it has been created by a service's activation
- ;; snippet (e.g., SSH host keys). Delete it.
- (delete-file* file)
- (call-with-output-file file
- (lambda (output)
- (dump port output size)
- (chmod file mode))))
- files sizes modes)
- (log "received ~a secret files~%" (length files))
- files)
- (_
- (log "invalid secrets received~%")
- #f)))
- (let* ((port (wait-for-client port))
- (result (and=> port read-secrets)))
- (when port
- (close-port port))
- result))
- ;;; Local Variables:
- ;;; eval: (put 'with-modules 'scheme-indent-function 1)
- ;;; End:
- ;;; secret-service.scm ends here
|