Merge pull request #4618 from NixOS/ca/sign-drvoutputs

Sign the derivation outputs
This commit is contained in:
Eelco Dolstra 2021-03-15 16:35:41 +01:00 committed by GitHub
commit a5e21aa13c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 163 additions and 19 deletions

View file

@ -925,6 +925,8 @@ void DerivationGoal::resolvedFinished() {
if (realisation) { if (realisation) {
auto newRealisation = *realisation; auto newRealisation = *realisation;
newRealisation.id = DrvOutput{initialOutputs.at(wantedOutput).outputHash, wantedOutput}; newRealisation.id = DrvOutput{initialOutputs.at(wantedOutput).outputHash, wantedOutput};
newRealisation.signatures.clear();
signRealisation(newRealisation);
worker.store.registerDrvOutput(newRealisation); worker.store.registerDrvOutput(newRealisation);
} else { } else {
// If we don't have a realisation, then it must mean that something // If we don't have a realisation, then it must mean that something

View file

@ -180,6 +180,9 @@ struct DerivationGoal : public Goal
/* Open a log file and a pipe to it. */ /* Open a log file and a pipe to it. */
Path openLogFile(); Path openLogFile();
/* Sign the newly built realisation if the store allows it */
virtual void signRealisation(Realisation&) {}
/* Close the log file. */ /* Close the log file. */
void closeLogFile(); void closeLogFile();

View file

@ -2615,13 +2615,22 @@ void LocalDerivationGoal::registerOutputs()
but it's fine to do in all cases. */ but it's fine to do in all cases. */
if (settings.isExperimentalFeatureEnabled("ca-derivations")) { if (settings.isExperimentalFeatureEnabled("ca-derivations")) {
for (auto& [outputName, newInfo] : infos) for (auto& [outputName, newInfo] : infos) {
worker.store.registerDrvOutput(Realisation{ auto thisRealisation = Realisation{
.id = DrvOutput{initialOutputs.at(outputName).outputHash, outputName}, .id = DrvOutput{initialOutputs.at(outputName).outputHash,
.outPath = newInfo.path}); outputName},
.outPath = newInfo.path};
signRealisation(thisRealisation);
worker.store.registerDrvOutput(thisRealisation);
}
} }
} }
void LocalDerivationGoal::signRealisation(Realisation & realisation)
{
getLocalStore().signRealisation(realisation);
}
void LocalDerivationGoal::checkOutputs(const std::map<Path, ValidPathInfo> & outputs) void LocalDerivationGoal::checkOutputs(const std::map<Path, ValidPathInfo> & outputs)
{ {

View file

@ -161,6 +161,8 @@ struct LocalDerivationGoal : public DerivationGoal
as valid. */ as valid. */
void registerOutputs() override; void registerOutputs() override;
void signRealisation(Realisation &) override;
/* Check that an output meets the requirements specified by the /* Check that an output meets the requirements specified by the
'outputChecks' attribute (or the legacy 'outputChecks' attribute (or the legacy
'{allowed,disallowed}{References,Requisites}' attributes). */ '{allowed,disallowed}{References,Requisites}' attributes). */

View file

@ -142,7 +142,7 @@ void PathSubstitutionGoal::tryNext()
/* Bail out early if this substituter lacks a valid /* Bail out early if this substituter lacks a valid
signature. LocalStore::addToStore() also checks for this, but signature. LocalStore::addToStore() also checks for this, but
only after we've downloaded the path. */ only after we've downloaded the path. */
if (!sub->isTrusted && worker.store.pathInfoIsTrusted(*info)) if (!sub->isTrusted && worker.store.pathInfoIsUntrusted(*info))
{ {
warn("substituter '%s' does not have a valid signature for path '%s'", warn("substituter '%s' does not have a valid signature for path '%s'",
sub->getUri(), worker.store.printStorePath(storePath)); sub->getUri(), worker.store.printStorePath(storePath));

View file

@ -6,6 +6,7 @@ create table if not exists Realisations (
drvPath text not null, drvPath text not null,
outputName text not null, -- symbolic output id, usually "out" outputName text not null, -- symbolic output id, usually "out"
outputPath integer not null, outputPath integer not null,
signatures text, -- space-separated list
primary key (drvPath, outputName), primary key (drvPath, outputName),
foreign key (outputPath) references ValidPaths(id) on delete cascade foreign key (outputPath) references ValidPaths(id) on delete cascade
); );

View file

@ -310,13 +310,13 @@ LocalStore::LocalStore(const Params & params)
if (settings.isExperimentalFeatureEnabled("ca-derivations")) { if (settings.isExperimentalFeatureEnabled("ca-derivations")) {
state->stmts->RegisterRealisedOutput.create(state->db, state->stmts->RegisterRealisedOutput.create(state->db,
R"( R"(
insert or replace into Realisations (drvPath, outputName, outputPath) insert or replace into Realisations (drvPath, outputName, outputPath, signatures)
values (?, ?, (select id from ValidPaths where path = ?)) values (?, ?, (select id from ValidPaths where path = ?), ?)
; ;
)"); )");
state->stmts->QueryRealisedOutput.create(state->db, state->stmts->QueryRealisedOutput.create(state->db,
R"( R"(
select Output.path from Realisations select Output.path, Realisations.signatures from Realisations
inner join ValidPaths as Output on Output.id = Realisations.outputPath inner join ValidPaths as Output on Output.id = Realisations.outputPath
where drvPath = ? and outputName = ? where drvPath = ? and outputName = ?
; ;
@ -652,6 +652,14 @@ void LocalStore::checkDerivationOutputs(const StorePath & drvPath, const Derivat
} }
} }
void LocalStore::registerDrvOutput(const Realisation & info, CheckSigsFlag checkSigs)
{
settings.requireExperimentalFeature("ca-derivations");
if (checkSigs == NoCheckSigs || !realisationIsUntrusted(info))
registerDrvOutput(info);
else
throw Error("cannot register realisation '%s' because it lacks a valid signature", info.outPath.to_string());
}
void LocalStore::registerDrvOutput(const Realisation & info) void LocalStore::registerDrvOutput(const Realisation & info)
{ {
@ -662,6 +670,7 @@ void LocalStore::registerDrvOutput(const Realisation & info)
(info.id.strHash()) (info.id.strHash())
(info.id.outputName) (info.id.outputName)
(printStorePath(info.outPath)) (printStorePath(info.outPath))
(concatStringsSep(" ", info.signatures))
.exec(); .exec();
}); });
} }
@ -1102,15 +1111,20 @@ const PublicKeys & LocalStore::getPublicKeys()
return *state->publicKeys; return *state->publicKeys;
} }
bool LocalStore::pathInfoIsTrusted(const ValidPathInfo & info) bool LocalStore::pathInfoIsUntrusted(const ValidPathInfo & info)
{ {
return requireSigs && !info.checkSignatures(*this, getPublicKeys()); return requireSigs && !info.checkSignatures(*this, getPublicKeys());
} }
bool LocalStore::realisationIsUntrusted(const Realisation & realisation)
{
return requireSigs && !realisation.checkSignatures(getPublicKeys());
}
void LocalStore::addToStore(const ValidPathInfo & info, Source & source, void LocalStore::addToStore(const ValidPathInfo & info, Source & source,
RepairFlag repair, CheckSigsFlag checkSigs) RepairFlag repair, CheckSigsFlag checkSigs)
{ {
if (checkSigs && pathInfoIsTrusted(info)) if (checkSigs && pathInfoIsUntrusted(info))
throw Error("cannot add path '%s' because it lacks a valid signature", printStorePath(info.path)); throw Error("cannot add path '%s' because it lacks a valid signature", printStorePath(info.path));
addTempRoot(info.path); addTempRoot(info.path);
@ -1612,6 +1626,18 @@ void LocalStore::addSignatures(const StorePath & storePath, const StringSet & si
} }
void LocalStore::signRealisation(Realisation & realisation)
{
// FIXME: keep secret keys in memory.
auto secretKeyFiles = settings.secretKeyFiles;
for (auto & secretKeyFile : secretKeyFiles.get()) {
SecretKey secretKey(readFile(secretKeyFile));
realisation.sign(secretKey);
}
}
void LocalStore::signPathInfo(ValidPathInfo & info) void LocalStore::signPathInfo(ValidPathInfo & info)
{ {
// FIXME: keep secret keys in memory. // FIXME: keep secret keys in memory.
@ -1649,8 +1675,9 @@ std::optional<const Realisation> LocalStore::queryRealisation(
if (!use.next()) if (!use.next())
return std::nullopt; return std::nullopt;
auto outputPath = parseStorePath(use.getStr(0)); auto outputPath = parseStorePath(use.getStr(0));
return Ret{ auto signatures = tokenizeString<StringSet>(use.getStr(1));
Realisation{.id = id, .outPath = outputPath}}; return Ret{Realisation{
.id = id, .outPath = outputPath, .signatures = signatures}};
}); });
} }
} // namespace nix } // namespace nix

