Skip to content

Commit f3c0736

Browse files
committed
Add expectedMeasurement and measure CLI command
1 parent a1769de commit f3c0736

File tree

4 files changed

+108
-22
lines changed

4 files changed

+108
-22
lines changed

src/workerd/server/server.c++

+44-7
Original file line numberDiff line numberDiff line change
@@ -2245,6 +2245,21 @@ private:
22452245

22462246
// =======================================================================================
22472247

2248+
kj::Array<kj::byte> measureConfig(config::Worker::Reader& config) {
2249+
auto confWords = capnp::canonicalize(config);
2250+
EVP_MD_CTX* digestCtx = EVP_MD_CTX_new();
2251+
const EVP_MD* digestAlg = EVP_sha384();
2252+
KJ_DEFER(EVP_MD_CTX_free(digestCtx));
2253+
KJ_REQUIRE(EVP_DigestInit_ex(digestCtx, digestAlg, nullptr));
2254+
KJ_REQUIRE(EVP_DigestUpdate(digestCtx,
2255+
reinterpret_cast<kj::byte*>(confWords.begin()), confWords.size() * sizeof(capnp::word)));
2256+
auto digest = kj::heapArray<kj::byte>(EVP_MD_CTX_size(digestCtx));
2257+
uint digestSize = 0;
2258+
KJ_REQUIRE(EVP_DigestFinal_ex(digestCtx, digest.begin(), &digestSize));
2259+
KJ_ASSERT(digestSize == digest.size());
2260+
return digest;
2261+
}
2262+
22482263
class Server::WorkerdApiService final: public Service, private WorkerInterface {
22492264
// Service used when the service is configured as network service.
22502265

@@ -2269,20 +2284,31 @@ private:
22692284
return requestBody.readAllText().then([this, &headers, &response](auto confJson) {
22702285
capnp::MallocMessageBuilder confArena;
22712286
capnp::JsonCodec json;
2272-
json.handleByAnnotation<config::Worker>();
2273-
auto conf = confArena.initRoot<config::Worker>();
2287+
json.handleByAnnotation<config::NewWorker>();
2288+
auto conf = confArena.initRoot<config::NewWorker>();
22742289
json.decode(confJson, conf);
22752290

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

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

2295+
kj::Maybe<kj::Array<kj::byte>> expectedMeasurement = kj::none;
2296+
if (conf.hasExpectedMeasurement()) {
2297+
auto res = kj::decodeHex(conf.getExpectedMeasurement());
2298+
if (res.hadErrors) {
2299+
auto out = response.send(400, "Bad Request", headers, kj::none);
2300+
auto errMsg = "invalid expected measurement"_kjc.asBytes();
2301+
return out->write(errMsg.begin(), errMsg.size());
2302+
}
2303+
expectedMeasurement = kj::mv(res);
2304+
}
2305+
22802306
kj::Maybe<kj::String> configError = kj::none;
22812307
auto workerService = server.makeWorker(
2282-
id, conf.asReader(), {},
2283-
[&configError](auto err) {
2284-
configError = kj::mv(err);
2285-
});
2308+
id, conf.getWorker().asReader(), {},
2309+
[&configError](auto err) { configError = kj::mv(err); },
2310+
kj::mv(expectedMeasurement)
2311+
);
22862312
KJ_IF_SOME(err, configError) {
22872313
throw KJ_EXCEPTION(FAILED, err);
22882314
}
@@ -2704,8 +2730,19 @@ void Server::abortAllActors() {
27042730

27052731
kj::Own<Server::Service> Server::makeWorker(kj::StringPtr name, config::Worker::Reader conf,
27062732
capnp::List<config::Extension>::Reader extensions,
2707-
kj::Function<void(kj::String)> reportConfigError) {
2733+
kj::Function<void(kj::String)> reportConfigError,
2734+
kj::Maybe<kj::Array<kj::byte>> expectedMeasurement) {
27082735
TRACE_EVENT("workerd", "Server::makeWorker()", "name", name.cStr());
2736+
2737+
KJ_IF_SOME(expected, expectedMeasurement) {
2738+
auto measurement = measureConfig(conf);
2739+
if (measurement != expected) {
2740+
reportConfigError(kj::str("service ", name, ": measurement mismatched.",
2741+
" expected ", kj::encodeHex(expected), " but got ", kj::encodeHex(measurement)));
2742+
return makeInvalidConfigService();
2743+
}
2744+
}
2745+
27092746
auto& localActorConfigs = KJ_ASSERT_NONNULL(actorConfigs.find(name));
27102747

27112748
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,
@@ -257,4 +258,6 @@ class EmptyReadOnlyActorStorageImpl final: public rpc::ActorStorage::Stage::Serv
257258
};
258259
};
259260

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

src/workerd/server/workerd.c++

+48-14
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,8 @@ public:
549549
"run the server")
550550
.addSubCommand("compile", KJ_BIND_METHOD(*this, getCompile),
551551
"create a self-contained binary")
552+
.addSubCommand("measure", KJ_BIND_METHOD(*this, getMeasure),
553+
"measure the provided worker configuration")
552554
.addSubCommand("test", KJ_BIND_METHOD(*this, getTest),
553555
"run unit tests")
554556
.build();
@@ -558,10 +560,13 @@ public:
558560
// "explain": Produces human-friendly description of the config.
559561
} else {
560562
// We already have a config, meaning this must be a compiled binary.
561-
auto builder = kj::MainBuilder(context, getVersionString(),
562-
"Serve requests based on the compiled config.",
563-
"This binary has an embedded configuration.");
564-
return addServeOptions(builder);
563+
return kj::MainBuilder(context, getVersionString(),
564+
"Runs the Workers JavaScript/Wasm runtime.")
565+
.addSubCommand("serve", KJ_BIND_METHOD(*this, getServe),
566+
"run the server")
567+
.addSubCommand("measure", KJ_BIND_METHOD(*this, getMeasure),
568+
"measure the provided worker configuration")
569+
.build();
565570
}
566571
}
567572

