From 2efa5d8706014a4f78c3d0041c71ba92c5a54e77 Mon Sep 17 00:00:00 2001 From: Nick Hynes Date: Sat, 2 Mar 2024 15:10:00 +0800 Subject: [PATCH] Add expectedMeasurement and measure CLI command --- src/workerd/server/server.c++ | 51 ++++++++++++++++++++++---- src/workerd/server/server.h | 5 ++- src/workerd/server/workerd.c++ | 62 ++++++++++++++++++++++++-------- src/workerd/server/workerd.capnp | 12 +++++++ 4 files changed, 108 insertions(+), 22 deletions(-) diff --git a/src/workerd/server/server.c++ b/src/workerd/server/server.c++ index 0eacabfa35a..8e7f2de66fb 100644 --- a/src/workerd/server/server.c++ +++ b/src/workerd/server/server.c++ @@ -2245,6 +2245,21 @@ private: // ======================================================================================= +kj::Array measureConfig(config::Worker::Reader& config) { + auto confWords = capnp::canonicalize(config); + EVP_MD_CTX* digestCtx = EVP_MD_CTX_new(); + const EVP_MD* digestAlg = EVP_sha384(); + KJ_DEFER(EVP_MD_CTX_free(digestCtx)); + KJ_REQUIRE(EVP_DigestInit_ex(digestCtx, digestAlg, nullptr)); + KJ_REQUIRE(EVP_DigestUpdate(digestCtx, + reinterpret_cast(confWords.begin()), confWords.size() * sizeof(capnp::word))); + auto digest = kj::heapArray(EVP_MD_CTX_size(digestCtx)); + uint digestSize = 0; + KJ_REQUIRE(EVP_DigestFinal_ex(digestCtx, digest.begin(), &digestSize)); + KJ_ASSERT(digestSize == digest.size()); + return digest; +} + class Server::WorkerdApiService final: public Service, private WorkerInterface { // Service used when the service is configured as network service. @@ -2269,20 +2284,31 @@ private: return requestBody.readAllText().then([this, &headers, &response](auto confJson) { capnp::MallocMessageBuilder confArena; capnp::JsonCodec json; - json.handleByAnnotation(); - auto conf = confArena.initRoot(); + json.handleByAnnotation(); + auto conf = confArena.initRoot(); json.decode(confJson, conf); kj::String id = workerd::randomUUID(kj::none); server.actorConfigs.insert(kj::str(id), {}); + kj::Maybe> expectedMeasurement = kj::none; + if (conf.hasExpectedMeasurement()) { + auto res = kj::decodeHex(conf.getExpectedMeasurement()); + if (res.hadErrors) { + auto out = response.send(400, "Bad Request", headers, kj::none); + auto errMsg = "invalid expected measurement"_kjc.asBytes(); + return out->write(errMsg.begin(), errMsg.size()); + } + expectedMeasurement = kj::mv(res); + } + kj::Maybe configError = kj::none; auto workerService = server.makeWorker( - id, conf.asReader(), {}, - [&configError](auto err) { - configError = kj::mv(err); - }); + id, conf.getWorker().asReader(), {}, + [&configError](auto err) { configError = kj::mv(err); }, + kj::mv(expectedMeasurement) + ); KJ_IF_SOME(err, configError) { throw KJ_EXCEPTION(FAILED, err); } @@ -2704,8 +2730,19 @@ void Server::abortAllActors() { kj::Own Server::makeWorker(kj::StringPtr name, config::Worker::Reader conf, capnp::List::Reader extensions, - kj::Function reportConfigError) { + kj::Function reportConfigError, + kj::Maybe> expectedMeasurement) { TRACE_EVENT("workerd", "Server::makeWorker()", "name", name.cStr()); + + KJ_IF_SOME(expected, expectedMeasurement) { + auto measurement = measureConfig(conf); + if (measurement != expected) { + reportConfigError(kj::str("service ", name, ": measurement mismatched.", + " expected ", kj::encodeHex(expected), " but got ", kj::encodeHex(measurement))); + return makeInvalidConfigService(); + } + } + auto& localActorConfigs = KJ_ASSERT_NONNULL(actorConfigs.find(name)); struct ErrorReporter: public Worker::ValidationErrorReporter { diff --git a/src/workerd/server/server.h b/src/workerd/server/server.h index 7cc1993b63c..107cc6915f4 100644 --- a/src/workerd/server/server.h +++ b/src/workerd/server/server.h @@ -173,7 +173,8 @@ class Server: private kj::TaskSet::ErrorHandler { kj::HttpHeaderTable::Builder& headerTableBuilder); kj::Own makeWorker(kj::StringPtr name, config::Worker::Reader conf, capnp::List::Reader extensions, - kj::Function reportConfigError); + kj::Function reportConfigError, + kj::Maybe> expectedMeasurement = kj::none); kj::Own makeService( config::Service::Reader conf, kj::HttpHeaderTable::Builder& headerTableBuilder, @@ -257,4 +258,6 @@ class EmptyReadOnlyActorStorageImpl final: public rpc::ActorStorage::Stage::Serv }; }; +kj::Array measureConfig(config::Worker::Reader& config); + } // namespace workerd::server diff --git a/src/workerd/server/workerd.c++ b/src/workerd/server/workerd.c++ index 2ded97deaa5..308978304a9 100644 --- a/src/workerd/server/workerd.c++ +++ b/src/workerd/server/workerd.c++ @@ -549,6 +549,8 @@ public: "run the server") .addSubCommand("compile", KJ_BIND_METHOD(*this, getCompile), "create a self-contained binary") + .addSubCommand("measure", KJ_BIND_METHOD(*this, getMeasure), + "measure the provided worker configuration") .addSubCommand("test", KJ_BIND_METHOD(*this, getTest), "run unit tests") .build(); @@ -558,10 +560,13 @@ public: // "explain": Produces human-friendly description of the config. } else { // We already have a config, meaning this must be a compiled binary. - auto builder = kj::MainBuilder(context, getVersionString(), - "Serve requests based on the compiled config.", - "This binary has an embedded configuration."); - return addServeOptions(builder); + return kj::MainBuilder(context, getVersionString(), + "Runs the Workers JavaScript/Wasm runtime.") + .addSubCommand("serve", KJ_BIND_METHOD(*this, getServe), + "run the server") + .addSubCommand("measure", KJ_BIND_METHOD(*this, getMeasure), + "measure the provided worker configuration") + .build(); } } @@ -688,6 +693,16 @@ public: .build(); } + kj::MainFunc getMeasure() { + return kj::MainBuilder(context, getVersionString(), + "Measures the provided worker config and outputs the hash.", + "Loads a worker's code and config in the same way as would be done by the " + "`workerd.createWorker` method, hashes the full config, and returns the hash.") + .expectArg("", CLI_METHOD(parseWorkerConfigFile)) + .callAfterParsing(CLI_METHOD(measure)) + .build(); + } + void addImportPath(kj::StringPtr pathStr) { auto path = fs->getCurrentPath().evalNative(pathStr); if (fs->getRoot().tryOpenSubdir(path) != kj::none) { @@ -811,6 +826,19 @@ public: } void parseConfigFile(kj::StringPtr pathStr) { + config = parseCapnpConfig(pathStr); + // We'll fail at getConfig() if there are multiple top level Config objects. + // The error message says that you have to specify which config to use, but + // it's not clear that there is any mechanism to do that. + util::Autogate::initAutogate(getConfig().getAutogates()); + } + + void parseWorkerConfigFile(kj::StringPtr pathStr) { + workerConfig = parseCapnpConfig(pathStr); + } + + template + kj::Maybe parseCapnpConfig(kj::StringPtr pathStr) { if (pathStr == "-") { // Read from stdin. @@ -826,8 +854,8 @@ public: #else auto reader = kj::heap(STDIN_FILENO, CONFIG_READER_OPTIONS); #endif - config = reader->getRoot(); configOwner = kj::mv(reader); + return reader->getRoot(); } else { // Read file from disk. auto path = fs->getCurrentPath().evalNative(pathStr); @@ -840,11 +868,11 @@ public: mapping.size() / sizeof(capnp::word)); auto reader = kj::heap(words, CONFIG_READER_OPTIONS) .attach(kj::mv(mapping)); - config = reader->getRoot(); configOwner = kj::mv(reader); + return reader->getRoot(); } else { // Interpret as schema file. - schemaParser.loadCompiledTypeAndDependencies(); + schemaParser.loadCompiledTypeAndDependencies(); parsedSchema = schemaParser.parseFile( kj::heap(fs->getRoot(), fs->getCurrentPath(), @@ -857,18 +885,14 @@ public: auto constSchema = nested.asConst(); auto type = constSchema.getType(); if (type.isStruct() && - type.asStruct().getProto().getId() == capnp::typeId()) { - topLevelConfigConstants.add(constSchema); + type.asStruct().getProto().getId() == capnp::typeId()) { + return constSchema.as(); } } } + return kj::none; } } - - // We'll fail at getConfig() if there are multiple top level Config objects. - // The error message says that you have to specify which config to use, but - // it's not clear that there is any mechanism to do that. - util::Autogate::initAutogate(getConfig().getAutogates()); } void setConstName(kj::StringPtr name) { @@ -1075,6 +1099,15 @@ public: } } + void measure() { + if (hadErrors) context.exit(); + auto measurement = workerd::server::measureConfig( + KJ_UNWRAP_OR(workerConfig, CLI_ERROR("no worker config provided"))); + auto measurementHex = kj::encodeHex(measurement); + kj::FdOutputStream out{STDOUT_FILENO}; + out.write(measurementHex.asBytes().begin(), measurementHex.size()); + } + [[noreturn]] void serve() noexcept { serveImpl([&](jsg::V8System& v8System, config::Config::Reader config) { #if _WIN32 @@ -1164,6 +1197,7 @@ private: kj::Own configOwner; // backing object for `config`, if it's not `schemaParser`. kj::Maybe config; + kj::Maybe workerConfig; kj::Vector inheritedFds; diff --git a/src/workerd/server/workerd.capnp b/src/workerd/server/workerd.capnp index 9402b787bb9..aaab0ed65f6 100644 --- a/src/workerd/server/workerd.capnp +++ b/src/workerd/server/workerd.capnp @@ -915,3 +915,15 @@ struct Extension { # Raw source code of ES module. } } + +# ======================================================================================== +# Unsafe Evalualuation + +struct NewWorker { + worker @0 :Worker; + + expectedMeasurement @1 :Text; + # The expected measurement of the worker. The measurement is computed as the SHA-512 hash of the + # binary format of the worker's capnp config. If this field is set and the measurement does not + # match, the worker will not run. +}