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 /nix | |
| 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
Diffstat (limited to 'nix')
| -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 |
5 files changed, 70 insertions, 14 deletions
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 |
