Skip to content

Commit 888dcdd

Browse files
committed
Add expectedMeasurement and measure CLI command
1 parent dad51d6 commit 888dcdd

File tree

4 files changed

+123
-21
lines changed

4 files changed

+123
-21
lines changed

src/workerd/server/server.c++

+44-7
Original file line numberDiff line numberDiff line change
@@ -2319,6 +2319,21 @@ private:
23192319

23202320
// =======================================================================================
23212321

2322+
kj::Array<kj::byte> measureConfig(config::Worker::Reader& config) {
2323+
auto confWords = capnp::canonicalize(config);
2324+
EVP_MD_CTX* digestCtx = EVP_MD_CTX_new();
2325+
const EVP_MD* digestAlg = EVP_sha384();
2326+
KJ_DEFER(EVP_MD_CTX_free(digestCtx));
2327+
KJ_REQUIRE(EVP_DigestInit_ex(digestCtx, digestAlg, nullptr));
2328+
KJ_REQUIRE(EVP_DigestUpdate(digestCtx,
2329+
reinterpret_cast<kj::byte*>(confWords.begin()), confWords.size() * sizeof(capnp::word)));
2330+
auto digest = kj::heapArray<kj::byte>(EVP_MD_CTX_size(digestCtx));
2331+
uint digestSize = 0;
2332+
KJ_REQUIRE(EVP_DigestFinal_ex(digestCtx, digest.begin(), &digestSize));
2333+
KJ_ASSERT(digestSize == digest.size());
2334+
return digest;
2335+
}
2336+
23222337
class Server::WorkerdApiService final: public Service, private WorkerInterface {
23232338
// Service used when the service is configured as network service.
23242339

@@ -2343,20 +2358,31 @@ private:
23432358
return requestBody.readAllText().then([this, &headers, &response](auto confJson) {
23442359
capnp::MallocMessageBuilder confArena;
23452360
capnp::JsonCodec json;
2346-
json.handleByAnnotation<config::Worker>();
2347-
auto conf = confArena.initRoot<config::Worker>();
2361+
json.handleByAnnotation<config::NewWorker>();
2362+
auto conf = confArena.initRoot<config::NewWorker>();
23482363
json.decode(confJson, conf);
23492364

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

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

2369+
kj::Maybe<kj::Array<kj::byte>> expectedMeasurement = kj::none;
2370+
if (conf.hasExpectedMeasurement()) {
2371+
auto res = kj::decodeHex(conf.getExpectedMeasurement());
2372+
if (res.hadErrors) {
2373+
auto out = response.send(400, "Bad Request", headers, kj::none);
2374+
auto errMsg = "invalid expected measurement"_kjc.asBytes();
2375+
return out->write(errMsg.begin(), errMsg.size());
2376+
}
2377+
expectedMeasurement = kj::mv(res);
2378+
}
2379+
23542380
kj::Maybe<kj::String> configError = kj::none;
23552381
auto workerService = server.makeWorker(
2356-
id, conf.asReader(), {},
2357-
[&configError](auto err) {
2358-
configError = kj::mv(err);
2359-
});
2382+
id, conf.getWorker().asReader(), {},
2383+
[&configError](auto err) { configError = kj::mv(err); },
2384+
kj::mv(expectedMeasurement)
2385+
);
23602386
KJ_IF_SOME(err, configError) {
23612387
throw KJ_EXCEPTION(FAILED, err);
23622388
}
@@ -2778,8 +2804,19 @@ void Server::abortAllActors() {
27782804

27792805
kj::Own<Server::Service> Server::makeWorker(kj::StringPtr name, config::Worker::Reader conf,
27802806
capnp::List<config::Extension>::Reader extensions,
2781-
kj::Function<void(kj::String)> reportConfigError) {
2807+
kj::Function<void(kj::String)> reportConfigError,
2808+
kj::Maybe<kj::Array<kj::byte>> expectedMeasurement) {
27822809
TRACE_EVENT("workerd", "Server::makeWorker()", "name", name.cStr());
2810+
2811+
KJ_IF_SOME(expected, expectedMeasurement) {
2812+
auto measurement = measureConfig(conf);
2813+
if (measurement != expected) {
2814+
reportConfigError(kj::str("service ", name, ": measurement mismatched.",
2815+
" expected ", kj::encodeHex(expected), " but got ", kj::encodeHex(measurement)));
2816+
return makeInvalidConfigService();
2817+
}
2818+
}
2819+
27832820
auto& localActorConfigs = KJ_ASSERT_NONNULL(actorConfigs.find(name));
27842821

27852822
struct ErrorReporter: public Worker::ValidationErrorReporter {

src/workerd/server/server.h

+4-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ class Server: private kj::TaskSet::ErrorHandler {
173173
kj::HttpHeaderTable::Builder& headerTableBuilder);
174174
kj::Own<Service> makeWorker(kj::StringPtr name, config::Worker::Reader conf,
175175
capnp::List<config::Extension>::Reader extensions,
176-
kj::Function<void(kj::String)> reportConfigError);
176+
kj::Function<void(kj::String)> reportConfigError,
177+
kj::Maybe<kj::Array<kj::byte>> expectedMeasurement = kj::none);
177178
kj::Own<Service> makeService(
178179
config::Service::Reader conf,
179180
kj::HttpHeaderTable::Builder& headerTableBuilder,
@@ -258,4 +259,6 @@ class EmptyReadOnlyActorStorageImpl final: public rpc::ActorStorage::Stage::Serv
258259
};
259260
};
260261

262+
kj::Array<kj::byte> measureConfig(config::Worker::Reader& config);
263+
261264
} // namespace workerd::server

src/workerd/server/workerd.c++

+63-13
Original file line numberDiff line numberDiff line change
@@ -667,6 +667,8 @@ public:
667667
"run the server")
668668
.addSubCommand("compile", KJ_BIND_METHOD(*this, getCompile),
669669
"create a self-contained binary")
670+
.addSubCommand("measure", KJ_BIND_METHOD(*this, getMeasure),
671+
"measure the provided worker configuration")
670672
.addSubCommand("test", KJ_BIND_METHOD(*this, getTest),
671673
"run unit tests")
672674
.build();
@@ -679,7 +681,13 @@ public:
679681
auto builder = kj::MainBuilder(context, getVersionString(),
680682
"Serve requests based on the compiled config.",
681683
"This binary has an embedded configuration.");
682-
return addServeOptions(builder);
684+
return kj::MainBuilder(context, getVersionString(),
685+
"Runs the Workers JavaScript/Wasm runtime.")
686+
.addSubCommand("serve", KJ_BIND_METHOD(*this, getServeFromEmbeddedConfig),
687+
"run the server")
688+
.addSubCommand("measure", KJ_BIND_METHOD(*this, getMeasure),
689+
"measure the provided worker configuration")
690+
.build();
683691
}
684692
}
685693

