Skip to content

Commit

Permalink
Add expectedMeasurement and measure CLI command
Browse files Browse the repository at this point in the history
  • Loading branch information
nhynes committed Mar 2, 2024
1 parent a1769de commit 2efa5d8
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 22 deletions.
51 changes: 44 additions & 7 deletions src/workerd/server/server.c++
Original file line number Diff line number Diff line change
Expand Up @@ -2245,6 +2245,21 @@ private:

// =======================================================================================

kj::Array<kj::byte> 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<kj::byte*>(confWords.begin()), confWords.size() * sizeof(capnp::word)));
auto digest = kj::heapArray<kj::byte>(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.

Expand All @@ -2269,20 +2284,31 @@ private:
return requestBody.readAllText().then([this, &headers, &response](auto confJson) {
capnp::MallocMessageBuilder confArena;
capnp::JsonCodec json;
json.handleByAnnotation<config::Worker>();
auto conf = confArena.initRoot<config::Worker>();
json.handleByAnnotation<config::NewWorker>();
auto conf = confArena.initRoot<config::NewWorker>();
json.decode(confJson, conf);

kj::String id = workerd::randomUUID(kj::none);

server.actorConfigs.insert(kj::str(id), {});

kj::Maybe<kj::Array<kj::byte>> 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<kj::String> 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);
}
Expand Down Expand Up @@ -2704,8 +2730,19 @@ void Server::abortAllActors() {

kj::Own<Server::Service> Server::makeWorker(kj::StringPtr name, config::Worker::Reader conf,
capnp::List<config::Extension>::Reader extensions,
kj::Function<void(kj::String)> reportConfigError) {
kj::Function<void(kj::String)> reportConfigError,
kj::Maybe<kj::Array<kj::byte>> 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 {
Expand Down
5 changes: 4 additions & 1 deletion src/workerd/server/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ class Server: private kj::TaskSet::ErrorHandler {
kj::HttpHeaderTable::Builder& headerTableBuilder);
kj::Own<Service> makeWorker(kj::StringPtr name, config::Worker::Reader conf,
capnp::List<config::Extension>::Reader extensions,
kj::Function<void(kj::String)> reportConfigError);
kj::Function<void(kj::String)> reportConfigError,
kj::Maybe<kj::Array<kj::byte>> expectedMeasurement = kj::none);
kj::Own<Service> makeService(
config::Service::Reader conf,
kj::HttpHeaderTable::Builder& headerTableBuilder,
Expand Down Expand Up @@ -257,4 +258,6 @@ class EmptyReadOnlyActorStorageImpl final: public rpc::ActorStorage::Stage::Serv
};
};

kj::Array<kj::byte> measureConfig(config::Worker::Reader& config);

} // namespace workerd::server
62 changes: 48 additions & 14 deletions src/workerd/server/workerd.c++
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
}
}

Expand Down Expand Up @@ -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("<config-file>", 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) {
Expand Down Expand Up @@ -811,6 +826,19 @@ public:
}

void parseConfigFile(kj::StringPtr pathStr) {
config = parseCapnpConfig<config::Config>(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<config::Worker>(pathStr);
}

template <typename T>
kj::Maybe<typename T::Reader> parseCapnpConfig(kj::StringPtr pathStr) {
if (pathStr == "-") {
// Read from stdin.

Expand All @@ -826,8 +854,8 @@ public:
#else
auto reader = kj::heap<capnp::StreamFdMessageReader>(STDIN_FILENO, CONFIG_READER_OPTIONS);
#endif
config = reader->getRoot<config::Config>();
configOwner = kj::mv(reader);
return reader->getRoot<T>();
} else {
// Read file from disk.
auto path = fs->getCurrentPath().evalNative(pathStr);
Expand All @@ -840,11 +868,11 @@ public:
mapping.size() / sizeof(capnp::word));
auto reader = kj::heap<capnp::FlatArrayMessageReader>(words, CONFIG_READER_OPTIONS)
.attach(kj::mv(mapping));
config = reader->getRoot<config::Config>();
configOwner = kj::mv(reader);
return reader->getRoot<T>();
} else {
// Interpret as schema file.
schemaParser.loadCompiledTypeAndDependencies<config::Config>();
schemaParser.loadCompiledTypeAndDependencies<T>();

parsedSchema = schemaParser.parseFile(
kj::heap<SchemaFileImpl>(fs->getRoot(), fs->getCurrentPath(),
Expand All @@ -857,18 +885,14 @@ public:
auto constSchema = nested.asConst();
auto type = constSchema.getType();
if (type.isStruct() &&
type.asStruct().getProto().getId() == capnp::typeId<config::Config>()) {
topLevelConfigConstants.add(constSchema);
type.asStruct().getProto().getId() == capnp::typeId<T>()) {
return constSchema.as<T>();
}
}
}
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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1164,6 +1197,7 @@ private:

kj::Own<void> configOwner; // backing object for `config`, if it's not `schemaParser`.
kj::Maybe<config::Config::Reader> config;
kj::Maybe<config::Worker::Reader> workerConfig;

kj::Vector<int> inheritedFds;

Expand Down
12 changes: 12 additions & 0 deletions src/workerd/server/workerd.capnp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

0 comments on commit 2efa5d8

Please sign in to comment.