Until now, the secret service had a hard-coded TCP endpoint on port 1004. This change lets users specify arbitrary socket addresses. * gnu/build/secret-service.scm (socket-address->string): New procedure, taken from Shepherd. (secret-service-send-secrets): Replace ‘port’ by ‘address’ and adjust accordingly. (secret-service-receive-secrets): Likewise. * gnu/services/virtualization.scm (secret-service-shepherd-services): Likewise. (secret-service-operating-system): Add optional ‘address’ parameter and honor it. Adjust ‘start’ method accordingly. Change-Id: I87a9514f1c170dca756ce76083d7182c6ebf6578
		
			
				
	
	
		
			275 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Scheme
		
	
	
	
	
	
			
		
		
	
	
			275 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Scheme
		
	
	
	
	
	
| ;;; GNU Guix --- Functional package management for GNU
 | |
| ;;; Copyright © 2020-2023 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 (socket-address->string address)
 | |
|   "Return a human-readable representation of ADDRESS, an object as returned by
 | |
| 'make-socket-address'."
 | |
|   (let ((family (sockaddr:fam address)))
 | |
|     (cond ((= AF_INET family)
 | |
|            (string-append (inet-ntop AF_INET (sockaddr:addr address))
 | |
|                           ":" (number->string (sockaddr:port address))))
 | |
|           ((= AF_INET6 family)
 | |
|            (string-append "[" (inet-ntop AF_INET6 (sockaddr:addr address)) "]"
 | |
|                           ":" (number->string (sockaddr:port address))))
 | |
|           ((= AF_UNIX family)
 | |
|            (sockaddr:path address))
 | |
|           (else
 | |
|            (object->string address)))))
 | |
| 
 | |
| (define* (secret-service-send-secrets address secret-root
 | |
|                                       #:key (retry 60)
 | |
|                                       (handshake-timeout 180))
 | |
|   "Copy all files under SECRET-ROOT by connecting to secret-service listening
 | |
| at ADDRESS, an address as returned by 'make-socket-address'.  If connection
 | |
| 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~%" (socket-address->string address))
 | |
| 
 | |
|   (let ((sock (socket AF_INET (logior SOCK_CLOEXEC SOCK_STREAM) 0))
 | |
|         (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 address)
 | |
|         (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 ~a~%"
 | |
|                 (socket-address->string address))
 | |
|            (close-port sock)
 | |
|            secret-root)
 | |
|           (x
 | |
|            (log "invalid handshake ~s~%" x)
 | |
|            (close-port sock)
 | |
|            #f))
 | |
|         (begin                                    ;timeout
 | |
|          (log "timeout while sending files to ~a~%"
 | |
|               (socket-address->string address))
 | |
|          (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 address)
 | |
|   "Listen to ADDRESS, an address returned by 'make-socket-address', 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 address)
 | |
|     ;; Wait for a connection on ADDRESS.  Note: virtio-serial ports are safer
 | |
|     ;; than TCP connections but they are (presumably) unsupported on GNU/Hurd.
 | |
|     (let ((sock (socket AF_INET (logior SOCK_CLOEXEC SOCK_STREAM) 0)))
 | |
|       (bind sock address)
 | |
|       (listen sock 1)
 | |
|       (log "waiting for secrets on ~a...~%"
 | |
|            (socket-address->string address))
 | |
| 
 | |
|       (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 address))
 | |
|          (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
 |