View file

@ -136,7 +136,8 @@ public:
void querySubstitutablePathInfos(const StorePathCAMap & paths, void querySubstitutablePathInfos(const StorePathCAMap & paths,
SubstitutablePathInfos & infos) override; SubstitutablePathInfos & infos) override;
bool pathInfoIsTrusted(const ValidPathInfo &) override; bool pathInfoIsUntrusted(const ValidPathInfo &) override;
bool realisationIsUntrusted(const Realisation & ) override;
void addToStore(const ValidPathInfo & info, Source & source, void addToStore(const ValidPathInfo & info, Source & source,
RepairFlag repair, CheckSigsFlag checkSigs) override; RepairFlag repair, CheckSigsFlag checkSigs) override;
@ -202,6 +203,7 @@ public:
/* Register the store path 'output' as the output named 'outputName' of /* Register the store path 'output' as the output named 'outputName' of
derivation 'deriver'. */ derivation 'deriver'. */
void registerDrvOutput(const Realisation & info) override; void registerDrvOutput(const Realisation & info) override;
void registerDrvOutput(const Realisation & info, CheckSigsFlag checkSigs) override;
void cacheDrvOutputMapping(State & state, const uint64_t deriver, const string & outputName, const StorePath & output); void cacheDrvOutputMapping(State & state, const uint64_t deriver, const string & outputName, const StorePath & output);
std::optional<const Realisation> queryRealisation(const DrvOutput&) override; std::optional<const Realisation> queryRealisation(const DrvOutput&) override;
@ -272,9 +274,10 @@ private:
bool isValidPath_(State & state, const StorePath & path); bool isValidPath_(State & state, const StorePath & path);
void queryReferrers(State & state, const StorePath & path, StorePathSet & referrers); void queryReferrers(State & state, const StorePath & path, StorePathSet & referrers);
/* Add signatures to a ValidPathInfo using the secret keys /* Add signatures to a ValidPathInfo or Realisation using the secret keys
specified by the secret-key-files option. */ specified by the secret-key-files option. */
void signPathInfo(ValidPathInfo & info); void signPathInfo(ValidPathInfo & info);
void signRealisation(Realisation &);
Path getRealStoreDir() override { return realStoreDir; } Path getRealStoreDir() override { return realStoreDir; }

