src/ccf/ccf-provider-common/constitution/actions.js (1,397 lines of code) (raw):
// From https://github.com/microsoft/CCF/blob/main/samples/constitutions/default/actions.js commit: 0d6b1cc
class Action {
constructor(validate, apply) {
this.validate = validate;
this.apply = apply;
}
}
function parseUrl(url) {
// From https://tools.ietf.org/html/rfc3986#appendix-B
const re = new RegExp(
"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"
);
const groups = url.match(re);
if (!groups) {
throw new TypeError(`${url} is not a valid URL.`);
}
return {
scheme: groups[2],
authority: groups[4],
path: groups[5],
query: groups[7],
fragment: groups[9]
};
}
function hexStrToBuf(hexStr) {
const result = [];
for (let i = 0; i < hexStr.length; i += 2) {
const octet = hexStr.slice(i, i + 2);
if (octet.length != 2 || octet.match(/[G-Z\s]/i)) {
throw new Error("Hex string invalid");
}
result.push(parseInt(octet, 16));
}
return new Uint8Array(result).buffer;
}
function checkType(value, type, field) {
const optional = type.endsWith("?");
if (optional) {
if (value === null || value === undefined) {
return;
}
type = type.slice(0, -1);
}
if (type === "array") {
if (!Array.isArray(value)) {
throw new Error(`${field} must be an array`);
}
} else if (type === "integer") {
if (!Number.isInteger(value)) {
throw new Error(`${field} must be an integer`);
}
} else if (typeof value !== type) {
throw new Error(`${field} must be of type ${type} but is ${typeof value}`);
}
}
function checkEnum(value, members, field) {
if (!members.includes(value)) {
throw new Error(`${field} must be one of ${members}`);
}
}
function checkBounds(value, low, high, field) {
if (low !== null && value < low) {
throw new Error(`${field} must be greater than ${low}`);
}
if (high !== null && value > high) {
throw new Error(`${field} must be lower than ${high}`);
}
}
function checkLength(value, min, max, field) {
if (min !== null && value.length < min) {
throw new Error(`${field} must be an array of minimum ${min} elements`);
}
if (max !== null && value.length > max) {
throw new Error(`${field} must be an array of maximum ${max} elements`);
}
}
function checkNone(args) {
if (args !== null && args !== undefined) {
throw new Error(`Proposal does not accept any argument, found "${args}"`);
}
}
function checkEntityId(value, field) {
checkType(value, "string", field);
// This should be the hex-encoding of a SHA256 digest. This is 32 bytes long, so
// produces 64 hex characters.
const digestLength = 64;
if (value.length !== digestLength) {
throw new Error(`${field} must contain exactly ${digestLength} characters`);
}
const re = new RegExp("^[a-fA-F0-9]*$");
if (!re.test(value)) {
throw new Error(`${field} contains non-hexadecimal character`);
}
}
function getSingletonKvKey() {
// When a KV map only contains one value, this is the key at which
// the value is recorded
return new ArrayBuffer(8);
}
function getActiveRecoveryMembersCount() {
let activeRecoveryMembersCount = 0;
ccf.kv["public:ccf.gov.members.encryption_public_keys"].forEach((_, k) => {
let rawMemberInfo = ccf.kv["public:ccf.gov.members.info"].get(k);
if (rawMemberInfo === undefined) {
throw new Error(`Recovery member ${ccf.bufToStr(k)} has no information`);
}
const memberInfo = ccf.bufToJsonCompatible(rawMemberInfo);
if (memberInfo.status === "Active") {
activeRecoveryMembersCount++;
}
});
return activeRecoveryMembersCount;
}
function checkJwks(value, field) {
checkType(value, "object", field);
checkType(value.keys, "array", `${field}.keys`);
for (const [i, jwk] of value.keys.entries()) {
checkType(jwk.kid, "string", `${field}.keys[${i}].kid`);
checkType(jwk.kty, "string", `${field}.keys[${i}].kty`);
checkType(jwk.x5c, "array", `${field}.keys[${i}].x5c`);
checkLength(jwk.x5c, 1, null, `${field}.keys[${i}].x5c`);
for (const [j, b64der] of jwk.x5c.entries()) {
checkType(b64der, "string", `${field}.keys[${i}].x5c[${j}]`);
const pem =
"-----BEGIN CERTIFICATE-----\n" +
b64der +
"\n-----END CERTIFICATE-----";
checkX509CertBundle(pem, `${field}.keys[${i}].x5c[${j}]`);
}
}
}
function checkX509CertBundle(value, field) {
if (!ccf.crypto.isValidX509CertBundle(value)) {
throw new Error(
`${field} must be a valid X509 certificate (bundle) in PEM format`
);
}
}
function invalidateOtherOpenProposals(proposalIdToRetain) {
const proposalsMap = ccf.kv["public:ccf.gov.proposals_info"];
proposalsMap.forEach((v, k) => {
let proposalId = ccf.bufToStr(k);
if (proposalId !== proposalIdToRetain) {
let info = ccf.bufToJsonCompatible(v);
if (info.state === "Open") {
info.state = "Dropped";
proposalsMap.set(k, ccf.jsonCompatibleToBuf(info));
}
}
});
}
function setServiceCertificateValidityPeriod(validFrom, validityPeriodDays) {
const rawConfig = ccf.kv["public:ccf.gov.service.config"].get(
getSingletonKvKey()
);
if (rawConfig === undefined) {
throw new Error("Service configuration could not be found");
}
const serviceConfig = ccf.bufToJsonCompatible(rawConfig);
const default_validity_period_days = 365;
const max_allowed_cert_validity_period_days =
serviceConfig.maximum_service_certificate_validity_days ??
default_validity_period_days;
if (
validityPeriodDays !== undefined &&
validityPeriodDays > max_allowed_cert_validity_period_days
) {
throw new Error(
`Validity period ${validityPeriodDays} (days) is not allowed: service max allowed is ${max_allowed_cert_validity_period_days} (days)`
);
}
const renewed_service_certificate = ccf.network.generateNetworkCertificate(
validFrom,
validityPeriodDays ?? max_allowed_cert_validity_period_days
);
const serviceInfoTable = "public:ccf.gov.service.info";
const rawServiceInfo = ccf.kv[serviceInfoTable].get(getSingletonKvKey());
if (rawServiceInfo === undefined) {
throw new Error("Service info could not be found");
}
const serviceInfo = ccf.bufToJsonCompatible(rawServiceInfo);
serviceInfo.cert = renewed_service_certificate;
ccf.kv[serviceInfoTable].set(
getSingletonKvKey(),
ccf.jsonCompatibleToBuf(serviceInfo)
);
}
function setNodeCertificateValidityPeriod(
nodeId,
nodeInfo,
validFrom,
validityPeriodDays
) {
if (nodeInfo.certificate_signing_request === undefined) {
throw new Error(`Node ${nodeId} has no certificate signing request`);
}
const rawConfig = ccf.kv["public:ccf.gov.service.config"].get(
getSingletonKvKey()
);
if (rawConfig === undefined) {
throw new Error("Service configuration could not be found");
}
const serviceConfig = ccf.bufToJsonCompatible(rawConfig);
const default_validity_period_days = 365;
const max_allowed_cert_validity_period_days =
serviceConfig.maximum_node_certificate_validity_days ??
default_validity_period_days;
if (
validityPeriodDays !== undefined &&
validityPeriodDays > max_allowed_cert_validity_period_days
) {
throw new Error(
`Validity period ${validityPeriodDays} (days) is not allowed: service max allowed is ${max_allowed_cert_validity_period_days} (days)`
);
}
const endorsed_node_cert = ccf.network.generateEndorsedCertificate(
nodeInfo.certificate_signing_request,
validFrom,
validityPeriodDays ?? max_allowed_cert_validity_period_days
);
ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set(
ccf.strToBuf(nodeId),
ccf.strToBuf(endorsed_node_cert)
);
}
function checkRecoveryThreshold(config, new_config) {
const from = config.recovery_threshold;
const to = new_config.recovery_threshold;
if (to === undefined || from === to) {
return;
}
const service_info = "public:ccf.gov.service.info";
const rawService = ccf.kv[service_info].get(getSingletonKvKey());
if (rawService === undefined) {
throw new Error("Service information could not be found");
}
const service = ccf.bufToJsonCompatible(rawService);
if (service.status === "WaitingForRecoveryShares") {
throw new Error(
`Cannot set recovery threshold if service is ${service.status}`
);
} else if (service.status === "Open") {
let activeRecoveryMembersCount = getActiveRecoveryMembersCount();
if (new_config.recovery_threshold > activeRecoveryMembersCount) {
throw new Error(
`Cannot set recovery threshold to ${new_config.recovery_threshold}: recovery threshold would be greater than the number of recovery members ${activeRecoveryMembersCount}`
);
}
}
}
function checkReconfigurationType(config, new_config) {
const from = config.reconfiguration_type;
const to = new_config.reconfiguration_type;
if (from !== to && to !== undefined) {
if (
!(
(from === undefined || from === "OneTransaction") &&
to === "TwoTransaction"
)
) {
throw new Error(
`Cannot change reconfiguration type from ${from} to ${to}.`
);
}
}
}
function updateServiceConfig(new_config) {
const service_config_table = "public:ccf.gov.service.config";
const rawConfig = ccf.kv[service_config_table].get(getSingletonKvKey());
if (rawConfig === undefined) {
throw new Error("Service configuration could not be found");
}
let config = ccf.bufToJsonCompatible(rawConfig);
// First run all checks
checkReconfigurationType(config, new_config);
checkRecoveryThreshold(config, new_config);
// Then all updates
if (new_config.reconfiguration_type !== undefined) {
config.reconfiguration_type = new_config.reconfiguration_type;
}
let need_recovery_threshold_refresh = false;
if (
new_config.recovery_threshold !== undefined &&
new_config.recovery_threshold !== config.recovery_threshold
) {
config.recovery_threshold = new_config.recovery_threshold;
need_recovery_threshold_refresh = true;
}
if (new_config.recent_cose_proposals_window_size !== undefined) {
config.recent_cose_proposals_window_size =
new_config.recent_cose_proposals_window_size;
}
ccf.kv[service_config_table].set(
getSingletonKvKey(),
ccf.jsonCompatibleToBuf(config)
);
if (need_recovery_threshold_refresh) {
ccf.node.triggerRecoverySharesRefresh();
}
}
const actions = new Map([
[
"set_constitution",
new Action(
function (args) {
checkType(args.constitution, "string");
},
function (args, proposalId) {
ccf.kv["public:ccf.gov.constitution"].set(
getSingletonKvKey(),
ccf.jsonCompatibleToBuf(args.constitution)
);
// Changing the constitution changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification
invalidateOtherOpenProposals(proposalId);
}
)
],
[
"set_member",
new Action(
function (args) {
checkX509CertBundle(args.cert, "cert");
checkType(args.member_data, "object?", "member_data");
const recovery_role = args.recovery_role;
if (recovery_role !== undefined) {
checkEnum(
recovery_role,
["NonParticipant", "Participant", "Owner"],
"recovery_role"
);
}
if (
args.encryption_pub_key == null &&
args.recovery_role !== null &&
args.recovery_role !== undefined &&
args.recovery_role !== "NonParticipant"
) {
throw new Error(
"Cannot specify a recovery_role value when encryption_pub_key is not specified"
);
}
},
function (args) {
const memberId = ccf.pemToId(args.cert);
const rawMemberId = ccf.strToBuf(memberId);
ccf.kv["public:ccf.gov.members.certs"].set(
rawMemberId,
ccf.strToBuf(args.cert)
);
if (args.encryption_pub_key == null) {
ccf.kv["public:ccf.gov.members.encryption_public_keys"].delete(
rawMemberId
);
} else {
ccf.kv["public:ccf.gov.members.encryption_public_keys"].set(
rawMemberId,
ccf.strToBuf(args.encryption_pub_key)
);
}
let member_info = {};
member_info.member_data = args.member_data;
member_info.recovery_role = args.recovery_role;
member_info.status = "Accepted";
ccf.kv["public:ccf.gov.members.info"].set(
rawMemberId,
ccf.jsonCompatibleToBuf(member_info)
);
const rawSignature = ccf.kv["public:ccf.internal.signatures"].get(
getSingletonKvKey()
);
if (rawSignature === undefined) {
ccf.kv["public:ccf.gov.members.acks"].set(rawMemberId);
} else {
const signature = ccf.bufToJsonCompatible(rawSignature);
const ack = {};
ack.state_digest = signature.root;
ccf.kv["public:ccf.gov.members.acks"].set(
rawMemberId,
ccf.jsonCompatibleToBuf(ack)
);
}
}
)
],
[
"remove_member",
new Action(
function (args) {
checkEntityId(args.member_id, "member_id");
},
function (args) {
const rawMemberId = ccf.strToBuf(args.member_id);
const rawMemberInfo =
ccf.kv["public:ccf.gov.members.info"].get(rawMemberId);
if (rawMemberInfo === undefined) {
return; // Idempotent
}
const memberInfo = ccf.bufToJsonCompatible(rawMemberInfo);
const isActiveMember = memberInfo.status == "Active";
const isRecoveryMember = ccf.kv[
"public:ccf.gov.members.encryption_public_keys"
].has(rawMemberId)
? true
: false;
// If the member is an active recovery member, check that there
// would still be a sufficient number of recovery members left
// to recover the service
if (isActiveMember && isRecoveryMember) {
const rawConfig = ccf.kv["public:ccf.gov.service.config"].get(
getSingletonKvKey()
);
if (rawConfig === undefined) {
throw new Error("Service configuration could not be found");
}
const config = ccf.bufToJsonCompatible(rawConfig);
const activeRecoveryMembersCountAfter =
getActiveRecoveryMembersCount() - 1;
if (activeRecoveryMembersCountAfter < config.recovery_threshold) {
throw new Error(
`Number of active recovery members (${activeRecoveryMembersCountAfter}) would be less than recovery threshold (${config.recovery_threshold})`
);
}
}
ccf.kv["public:ccf.gov.members.info"].delete(rawMemberId);
ccf.kv["public:ccf.gov.members.encryption_public_keys"].delete(
rawMemberId
);
ccf.kv["public:ccf.gov.members.certs"].delete(rawMemberId);
ccf.kv["public:ccf.gov.members.acks"].delete(rawMemberId);
ccf.kv["public:ccf.gov.history"].delete(rawMemberId);
if (isActiveMember && isRecoveryMember) {
// A retired recovery member should not have access to the private
// ledger going forward so rekey the ledger, issuing new shares to
// remaining active recovery members
ccf.node.triggerLedgerRekey();
}
}
)
],
[
"set_member_data",
new Action(
function (args) {
checkEntityId(args.member_id, "member_id");
checkType(args.member_data, "object", "member_data");
},
function (args) {
let member_id = ccf.strToBuf(args.member_id);
let members_info = ccf.kv["public:ccf.gov.members.info"];
let member_info = members_info.get(member_id);
if (member_info === undefined) {
throw new Error(`Member ${args.member_id} does not exist`);
}
let mi = ccf.bufToJsonCompatible(member_info);
mi.member_data = args.member_data;
members_info.set(member_id, ccf.jsonCompatibleToBuf(mi));
}
)
],
[
"set_user",
new Action(
function (args) {
checkX509CertBundle(args.cert, "cert");
checkType(args.user_data, "object?", "user_data");
},
function (args) {
let userId = ccf.pemToId(args.cert);
let rawUserId = ccf.strToBuf(userId);
ccf.kv["public:ccf.gov.users.certs"].set(
rawUserId,
ccf.strToBuf(args.cert)
);
if (args.user_data !== null && args.user_data !== undefined) {
let userInfo = {};
userInfo.user_data = args.user_data;
ccf.kv["public:ccf.gov.users.info"].set(
rawUserId,
ccf.jsonCompatibleToBuf(userInfo)
);
} else {
ccf.kv["public:ccf.gov.users.info"].delete(rawUserId);
}
}
)
],
[
"remove_user",
new Action(
function (args) {
checkEntityId(args.user_id, "user_id");
},
function (args) {
const user_id = ccf.strToBuf(args.user_id);
ccf.kv["public:ccf.gov.users.certs"].delete(user_id);
ccf.kv["public:ccf.gov.users.info"].delete(user_id);
}
)
],
[
"set_user_data",
new Action(
function (args) {
checkEntityId(args.user_id, "user_id");
checkType(args.user_data, "object?", "user_data");
},
function (args) {
const userId = ccf.strToBuf(args.user_id);
if (args.user_data !== null && args.user_data !== undefined) {
let userInfo = {};
userInfo.user_data = args.user_data;
ccf.kv["public:ccf.gov.users.info"].set(
userId,
ccf.jsonCompatibleToBuf(userInfo)
);
} else {
ccf.kv["public:ccf.gov.users.info"].delete(userId);
}
}
)
],
[
"set_recovery_threshold",
new Action(
function (args) {
checkType(args.recovery_threshold, "integer", "threshold");
checkBounds(args.recovery_threshold, 1, 254, "threshold");
},
function (args) {
updateServiceConfig(args);
}
)
],
[
"trigger_recovery_shares_refresh",
new Action(
function (args) {
checkNone(args);
},
function (args) {
ccf.node.triggerRecoverySharesRefresh();
}
)
],
[
"trigger_ledger_rekey",
new Action(
function (args) {
checkNone(args);
},
function (args) {
ccf.node.triggerLedgerRekey();
}
)
],
[
"transition_service_to_open",
new Action(
function (args) {
checkType(
args.next_service_identity,
"string",
"next service identity (PEM certificate)"
);
checkX509CertBundle(
args.next_service_identity,
"next_service_identity"
);
checkType(
args.previous_service_identity,
"string?",
"previous service identity (PEM certificate)"
);
if (args.previous_service_identity !== undefined) {
checkX509CertBundle(
args.previous_service_identity,
"previous_service_identity"
);
}
},
function (args) {
const service_info = "public:ccf.gov.service.info";
const rawService = ccf.kv[service_info].get(getSingletonKvKey());
if (rawService === undefined) {
throw new Error("Service information could not be found");
}
const service = ccf.bufToJsonCompatible(rawService);
if (
service.status === "Recovering" &&
(args.previous_service_identity === undefined ||
args.next_service_identity === undefined)
) {
throw new Error(
`Opening a recovering network requires both, the previous and the next service identity`
);
}
const previous_identity =
args.previous_service_identity !== undefined
? ccf.strToBuf(args.previous_service_identity)
: undefined;
const next_identity = ccf.strToBuf(args.next_service_identity);
ccf.node.transitionServiceToOpen(previous_identity, next_identity);
}
)
],
[
"set_js_app",
new Action(
function (args) {
const bundle = args.bundle;
checkType(bundle, "object", "bundle");
let prefix = "bundle.modules";
checkType(bundle.modules, "array", prefix);
for (const [i, module] of bundle.modules.entries()) {
checkType(module, "object", `${prefix}[${i}]`);
checkType(module.name, "string", `${prefix}[${i}].name`);
checkType(module.module, "string", `${prefix}[${i}].module`);
}
prefix = "bundle.metadata";
checkType(bundle.metadata, "object", prefix);
checkType(bundle.metadata.endpoints, "object", `${prefix}.endpoints`);
for (const [url, endpoint] of Object.entries(
bundle.metadata.endpoints
)) {
checkType(endpoint, "object", `${prefix}.endpoints["${url}"]`);
for (const [method, info] of Object.entries(endpoint)) {
const prefix2 = `${prefix}.endpoints["${url}"]["${method}"]`;
checkType(info, "object", prefix2);
checkType(info.js_module, "string", `${prefix2}.js_module`);
checkType(info.js_function, "string", `${prefix2}.js_function`);
checkEnum(
info.mode,
["readwrite", "readonly", "historical"],
`${prefix2}.mode`
);
checkEnum(
info.forwarding_required,
["sometimes", "always", "never"],
`${prefix2}.forwarding_required`
);
const redirection_strategy = info.redirection_strategy;
if (redirection_strategy !== undefined) {
checkEnum(
info.redirection_strategy,
["none", "to_primary", "to_backup"],
`${prefix2}.redirection_strategy`
);
}
checkType(info.openapi, "object?", `${prefix2}.openapi`);
checkType(
info.openapi_hidden,
"boolean?",
`${prefix2}.openapi_hidden`
);
checkType(
info.authn_policies,
"array",
`${prefix2}.authn_policies`
);
for (const [i, policy] of info.authn_policies.entries()) {
if (typeof policy === "string") {
// May still be an unrecognised value. That will only throw later
continue;
} else if (typeof policy === "object") {
const constituents = policy["all_of"];
checkType(
constituents,
"array",
`${prefix2}.authn_policies[${i}].all_of`
);
for (const [j, sub_policy] of constituents.entries()) {
checkType(
sub_policy,
"string",
`${prefix2}.authn_policies[${i}].all_of[${j}]`
);
}
} else {
throw new Error(
`${prefix2}.authn_policies[${i}] must be of type string or object but is ${typeof policy}`
);
}
}
if (!bundle.modules.some((m) => m.name === info.js_module)) {
throw new Error(`module '${info.js_module}' not found in bundle`);
}
}
}
checkType(
args.disable_bytecode_cache,
"boolean?",
"disable_bytecode_cache"
);
},
function (args) {
const modulesMap = ccf.kv["public:ccf.gov.modules"];
const modulesQuickJsBytecodeMap =
ccf.kv["public:ccf.gov.modules_quickjs_bytecode"];
const modulesQuickJsVersionVal =
ccf.kv["public:ccf.gov.modules_quickjs_version"];
const interpreterFlushVal = ccf.kv["public:ccf.gov.interpreter.flush"];
const endpointsMap = ccf.kv["public:ccf.gov.endpoints"];
modulesMap.clear();
endpointsMap.clear();
const bundle = args.bundle;
for (const module of bundle.modules) {
const path = "/" + module.name;
const pathBuf = ccf.strToBuf(path);
const moduleBuf = ccf.strToBuf(module.module);
modulesMap.set(pathBuf, moduleBuf);
}
if (args.disable_bytecode_cache) {
modulesQuickJsBytecodeMap.clear();
modulesQuickJsVersionVal.clear();
} else {
ccf.refreshAppBytecodeCache();
}
interpreterFlushVal.set(
getSingletonKvKey(),
ccf.jsonCompatibleToBuf(true)
);
for (const [url, endpoint] of Object.entries(
bundle.metadata.endpoints
)) {
for (const [method, info] of Object.entries(endpoint)) {
const key = `${method.toUpperCase()} ${url}`;
const keyBuf = ccf.strToBuf(key);
info.js_module = "/" + info.js_module;
const infoBuf = ccf.jsonCompatibleToBuf(info);
endpointsMap.set(keyBuf, infoBuf);
}
}
}
)
],
[
"remove_js_app",
new Action(
function (args) {},
function (args) {
const modulesMap = ccf.kv["public:ccf.gov.modules"];
const modulesQuickJsBytecodeMap =
ccf.kv["public:ccf.gov.modules_quickjs_bytecode"];
const interpreterFlushVal = ccf.kv["public:ccf.gov.interpreter.flush"];
const modulesQuickJsVersionVal =
ccf.kv["public:ccf.gov.modules_quickjs_version"];
const endpointsMap = ccf.kv["public:ccf.gov.endpoints"];
modulesMap.clear();
modulesQuickJsBytecodeMap.clear();
modulesQuickJsVersionVal.clear();
interpreterFlushVal.clear();
endpointsMap.clear();
}
)
],
[
"set_js_runtime_options",
new Action(
function (args) {
checkType(args.max_heap_bytes, "integer", "max_heap_bytes");
checkType(args.max_stack_bytes, "integer", "max_stack_bytes");
checkType(
args.max_execution_time_ms,
"integer",
"max_execution_time_ms"
);
checkType(
args.log_exception_details,
"boolean?",
"log_exception_details"
);
checkType(
args.return_exception_details,
"boolean?",
"return_exception_details"
);
checkType(
args.max_cached_interpreters,
"integer?",
"max_cached_interpreters"
);
},
function (args) {
const js_engine_map = ccf.kv["public:ccf.gov.js_runtime_options"];
js_engine_map.set(getSingletonKvKey(), ccf.jsonCompatibleToBuf(args));
}
)
],
[
"refresh_js_app_bytecode_cache",
new Action(
function (args) {},
function (args) {
ccf.refreshAppBytecodeCache();
}
)
],
[
"set_ca_cert_bundle",
new Action(
function (args) {
checkType(args.name, "string", "name");
checkX509CertBundle(args.cert_bundle, "cert_bundle");
},
function (args) {
const name = args.name;
const bundle = args.cert_bundle;
const nameBuf = ccf.strToBuf(name);
const bundleBuf = ccf.jsonCompatibleToBuf(bundle);
ccf.kv["public:ccf.gov.tls.ca_cert_bundles"].set(nameBuf, bundleBuf);
}
)
],
[
"remove_ca_cert_bundle",
new Action(
function (args) {
checkType(args.name, "string", "name");
},
function (args) {
const name = args.name;
const nameBuf = ccf.strToBuf(name);
ccf.kv["public:ccf.gov.tls.ca_cert_bundles"].delete(nameBuf);
}
)
],
[
"set_jwt_issuer",
new Action(
function (args) {
checkType(args.issuer, "string", "issuer");
checkType(args.auto_refresh, "boolean?", "auto_refresh");
checkType(args.ca_cert_bundle_name, "string?", "ca_cert_bundle_name");
checkType(args.jwks, "object?", "jwks");
if (args.jwks) {
checkJwks(args.jwks, "jwks");
}
if (args.auto_refresh) {
if (!args.ca_cert_bundle_name) {
throw new Error(
"ca_cert_bundle_name is missing but required if auto_refresh is true"
);
}
let url;
try {
url = parseUrl(args.issuer);
} catch (e) {
throw new Error("issuer must be a URL if auto_refresh is true");
}
if (url.scheme != "https") {
throw new Error(
"issuer must be a URL starting with https:// if auto_refresh is true"
);
}
if (url.query || url.fragment) {
throw new Error(
"issuer must be a URL without query/fragment if auto_refresh is true"
);
}
}
},
function (args) {
if (args.auto_refresh) {
const caCertBundleName = args.ca_cert_bundle_name;
const caCertBundleNameBuf = ccf.strToBuf(args.ca_cert_bundle_name);
if (
!ccf.kv["public:ccf.gov.tls.ca_cert_bundles"].has(
caCertBundleNameBuf
)
) {
throw new Error(
`No CA cert bundle found with name '${caCertBundleName}'`
);
}
}
const issuer = args.issuer;
const jwks = args.jwks;
delete args.jwks;
const metadata = args;
if (jwks) {
ccf.setJwtPublicSigningKeys(issuer, metadata, jwks);
}
const issuerBuf = ccf.strToBuf(issuer);
const metadataBuf = ccf.jsonCompatibleToBuf(metadata);
ccf.kv["public:ccf.gov.jwt.issuers"].set(issuerBuf, metadataBuf);
}
)
],
[
"set_jwt_public_signing_keys",
new Action(
function (args) {
checkType(args.issuer, "string", "issuer");
checkJwks(args.jwks, "jwks");
},
function (args) {
const issuer = args.issuer;
const issuerBuf = ccf.strToBuf(issuer);
const metadataBuf = ccf.kv["public:ccf.gov.jwt.issuers"].get(issuerBuf);
if (metadataBuf === undefined) {
throw new Error(`issuer ${issuer} not found`);
}
const metadata = ccf.bufToJsonCompatible(metadataBuf);
const jwks = args.jwks;
ccf.setJwtPublicSigningKeys(issuer, metadata, jwks);
}
)
],
[
"remove_jwt_issuer",
new Action(
function (args) {
checkType(args.issuer, "string", "issuer");
},
function (args) {
const issuerBuf = ccf.strToBuf(args.issuer);
if (!ccf.kv["public:ccf.gov.jwt.issuers"].has(issuerBuf)) {
return;
}
ccf.kv["public:ccf.gov.jwt.issuers"].delete(issuerBuf);
ccf.removeJwtPublicSigningKeys(args.issuer);
}
)
],
[
"add_node_code",
new Action(
function (args) {
checkType(args.code_id, "string", "code_id");
},
function (args, proposalId) {
const codeId = ccf.strToBuf(args.code_id);
const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin");
ccf.kv["public:ccf.gov.nodes.code_ids"].set(codeId, ALLOWED);
// Adding a new allowed code ID changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification
invalidateOtherOpenProposals(proposalId);
}
)
],
[
"add_snp_measurement",
new Action(
function (args) {
checkType(args.measurement, "string", "measurement");
},
function (args, proposalId) {
const measurement = ccf.strToBuf(args.measurement);
const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToJoin");
ccf.kv["public:ccf.gov.nodes.snp.measurements"].set(
measurement,
ALLOWED
);
// Adding a new allowed measurement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification
invalidateOtherOpenProposals(proposalId);
}
)
],
[
"add_snp_uvm_endorsement",
new Action(
function (args) {
checkType(args.did, "string", "did");
checkType(args.feed, "string", "feed");
checkType(args.svn, "string", "svn");
},
function (args, proposalId) {
let uvmEndorsementsForDID = ccf.kv[
"public:ccf.gov.nodes.snp.uvm_endorsements"
].get(ccf.strToBuf(args.did));
let uvme = {};
if (uvmEndorsementsForDID !== undefined) {
uvme = ccf.bufToJsonCompatible(uvmEndorsementsForDID);
}
uvme[args.feed] = { svn: args.svn };
ccf.kv["public:ccf.gov.nodes.snp.uvm_endorsements"].set(
ccf.strToBuf(args.did),
ccf.jsonCompatibleToBuf(uvme)
);
// Adding a new allowed UVM endorsement changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification
invalidateOtherOpenProposals(proposalId);
}
)
],
[
"add_executor_node_code",
new Action(
function (args) {
checkType(args.executor_code_id, "string", "executor_code_id");
},
function (args) {
const codeId = ccf.strToBuf(args.executor_code_id);
const ALLOWED = ccf.jsonCompatibleToBuf("AllowedToExecute");
ccf.kv["public:ccf.gov.nodes.executor_code_ids"].set(codeId, ALLOWED);
}
)
],
[
"add_snp_host_data",
new Action(
function (args) {
checkType(args.security_policy, "string", "security_policy");
checkType(args.host_data, "string", "host_data");
// If optional security policy is specified, make sure its
// SHA-256 digest is the specified host data
if (args.security_policy != "") {
const securityPolicyDigest = ccf.bufToStr(
ccf.crypto.digest("SHA-256", ccf.strToBuf(args.security_policy))
);
const hostData = ccf.bufToStr(hexStrToBuf(args.host_data));
if (securityPolicyDigest != hostData) {
throw new Error(
`The hash of raw policy ${securityPolicyDigest} does not match digest ${hostData}`
);
}
}
},
function (args, proposalId) {
ccf.kv["public:ccf.gov.nodes.snp.host_data"].set(
ccf.strToBuf(args.host_data),
ccf.jsonCompatibleToBuf(args.security_policy)
);
// Adding a new allowed host data changes the semantics of any other open proposals, so invalidate them to avoid confusion or malicious vote modification
invalidateOtherOpenProposals(proposalId);
}
)
],
[
"remove_snp_host_data",
new Action(
function (args) {
checkType(args.host_data, "string", "host_data");
},
function (args) {
const hostData = ccf.strToBuf(args.host_data);
ccf.kv["public:ccf.gov.nodes.snp.host_data"].delete(hostData);
}
)
],
[
"remove_snp_measurement",
new Action(
function (args) {
checkType(args.measurement, "string", "measurement");
},
function (args) {
const measurement = ccf.strToBuf(args.measurement);
ccf.kv["public:ccf.gov.nodes.snp.measurements"].delete(measurement);
}
)
],
[
"remove_snp_uvm_endorsement",
new Action(
function (args) {
checkType(args.did, "string", "did");
checkType(args.feed, "string", "feed");
},
function (args) {
let uvmEndorsementsForDID = ccf.kv[
"public:ccf.gov.nodes.snp.uvm_endorsements"
].get(ccf.strToBuf(args.did));
let uvme = {};
if (uvmEndorsementsForDID !== undefined) {
uvme = ccf.bufToJsonCompatible(uvmEndorsementsForDID);
}
delete uvme[args.feed];
if (Object.keys(uvme).length === 0) {
// Delete DID if no feed are left
ccf.kv["public:ccf.gov.nodes.snp.uvm_endorsements"].delete(
ccf.strToBuf(args.did)
);
} else {
ccf.kv["public:ccf.gov.nodes.snp.uvm_endorsements"].set(
ccf.strToBuf(args.did),
ccf.jsonCompatibleToBuf(uvme)
);
}
}
)
],
[
"set_node_data",
new Action(
function (args) {
checkEntityId(args.node_id, "node_id");
},
function (args) {
let node_id = ccf.strToBuf(args.node_id);
let nodes_info = ccf.kv["public:ccf.gov.nodes.info"];
let node_info = nodes_info.get(node_id);
if (node_info === undefined) {
throw new Error(`Node ${node_id} does not exist`);
}
let ni = ccf.bufToJsonCompatible(node_info);
ni.node_data = args.node_data;
nodes_info.set(node_id, ccf.jsonCompatibleToBuf(ni));
}
)
],
[
"transition_node_to_trusted",
new Action(
function (args) {
checkEntityId(args.node_id, "node_id");
checkType(args.valid_from, "string", "valid_from");
if (args.validity_period_days !== undefined) {
checkType(
args.validity_period_days,
"integer",
"validity_period_days"
);
checkBounds(
args.validity_period_days,
1,
null,
"validity_period_days"
);
}
},
function (args) {
const rawConfig = ccf.kv["public:ccf.gov.service.config"].get(
getSingletonKvKey()
);
if (rawConfig === undefined) {
throw new Error("Service configuration could not be found");
}
const serviceConfig = ccf.bufToJsonCompatible(rawConfig);
const node = ccf.kv["public:ccf.gov.nodes.info"].get(
ccf.strToBuf(args.node_id)
);
if (node === undefined) {
throw new Error(`No such node: ${args.node_id}`);
}
const nodeInfo = ccf.bufToJsonCompatible(node);
if (nodeInfo.status === "Pending") {
nodeInfo.status = "Trusted";
nodeInfo.ledger_secret_seqno =
ccf.network.getLatestLedgerSecretSeqno();
ccf.kv["public:ccf.gov.nodes.info"].set(
ccf.strToBuf(args.node_id),
ccf.jsonCompatibleToBuf(nodeInfo)
);
// Also generate and record service-endorsed node certificate from node CSR
if (nodeInfo.certificate_signing_request !== undefined) {
// Note: CSR and node certificate validity config are only present from 2.x
const default_validity_period_days = 365;
const max_allowed_cert_validity_period_days =
serviceConfig.maximum_node_certificate_validity_days ??
default_validity_period_days;
if (
args.validity_period_days !== undefined &&
args.validity_period_days > max_allowed_cert_validity_period_days
) {
throw new Error(
`Validity period ${args.validity_period_days} is not allowed: max allowed is ${max_allowed_cert_validity_period_days}`
);
}
const endorsed_node_cert = ccf.network.generateEndorsedCertificate(
nodeInfo.certificate_signing_request,
args.valid_from,
args.validity_period_days ?? max_allowed_cert_validity_period_days
);
ccf.kv["public:ccf.gov.nodes.endorsed_certificates"].set(
ccf.strToBuf(args.node_id),
ccf.strToBuf(endorsed_node_cert)
);
}
}
}
)
],
[
"remove_node_code",
new Action(
function (args) {
checkType(args.code_id, "string", "code_id");
},
function (args) {
const codeId = ccf.strToBuf(args.code_id);
ccf.kv["public:ccf.gov.nodes.code_ids"].delete(codeId);
}
)
],
[
"remove_executor_node_code",
new Action(
function (args) {
checkType(args.executor_code_id, "string", "executor_code_id");
},
function (args) {
const codeId = ccf.strToBuf(args.executor_code_id);
ccf.kv["public:ccf.gov.nodes.executor_code_ids"].delete(codeId);
}
)
],
[
"remove_node",
new Action(
function (args) {
checkEntityId(args.node_id, "node_id");
},
function (args) {
const rawConfig = ccf.kv["public:ccf.gov.service.config"].get(
getSingletonKvKey()
);
if (rawConfig === undefined) {
throw new Error("Service configuration could not be found");
}
const serviceConfig = ccf.bufToJsonCompatible(rawConfig);
const node = ccf.kv["public:ccf.gov.nodes.info"].get(
ccf.strToBuf(args.node_id)
);
if (node === undefined) {
return;
}
const node_obj = ccf.bufToJsonCompatible(node);
if (node_obj.status === "Pending") {
ccf.kv["public:ccf.gov.nodes.info"].delete(
ccf.strToBuf(args.node_id)
);
} else {
node_obj.status = "Retired";
ccf.kv["public:ccf.gov.nodes.info"].set(
ccf.strToBuf(args.node_id),
ccf.jsonCompatibleToBuf(node_obj)
);
}
}
)
],
[
"set_node_certificate_validity",
new Action(
function (args) {
checkEntityId(args.node_id, "node_id");
checkType(args.valid_from, "string", "valid_from");
if (args.validity_period_days !== undefined) {
checkType(
args.validity_period_days,
"integer",
"validity_period_days"
);
checkBounds(
args.validity_period_days,
1,
null,
"validity_period_days"
);
}
},
function (args) {
const node = ccf.kv["public:ccf.gov.nodes.info"].get(
ccf.strToBuf(args.node_id)
);
if (node === undefined) {
throw new Error(`No such node: ${args.node_id}`);
}
const nodeInfo = ccf.bufToJsonCompatible(node);
if (nodeInfo.status !== "Trusted") {
throw new Error(`Node ${args.node_id} is not trusted`);
}
setNodeCertificateValidityPeriod(
args.node_id,
nodeInfo,
args.valid_from,
args.validity_period_days
);
}
)
],
[
"set_all_nodes_certificate_validity",
new Action(
function (args) {
checkType(args.valid_from, "string", "valid_from");
if (args.validity_period_days !== undefined) {
checkType(
args.validity_period_days,
"integer",
"validity_period_days"
);
checkBounds(
args.validity_period_days,
1,
null,
"validity_period_days"
);
}
},
function (args) {
ccf.kv["public:ccf.gov.nodes.info"].forEach((v, k) => {
const nodeId = ccf.bufToStr(k);
const nodeInfo = ccf.bufToJsonCompatible(v);
if (nodeInfo.status === "Trusted") {
setNodeCertificateValidityPeriod(
nodeId,
nodeInfo,
args.valid_from,
args.validity_period_days
);
}
});
}
)
],
[
"set_service_certificate_validity",
new Action(
function (args) {
checkType(args.valid_from, "string", "valid_from");
if (args.validity_period_days !== undefined) {
checkType(
args.validity_period_days,
"integer",
"validity_period_days"
);
checkBounds(
args.validity_period_days,
1,
null,
"validity_period_days"
);
}
},
function (args) {
setServiceCertificateValidityPeriod(
args.valid_from,
args.validity_period_days
);
}
)
],
[
"set_service_configuration",
new Action(
function (args) {
for (var key in args) {
if (
![
"reconfiguration_type",
"recovery_threshold",
"recent_cose_proposals_window_size"
].includes(key)
) {
throw new Error(
`Cannot change ${key} via set_service_configuration.`
);
}
}
checkType(args.reconfiguration_type, "string?", "reconfiguration type");
checkType(args.recovery_threshold, "integer?", "recovery threshold");
checkBounds(args.recovery_threshold, 1, 254, "recovery threshold");
checkType(
args.recent_cose_proposals_window_size,
"integer?",
"recent cose proposals window size"
);
checkBounds(
args.recent_cose_proposals_window_size,
1,
10000,
"recent cose proposals window size"
);
},
function (args) {
updateServiceConfig(args);
}
)
],
[
"trigger_ledger_chunk",
new Action(
function (args) {},
function (args, proposalId) {
ccf.node.triggerLedgerChunk();
}
)
],
[
"trigger_snapshot",
new Action(
function (args) {},
function (args, proposalId) {
ccf.node.triggerSnapshot();
}
)
],
[
"trigger_acme_refresh",
new Action(
function (args) {
checkType(
args.interfaces,
"array?",
"interfaces to refresh the certificates for"
);
},
function (args, proposalId) {
ccf.node.triggerACMERefresh(args.interfaces);
}
)
],
[
"assert_service_identity",
new Action(
function (args) {
checkX509CertBundle(args.service_identity, "service_identity");
const service_info = "public:ccf.gov.service.info";
const rawService = ccf.kv[service_info].get(getSingletonKvKey());
if (rawService === undefined) {
throw new Error("Service information could not be found");
}
const service = ccf.bufToJsonCompatible(rawService);
if (service.cert !== args.service_identity) {
throw new Error("Service identity certificate mismatch");
}
},
function (args) {}
)
]
]);