@@ -688,6 +693,16 @@ public:
688693
.build();
689694
}
690695

696+
kj::MainFunc getMeasure() {
697+
return kj::MainBuilder(context, getVersionString(),
698+
"Measures the provided worker config and outputs the hash.",
699+
"Loads a worker's code and config in the same way as would be done by the "
700+
"`workerd.createWorker` method, hashes the full config, and returns the hash.")
701+
.expectArg("<config-file>", CLI_METHOD(parseWorkerConfigFile))
702+
.callAfterParsing(CLI_METHOD(measure))
703+
.build();
704+
}
705+
691706
void addImportPath(kj::StringPtr pathStr) {
692707
auto path = fs->getCurrentPath().evalNative(pathStr);
693708
if (fs->getRoot().tryOpenSubdir(path) != kj::none) {
@@ -811,6 +826,19 @@ public:
811826
}
812827

813828
void parseConfigFile(kj::StringPtr pathStr) {
829+
config = parseCapnpConfig<config::Config>(pathStr);
830+
// We'll fail at getConfig() if there are multiple top level Config objects.
831+
// The error message says that you have to specify which config to use, but
832+
// it's not clear that there is any mechanism to do that.
833+
util::Autogate::initAutogate(getConfig().getAutogates());
834+
}
835+
836+
void parseWorkerConfigFile(kj::StringPtr pathStr) {
837+
workerConfig = parseCapnpConfig<config::Worker>(pathStr);
838+
}
839+
840+
template <typename T>
841+
kj::Maybe<typename T::Reader> parseCapnpConfig(kj::StringPtr pathStr) {
814842
if (pathStr == "-") {
815843
// Read from stdin.
816844

@@ -826,8 +854,8 @@ public:
826854
#else
827855
auto reader = kj::heap<capnp::StreamFdMessageReader>(STDIN_FILENO, CONFIG_READER_OPTIONS);
828856
#endif
829-
config = reader->getRoot<config::Config>();
830857
configOwner = kj::mv(reader);
858+
return reader->getRoot<T>();
831859
} else {
832860
// Read file from disk.
833861
auto path = fs->getCurrentPath().evalNative(pathStr);
@@ -840,11 +868,11 @@ public:
840868
mapping.size() / sizeof(capnp::word));
841869
auto reader = kj::heap<capnp::FlatArrayMessageReader>(words, CONFIG_READER_OPTIONS)
842870
.attach(kj::mv(mapping));
843-
config = reader->getRoot<config::Config>();
844871
configOwner = kj::mv(reader);
872+
return reader->getRoot<T>();
845873
} else {
846874
// Interpret as schema file.
847-
schemaParser.loadCompiledTypeAndDependencies<config::Config>();
875+
schemaParser.loadCompiledTypeAndDependencies<T>();
848876

849877
parsedSchema = schemaParser.parseFile(
850878
kj::heap<SchemaFileImpl>(fs->getRoot(), fs->getCurrentPath(),
@@ -857,18 +885,14 @@ public:
857885
auto constSchema = nested.asConst();
858886
auto type = constSchema.getType();
859887
if (type.isStruct() &&
860-
type.asStruct().getProto().getId() == capnp::typeId<config::Config>()) {
861-
topLevelConfigConstants.add(constSchema);
888+
type.asStruct().getProto().getId() == capnp::typeId<T>()) {
889+
return constSchema.as<T>();
862890
}
863891
}
864892
}
893+
return kj::none;
865894
}
866895
}
867-
868-
// We'll fail at getConfig() if there are multiple top level Config objects.
869-
// The error message says that you have to specify which config to use, but
870-
// it's not clear that there is any mechanism to do that.
871-
util::Autogate::initAutogate(getConfig().getAutogates());
872896
}
873897

874898
void setConstName(kj::StringPtr name) {
@@ -1075,6 +1099,15 @@ public:
10751099
}
10761100
}
10771101

1102+
void measure() {
1103+
if (hadErrors) context.exit();
1104+
auto measurement = workerd::server::measureConfig(
1105+
KJ_UNWRAP_OR(workerConfig, CLI_ERROR("no worker config provided")));
1106+
auto measurementHex = kj::encodeHex(measurement);
1107+
kj::FdOutputStream out{STDOUT_FILENO};
1108+
out.write(measurementHex.asBytes().begin(), measurementHex.size());
1109+
}
1110+
10781111
[[noreturn]] void serve() noexcept {
10791112
serveImpl([&](jsg::V8System& v8System, config::Config::Reader config) {
10801113
#if _WIN32
@@ -1164,6 +1197,7 @@ private:
11641197

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

11681202
kj::Vector<int> inheritedFds;
11691203

src/workerd/server/workerd.capnp

+12
Original file line numberDiff line numberDiff line change
@@ -915,3 +915,15 @@ struct Extension {
915915
# Raw source code of ES module.
916916
}
917917
}
918+
919+
# ========================================================================================
920+
# Unsafe Evalualuation
921+
922+
struct NewWorker {
923+
worker @0 :Worker;
924+
925+
expectedMeasurement @1 :Text;
926+
# The expected measurement of the worker. The measurement is computed as the SHA-512 hash of the
927+
# binary format of the worker's capnp config. If this field is set and the measurement does not
928+
# match, the worker will not run.
929+
}

0 commit comments

Comments
 (0)