dvVerifyModuleManifest function
Whether the parent should mount the module document describes.
trustedKeys maps a key id to the public key the parent has for it. An
empty map refuses everything: a parent with no configured keys has not
been configured, and the safe reading of "no keys are trusted" is that
none are.
lastAcceptedVersion is the newest version this parent has already
mounted. An older one is refused even though it is validly signed: a
manifest does not expire, so replaying an old one is how a vulnerability
that was fixed comes back.
Implementation
DVModuleTrust dvVerifyModuleManifest(
String document, {
required Map<String, String> trustedKeys,
required String expectedId,
String? lastAcceptedVersion,
String? parentVersion,
String? mountPath,
Set<String> parentRoutes = const <String>{},
Set<String>? targetCapabilities,
}) {
Object? parsed;
try {
parsed = jsonDecode(document);
} on Object {
return const DVModuleTrust.refused(
'The module manifest is not JSON, so nothing in it can be trusted.',
);
}
if (parsed is! Map) {
return const DVModuleTrust.refused(
'The module manifest is not an object.',
);
}
final Object? body = parsed['manifest'];
final Object? keyId = parsed['keyId'];
final Object? signature = parsed['signature'];
if (body is! Map || keyId is! String || signature is! String) {
return const DVModuleTrust.refused(
'A signed module manifest carries a manifest, a keyId and a signature; '
'this one does not.',
);
}
final DVModuleManifest manifest = DVModuleManifest.fromJson(body);
final String? publicKey = trustedKeys[keyId];
if (publicKey == null) {
return DVModuleTrust.refused(
'The manifest names key "$keyId", which this application does not '
'trust. Its signature was not checked.',
);
}
if (!_verifies(manifest.canonical(), signature, publicKey)) {
// Deliberately one message for "edited after signing" and "signed by
// somebody else": from here they are the same fact -- these bytes were
// not signed by that key -- and guessing which would be a guess.
return DVModuleTrust.refused(
'The manifest\'s signature does not match key "$keyId", so it was '
'either signed by somebody else or edited after it was signed.',
);
}
if (manifest.id != expectedId) {
return DVModuleTrust.refused(
'The manifest is for module "${manifest.id}", and this application is '
'mounting "$expectedId".',
);
}
if (lastAcceptedVersion != null &&
_compareVersions(manifest.version, lastAcceptedVersion) < 0) {
return DVModuleTrust.refused(
'The manifest is version ${manifest.version} and this application has '
'already accepted $lastAcceptedVersion. An older manifest is signed '
'just as validly as a newer one, so it is refused rather than rolled '
'back to.',
);
}
final String? requires = manifest.requiresParent;
if (requires != null && requires.isNotEmpty && parentVersion != null) {
if (!_satisfies(parentVersion, requires)) {
return DVModuleTrust.refused(
'The module needs a parent $requires and this one is $parentVersion.',
);
}
}
if (targetCapabilities != null) {
final List<String> missing = manifest.capabilities
.where((String c) => !targetCapabilities.contains(c))
.toList()
..sort();
if (missing.isNotEmpty) {
return DVModuleTrust.refused(
'The module needs ${missing.join(', ')}, which this target does not '
'provide.',
);
}
}
if (parentRoutes.isNotEmpty) {
final String base = (mountPath ?? '').replaceAll(RegExp(r'/+$'), '');
final List<String> collisions = <String>[
for (final String route in manifest.routes)
if (parentRoutes.contains('$base$route')) '$base$route',
];
if (collisions.isNotEmpty) {
return DVModuleTrust.refused(
'Mounted at "$base" the module would answer for '
'${collisions.join(', ')}, which this application already serves.',
);
}
}
return DVModuleTrust._(true, 'accepted', manifest);
}