@@ -742,10 +750,19 @@ public:
742750
}
743751

744752
kj::MainFunc getServe() {
745-
auto builder = kj::MainBuilder(context, getVersionString(),
753+
auto builder = getServeBuilder();
754+
return addServeOptions(addConfigParsingOptions(builder));
755+
}
756+
757+
kj::MainFunc getServeFromEmbeddedConfig() {
758+
auto builder = getServeBuilder();
759+
return addServeOptions(builder);
760+
}
761+
762+
kj::MainBuilder getServeBuilder() {
763+
return kj::MainBuilder(context, getVersionString(),
746764
"Serve requests based on a config.",
747765
"Serves requests based on the configuration specified in <config-file>.");
748-
return addServeOptions(addConfigParsingOptions(builder));
749766
}
750767

751768
kj::MainFunc getTest() {
@@ -806,6 +823,16 @@ public:
806823
.build();
807824
}
808825

826+
kj::MainFunc getMeasure() {
827+
return kj::MainBuilder(context, getVersionString(),
828+
"Measures the provided worker config and outputs the hash.",
829+
"Loads a worker's code and config in the same way as would be done by the "
830+
"`workerd.createWorker` method, hashes the full config, and returns the hash.")
831+
.expectArg("<config-file>", CLI_METHOD(parseWorkerConfigFile))
832+
.callAfterParsing(CLI_METHOD(measure))
833+
.build();
834+
}
835+
809836
void addImportPath(kj::StringPtr pathStr) {
810837
auto path = fs->getCurrentPath().evalNative(pathStr);
811838
if (fs->getRoot().tryOpenSubdir(path) != kj::none) {
@@ -929,6 +956,19 @@ public:
929956
}
930957

931958
void parseConfigFile(kj::StringPtr pathStr) {
959+
config = parseCapnpConfig<config::Config>(pathStr);
960+
// We'll fail at getConfig() if there are multiple top level Config objects.
961+
// The error message says that you have to specify which config to use, but
962+
// it's not clear that there is any mechanism to do that.
963+
util::Autogate::initAutogate(getConfig().getAutogates());
964+
}
965+
966+
void parseWorkerConfigFile(kj::StringPtr pathStr) {
967+
workerConfig = parseCapnpConfig<config::Worker>(pathStr);
968+
}
969+
970+
template <typename T>
971+
kj::Maybe<typename T::Reader> parseCapnpConfig(kj::StringPtr pathStr) {
932972
if (pathStr == "-") {
933973
// Read from stdin.
934974

@@ -944,8 +984,8 @@ public:
944984
#else
945985
auto reader = kj::heap<capnp::StreamFdMessageReader>(STDIN_FILENO, CONFIG_READER_OPTIONS);
946986
#endif
947-
config = reader->getRoot<config::Config>();
948987
configOwner = kj::mv(reader);
988+
return reader->getRoot<T>();
949989
} else {
950990
// Read file from disk.
951991
auto path = fs->getCurrentPath().evalNative(pathStr);
@@ -958,11 +998,11 @@ public:
958998
mapping.size() / sizeof(capnp::word));
959999
auto reader = kj::heap<capnp::FlatArrayMessageReader>(words, CONFIG_READER_OPTIONS)
9601000
.attach(kj::mv(mapping));
961-
config = reader->getRoot<config::Config>();
9621001
configOwner = kj::mv(reader);
1002+
return reader->getRoot<T>();
9631003
} else {
9641004
// Interpret as schema file.
965-
schemaParser.loadCompiledTypeAndDependencies<config::Config>();
1005+
schemaParser.loadCompiledTypeAndDependencies<T>();
9661006

9671007
parsedSchema = schemaParser.parseFile(
9681008
kj::heap<SchemaFileImpl>(fs->getRoot(), fs->getCurrentPath(),
@@ -975,18 +1015,14 @@ public:
9751015
auto constSchema = nested.asConst();
9761016
auto type = constSchema.getType();
9771017
if (type.isStruct() &&
978-
type.asStruct().getProto().getId() == capnp::typeId<config::Config>()) {
979-
topLevelConfigConstants.add(constSchema);
1018+
type.asStruct().getProto().getId() == capnp::typeId<T>()) {
1019+
return constSchema.as<T>();
9801020
}
9811021
}
9821022
}
1023+
return kj::none;
9831024
}
9841025
}
985-
986-
// We'll fail at getConfig() if there are multiple top level Config objects.
987-
// The error message says that you have to specify which config to use, but
988-
// it's not clear that there is any mechanism to do that.
989-
util::Autogate::initAutogate(getConfig().getAutogates());
9901026
}
9911027

