download: Verify TLS certificates unless asked not to.
Fixes <http://bugs.gnu.org/24466>. Reported by Leo Famulari <leo@famulari.name>. * guix/build/download.scm (%x509-certificate-directory): New variable. (make-credendials-with-ca-trust-files, peer-certificate) (assert-valid-server-certificate, print-tls-certificate-error): New procedures. Add 'print-tls-certificate-error' as an exception printer for 'tls-certificate-error'. (tls-wrap): Add #:verify-certificate? parameter and honor it. (open-connection-for-uri): Likewise. (http-fetch): Likewise. (url-fetch): Likewise. * guix/download.scm (url-fetch)[builder]: Pass #:verify-certificate? #f. * guix/scripts/lint.scm (probe-uri): Add case for 'tls-certificate-error'. (validate-uri): Likewise. * doc/guix.texi (Invoking guix download): Mention 'SSL_CERT_DIR'.
This commit is contained in:
parent
a00fbe8adf
commit
bc3c41ce36
|
@ -4768,6 +4768,11 @@ they are not available, an error is raised. @xref{Guile Preparations,
|
||||||
how to install the GnuTLS bindings for Guile,, gnutls-guile,
|
how to install the GnuTLS bindings for Guile,, gnutls-guile,
|
||||||
GnuTLS-Guile}, for more information.
|
GnuTLS-Guile}, for more information.
|
||||||
|
|
||||||
|
@command{guix download} verifies HTTPS server certificates by loading
|
||||||
|
the certificates of X.509 authorities from the directory pointed to by
|
||||||
|
the @code{SSL_CERT_DIR} environment variable (@pxref{X.509
|
||||||
|
Certificates}).
|
||||||
|
|
||||||
The following option is available:
|
The following option is available:
|
||||||
|
|
||||||
@table @code
|
@table @code
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
#:use-module (srfi srfi-11)
|
#:use-module (srfi srfi-11)
|
||||||
#:use-module (srfi srfi-19)
|
#:use-module (srfi srfi-19)
|
||||||
#:use-module (srfi srfi-26)
|
#:use-module (srfi srfi-26)
|
||||||
|
#:autoload (ice-9 ftw) (scandir)
|
||||||
#:use-module (ice-9 match)
|
#:use-module (ice-9 match)
|
||||||
#:use-module (ice-9 format)
|
#:use-module (ice-9 format)
|
||||||
#:export (open-socket-for-uri
|
#:export (open-socket-for-uri
|
||||||
|
@ -273,14 +274,78 @@ out if the connection could not be established in less than TIMEOUT seconds."
|
||||||
session record port using PORT as its underlying communication port."
|
session record port using PORT as its underlying communication port."
|
||||||
(hashq-set! %tls-ports record-port port))
|
(hashq-set! %tls-ports record-port port))
|
||||||
|
|
||||||
(define (tls-wrap port server)
|
(define %x509-certificate-directory
|
||||||
|
;; The directory where X.509 authority PEM certificates are stored.
|
||||||
|
(make-parameter (or (getenv "GUIX_TLS_CERTIFICATE_DIRECTORY")
|
||||||
|
(getenv "SSL_CERT_DIR")))) ;like OpenSSL
|
||||||
|
|
||||||
|
(define (make-credendials-with-ca-trust-files directory)
|
||||||
|
"Return certificate credentials with X.509 authority certificates read from
|
||||||
|
DIRECTORY. Those authority certificates are checked when
|
||||||
|
'peer-certificate-status' is later called."
|
||||||
|
(let ((cred (make-certificate-credentials))
|
||||||
|
(files (or (scandir directory
|
||||||
|
(lambda (file)
|
||||||
|
(string-suffix? ".pem" file)))
|
||||||
|
'())))
|
||||||
|
(for-each (lambda (file)
|
||||||
|
(set-certificate-credentials-x509-trust-file!
|
||||||
|
cred (string-append directory "/" file)
|
||||||
|
x509-certificate-format/pem))
|
||||||
|
(or files '()))
|
||||||
|
cred))
|
||||||
|
|
||||||
|
(define (peer-certificate session)
|
||||||
|
"Return the certificate of the remote peer in SESSION."
|
||||||
|
(match (session-peer-certificate-chain session)
|
||||||
|
((first _ ...)
|
||||||
|
(import-x509-certificate first x509-certificate-format/der))))
|
||||||
|
|
||||||
|
(define (assert-valid-server-certificate session server)
|
||||||
|
"Return #t if the certificate of the remote peer for SESSION is a valid
|
||||||
|
certificate for SERVER, where SERVER is the expected host name of peer."
|
||||||
|
(define cert
|
||||||
|
(peer-certificate session))
|
||||||
|
|
||||||
|
;; First check whether the server's certificate matches SERVER.
|
||||||
|
(unless (x509-certificate-matches-hostname? cert server)
|
||||||
|
(throw 'tls-certificate-error 'host-mismatch cert server))
|
||||||
|
|
||||||
|
;; Second check its validity and reachability from the set of authority
|
||||||
|
;; certificates loaded via 'set-certificate-credentials-x509-trust-file!'.
|
||||||
|
(match (peer-certificate-status session)
|
||||||
|
(() ;certificate is valid
|
||||||
|
#t)
|
||||||
|
((statuses ...)
|
||||||
|
(throw 'tls-certificate-error 'invalid-certificate cert server
|
||||||
|
statuses))))
|
||||||
|
|
||||||
|
(define (print-tls-certificate-error port key args default-printer)
|
||||||
|
"Print the TLS certificate error represented by ARGS in an intelligible
|
||||||
|
way."
|
||||||
|
(match args
|
||||||
|
(('host-mismatch cert server)
|
||||||
|
(format port
|
||||||
|
"X.509 server certificate for '~a' does not match: ~a~%"
|
||||||
|
server (x509-certificate-dn cert)))
|
||||||
|
(('invalid-certificate cert server statuses)
|
||||||
|
(format port
|
||||||
|
"X.509 certificate of '~a' could not be verified:~%~{ ~a~%~}"
|
||||||
|
server
|
||||||
|
(map certificate-status->string statuses)))))
|
||||||
|
|
||||||
|
(set-exception-printer! 'tls-certificate-error
|
||||||
|
print-tls-certificate-error)
|
||||||
|
|
||||||
|
(define* (tls-wrap port server #:key (verify-certificate? #t))
|
||||||
"Return PORT wrapped in a TLS connection to SERVER. SERVER must be a DNS
|
"Return PORT wrapped in a TLS connection to SERVER. SERVER must be a DNS
|
||||||
host name without trailing dot."
|
host name without trailing dot."
|
||||||
(define (log level str)
|
(define (log level str)
|
||||||
(format (current-error-port)
|
(format (current-error-port)
|
||||||
"gnutls: [~a|~a] ~a" (getpid) level str))
|
"gnutls: [~a|~a] ~a" (getpid) level str))
|
||||||
|
|
||||||
(let ((session (make-session connection-end/client)))
|
(let ((session (make-session connection-end/client))
|
||||||
|
(ca-certs (%x509-certificate-directory)))
|
||||||
|
|
||||||
;; Some servers such as 'cloud.github.com' require the client to support
|
;; Some servers such as 'cloud.github.com' require the client to support
|
||||||
;; the 'SERVER NAME' extension. However, 'set-session-server-name!' is
|
;; the 'SERVER NAME' extension. However, 'set-session-server-name!' is
|
||||||
|
@ -301,13 +366,27 @@ host name without trailing dot."
|
||||||
;; <https://tools.ietf.org/html/rfc7568>.
|
;; <https://tools.ietf.org/html/rfc7568>.
|
||||||
(set-session-priorities! session "NORMAL:%COMPAT:-VERS-SSL3.0")
|
(set-session-priorities! session "NORMAL:%COMPAT:-VERS-SSL3.0")
|
||||||
|
|
||||||
(set-session-credentials! session (make-certificate-credentials))
|
(set-session-credentials! session
|
||||||
|
(if (and verify-certificate? ca-certs)
|
||||||
|
(make-credendials-with-ca-trust-files
|
||||||
|
ca-certs)
|
||||||
|
(make-certificate-credentials)))
|
||||||
|
|
||||||
;; Uncomment the following lines in case of debugging emergency.
|
;; Uncomment the following lines in case of debugging emergency.
|
||||||
;;(set-log-level! 10)
|
;;(set-log-level! 10)
|
||||||
;;(set-log-procedure! log)
|
;;(set-log-procedure! log)
|
||||||
|
|
||||||
(handshake session)
|
(handshake session)
|
||||||
|
|
||||||
|
;; Verify the server's certificate if needed.
|
||||||
|
(when verify-certificate?
|
||||||
|
(catch 'tls-certificate-error
|
||||||
|
(lambda ()
|
||||||
|
(assert-valid-server-certificate session server))
|
||||||
|
(lambda args
|
||||||
|
(close-port port)
|
||||||
|
(apply throw args))))
|
||||||
|
|
||||||
(let ((record (session-record-port session)))
|
(let ((record (session-record-port session)))
|
||||||
;; Since we use `fileno' above, the file descriptor behind PORT would be
|
;; Since we use `fileno' above, the file descriptor behind PORT would be
|
||||||
;; closed when PORT is GC'd. If we used `port->fdes', it would instead
|
;; closed when PORT is GC'd. If we used `port->fdes', it would instead
|
||||||
|
@ -374,9 +453,13 @@ ETIMEDOUT error is raised."
|
||||||
(apply throw args)
|
(apply throw args)
|
||||||
(loop (cdr addresses))))))))
|
(loop (cdr addresses))))))))
|
||||||
|
|
||||||
(define* (open-connection-for-uri uri #:key timeout)
|
(define* (open-connection-for-uri uri
|
||||||
|
#:key
|
||||||
|
timeout
|
||||||
|
(verify-certificate? #t))
|
||||||
"Like 'open-socket-for-uri', but also handle HTTPS connections. The
|
"Like 'open-socket-for-uri', but also handle HTTPS connections. The
|
||||||
resulting port must be closed with 'close-connection'."
|
resulting port must be closed with 'close-connection'. When
|
||||||
|
VERIFY-CERTIFICATE? is true, verify HTTPS server certificates."
|
||||||
(define https?
|
(define https?
|
||||||
(eq? 'https (uri-scheme uri)))
|
(eq? 'https (uri-scheme uri)))
|
||||||
|
|
||||||
|
@ -403,7 +486,8 @@ resulting port must be closed with 'close-connection'."
|
||||||
(setvbuf s _IOFBF %http-receive-buffer-size)
|
(setvbuf s _IOFBF %http-receive-buffer-size)
|
||||||
|
|
||||||
(if https?
|
(if https?
|
||||||
(tls-wrap s (uri-host uri))
|
(tls-wrap s (uri-host uri)
|
||||||
|
#:verify-certificate? verify-certificate?)
|
||||||
s)))))
|
s)))))
|
||||||
|
|
||||||
(define (close-connection port)
|
(define (close-connection port)
|
||||||
|
@ -588,10 +672,11 @@ Return the resulting target URI."
|
||||||
#:query (uri-query ref)
|
#:query (uri-query ref)
|
||||||
#:fragment (uri-fragment ref)))))
|
#:fragment (uri-fragment ref)))))
|
||||||
|
|
||||||
(define* (http-fetch uri file #:key timeout)
|
(define* (http-fetch uri file #:key timeout (verify-certificate? #t))
|
||||||
"Fetch data from URI and write it to FILE; when TIMEOUT is true, bail out if
|
"Fetch data from URI and write it to FILE; when TIMEOUT is true, bail out if
|
||||||
the connection could not be established in less than TIMEOUT seconds. Return
|
the connection could not be established in less than TIMEOUT seconds. Return
|
||||||
FILE on success."
|
FILE on success. When VERIFY-CERTIFICATE? is true, verify HTTPS
|
||||||
|
certificates; otherwise simply ignore them."
|
||||||
|
|
||||||
(define post-2.0.7?
|
(define post-2.0.7?
|
||||||
(or (> (string->number (major-version)) 2)
|
(or (> (string->number (major-version)) 2)
|
||||||
|
@ -618,7 +703,10 @@ FILE on success."
|
||||||
(_ '()))))
|
(_ '()))))
|
||||||
|
|
||||||
(let*-values (((connection)
|
(let*-values (((connection)
|
||||||
(open-connection-for-uri uri #:timeout timeout))
|
(open-connection-for-uri uri
|
||||||
|
#:timeout timeout
|
||||||
|
#:verify-certificate?
|
||||||
|
verify-certificate?))
|
||||||
((resp bv-or-port)
|
((resp bv-or-port)
|
||||||
;; XXX: `http-get*' was introduced in 2.0.7, and replaced by
|
;; XXX: `http-get*' was introduced in 2.0.7, and replaced by
|
||||||
;; #:streaming? in 2.0.8. We know we're using it within the
|
;; #:streaming? in 2.0.8. We know we're using it within the
|
||||||
|
@ -659,7 +747,9 @@ FILE on success."
|
||||||
(format #t "following redirection to `~a'...~%"
|
(format #t "following redirection to `~a'...~%"
|
||||||
(uri->string uri))
|
(uri->string uri))
|
||||||
(close connection)
|
(close connection)
|
||||||
(http-fetch uri file #:timeout timeout)))
|
(http-fetch uri file
|
||||||
|
#:timeout timeout
|
||||||
|
#:verify-certificate? verify-certificate?)))
|
||||||
(else
|
(else
|
||||||
(error "download failed" (uri->string uri)
|
(error "download failed" (uri->string uri)
|
||||||
code (response-reason-phrase resp))))))
|
code (response-reason-phrase resp))))))
|
||||||
|
@ -699,7 +789,7 @@ Return a list of URIs."
|
||||||
|
|
||||||
(define* (url-fetch url file
|
(define* (url-fetch url file
|
||||||
#:key
|
#:key
|
||||||
(timeout 10)
|
(timeout 10) (verify-certificate? #t)
|
||||||
(mirrors '()) (content-addressed-mirrors '())
|
(mirrors '()) (content-addressed-mirrors '())
|
||||||
(hashes '()))
|
(hashes '()))
|
||||||
"Fetch FILE from URL; URL may be either a single string, or a list of
|
"Fetch FILE from URL; URL may be either a single string, or a list of
|
||||||
|
@ -713,7 +803,10 @@ HASHES must be a list of algorithm/hash pairs, where each algorithm is a
|
||||||
symbol such as 'sha256 and each hash is a bytevector.
|
symbol such as 'sha256 and each hash is a bytevector.
|
||||||
CONTENT-ADDRESSED-MIRRORS must be a list of procedures that, given a hash
|
CONTENT-ADDRESSED-MIRRORS must be a list of procedures that, given a hash
|
||||||
algorithm and a hash, return a URL where the specified data can be retrieved
|
algorithm and a hash, return a URL where the specified data can be retrieved
|
||||||
or #f."
|
or #f.
|
||||||
|
|
||||||
|
When VERIFY-CERTIFICATE? is true, validate HTTPS server certificates;
|
||||||
|
otherwise simply ignore them."
|
||||||
(define uri
|
(define uri
|
||||||
(append-map (cut maybe-expand-mirrors <> mirrors)
|
(append-map (cut maybe-expand-mirrors <> mirrors)
|
||||||
(match url
|
(match url
|
||||||
|
@ -725,9 +818,13 @@ or #f."
|
||||||
file (uri->string uri))
|
file (uri->string uri))
|
||||||
(case (uri-scheme uri)
|
(case (uri-scheme uri)
|
||||||
((http https)
|
((http https)
|
||||||
(false-if-exception* (http-fetch uri file #:timeout timeout)))
|
(false-if-exception* (http-fetch uri file
|
||||||
|
#:verify-certificate?
|
||||||
|
verify-certificate?
|
||||||
|
#:timeout timeout)))
|
||||||
((ftp)
|
((ftp)
|
||||||
(false-if-exception* (ftp-fetch uri file #:timeout timeout)))
|
(false-if-exception* (ftp-fetch uri file
|
||||||
|
#:timeout timeout)))
|
||||||
(else
|
(else
|
||||||
(format #t "skipping URI with unsupported scheme: ~s~%"
|
(format #t "skipping URI with unsupported scheme: ~s~%"
|
||||||
uri)
|
uri)
|
||||||
|
|
|
@ -372,7 +372,11 @@ in the store."
|
||||||
#:hashes
|
#:hashes
|
||||||
(value-from-environment "guix download hashes")
|
(value-from-environment "guix download hashes")
|
||||||
#:content-addressed-mirrors
|
#:content-addressed-mirrors
|
||||||
(primitive-load #$%content-addressed-mirror-file))))))
|
(primitive-load #$%content-addressed-mirror-file)
|
||||||
|
|
||||||
|
;; No need to validate certificates since we know the
|
||||||
|
;; hash of the expected result.
|
||||||
|
#:verify-certificate? #f)))))
|
||||||
|
|
||||||
(let ((uri (and (string? url) (string->uri url))))
|
(let ((uri (and (string? url) (string->uri url))))
|
||||||
(if (or (and (string? url) (not uri))
|
(if (or (and (string? url) (not uri))
|
||||||
|
|
|
@ -369,7 +369,8 @@ for connections to complete; when TIMEOUT is #f, wait as long as needed."
|
||||||
;; This can happen if the server returns an invalid HTTP header,
|
;; This can happen if the server returns an invalid HTTP header,
|
||||||
;; as is the case with the 'Date' header at sqlite.org.
|
;; as is the case with the 'Date' header at sqlite.org.
|
||||||
(values 'invalid-http-response #f))
|
(values 'invalid-http-response #f))
|
||||||
((getaddrinfo-error system-error gnutls-error)
|
((getaddrinfo-error system-error
|
||||||
|
gnutls-error tls-certificate-error)
|
||||||
(values key args))
|
(values key args))
|
||||||
(else
|
(else
|
||||||
(apply throw key args))))))
|
(apply throw key args))))))
|
||||||
|
@ -457,6 +458,15 @@ suspiciously small file (~a bytes)")
|
||||||
(cons status argument))))
|
(cons status argument))))
|
||||||
field)
|
field)
|
||||||
#f)
|
#f)
|
||||||
|
((tls-certificate-error)
|
||||||
|
(emit-warning package
|
||||||
|
(format #f
|
||||||
|
(_ "TLS certificate error: ~a")
|
||||||
|
(call-with-output-string
|
||||||
|
(lambda (port)
|
||||||
|
(print-exception port #f
|
||||||
|
'tls-certificate-error
|
||||||
|
argument))))))
|
||||||
((invalid-http-response gnutls-error)
|
((invalid-http-response gnutls-error)
|
||||||
;; Probably a misbehaving server; ignore.
|
;; Probably a misbehaving server; ignore.
|
||||||
#f)
|
#f)
|
||||||
|
|
Loading…
Reference in New Issue