guix-mirrors/gnu/build/secret-service.scm
Ludovic Courtès 4ed3c827c6
services: secret-service: Add #:timeout to ‘secret-service-receive-secrets’.
* gnu/build/secret-service.scm (secret-service-receive-secrets):
Add #:timeout parameter and honor it.

Change-Id: I4b6720444a28e1424ede07b6c329cd355b27b5e3
2025-09-21 17:57:16 +02:00

232 lines
8.9 KiB
Scheme

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2020-2023, 2025 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)
#:autoload (fibers io-wakeup) (wait-until-port-readable-operation)
#:autoload (fibers operations) (perform-operation
choice-operation
wrap-operation)
#:autoload (fibers timers) (sleep-operation)
#: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.
;;;
;;; Note: This code runs within the 'shepherd' process, hence the use of
;;; Fibers.
;;;
;;; 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 (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."
(perform-operation
(choice-operation
(wrap-operation (wait-until-port-readable-operation port)
(const #t))
(wrap-operation (sleep-operation timeout)
(const #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_NONBLOCK SOCK_STREAM)
0))
(sleep (module-ref (resolve-interface '(fibers)) '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
#:key (timeout 60))
"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 if TIMEOUT seconds
passed without receiving any files or if some other failure occurred."
(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_NONBLOCK SOCK_STREAM)
0)))
(bind sock address)
(listen sock 1)
(log "waiting for secrets on ~a...~%"
(socket-address->string address))
(if (wait-for-readable-fd sock timeout)
(match (accept sock (logior SOCK_CLOEXEC SOCK_NONBLOCK))
((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))
(begin
(log "did not receive any secrets; time out~%")
(close-port sock)
#f))))
(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 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