9921028
void setConstName(kj::StringPtr name) {
@@ -1193,6 +1229,19 @@ public:
11931229
}
11941230
}
11951231

1232+
void measure() {
1233+
if (hadErrors) context.exit();
1234+
auto measurement = workerd::server::measureConfig(
1235+
KJ_UNWRAP_OR(workerConfig, CLI_ERROR("no worker config provided")));
1236+
auto measurementHex = kj::encodeHex(measurement);
1237+
#if _WIN32
1238+
kj::FdOutputStream out(_fileno(stdout));
1239+
#else
1240+
kj::FdOutputStream out(STDOUT_FILENO);
1241+
#endif
1242+
out.write(measurementHex.asBytes().begin(), measurementHex.size());
1243+
}
1244+
11961245
[[noreturn]] void serve() noexcept {
11971246
serveImpl([&](jsg::V8System& v8System, config::Config::Reader config) {
11981247
#if _WIN32
@@ -1286,6 +1335,7 @@ private:
12861335

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

12901340
kj::Vector<int> inheritedFds;
12911341

src/workerd/server/workerd.capnp

+12
Original file line numberDiff line numberDiff line change
@@ -921,3 +921,15 @@ struct Extension {
921921
# Raw source code of ES module.
922922
}
923923
}
924+
925+
# ========================================================================================
926+
# Unsafe Evalualuation
927+
928+
struct NewWorker {
929+
worker @0 :Worker;
930+
931+
expectedMeasurement @1 :Text;
932+
# The expected measurement of the worker. The measurement is computed as the SHA-512 hash of the
933+
# binary format of the worker's capnp config. If this field is set and the measurement does not
934+
# match, the worker will not run.
935+
}

0 commit comments

Comments
 (0)