diff options
| author | Ludovic Courtès <ludo@gnu.org> | 2026-05-30 23:54:05 +0200 |
|---|---|---|
| committer | Ludovic Courtès <ludo@gnu.org> | 2026-08-17 16:47:36 +0200 |
| commit | 64d4de2a920445e5992f020e56490f5fcbdbba7c (patch) | |
| tree | a869360b32fbc5fb0f49d297708ddde801aa3a95 | |
| parent | 8cb871e0c69030599fa8e8410f1ecaee21ddbbd0 (diff) | |
daemon: Bypass authentication when importing content-addressed store items.
This puts ‘importPaths’ on par with ‘addToStore’ and ‘addTextToStore’: since
the two latter RPCs let anyone add content-addressed items in the store,
there’s no reason for ‘importPaths’ to require signatures by authorized keys
on these content-addressed items.
This will allow for things like ‘guix copy’ of .drv items without
authorization, or ‘guix deploy’ with (build-locally? #f) without
authorization.
* nix/libstore/store-api.hh (isContentAddressedPath): New prototype.
* nix/libstore/store-api.cc (isContentAddressedPath): New function.
* nix/libutil/util.hh (isPlainFile): New prototype.
* nix/libutil/util.cc (isPlainFile): New function.
* nix/libstore/local-store.cc (LocalStore::importPath): Define ‘narHash’ and
‘contentAddressed’. Allow unsigned imports when ‘contentAddressed’ is true;
bypass signature verification when ‘contentAddressed’ is true.
* tests/store.scm ("import not signed"): Rewrite to not use a
content-addressed store item.
("import signed by unauthorized key"): Likewise.
("import not signed but content-addressed tree"): New test.
("import not signed but content-addressed regular file"): New test.
("import signed by unauthorized key but content-addressed"): New test.
("import with corrupt signature"): New test.
("import signed by authorized key but hash doesn't match"): New test.
("import with corrupt signature but content-addressed"): New test.
("import signed by authorized key, hash doesn't match, but content-addressed"):
New test.
* doc/guix.texi (Invoking guix archive): Document the exception for
content-addressed store items. Add anchor for ‘--authorize’.
(Invoking guix deploy): Document the benefit of (build-locally? #f).
Add cross-reference for ‘authorize?’.
Co-authored-by: Reepca Russelstein <reepca@russelstein.xyz>
Signed-off-by: Ludovic Courtès <ludo@gnu.org>
Merges: #8979
| -rw-r--r-- | doc/guix.texi | 12 | ||||
| -rw-r--r-- | nix/libstore/local-store.cc | 45 | ||||
| -rw-r--r-- | nix/libstore/store-api.cc | 23 | ||||
| -rw-r--r-- | nix/libstore/store-api.hh | 6 | ||||
| -rw-r--r-- | nix/libutil/util.cc | 7 | ||||
| -rw-r--r-- | nix/libutil/util.hh | 3 | ||||
| -rw-r--r-- | tests/store.scm | 295 |
7 files changed, 360 insertions, 31 deletions
diff --git a/doc/guix.texi b/doc/guix.texi index c5fed97da08..e7f1826542d 100644 --- a/doc/guix.texi +++ b/doc/guix.texi | |||
| @@ -5674,7 +5674,11 @@ references, corresponding derivation, and a digital signature. | |||
| 5674 | When exporting, the daemon digitally signs the contents of the archive, | 5674 | When exporting, the daemon digitally signs the contents of the archive, |
| 5675 | and that digital signature is appended. When importing, the daemon | 5675 | and that digital signature is appended. When importing, the daemon |
| 5676 | verifies the signature and rejects the import in case of an invalid | 5676 | verifies the signature and rejects the import in case of an invalid |
| 5677 | signature or if the signing key is not authorized. | 5677 | signature or if the signing key is not authorized; as an exception, |
| 5678 | store items that are @dfn{content-addressed}---source files and | ||
| 5679 | @file{.drv} files (@pxref{Derivations})---can be imported even if they | ||
| 5680 | lack an authorized signature because adding these to the store is not | ||
| 5681 | creating any new risk. | ||
| 5678 | @c FIXME: Add xref to daemon doc about signatures. | 5682 | @c FIXME: Add xref to daemon doc about signatures. |
| 5679 | 5683 | ||
| 5680 | The main options are: | 5684 | The main options are: |
| @@ -5724,6 +5728,7 @@ Alternatively, @var{parameters} can specify | |||
| 5724 | public-key related Functions, @code{gcry_pk_genkey},, gcrypt, The | 5728 | public-key related Functions, @code{gcry_pk_genkey},, gcrypt, The |
| 5725 | Libgcrypt Reference Manual}). | 5729 | Libgcrypt Reference Manual}). |
| 5726 | 5730 | ||
| 5731 | @anchor{archive-authorization} | ||
| 5727 | @item --authorize | 5732 | @item --authorize |
| 5728 | @cindex authorizing, archives | 5733 | @cindex authorizing, archives |
| 5729 | Authorize imports signed by the public key passed on standard input. | 5734 | Authorize imports signed by the public key passed on standard input. |
| @@ -51032,12 +51037,15 @@ with an @code{environment} of @code{managed-host-environment-type}. | |||
| 51032 | @item @code{host-name} | 51037 | @item @code{host-name} |
| 51033 | @item @code{build-locally?} (default: @code{#t}) | 51038 | @item @code{build-locally?} (default: @code{#t}) |
| 51034 | If false, system derivations will be built on the machine being deployed to. | 51039 | If false, system derivations will be built on the machine being deployed to. |
| 51040 | As a bonus, when false, one does not need the target machine to have the | ||
| 51041 | key of the deployment machine among its authorized keys and thus the | ||
| 51042 | @code{authorize?} field below can remain @code{#f}. | ||
| 51035 | @item @code{system} | 51043 | @item @code{system} |
| 51036 | The system type describing the architecture of the machine being deployed | 51044 | The system type describing the architecture of the machine being deployed |
| 51037 | to---e.g., @code{"x86_64-linux"}. | 51045 | to---e.g., @code{"x86_64-linux"}. |
| 51038 | @item @code{authorize?} (default: @code{#t}) | 51046 | @item @code{authorize?} (default: @code{#t}) |
| 51039 | If true, the coordinator's signing key will be added to the remote's ACL | 51047 | If true, the coordinator's signing key will be added to the remote's ACL |
| 51040 | keyring. | 51048 | keyring (@pxref{archive-authorization, authorizing imports}). |
| 51041 | @item @code{port} (default: @code{22}) | 51049 | @item @code{port} (default: @code{22}) |
| 51042 | @item @code{user} (default: @code{"root"}) | 51050 | @item @code{user} (default: @code{"root"}) |
| 51043 | @item @code{identity} (default: @code{#f}) | 51051 | @item @code{identity} (default: @code{#f}) |
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc index 6b5c388efc9..c38dd2072d4 100644 --- a/nix/libstore/local-store.cc +++ b/nix/libstore/local-store.cc | |||
| @@ -1336,6 +1336,8 @@ Path LocalStore::importPath(Source & source) | |||
| 1336 | 1336 | ||
| 1337 | restorePath(unpacked, hashAndReadSource); | 1337 | restorePath(unpacked, hashAndReadSource); |
| 1338 | 1338 | ||
| 1339 | Hash narHash = hashAndReadSource.hashSink.currentHash().first; | ||
| 1340 | |||
| 1339 | unsigned int magic = readInt(hashAndReadSource); | 1341 | unsigned int magic = readInt(hashAndReadSource); |
| 1340 | if (magic != EXPORT_MAGIC) | 1342 | if (magic != EXPORT_MAGIC) |
| 1341 | throw Error("normalized archive cannot be imported; wrong format"); | 1343 | throw Error("normalized archive cannot be imported; wrong format"); |
| @@ -1355,21 +1357,36 @@ Path LocalStore::importPath(Source & source) | |||
| 1355 | hashAndReadSource.hashing = false; | 1357 | hashAndReadSource.hashing = false; |
| 1356 | 1358 | ||
| 1357 | bool haveSignature = readInt(hashAndReadSource) == 1; | 1359 | bool haveSignature = readInt(hashAndReadSource) == 1; |
| 1360 | string signature; | ||
| 1361 | if (haveSignature) | ||
| 1362 | signature = readString(hashAndReadSource); | ||
| 1358 | 1363 | ||
| 1359 | if (!haveSignature) | 1364 | try { |
| 1360 | throw Error(std::format("imported archive of `{}' lacks a signature", dstPath)); | 1365 | /* First, check whether there is a valid, authorized, matching |
| 1361 | 1366 | signature. */ | |
| 1362 | string signature = readString(hashAndReadSource); | 1367 | if (haveSignature) { |
| 1363 | string hash2 = verifySignature(signature); | 1368 | string hash2 = verifySignature(signature); |
| 1364 | 1369 | if (printHash(hash) != hash2) | |
| 1365 | /* Note: runProgram() throws an exception if the signature | 1370 | throw AuthenticationError( |
| 1366 | is invalid. */ | 1371 | "signed hash doesn't match actual contents of imported " |
| 1367 | 1372 | "archive; archive could be corrupt, or someone is trying " | |
| 1368 | if (printHash(hash) != hash2) | 1373 | "to import a Trojan horse"); |
| 1369 | throw Error( | 1374 | } |
| 1370 | "signed hash doesn't match actual contents of imported " | 1375 | else |
| 1371 | "archive; archive could be corrupt, or someone is trying " | 1376 | throw AuthenticationError(std::format("imported archive of `{}' lacks a signature", dstPath)); |
| 1372 | "to import a Trojan horse"); | 1377 | } |
| 1378 | catch (AuthenticationError &e) { | ||
| 1379 | /* Second, check whether 'dstPath' is content-addressed--e.g., a store | ||
| 1380 | item created by 'addToStore' or 'addTextToStore'. XXX: This can | ||
| 1381 | yield an extra 'hashFile' call and is limited to SHA256. */ | ||
| 1382 | if (!(deriver == "" | ||
| 1383 | && (isContentAddressedPath(dstPath, narHash, references, true) | ||
| 1384 | || (isPlainFile(unpacked) | ||
| 1385 | && isContentAddressedPath(dstPath, hashFile(htSHA256, unpacked), | ||
| 1386 | references, false))))) | ||
| 1387 | /* Rethrow. */ | ||
| 1388 | throw; | ||
| 1389 | } | ||
| 1373 | 1390 | ||
| 1374 | /* Do the actual import. */ | 1391 | /* Do the actual import. */ |
| 1375 | 1392 | ||
diff --git a/nix/libstore/store-api.cc b/nix/libstore/store-api.cc index feabc821678..43f46176b5e 100644 --- a/nix/libstore/store-api.cc +++ b/nix/libstore/store-api.cc | |||
| @@ -258,6 +258,29 @@ Path computeStorePathForText(const string & name, const string & s, | |||
| 258 | return makeStorePath(type, hash, name); | 258 | return makeStorePath(type, hash, name); |
| 259 | } | 259 | } |
| 260 | 260 | ||
| 261 | bool isContentAddressedPath(const Path & path, const Hash & hash, | ||
| 262 | const PathSet & references, bool recursive) | ||
| 263 | { | ||
| 264 | /* Check whether PATH corresponds to something introduced by 'addToStore' | ||
| 265 | or by 'addTextToStore'. For simplicity, anything with a hash other | ||
| 266 | than SHA256 is omitted: this returns false even though they are | ||
| 267 | content-addressed as well. */ | ||
| 268 | string name = storePathToName(path); | ||
| 269 | if (recursive) { | ||
| 270 | /* HASH is interpreted as the nar hash. This can only come from | ||
| 271 | 'addToStore'. */ | ||
| 272 | return references.empty() && | ||
| 273 | path == makeFixedOutputPath(true, htSHA256, hash, name); | ||
| 274 | } else { | ||
| 275 | /* HASH is interpreted as the content hash. This can come from | ||
| 276 | 'addTextToStore' ("text" type, possibly with references) or from | ||
| 277 | 'addToStore' ("output:out" type). */ | ||
| 278 | string type = textTypeWithReferences(references); | ||
| 279 | return path == makeStorePath(type, hash, name) | ||
| 280 | || (references.empty() && | ||
| 281 | path == makeFixedOutputPath(false, htSHA256, hash, name)); | ||
| 282 | } | ||
| 283 | } | ||
| 261 | 284 | ||
| 262 | /* Return a string accepted by decodeValidPathInfo() that | 285 | /* Return a string accepted by decodeValidPathInfo() that |
| 263 | registers the specified paths as valid. Note: it's the | 286 | registers the specified paths as valid. Note: it's the |
diff --git a/nix/libstore/store-api.hh b/nix/libstore/store-api.hh index 29b33c0be9c..398ccd260ea 100644 --- a/nix/libstore/store-api.hh +++ b/nix/libstore/store-api.hh | |||
| @@ -362,6 +362,12 @@ Path makeFixedOutputPath(bool recursive, | |||
| 362 | Path computeStorePathForText(const string & name, const string & s, | 362 | Path computeStorePathForText(const string & name, const string & s, |
| 363 | const PathSet & references); | 363 | const PathSet & references); |
| 364 | 364 | ||
| 365 | /* Return true if PATH, whose content has the given HASH, refers to a | ||
| 366 | content-addressed file as added by 'addTextToStore' or 'addToStore'. HASH | ||
| 367 | is interpreted as a SHA256 nar hash when RECURSIVE is true, and as a SHA256 | ||
| 368 | file content hash when RECURSIVE is false. */ | ||
| 369 | bool isContentAddressedPath(const Path & path, const Hash & hash, | ||
| 370 | const PathSet & references, bool recursive); | ||
| 365 | 371 | ||
| 366 | /* Remove the temporary roots file for this process. Any temporary | 372 | /* Remove the temporary roots file for this process. Any temporary |
| 367 | root becomes garbage after this point unless it has been registered | 373 | root becomes garbage after this point unless it has been registered |
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc index 95f293ff10f..72c8d38bd50 100644 --- a/nix/libutil/util.cc +++ b/nix/libutil/util.cc | |||
| @@ -287,6 +287,13 @@ unsigned char getFileType(const Path & path) | |||
| 287 | return DT_UNKNOWN; | 287 | return DT_UNKNOWN; |
| 288 | } | 288 | } |
| 289 | 289 | ||
| 290 | bool isPlainFile(const Path & path) | ||
| 291 | { | ||
| 292 | struct stat st = lstat(path); | ||
| 293 | return S_ISREG(st.st_mode) | ||
| 294 | && ((st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) == 0); | ||
| 295 | } | ||
| 296 | |||
| 290 | 297 | ||
| 291 | string readFile(int fd) | 298 | string readFile(int fd) |
| 292 | { | 299 | { |
diff --git a/nix/libutil/util.hh b/nix/libutil/util.hh index 44d579f3d6a..ee8e5461b42 100644 --- a/nix/libutil/util.hh +++ b/nix/libutil/util.hh | |||
| @@ -65,6 +65,9 @@ Path readLink(const Path & path); | |||
| 65 | 65 | ||
| 66 | bool isLink(const Path & path); | 66 | bool isLink(const Path & path); |
| 67 | 67 | ||
| 68 | /* Return true if the file at PATH is a regular, non-executable file. */ | ||
| 69 | bool isPlainFile(const Path & path); | ||
| 70 | |||
| 68 | /* Read the contents of a directory. The entries `.' and `..' are | 71 | /* Read the contents of a directory. The entries `.' and `..' are |
| 69 | removed. */ | 72 | removed. */ |
| 70 | struct DirEntry | 73 | struct DirEntry |
diff --git a/tests/store.scm b/tests/store.scm index e7ba7469047..ba634ce2205 100644 --- a/tests/store.scm +++ b/tests/store.scm | |||
| @@ -30,7 +30,8 @@ | |||
| 30 | #:use-module (guix derivations) | 30 | #:use-module (guix derivations) |
| 31 | #:use-module ((guix modules) | 31 | #:use-module ((guix modules) |
| 32 | #:select (source-module-closure)) | 32 | #:select (source-module-closure)) |
| 33 | #:use-module (guix serialization) | 33 | #:use-module ((guix serialization) |
| 34 | #:hide (store-path read-string)) | ||
| 34 | #:use-module (guix build utils) | 35 | #:use-module (guix build utils) |
| 35 | #:use-module ((gnu build linux-container) | 36 | #:use-module ((gnu build linux-container) |
| 36 | #:select (unprivileged-user-namespace-supported?)) | 37 | #:select (unprivileged-user-namespace-supported?)) |
| @@ -1456,18 +1457,25 @@ System: x86_64-linux~%" | |||
| 1456 | 1457 | ||
| 1457 | 1458 | ||
| 1458 | (test-assert "import not signed" | 1459 | (test-assert "import not signed" |
| 1459 | (let* ((text (random-text)) | 1460 | (let* ((content (random-text)) |
| 1460 | (file (add-file-tree-to-store %store | 1461 | (name "fake-derivation") |
| 1461 | `("tree" directory | 1462 | (store-item (output-path "out" |
| 1462 | ("text" regular (data ,text)) | 1463 | (gcrypt:sha256 #vu8()) |
| 1463 | ("link" symlink "text")))) | 1464 | name)) |
| 1464 | (dump (call-with-bytevector-output-port | 1465 | (dump (call-with-bytevector-output-port |
| 1465 | (lambda (port) | 1466 | (lambda (port) |
| 1466 | (write-int 1 port) ;start | 1467 | (write-int 1 port) ;start |
| 1467 | 1468 | ||
| 1468 | (write-file file port) ;contents | 1469 | (write-file-tree name port ;contents |
| 1470 | #:file-type+size | ||
| 1471 | (lambda (_) | ||
| 1472 | (values 'regular | ||
| 1473 | (string-length content))) | ||
| 1474 | #:file-port | ||
| 1475 | (lambda (_) | ||
| 1476 | (open-input-string content))) | ||
| 1469 | (write-int #x4558494e port) ;%export-magic | 1477 | (write-int #x4558494e port) ;%export-magic |
| 1470 | (write-string file port) ;store item | 1478 | (write-string store-item port) ;store item |
| 1471 | (write-string-list '() port) ;references | 1479 | (write-string-list '() port) ;references |
| 1472 | (write-string "" port) ;deriver | 1480 | (write-string "" port) ;deriver |
| 1473 | (write-int 0 port) ;not signed | 1481 | (write-int 0 port) ;not signed |
| @@ -1485,11 +1493,11 @@ System: x86_64-linux~%" | |||
| 1485 | #f)))) | 1493 | #f)))) |
| 1486 | 1494 | ||
| 1487 | (test-assert "import signed by unauthorized key" | 1495 | (test-assert "import signed by unauthorized key" |
| 1488 | (let* ((text (random-text)) | 1496 | (let* ((content (random-text)) |
| 1489 | (file (add-file-tree-to-store %store | 1497 | (name "fake-derivation") |
| 1490 | `("tree" directory | 1498 | (store-item (output-path "out" |
| 1491 | ("text" regular (data ,text)) | 1499 | (gcrypt:sha256 #vu8()) |
| 1492 | ("link" symlink "text")))) | 1500 | name)) |
| 1493 | (key (gcrypt:generate-key | 1501 | (key (gcrypt:generate-key |
| 1494 | (gcrypt:string->canonical-sexp | 1502 | (gcrypt:string->canonical-sexp |
| 1495 | "(genkey (ecdsa (curve Ed25519) (flags rfc6979)))"))) | 1503 | "(genkey (ecdsa (curve Ed25519) (flags rfc6979)))"))) |
| @@ -1497,9 +1505,16 @@ System: x86_64-linux~%" | |||
| 1497 | (lambda (port) | 1505 | (lambda (port) |
| 1498 | (write-int 1 port) ;start | 1506 | (write-int 1 port) ;start |
| 1499 | 1507 | ||
| 1500 | (write-file file port) ;contents | 1508 | (write-file-tree name port ;contents |
| 1509 | #:file-type+size | ||
| 1510 | (lambda (_) | ||
| 1511 | (values 'regular | ||
| 1512 | (string-length content))) | ||
| 1513 | #:file-port | ||
| 1514 | (lambda (_) | ||
| 1515 | (open-input-string content))) | ||
| 1501 | (write-int #x4558494e port) ;%export-magic | 1516 | (write-int #x4558494e port) ;%export-magic |
| 1502 | (write-string file port) ;store item | 1517 | (write-string store-item port) ;store item |
| 1503 | (write-string-list '() port) ;references | 1518 | (write-string-list '() port) ;references |
| 1504 | (write-string "" port) ;deriver | 1519 | (write-string "" port) ;deriver |
| 1505 | (write-int 1 port) ;signed | 1520 | (write-int 1 port) ;signed |
| @@ -1524,6 +1539,256 @@ System: x86_64-linux~%" | |||
| 1524 | (pk 'unauthorized-imported imported) | 1539 | (pk 'unauthorized-imported imported) |
| 1525 | #f)))) | 1540 | #f)))) |
| 1526 | 1541 | ||
| 1542 | (test-assert "import with corrupt signature" | ||
| 1543 | (let* ((content (random-text)) | ||
| 1544 | (name "fake-derivation") | ||
| 1545 | (store-item (output-path "out" | ||
| 1546 | (gcrypt:sha256 #vu8()) | ||
| 1547 | name)) | ||
| 1548 | (dump (call-with-bytevector-output-port | ||
| 1549 | (lambda (port) | ||
| 1550 | (write-int 1 port) ;start | ||
| 1551 | |||
| 1552 | (write-file-tree name port ;contents | ||
| 1553 | #:file-type+size | ||
| 1554 | (lambda (_) | ||
| 1555 | (values 'regular | ||
| 1556 | (string-length content))) | ||
| 1557 | #:file-port | ||
| 1558 | (lambda (_) | ||
| 1559 | (open-input-string content))) | ||
| 1560 | (write-int #x4558494e port) ;%export-magic | ||
| 1561 | (write-string store-item port) ;store item | ||
| 1562 | (write-string-list '() port) ;references | ||
| 1563 | (write-string "" port) ;deriver | ||
| 1564 | (write-int 1 port) ;signed | ||
| 1565 | (write-string (object->string '(signature broken)) | ||
| 1566 | port) | ||
| 1567 | |||
| 1568 | (write-int 0 port))))) ;done | ||
| 1569 | |||
| 1570 | ;; Ensure 'import-paths' raises an exception. | ||
| 1571 | (guard (c ((store-protocol-error? c) | ||
| 1572 | (and (not (zero? (store-protocol-error-status c))) | ||
| 1573 | (string-contains (store-protocol-error-message c) | ||
| 1574 | "corrupt signature")))) | ||
| 1575 | (let* ((source (open-bytevector-input-port dump)) | ||
| 1576 | (imported (import-paths %store source))) | ||
| 1577 | (pk 'corrupt-signature-imported imported) | ||
| 1578 | #f)))) | ||
| 1579 | |||
| 1580 | (test-assert "import signed by authorized key but hash doesn't match" | ||
| 1581 | (let* ((content (random-text)) | ||
| 1582 | (name "fake-derivation") | ||
| 1583 | (store-item (output-path "out" | ||
| 1584 | (gcrypt:sha256 #vu8()) | ||
| 1585 | name)) | ||
| 1586 | ;; This key is known to be in the ACL by default. | ||
| 1587 | (public-key | ||
| 1588 | (call-with-input-file (string-append %config-directory "/signing-key.pub") | ||
| 1589 | (compose gcrypt:string->canonical-sexp get-string-all))) | ||
| 1590 | (private-key | ||
| 1591 | (call-with-input-file (string-append %config-directory "/signing-key.sec") | ||
| 1592 | (compose gcrypt:string->canonical-sexp get-string-all))) | ||
| 1593 | |||
| 1594 | (dump (call-with-bytevector-output-port | ||
| 1595 | (lambda (port) | ||
| 1596 | (write-int 1 port) ;start | ||
| 1597 | |||
| 1598 | (write-file-tree name port ;contents | ||
| 1599 | #:file-type+size | ||
| 1600 | (lambda (_) | ||
| 1601 | (values 'regular | ||
| 1602 | (string-length content))) | ||
| 1603 | #:file-port | ||
| 1604 | (lambda (_) | ||
| 1605 | (open-input-string content))) | ||
| 1606 | (write-int #x4558494e port) ;%export-magic | ||
| 1607 | (write-string store-item port) ;store item | ||
| 1608 | (write-string-list '() port) ;references | ||
| 1609 | (write-string "" port) ;deriver | ||
| 1610 | (write-int 1 port) ;signed | ||
| 1611 | (write-string (gcrypt:canonical-sexp->string | ||
| 1612 | (signature-sexp | ||
| 1613 | (gcrypt:bytevector->hash-data | ||
| 1614 | (gcrypt:sha256 #vu8(0 1 2)) | ||
| 1615 | #:key-type 'rsa) | ||
| 1616 | private-key | ||
| 1617 | public-key)) | ||
| 1618 | port) | ||
| 1619 | |||
| 1620 | (write-int 0 port))))) ;done | ||
| 1621 | |||
| 1622 | ;; Ensure 'import-paths' raises an exception. | ||
| 1623 | (guard (c ((store-protocol-error? c) | ||
| 1624 | (and (not (zero? (store-protocol-error-status c))) | ||
| 1625 | (string-contains (store-protocol-error-message c) | ||
| 1626 | "hash doesn't match")))) | ||
| 1627 | (let* ((source (open-bytevector-input-port dump)) | ||
| 1628 | (imported (import-paths %store source))) | ||
| 1629 | (pk 'hash-mismatch-imported imported) | ||
| 1630 | #f)))) | ||
| 1631 | |||
| 1632 | (test-assert "import not signed but content-addressed tree" | ||
| 1633 | (let* ((text (random-text)) | ||
| 1634 | (file (add-file-tree-to-store %store | ||
| 1635 | `("tree" directory | ||
| 1636 | ("text" regular (data ,text)) | ||
| 1637 | ("link" symlink "text")))) | ||
| 1638 | (dump (call-with-bytevector-output-port | ||
| 1639 | (lambda (port) | ||
| 1640 | (write-int 1 port) ;start | ||
| 1641 | |||
| 1642 | (write-file file port) ;contents | ||
| 1643 | (write-int #x4558494e port) ;%export-magic | ||
| 1644 | (write-string file port) ;store item | ||
| 1645 | (write-string-list '() port) ;references | ||
| 1646 | (write-string "" port) ;deriver | ||
| 1647 | (write-int 0 port) ;not signed | ||
| 1648 | |||
| 1649 | (write-int 0 port))))) ;done | ||
| 1650 | |||
| 1651 | ;; Ensure 'import-paths' completes despite the lack of signature. | ||
| 1652 | (let ((source (open-bytevector-input-port dump))) | ||
| 1653 | (match (import-paths %store source) | ||
| 1654 | ((imported) | ||
| 1655 | (string=? (pk 'imported imported) file)))))) | ||
| 1656 | |||
| 1657 | (test-assert "import not signed but content-addressed regular file" | ||
| 1658 | (let* ((name "content-addressed") | ||
| 1659 | (content (random-text)) | ||
| 1660 | (store-item (store-path "text" | ||
| 1661 | (gcrypt:sha256 (string->utf8 content)) | ||
| 1662 | name)) | ||
| 1663 | (dump (call-with-bytevector-output-port | ||
| 1664 | (lambda (port) | ||
| 1665 | (write-int 1 port) ;start | ||
| 1666 | (write-file-tree name port ;contents | ||
| 1667 | #:file-type+size | ||
| 1668 | (lambda (_) | ||
| 1669 | (values 'regular | ||
| 1670 | (string-length content))) | ||
| 1671 | #:file-port | ||
| 1672 | (lambda (_) | ||
| 1673 | (open-input-string content))) | ||
| 1674 | (write-int #x4558494e port) ;%export-magic | ||
| 1675 | (write-string store-item port) | ||
| 1676 | (write-string-list '() port) ;references | ||
| 1677 | (write-string "" port) ;deriver | ||
| 1678 | (write-int 0 port) ;not signed | ||
| 1679 | (write-int 0 port))))) ;done | ||
| 1680 | |||
| 1681 | ;; Ensure 'import-paths' completes despite the lack of signature. | ||
| 1682 | (let ((source (open-bytevector-input-port dump))) | ||
| 1683 | (match (import-paths %store source) | ||
| 1684 | ((imported) | ||
| 1685 | (and (string=? (pk 'imported imported) store-item) | ||
| 1686 | (string=? (call-with-input-file imported get-string-all) | ||
| 1687 | content))))))) | ||
| 1688 | |||
| 1689 | (test-assert "import signed by unauthorized key but content-addressed" | ||
| 1690 | (let* ((text (random-text)) | ||
| 1691 | (file (add-file-tree-to-store %store | ||
| 1692 | `("tree" directory | ||
| 1693 | ("text" regular (data ,text)) | ||
| 1694 | ("link" symlink "text")))) | ||
| 1695 | (key (gcrypt:generate-key | ||
| 1696 | (gcrypt:string->canonical-sexp | ||
| 1697 | "(genkey (ecdsa (curve Ed25519) (flags rfc6979)))"))) | ||
| 1698 | (dump (call-with-bytevector-output-port | ||
| 1699 | (lambda (port) | ||
| 1700 | (write-int 1 port) ;start | ||
| 1701 | |||
| 1702 | (write-file file port) ;contents | ||
| 1703 | (write-int #x4558494e port) ;%export-magic | ||
| 1704 | (write-string file port) ;store item | ||
| 1705 | (write-string-list '() port) ;references | ||
| 1706 | (write-string "" port) ;deriver | ||
| 1707 | (write-int 1 port) ;signed | ||
| 1708 | (write-string (gcrypt:canonical-sexp->string | ||
| 1709 | (signature-sexp | ||
| 1710 | (gcrypt:bytevector->hash-data | ||
| 1711 | (gcrypt:sha256 #vu8(0 1 2)) | ||
| 1712 | #:key-type 'ecc) | ||
| 1713 | (gcrypt:find-sexp-token key 'private-key) | ||
| 1714 | (gcrypt:find-sexp-token key 'public-key))) | ||
| 1715 | port) | ||
| 1716 | |||
| 1717 | (write-int 0 port))))) ;done | ||
| 1718 | |||
| 1719 | ;; Ensure 'import-paths' succeeds despite the unauthorized signature. | ||
| 1720 | (let ((source (open-bytevector-input-port dump))) | ||
| 1721 | (match (import-paths %store source) | ||
| 1722 | ((imported) | ||
| 1723 | (string=? imported file)))))) | ||
| 1724 | |||
| 1725 | (test-assert "import with corrupt signature but content-addressed" | ||
| 1726 | (let* ((text (random-text)) | ||
| 1727 | (file (add-file-tree-to-store %store | ||
| 1728 | `("tree" directory | ||
| 1729 | ("text" regular (data ,text)) | ||
| 1730 | ("link" symlink "text")))) | ||
| 1731 | (dump (call-with-bytevector-output-port | ||
| 1732 | (lambda (port) | ||
| 1733 | (write-int 1 port) ;start | ||
| 1734 | |||
| 1735 | (write-file file port) ;contents | ||
| 1736 | (write-int #x4558494e port) ;%export-magic | ||
| 1737 | (write-string file port) ;store item | ||
| 1738 | (write-string-list '() port) ;references | ||
| 1739 | (write-string "" port) ;deriver | ||
| 1740 | (write-int 1 port) ;signed | ||
| 1741 | (write-string (object->string '(signature broken)) | ||
| 1742 | port) | ||
| 1743 | |||
| 1744 | (write-int 0 port))))) ;done | ||
| 1745 | |||
| 1746 | ;; Ensure 'import-paths' succeeds despite the corrupt signature. | ||
| 1747 | (let ((source (open-bytevector-input-port dump))) | ||
| 1748 | (match (import-paths %store source) | ||
| 1749 | ((imported) | ||
| 1750 | (string=? imported file)))))) | ||
| 1751 | |||
| 1752 | (test-assert "import signed by authorized key, hash doesn't match, but content-addressed" | ||
| 1753 | (let* ((text (random-text)) | ||
| 1754 | (file (add-file-tree-to-store %store | ||
| 1755 | `("tree" directory | ||
| 1756 | ("text" regular (data ,text)) | ||
| 1757 | ("link" symlink "text")))) | ||
| 1758 | ;; This key is known to be in the ACL by default. | ||
| 1759 | (public-key | ||
| 1760 | (call-with-input-file (string-append %config-directory "/signing-key.pub") | ||
| 1761 | (compose gcrypt:string->canonical-sexp get-string-all))) | ||
| 1762 | (private-key | ||
| 1763 | (call-with-input-file (string-append %config-directory "/signing-key.sec") | ||
| 1764 | (compose gcrypt:string->canonical-sexp get-string-all))) | ||
| 1765 | (dump (call-with-bytevector-output-port | ||
| 1766 | (lambda (port) | ||
| 1767 | (write-int 1 port) ;start | ||
| 1768 | |||
| 1769 | (write-file file port) ;contents | ||
| 1770 | (write-int #x4558494e port) ;%export-magic | ||
| 1771 | (write-string file port) ;store item | ||
| 1772 | (write-string-list '() port) ;references | ||
| 1773 | (write-string "" port) ;deriver | ||
| 1774 | (write-int 1 port) ;signed | ||
| 1775 | (write-string (gcrypt:canonical-sexp->string | ||
| 1776 | (signature-sexp | ||
| 1777 | (gcrypt:bytevector->hash-data | ||
| 1778 | (gcrypt:sha256 #vu8(0 1 2)) | ||
| 1779 | #:key-type 'rsa) | ||
| 1780 | private-key | ||
| 1781 | public-key)) | ||
| 1782 | port) | ||
| 1783 | |||
| 1784 | (write-int 0 port))))) ;done | ||
| 1785 | |||
| 1786 | ;; Ensure 'import-paths' succeeds despite the hash mismatch. | ||
| 1787 | (let ((source (open-bytevector-input-port dump))) | ||
| 1788 | (match (import-paths %store source) | ||
| 1789 | ((imported) | ||
| 1790 | (string=? imported file)))))) | ||
| 1791 | |||
| 1527 | (test-assert "import corrupt path" | 1792 | (test-assert "import corrupt path" |
| 1528 | (let* ((text (random-text)) | 1793 | (let* ((text (random-text)) |
| 1529 | (file (add-text-to-store %store "text" text)) | 1794 | (file (add-text-to-store %store "text" text)) |