View file

@ -25,27 +25,69 @@ nlohmann::json Realisation::toJSON() const {
return nlohmann::json{ return nlohmann::json{
{"id", id.to_string()}, {"id", id.to_string()},
{"outPath", outPath.to_string()}, {"outPath", outPath.to_string()},
{"signatures", signatures},
}; };
} }
Realisation Realisation::fromJSON( Realisation Realisation::fromJSON(
const nlohmann::json& json, const nlohmann::json& json,
const std::string& whence) { const std::string& whence) {
auto getField = [&](std::string fieldName) -> std::string { auto getOptionalField = [&](std::string fieldName) -> std::optional<std::string> {
auto fieldIterator = json.find(fieldName); auto fieldIterator = json.find(fieldName);
if (fieldIterator == json.end()) if (fieldIterator == json.end())
return std::nullopt;
return *fieldIterator;
};
auto getField = [&](std::string fieldName) -> std::string {
if (auto field = getOptionalField(fieldName))
return *field;
else
throw Error( throw Error(
"Drv output info file '%1%' is corrupt, missing field %2%", "Drv output info file '%1%' is corrupt, missing field %2%",
whence, fieldName); whence, fieldName);
return *fieldIterator;
}; };
StringSet signatures;
if (auto signaturesIterator = json.find("signatures"); signaturesIterator != json.end())
signatures.insert(signaturesIterator->begin(), signaturesIterator->end());
return Realisation{ return Realisation{
.id = DrvOutput::parse(getField("id")), .id = DrvOutput::parse(getField("id")),
.outPath = StorePath(getField("outPath")), .outPath = StorePath(getField("outPath")),
.signatures = signatures,
}; };
} }
std::string Realisation::fingerprint() const
{
auto serialized = toJSON();
serialized.erase("signatures");
return serialized.dump();
}
void Realisation::sign(const SecretKey & secretKey)
{
signatures.insert(secretKey.signDetached(fingerprint()));
}
bool Realisation::checkSignature(const PublicKeys & publicKeys, const std::string & sig) const
{
return verifyDetached(fingerprint(), sig, publicKeys);
}
size_t Realisation::checkSignatures(const PublicKeys & publicKeys) const
{
// FIXME: Maybe we should return `maxSigs` if the realisation corresponds to
// an input-addressed one because in that case the drv is enough to check
// it but we can't know that here.
size_t good = 0;
for (auto & sig : signatures)
if (checkSignature(publicKeys, sig))
good++;
return good;
}
StorePath RealisedPath::path() const { StorePath RealisedPath::path() const {
return std::visit([](auto && arg) { return arg.getPath(); }, raw); return std::visit([](auto && arg) { return arg.getPath(); }, raw);
} }

View file

