349 lines
13 KiB
Scheme
349 lines
13 KiB
Scheme
;;; GNU Guix --- Functional package management for GNU
|
|
;;; Copyright © 2013-2022 Ludovic Courtès <ludo@gnu.org>
|
|
;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org>
|
|
;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
|
|
;;;
|
|
;;; 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 (guix narinfo)
|
|
#:use-module (guix pki)
|
|
#:use-module (guix i18n)
|
|
#:use-module (guix base32)
|
|
#:use-module (guix base64)
|
|
#:use-module (guix records)
|
|
#:use-module (guix diagnostics)
|
|
#:use-module (gcrypt hash)
|
|
#:use-module (gcrypt pk-crypto)
|
|
#:use-module (rnrs bytevectors)
|
|
#:use-module (srfi srfi-1)
|
|
#:use-module (srfi srfi-9)
|
|
#:use-module (srfi srfi-26)
|
|
#:use-module (ice-9 match)
|
|
#:use-module (ice-9 binary-ports)
|
|
#:use-module (web uri)
|
|
#:export (narinfo-signature->canonical-sexp
|
|
|
|
narinfo?
|
|
narinfo-path
|
|
narinfo-uris
|
|
narinfo-uri-base
|
|
narinfo-compressions
|
|
narinfo-file-hashes
|
|
narinfo-file-sizes
|
|
narinfo-hash
|
|
narinfo-size
|
|
narinfo-references
|
|
narinfo-deriver
|
|
narinfo-system
|
|
narinfo-signature
|
|
narinfo-contents
|
|
|
|
narinfo-hash-algorithm+value
|
|
|
|
narinfo-hash->sha256
|
|
narinfo-best-uri
|
|
|
|
valid-narinfo?
|
|
|
|
read-narinfo
|
|
write-narinfo
|
|
|
|
string->narinfo
|
|
narinfo->string
|
|
|
|
equivalent-narinfo?))
|
|
|
|
(define-record-type <narinfo>
|
|
(%make-narinfo path uri-base uris compressions file-sizes file-hashes
|
|
nar-hash nar-size references deriver system
|
|
signature contents)
|
|
narinfo?
|
|
(path narinfo-path)
|
|
(uri-base narinfo-uri-base) ;URI of the cache it originates from
|
|
(uris narinfo-uris) ;list of strings
|
|
(compressions narinfo-compressions) ;list of strings
|
|
(file-sizes narinfo-file-sizes) ;list of (integers | #f)
|
|
(file-hashes narinfo-file-hashes)
|
|
(nar-hash narinfo-hash)
|
|
(nar-size narinfo-size)
|
|
(references narinfo-references)
|
|
(deriver narinfo-deriver)
|
|
(system narinfo-system)
|
|
(signature narinfo-signature) ; canonical sexp
|
|
;; The original contents of a narinfo file. This field is needed because we
|
|
;; want to preserve the exact textual representation for verification purposes.
|
|
;; See <https://lists.gnu.org/archive/html/guix-devel/2014-02/msg00340.html>
|
|
;; for more information.
|
|
(contents narinfo-contents))
|
|
|
|
(define (narinfo-hash-algorithm+value narinfo)
|
|
"Return two values: the hash algorithm used by NARINFO and its value as a
|
|
bytevector."
|
|
(match (string-tokenize (narinfo-hash narinfo)
|
|
(char-set-complement (char-set #\:)))
|
|
((algorithm base32)
|
|
(values (lookup-hash-algorithm (string->symbol algorithm))
|
|
(nix-base32-string->bytevector base32)))
|
|
(_
|
|
(raise (formatted-message
|
|
(G_ "invalid narinfo hash: ~s") (narinfo-hash narinfo))))))
|
|
|
|
(define (narinfo-hash->sha256 hash)
|
|
"If the string HASH denotes a sha256 hash, return it as a bytevector.
|
|
Otherwise return #f."
|
|
(and (string-prefix? "sha256:" hash)
|
|
(nix-base32-string->bytevector (string-drop hash 7))))
|
|
|
|
(define (narinfo-signature->canonical-sexp str)
|
|
"Return the value of a narinfo's 'Signature' field as a canonical sexp."
|
|
(match (string-split str #\;)
|
|
((version host-name sig)
|
|
(let ((maybe-number (string->number version)))
|
|
(cond ((not (number? maybe-number))
|
|
(leave (G_ "signature version must be a number: ~s~%")
|
|
version))
|
|
;; Currently, there are no other versions.
|
|
((not (= 1 maybe-number))
|
|
(leave (G_ "unsupported signature version: ~a~%")
|
|
maybe-number))
|
|
(else
|
|
(let ((signature (utf8->string (base64-decode sig))))
|
|
(catch 'gcry-error
|
|
(lambda ()
|
|
(string->canonical-sexp signature))
|
|
(lambda (key proc err)
|
|
(leave (G_ "signature is not a valid \
|
|
s-expression: ~s~%")
|
|
signature))))))))
|
|
(x
|
|
(leave (G_ "invalid format of the signature field: ~a~%") x))))
|
|
|
|
(define (narinfo-maker str cache-url)
|
|
"Return a narinfo constructor for narinfos originating from CACHE-URL. STR
|
|
must contain the original contents of a narinfo file."
|
|
(lambda (path urls compressions file-hashes file-sizes
|
|
nar-hash nar-size references deriver system
|
|
signature)
|
|
"Return a new <narinfo> object."
|
|
(define len (length urls))
|
|
(%make-narinfo path cache-url
|
|
;; Handle the case where URL is a relative URL.
|
|
(map (lambda (url)
|
|
(or (string->uri url)
|
|
(string->uri
|
|
(if (string-suffix? "/" cache-url)
|
|
(string-append cache-url url)
|
|
(string-append cache-url "/" url)))))
|
|
urls)
|
|
compressions
|
|
(match file-sizes
|
|
(() (make-list len #f))
|
|
((lst ...) (map string->number lst)))
|
|
(match file-hashes
|
|
(() (make-list len #f))
|
|
((lst ...) (map string->number lst)))
|
|
nar-hash
|
|
(and=> nar-size string->number)
|
|
(string-tokenize references)
|
|
(match deriver
|
|
((or #f "") #f)
|
|
(_ deriver))
|
|
system
|
|
(false-if-exception
|
|
(and=> signature narinfo-signature->canonical-sexp))
|
|
str)))
|
|
|
|
(define fields->alist
|
|
;; The narinfo format is really just like recutils.
|
|
recutils->alist)
|
|
|
|
(define* (read-narinfo port #:optional url
|
|
#:key size)
|
|
"Read a narinfo from PORT. If URL is true, it must be a string used to
|
|
build full URIs from relative URIs found while reading PORT. When SIZE is
|
|
true, read at most SIZE bytes from PORT; otherwise, read as much as possible.
|
|
|
|
No authentication and authorization checks are performed here!"
|
|
(let ((str (utf8->string (if size
|
|
(get-bytevector-n port size)
|
|
(get-bytevector-all port)))))
|
|
(alist->record (call-with-input-string str fields->alist)
|
|
(narinfo-maker str url)
|
|
'("StorePath" "URL" "Compression"
|
|
"FileHash" "FileSize" "NarHash" "NarSize"
|
|
"References" "Deriver" "System"
|
|
"Signature")
|
|
'("URL" "Compression" "FileSize" "FileHash"))))
|
|
|
|
(define (narinfo-sha256 narinfo)
|
|
"Return the sha256 hash of NARINFO as a bytevector, or #f if NARINFO lacks a
|
|
'Signature' field."
|
|
(define %mandatory-fields
|
|
;; List of fields that must be signed. If they are not signed, the
|
|
;; narinfo is considered unsigned.
|
|
'("StorePath" "NarHash" "References"))
|
|
|
|
(let ((contents (narinfo-contents narinfo)))
|
|
(match (string-contains contents "Signature:")
|
|
(#f #f)
|
|
(index
|
|
(let* ((above-signature (string-take contents index))
|
|
(signed-fields (match (call-with-input-string above-signature
|
|
fields->alist)
|
|
(((fields . values) ...) fields))))
|
|
(and (every (cut member <> signed-fields) %mandatory-fields)
|
|
(sha256 (string->utf8 above-signature))))))))
|
|
|
|
(define* (valid-narinfo? narinfo #:optional (acl (current-acl))
|
|
#:key verbose?)
|
|
"Return #t if NARINFO's signature is valid and made by one of the keys in
|
|
ACL."
|
|
(let ((hash (narinfo-sha256 narinfo))
|
|
(signature (narinfo-signature narinfo))
|
|
(uri (uri->string (first (narinfo-uris narinfo)))))
|
|
(and hash signature
|
|
(signature-case (signature hash acl)
|
|
(valid-signature #t)
|
|
(invalid-signature
|
|
(when verbose?
|
|
(format (current-error-port)
|
|
"invalid signature for substitute at '~a'~%"
|
|
uri))
|
|
#f)
|
|
(hash-mismatch
|
|
(when verbose?
|
|
(format (current-error-port)
|
|
"hash mismatch for substitute at '~a'~%"
|
|
uri))
|
|
#f)
|
|
(unauthorized-key
|
|
(when verbose?
|
|
(format (current-error-port)
|
|
"substitute at '~a' is signed by an \
|
|
unauthorized party~%"
|
|
uri))
|
|
#f)
|
|
(corrupt-signature
|
|
(when verbose?
|
|
(format (current-error-port)
|
|
"corrupt signature for substitute at '~a'~%"
|
|
uri))
|
|
#f)))))
|
|
|
|
(define (write-narinfo narinfo port)
|
|
"Write NARINFO to PORT."
|
|
(put-bytevector port (string->utf8 (narinfo-contents narinfo))))
|
|
|
|
(define (narinfo->string narinfo)
|
|
"Return the external representation of NARINFO."
|
|
(call-with-output-string (cut write-narinfo narinfo <>)))
|
|
|
|
(define (string->narinfo str cache-uri)
|
|
"Return the narinfo represented by STR. Assume CACHE-URI as the base URI of
|
|
the cache STR originates form."
|
|
(call-with-input-string str (cut read-narinfo <> cache-uri)))
|
|
|
|
(define (equivalent-narinfo? narinfo1 narinfo2)
|
|
"Return true if NARINFO1 and NARINFO2 are equivalent--i.e., if they describe
|
|
the same store item. This ignores unnecessary metadata such as the Nar URL."
|
|
(and (string=? (narinfo-hash narinfo1)
|
|
(narinfo-hash narinfo2))
|
|
|
|
;; The following is not needed if all we want is to download a valid
|
|
;; nar, but it's necessary if we want valid narinfo.
|
|
(string=? (narinfo-path narinfo1)
|
|
(narinfo-path narinfo2))
|
|
(equal? (narinfo-references narinfo1)
|
|
(narinfo-references narinfo2))
|
|
|
|
(= (narinfo-size narinfo1)
|
|
(narinfo-size narinfo2))))
|
|
|
|
(define %compression-methods
|
|
;; Known compression methods and a thunk to determine whether they're
|
|
;; supported. See 'decompressed-port' in (guix utils).
|
|
`(("gzip" . ,(const #t))
|
|
("lzip" . ,(const #t))
|
|
("zstd" . ,(lambda ()
|
|
(resolve-module '(zstd) #t #f #:ensure #f)))
|
|
("xz" . ,(const #t))
|
|
("bzip2" . ,(const #t))
|
|
("none" . ,(const #t))))
|
|
|
|
(define (supported-compression? compression)
|
|
"Return true if COMPRESSION, a string, denotes a supported compression
|
|
method."
|
|
(match (assoc-ref %compression-methods compression)
|
|
(#f #f)
|
|
(supported? (supported?))))
|
|
|
|
(define (compresses-better? compression1 compression2)
|
|
"Return true if COMPRESSION1 generally compresses better than COMPRESSION2;
|
|
this is a rough approximation."
|
|
(match compression1
|
|
("none" #f)
|
|
("gzip" (string=? compression2 "none"))
|
|
("lzip" #t)
|
|
(_ (or (string=? compression2 "none")
|
|
(string=? compression2 "gzip")))))
|
|
|
|
(define (decompresses-faster? compression1 compression2)
|
|
"Return true if COMPRESSION1 generally has a higher decompression throughput
|
|
than COMPRESSION2."
|
|
(match compression1
|
|
("none" #t)
|
|
("zstd" #t)
|
|
("gzip" (string=? compression2 "lzip"))
|
|
(_ #f)))
|
|
|
|
(define* (narinfo-best-uri narinfo #:key fast-decompression?)
|
|
"Select the \"best\" URI to download NARINFO's nar, and return three values:
|
|
the URI, its compression method (a string), and the compressed file size.
|
|
When FAST-DECOMPRESSION? is true, prefer substitutes with faster
|
|
decompression (typically zstd) rather than substitutes with a higher
|
|
compression ratio (typically lzip)."
|
|
(define choices
|
|
(filter (match-lambda
|
|
((uri compression file-size)
|
|
(supported-compression? compression)))
|
|
(zip (narinfo-uris narinfo)
|
|
(narinfo-compressions narinfo)
|
|
(narinfo-file-sizes narinfo))))
|
|
|
|
(define (file-size<? c1 c2)
|
|
(match c1
|
|
((uri1 compression1 (? integer? file-size1))
|
|
(match c2
|
|
((uri2 compression2 (? integer? file-size2))
|
|
(< file-size1 file-size2))
|
|
(_ #t)))
|
|
((uri compression1 #f)
|
|
(match c2
|
|
((uri2 compression2 _)
|
|
(compresses-better? compression1 compression2))))
|
|
(_ #f))) ;we can't tell
|
|
|
|
(define (speed<? c1 c2)
|
|
(match c1
|
|
((uri1 compression1 . _)
|
|
(match c2
|
|
((uri2 compression2 . _)
|
|
(decompresses-faster? compression2 compression1))))))
|
|
|
|
(match (sort choices (if fast-decompression? (negate speed<?) file-size<?))
|
|
(((uri compression file-size) _ ...)
|
|
(values uri compression file-size))))
|