narinfo.scm 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2013-2022 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org>
  4. ;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
  5. ;;;
  6. ;;; This file is part of GNU Guix.
  7. ;;;
  8. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  9. ;;; under the terms of the GNU General Public License as published by
  10. ;;; the Free Software Foundation; either version 3 of the License, or (at
  11. ;;; your option) any later version.
  12. ;;;
  13. ;;; GNU Guix is distributed in the hope that it will be useful, but
  14. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;;; GNU General Public License for more details.
  17. ;;;
  18. ;;; You should have received a copy of the GNU General Public License
  19. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  20. (define-module (guix narinfo)
  21. #:use-module (guix pki)
  22. #:use-module (guix i18n)
  23. #:use-module (guix base32)
  24. #:use-module (guix base64)
  25. #:use-module (guix records)
  26. #:use-module (guix diagnostics)
  27. #:use-module (gcrypt hash)
  28. #:use-module (gcrypt pk-crypto)
  29. #:use-module (rnrs bytevectors)
  30. #:use-module (srfi srfi-1)
  31. #:use-module (srfi srfi-9)
  32. #:use-module (srfi srfi-26)
  33. #:use-module (ice-9 match)
  34. #:use-module (ice-9 binary-ports)
  35. #:use-module (web uri)
  36. #:export (narinfo-signature->canonical-sexp
  37. narinfo?
  38. narinfo-path
  39. narinfo-uris
  40. narinfo-uri-base
  41. narinfo-compressions
  42. narinfo-file-hashes
  43. narinfo-file-sizes
  44. narinfo-hash
  45. narinfo-size
  46. narinfo-references
  47. narinfo-deriver
  48. narinfo-system
  49. narinfo-signature
  50. narinfo-contents
  51. narinfo-hash-algorithm+value
  52. narinfo-hash->sha256
  53. narinfo-preferred-uris
  54. narinfo-best-uri
  55. valid-narinfo?
  56. read-narinfo
  57. write-narinfo
  58. string->narinfo
  59. narinfo->string
  60. equivalent-narinfo?))
  61. (define-record-type <narinfo>
  62. (%make-narinfo path uri-base uris compressions file-sizes file-hashes
  63. nar-hash nar-size references deriver system
  64. signature contents)
  65. narinfo?
  66. (path narinfo-path)
  67. (uri-base narinfo-uri-base) ;URI of the cache it originates from
  68. (uris narinfo-uris) ;list of strings
  69. (compressions narinfo-compressions) ;list of strings
  70. (file-sizes narinfo-file-sizes) ;list of (integers | #f)
  71. (file-hashes narinfo-file-hashes)
  72. (nar-hash narinfo-hash)
  73. (nar-size narinfo-size)
  74. (references narinfo-references)
  75. (deriver narinfo-deriver)
  76. (system narinfo-system)
  77. (signature narinfo-signature) ; canonical sexp
  78. ;; The original contents of a narinfo file. This field is needed because we
  79. ;; want to preserve the exact textual representation for verification purposes.
  80. ;; See <https://lists.gnu.org/archive/html/guix-devel/2014-02/msg00340.html>
  81. ;; for more information.
  82. (contents narinfo-contents))
  83. (define (narinfo-hash-algorithm+value narinfo)
  84. "Return two values: the hash algorithm used by NARINFO and its value as a
  85. bytevector."
  86. (match (string-tokenize (narinfo-hash narinfo)
  87. (char-set-complement (char-set #\:)))
  88. ((algorithm base32)
  89. (values (lookup-hash-algorithm (string->symbol algorithm))
  90. (nix-base32-string->bytevector base32)))
  91. (_
  92. (raise (formatted-message
  93. (G_ "invalid narinfo hash: ~s") (narinfo-hash narinfo))))))
  94. (define (narinfo-hash->sha256 hash)
  95. "If the string HASH denotes a sha256 hash, return it as a bytevector.
  96. Otherwise return #f."
  97. (and (string-prefix? "sha256:" hash)
  98. (nix-base32-string->bytevector (string-drop hash 7))))
  99. (define (narinfo-signature->canonical-sexp str)
  100. "Return the value of a narinfo's 'Signature' field as a canonical sexp."
  101. (match (string-split str #\;)
  102. ((version host-name sig)
  103. (let ((maybe-number (string->number version)))
  104. (cond ((not (number? maybe-number))
  105. (leave (G_ "signature version must be a number: ~s~%")
  106. version))
  107. ;; Currently, there are no other versions.
  108. ((not (= 1 maybe-number))
  109. (leave (G_ "unsupported signature version: ~a~%")
  110. maybe-number))
  111. (else
  112. (let ((signature (utf8->string (base64-decode sig))))
  113. (catch 'gcry-error
  114. (lambda ()
  115. (string->canonical-sexp signature))
  116. (lambda (key proc err)
  117. (leave (G_ "signature is not a valid \
  118. s-expression: ~s~%")
  119. signature))))))))
  120. (x
  121. (leave (G_ "invalid format of the signature field: ~a~%") x))))
  122. (define (narinfo-maker str cache-url)
  123. "Return a narinfo constructor for narinfos originating from CACHE-URL. STR
  124. must contain the original contents of a narinfo file."
  125. (lambda (path urls compressions file-hashes file-sizes
  126. nar-hash nar-size references deriver system
  127. signature)
  128. "Return a new <narinfo> object."
  129. (define len (length urls))
  130. (%make-narinfo path cache-url
  131. ;; Handle the case where URL is a relative URL.
  132. (map (lambda (url)
  133. (or (string->uri url)
  134. (string->uri
  135. (if (string-suffix? "/" cache-url)
  136. (string-append cache-url url)
  137. (string-append cache-url "/" url)))))
  138. urls)
  139. compressions
  140. (match file-sizes
  141. (() (make-list len #f))
  142. ((lst ...) (map string->number lst)))
  143. (match file-hashes
  144. (() (make-list len #f))
  145. ((lst ...) (map string->number lst)))
  146. nar-hash
  147. (and=> nar-size string->number)
  148. (string-tokenize references)
  149. (match deriver
  150. ((or #f "") #f)
  151. (_ deriver))
  152. system
  153. (false-if-exception
  154. (and=> signature narinfo-signature->canonical-sexp))
  155. str)))
  156. (define fields->alist
  157. ;; The narinfo format is really just like recutils.
  158. recutils->alist)
  159. (define* (read-narinfo port #:optional url
  160. #:key size)
  161. "Read a narinfo from PORT. If URL is true, it must be a string used to
  162. build full URIs from relative URIs found while reading PORT. When SIZE is
  163. true, read at most SIZE bytes from PORT; otherwise, read as much as possible.
  164. No authentication and authorization checks are performed here!"
  165. (let ((str (utf8->string (if size
  166. (get-bytevector-n port size)
  167. (get-bytevector-all port)))))
  168. (alist->record (call-with-input-string str fields->alist)
  169. (narinfo-maker str url)
  170. '("StorePath" "URL" "Compression"
  171. "FileHash" "FileSize" "NarHash" "NarSize"
  172. "References" "Deriver" "System"
  173. "Signature")
  174. '("URL" "Compression" "FileSize" "FileHash"))))
  175. (define (narinfo-sha256 narinfo)
  176. "Return the sha256 hash of NARINFO as a bytevector, or #f if NARINFO lacks a
  177. 'Signature' field."
  178. (define %mandatory-fields
  179. ;; List of fields that must be signed. If they are not signed, the
  180. ;; narinfo is considered unsigned.
  181. '("StorePath" "NarHash" "References"))
  182. (let ((contents (narinfo-contents narinfo)))
  183. (match (string-contains contents "Signature:")
  184. (#f #f)
  185. (index
  186. (let* ((above-signature (string-take contents index))
  187. (signed-fields (match (call-with-input-string above-signature
  188. fields->alist)
  189. (((fields . values) ...) fields))))
  190. (and (every (cut member <> signed-fields) %mandatory-fields)
  191. (sha256 (string->utf8 above-signature))))))))
  192. (define* (valid-narinfo? narinfo #:optional (acl (current-acl))
  193. #:key verbose?)
  194. "Return #t if NARINFO's signature is valid and made by one of the keys in
  195. ACL."
  196. (let ((hash (narinfo-sha256 narinfo))
  197. (signature (narinfo-signature narinfo))
  198. (uri (uri->string (first (narinfo-uris narinfo)))))
  199. (and hash signature
  200. (signature-case (signature hash acl)
  201. (valid-signature #t)
  202. (invalid-signature
  203. (when verbose?
  204. (format (current-error-port)
  205. "invalid signature for substitute at '~a'~%"
  206. uri))
  207. #f)
  208. (hash-mismatch
  209. (when verbose?
  210. (format (current-error-port)
  211. "hash mismatch for substitute at '~a'~%"
  212. uri))
  213. #f)
  214. (unauthorized-key
  215. (when verbose?
  216. (format (current-error-port)
  217. "substitute at '~a' is signed by an \
  218. unauthorized party~%"
  219. uri))
  220. #f)
  221. (corrupt-signature
  222. (when verbose?
  223. (format (current-error-port)
  224. "corrupt signature for substitute at '~a'~%"
  225. uri))
  226. #f)))))
  227. (define (write-narinfo narinfo port)
  228. "Write NARINFO to PORT."
  229. (put-bytevector port (string->utf8 (narinfo-contents narinfo))))
  230. (define (narinfo->string narinfo)
  231. "Return the external representation of NARINFO."
  232. (call-with-output-string (cut write-narinfo narinfo <>)))
  233. (define (string->narinfo str cache-uri)
  234. "Return the narinfo represented by STR. Assume CACHE-URI as the base URI of
  235. the cache STR originates form."
  236. (call-with-input-string str (cut read-narinfo <> cache-uri)))
  237. (define (equivalent-narinfo? narinfo1 narinfo2)
  238. "Return true if NARINFO1 and NARINFO2 are equivalent--i.e., if they describe
  239. the same store item. This ignores unnecessary metadata such as the Nar URL."
  240. (and (string=? (narinfo-hash narinfo1)
  241. (narinfo-hash narinfo2))
  242. ;; The following is not needed if all we want is to download a valid
  243. ;; nar, but it's necessary if we want valid narinfo.
  244. (string=? (narinfo-path narinfo1)
  245. (narinfo-path narinfo2))
  246. (equal? (narinfo-references narinfo1)
  247. (narinfo-references narinfo2))
  248. (= (narinfo-size narinfo1)
  249. (narinfo-size narinfo2))))
  250. (define %compression-methods
  251. ;; Known compression methods and a thunk to determine whether they're
  252. ;; supported. See 'decompressed-port' in (guix utils).
  253. `(("gzip" . ,(const #t))
  254. ("lzip" . ,(const #t))
  255. ("zstd" . ,(lambda ()
  256. (resolve-module '(zstd) #t #f #:ensure #f)))
  257. ("xz" . ,(const #t))
  258. ("bzip2" . ,(const #t))
  259. ("none" . ,(const #t))))
  260. (define (supported-compression? compression)
  261. "Return true if COMPRESSION, a string, denotes a supported compression
  262. method."
  263. (match (assoc-ref %compression-methods compression)
  264. (#f #f)
  265. (supported? (supported?))))
  266. (define (compresses-better? compression1 compression2)
  267. "Return true if COMPRESSION1 generally compresses better than COMPRESSION2;
  268. this is a rough approximation."
  269. (match compression1
  270. ("none" #f)
  271. ("gzip" (string=? compression2 "none"))
  272. ("lzip" #t)
  273. (_ (or (string=? compression2 "none")
  274. (string=? compression2 "gzip")))))
  275. (define (decompresses-faster? compression1 compression2)
  276. "Return true if COMPRESSION1 generally has a higher decompression throughput
  277. than COMPRESSION2."
  278. (match compression1
  279. ("none" #t)
  280. ("zstd" #t)
  281. ("gzip" (string=? compression2 "lzip"))
  282. (_ #f)))
  283. (define* (narinfo-preferred-uris narinfo #:key fast-decompression?)
  284. "Return the sorted list of \"preferred\" nar URIs from NARINFO (preferred
  285. comes first) where each entry is a tuple containing: the URI, its compression
  286. method (a string), and the compressed file size.
  287. When FAST-DECOMPRESSION? is true, prefer substitutes with faster
  288. decompression (typically zstd) rather than substitutes with a higher
  289. compression ratio (typically lzip)."
  290. (define choices
  291. (filter (match-lambda
  292. ((uri compression file-size)
  293. (supported-compression? compression)))
  294. (zip (narinfo-uris narinfo)
  295. (narinfo-compressions narinfo)
  296. (narinfo-file-sizes narinfo))))
  297. (define (file-size<? c1 c2)
  298. (match c1
  299. ((uri1 compression1 (? integer? file-size1))
  300. (match c2
  301. ((uri2 compression2 (? integer? file-size2))
  302. (< file-size1 file-size2))
  303. (_ #t)))
  304. ((uri compression1 #f)
  305. (match c2
  306. ((uri2 compression2 _)
  307. (compresses-better? compression1 compression2))))
  308. (_ #f))) ;we can't tell
  309. (define (speed<? c1 c2)
  310. (match c1
  311. ((uri1 compression1 . _)
  312. (match c2
  313. ((uri2 compression2 . _)
  314. (decompresses-faster? compression2 compression1))))))
  315. (sort choices (if fast-decompression? (negate speed<?) file-size<?)))
  316. (define* (narinfo-best-uri narinfo #:key fast-decompression?)
  317. "Select the \"best\" URI to download NARINFO's nar, and return three values:
  318. the URI, its compression method (a string), and the compressed file size.
  319. When FAST-DECOMPRESSION? is true, prefer substitutes with faster
  320. decompression (typically zstd) rather than substitutes with a higher
  321. compression ratio (typically lzip)."
  322. (match (narinfo-preferred-uris narinfo
  323. #:fast-decompression? fast-decompression?)
  324. (((uri compression file-size) _ ...)
  325. (values uri compression file-size))))