@ -3,6 +3,7 @@
#include "path.hh" #include "path.hh"
#include <nlohmann/json_fwd.hpp> #include <nlohmann/json_fwd.hpp>
#include "comparator.hh" #include "comparator.hh"
#include "crypto.hh"
namespace nix { namespace nix {
@ -25,9 +26,16 @@ struct Realisation {
DrvOutput id; DrvOutput id;
StorePath outPath; StorePath outPath;
StringSet signatures;
nlohmann::json toJSON() const; nlohmann::json toJSON() const;
static Realisation fromJSON(const nlohmann::json& json, const std::string& whence); static Realisation fromJSON(const nlohmann::json& json, const std::string& whence);
std::string fingerprint() const;
void sign(const SecretKey &);
bool checkSignature(const PublicKeys & publicKeys, const std::string & sig) const;
size_t checkSignatures(const PublicKeys & publicKeys) const;
StorePath getPath() const { return outPath; } StorePath getPath() const { return outPath; }
GENERATE_CMP(Realisation, me->id, me->outPath); GENERATE_CMP(Realisation, me->id, me->outPath);

View file

@ -798,7 +798,7 @@ std::map<StorePath, StorePath> copyPaths(ref<Store> srcStore, ref<Store> dstStor
auto pathsMap = copyPaths(srcStore, dstStore, storePaths, repair, checkSigs, substitute); auto pathsMap = copyPaths(srcStore, dstStore, storePaths, repair, checkSigs, substitute);
try { try {
for (auto & realisation : realisations) { for (auto & realisation : realisations) {
dstStore->registerDrvOutput(realisation); dstStore->registerDrvOutput(realisation, checkSigs);
} }
} catch (MissingExperimentalFeature & e) { } catch (MissingExperimentalFeature & e) {
// Don't fail if the remote doesn't support CA derivations is it might // Don't fail if the remote doesn't support CA derivations is it might

View file

@ -384,7 +384,12 @@ public:
we don't really want to add the dependencies listed in a nar info we we don't really want to add the dependencies listed in a nar info we
don't trust anyyways. don't trust anyyways.
*/ */
virtual bool pathInfoIsTrusted(const ValidPathInfo &) virtual bool pathInfoIsUntrusted(const ValidPathInfo &)
{
return true;
}
virtual bool realisationIsUntrusted(const Realisation & )
{ {
return true; return true;
} }
@ -480,6 +485,8 @@ public:
*/ */
virtual void registerDrvOutput(const Realisation & output) virtual void registerDrvOutput(const Realisation & output)
{ unsupported("registerDrvOutput"); } { unsupported("registerDrvOutput"); }
virtual void registerDrvOutput(const Realisation & output, CheckSigsFlag checkSigs)
{ return registerDrvOutput(output); }
/* Write a NAR dump of a store path. */ /* Write a NAR dump of a store path. */
virtual void narFromPath(const StorePath & path, Sink & sink) = 0; virtual void narFromPath(const StorePath & path, Sink & sink) = 0;

39
tests/ca/signatures.sh Normal file
View file

@ -0,0 +1,39 @@
source common.sh
# Globally enable the ca derivations experimental flag
sed -i 's/experimental-features = .*/& ca-derivations ca-references/' "$NIX_CONF_DIR/nix.conf"
clearStore
clearCache
nix-store --generate-binary-cache-key cache1.example.org $TEST_ROOT/sk1 $TEST_ROOT/pk1
pk1=$(cat $TEST_ROOT/pk1)
export REMOTE_STORE_DIR="$TEST_ROOT/remote_store"
export REMOTE_STORE="file://$REMOTE_STORE_DIR"
ensureCorrectlyCopied () {
attrPath="$1"
nix build --store "$REMOTE_STORE" --file ./content-addressed.nix "$attrPath"
}
testOneCopy () {
clearStore
rm -rf "$REMOTE_STORE_DIR"
attrPath="$1"
nix copy --to $REMOTE_STORE "$attrPath" --file ./content-addressed.nix \
--secret-key-files "$TEST_ROOT/sk1"
ensureCorrectlyCopied "$attrPath"
# Ensure that we can copy back what we put in the store
clearStore
nix copy --from $REMOTE_STORE \
--file ./content-addressed.nix "$attrPath" \
--trusted-public-keys $pk1
}
for attrPath in rootCA dependentCA transitivelyDependentCA dependentNonCA dependentFixedOutput; do
testOneCopy "$attrPath"
done

View file

@ -41,8 +41,9 @@ nix_tests = \
build.sh \ build.sh \
compute-levels.sh \ compute-levels.sh \
ca/build.sh \ ca/build.sh \
ca/nix-copy.sh \
ca/substitute.sh ca/substitute.sh
ca/signatures.sh \
ca/nix-copy.sh
# parallel.sh # parallel.sh
install-tests += $(foreach x, $(nix_tests), tests/$(x)) install-tests += $(foreach x, $(nix_tests), tests/$(x))