Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dist/cjs/SPMessage.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type SPKey = {
ringLevel: number;
permissions: '*' | string[];
allowedActions?: '*' | string[];
permissionsContext?: '*' | string[];
meta?: {
quantity?: number;
expires?: number;
Expand Down Expand Up @@ -83,6 +84,7 @@ export type SPKeyUpdate = {
purpose?: string[];
permissions?: string[];
allowedActions?: '*' | string[];
permissionsContext?: '*' | string[];
meta?: {
quantity?: number;
expires?: number;
Expand Down
9 changes: 5 additions & 4 deletions dist/cjs/internals.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const getMsgMeta = function (message, contractID, state, index) {
};
return result;
};
const keysToMap = function (keys_, height, authorizedKeys) {
const keysToMap = function (keys_, height, signingKeyId, authorizedKeys) {
// Using cloneDeep to ensure that the returned object is serializable
// Keys in a SPMessage may not be serializable (i.e., supported by the
// structured clone algorithm) when they contain encryptedIncomingData
Expand All @@ -97,6 +97,7 @@ const keysToMap = function (keys_, height, authorizedKeys) {
const keysCopy = (0, turtledash_1.cloneDeep)(keys);
return Object.fromEntries(keysCopy.map((key) => {
key._notBeforeHeight = height;
key._addedByKeyId = signingKeyId;
if (authorizedKeys?.[key.id]) {
if (authorizedKeys[key.id]._notAfterHeight == null) {
throw new errors_js_1.ChelErrorKeyAlreadyExists(`Cannot set existing unrevoked key: ${key.id}`);
Expand Down Expand Up @@ -826,7 +827,7 @@ exports.default = (0, sbp_1.default)('sbp/selectors/register', {
},
[SPMessage_js_1.SPMessage.OP_CONTRACT](v) {
state._vm.type = v.type;
const keys = keysToMap.call(self, v.keys, height);
const keys = keysToMap.call(self, v.keys, height, signingKeyId);
state._vm.authorizedKeys = keys;
// Loop through the keys in the contract and try to decrypt all of the private keys
// Example: in the identity contract you have the IEK, IPK, CSK, and CEK.
Expand Down Expand Up @@ -1189,7 +1190,7 @@ exports.default = (0, sbp_1.default)('sbp/selectors/register', {
state._vm.props[v.key] = v.value;
},
[SPMessage_js_1.SPMessage.OP_KEY_ADD](v) {
const keys = keysToMap.call(self, v, height, state._vm.authorizedKeys);
const keys = keysToMap.call(self, v, height, signingKeyId, state._vm.authorizedKeys);
const keysArray = Object.values(v);
keysArray.forEach((k) => {
if ((0, turtledash_1.has)(state._vm.authorizedKeys, k.id) &&
Expand Down Expand Up @@ -1374,7 +1375,7 @@ exports.default = (0, sbp_1.default)('sbp/selectors/register', {
const stateForValidation = opT === SPMessage_js_1.SPMessage.OP_CONTRACT && !state?._vm?.authorizedKeys
? {
_vm: {
authorizedKeys: keysToMap.call(this, opV.keys, height)
authorizedKeys: keysToMap.call(this, opV.keys, height, signingKeyId)
}
}
: state;
Expand Down
2 changes: 2 additions & 0 deletions dist/cjs/types.d.cts
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,10 @@ export type ChelContractKey = {
ringLevel: number;
permissions: '*' | string[];
allowedActions?: '*' | string[];
permissionsContext?: '*' | string[];
_notBeforeHeight: number;
_notAfterHeight?: number | undefined;
_addedByKeyId: string;
_private?: string;
foreignKey?: string;
meta?: {
Expand Down
21 changes: 19 additions & 2 deletions dist/cjs/utils.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ const validateKeyPermissions = (msg, config, state, signingKeyId, opT, opV) => {
!Array.isArray(signingKey.purpose) ||
!signingKey.purpose.includes('sig') ||
(signingKey.permissions !== '*' &&
(!Array.isArray(signingKey.permissions) || !signingKey.permissions.includes(opT)))) {
(!Array.isArray(signingKey.permissions) || (!signingKey.permissions.includes(opT) &&
!(opT === SPMessage_js_1.SPMessage.OP_KEY_DEL &&
signingKey.permissions.includes(`${opT}#self`)))))) {
(0, exports.logEvtError)(msg, `Signing key ${signingKeyId} is missing permissions for operation ${opT}`);
return false;
}
Expand Down Expand Up @@ -167,7 +169,13 @@ const validateKeyAddPermissions = function (contractID, signingKey, state, v, sk
}
if (signingKeyPermissions !== '*') {
if (!Array.isArray(k.permissions) ||
!k.permissions.reduce((acc, cv) => acc && signingKeyPermissions.has(cv), true)) {
k.permissions.some((cv) => {
if (cv === `${SPMessage_js_1.SPMessage.OP_KEY_DEL}#self`) {
// Granting `kd#self` requires `kd`
cv = SPMessage_js_1.SPMessage.OP_KEY_DEL;
}
return !signingKeyPermissions.has(cv);
})) {
throw new Error('Unable to add or update a key with more permissions than the signing key. signingKey permissions: ' +
String(signingKey?.permissions) +
'; key add permissions: ' +
Expand Down Expand Up @@ -195,6 +203,7 @@ const validateKeyDelPermissions = function (contractID, signingKey, state, v) {
signingKey.id);
}
const localSigningKey = state._vm.authorizedKeys[signingKey.id];
const selfDeleteOnly = localSigningKey.permissions !== '*' && !localSigningKey.permissions.includes(SPMessage_js_1.SPMessage.OP_KEY_DEL);
v.forEach((wid) => {
const data = this.config.unwrapMaybeEncryptedData(wid);
if (!data)
Expand All @@ -216,6 +225,14 @@ const validateKeyDelPermissions = function (contractID, signingKey, state, v) {
' but attempted to remove a key with ringLevel ' +
k.ringLevel);
}
if (selfDeleteOnly) {
if (!k._addedByKeyId || !state._vm.authorizedKeys[k._addedByKeyId]) {
throw new Error('Missing or invalid _addedByKeyId');
}
if (state._vm.authorizedKeys[k._addedByKeyId].name !== localSigningKey.name) {
throw new Error('Key was added by a different key');
}
}
});
};
exports.validateKeyDelPermissions = validateKeyDelPermissions;
Expand Down
2 changes: 2 additions & 0 deletions dist/esm/SPMessage.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type SPKey = {
ringLevel: number;
permissions: '*' | string[];
allowedActions?: '*' | string[];
permissionsContext?: '*' | string[];
meta?: {
quantity?: number;
expires?: number;
Expand Down Expand Up @@ -83,6 +84,7 @@ export type SPKeyUpdate = {
purpose?: string[];
permissions?: string[];
allowedActions?: '*' | string[];
permissionsContext?: '*' | string[];
meta?: {
quantity?: number;
expires?: number;
Expand Down
9 changes: 5 additions & 4 deletions dist/esm/internals.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const getMsgMeta = function (message, contractID, state, index) {
};
return result;
};
const keysToMap = function (keys_, height, authorizedKeys) {
const keysToMap = function (keys_, height, signingKeyId, authorizedKeys) {
// Using cloneDeep to ensure that the returned object is serializable
// Keys in a SPMessage may not be serializable (i.e., supported by the
// structured clone algorithm) when they contain encryptedIncomingData
Expand All @@ -62,6 +62,7 @@ const keysToMap = function (keys_, height, authorizedKeys) {
const keysCopy = cloneDeep(keys);
return Object.fromEntries(keysCopy.map((key) => {
key._notBeforeHeight = height;
key._addedByKeyId = signingKeyId;
if (authorizedKeys?.[key.id]) {
if (authorizedKeys[key.id]._notAfterHeight == null) {
throw new ChelErrorKeyAlreadyExists(`Cannot set existing unrevoked key: ${key.id}`);
Expand Down Expand Up @@ -791,7 +792,7 @@ export default sbp('sbp/selectors/register', {
},
[SPMessage.OP_CONTRACT](v) {
state._vm.type = v.type;
const keys = keysToMap.call(self, v.keys, height);
const keys = keysToMap.call(self, v.keys, height, signingKeyId);
state._vm.authorizedKeys = keys;
// Loop through the keys in the contract and try to decrypt all of the private keys
// Example: in the identity contract you have the IEK, IPK, CSK, and CEK.
Expand Down Expand Up @@ -1154,7 +1155,7 @@ export default sbp('sbp/selectors/register', {
state._vm.props[v.key] = v.value;
},
[SPMessage.OP_KEY_ADD](v) {
const keys = keysToMap.call(self, v, height, state._vm.authorizedKeys);
const keys = keysToMap.call(self, v, height, signingKeyId, state._vm.authorizedKeys);
const keysArray = Object.values(v);
keysArray.forEach((k) => {
if (has(state._vm.authorizedKeys, k.id) &&
Expand Down Expand Up @@ -1339,7 +1340,7 @@ export default sbp('sbp/selectors/register', {
const stateForValidation = opT === SPMessage.OP_CONTRACT && !state?._vm?.authorizedKeys
? {
_vm: {
authorizedKeys: keysToMap.call(this, opV.keys, height)
authorizedKeys: keysToMap.call(this, opV.keys, height, signingKeyId)
}
}
: state;
Expand Down
2 changes: 2 additions & 0 deletions dist/esm/types.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,10 @@ export type ChelContractKey = {
ringLevel: number;
permissions: '*' | string[];
allowedActions?: '*' | string[];
permissionsContext?: '*' | string[];
_notBeforeHeight: number;
_notAfterHeight?: number | undefined;
_addedByKeyId: string;
_private?: string;
foreignKey?: string;
meta?: {
Expand Down
21 changes: 19 additions & 2 deletions dist/esm/utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ export const validateKeyPermissions = (msg, config, state, signingKeyId, opT, op
!Array.isArray(signingKey.purpose) ||
!signingKey.purpose.includes('sig') ||
(signingKey.permissions !== '*' &&
(!Array.isArray(signingKey.permissions) || !signingKey.permissions.includes(opT)))) {
(!Array.isArray(signingKey.permissions) || (!signingKey.permissions.includes(opT) &&
!(opT === SPMessage.OP_KEY_DEL &&
signingKey.permissions.includes(`${opT}#self`)))))) {
logEvtError(msg, `Signing key ${signingKeyId} is missing permissions for operation ${opT}`);
return false;
}
Expand Down Expand Up @@ -151,7 +153,13 @@ export const validateKeyAddPermissions = function (contractID, signingKey, state
}
if (signingKeyPermissions !== '*') {
if (!Array.isArray(k.permissions) ||
!k.permissions.reduce((acc, cv) => acc && signingKeyPermissions.has(cv), true)) {
k.permissions.some((cv) => {
if (cv === `${SPMessage.OP_KEY_DEL}#self`) {
// Granting `kd#self` requires `kd`
cv = SPMessage.OP_KEY_DEL;
}
return !signingKeyPermissions.has(cv);
})) {
throw new Error('Unable to add or update a key with more permissions than the signing key. signingKey permissions: ' +
String(signingKey?.permissions) +
'; key add permissions: ' +
Expand All @@ -178,6 +186,7 @@ export const validateKeyDelPermissions = function (contractID, signingKey, state
signingKey.id);
}
const localSigningKey = state._vm.authorizedKeys[signingKey.id];
const selfDeleteOnly = localSigningKey.permissions !== '*' && !localSigningKey.permissions.includes(SPMessage.OP_KEY_DEL);
v.forEach((wid) => {
const data = this.config.unwrapMaybeEncryptedData(wid);
if (!data)
Expand All @@ -199,6 +208,14 @@ export const validateKeyDelPermissions = function (contractID, signingKey, state
' but attempted to remove a key with ringLevel ' +
k.ringLevel);
}
if (selfDeleteOnly) {
if (!k._addedByKeyId || !state._vm.authorizedKeys[k._addedByKeyId]) {
throw new Error('Missing or invalid _addedByKeyId');
}
if (state._vm.authorizedKeys[k._addedByKeyId].name !== localSigningKey.name) {
throw new Error('Key was added by a different key');
}
}
});
};
export const validateKeyUpdatePermissions = function (contractID, signingKey, state, v) {
Expand Down
2 changes: 2 additions & 0 deletions src/SPMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type SPKey = {
ringLevel: number;
permissions: '*' | string[];
allowedActions?: '*' | string[];
permissionsContext?: '*' | string[];
meta?: {
quantity?: number;
expires?: number;
Expand Down Expand Up @@ -107,6 +108,7 @@ export type SPKeyUpdate = {
purpose?: string[];
permissions?: string[];
allowedActions?: '*' | string[];
permissionsContext?: '*' | string[];
meta?: {
quantity?: number;
expires?: number;
Expand Down
10 changes: 7 additions & 3 deletions src/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const keysToMap = function (
this: CheloniaContext,
keys_: (SPKey | EncryptedData<SPKey>)[],
height: number,
signingKeyId: string,
authorizedKeys?: ChelContractState['_vm']['authorizedKeys']
): ChelContractState['_vm']['authorizedKeys'] {
// Using cloneDeep to ensure that the returned object is serializable
Expand All @@ -148,6 +149,7 @@ const keysToMap = function (
return Object.fromEntries(
keysCopy.map((key) => {
key._notBeforeHeight = height
key._addedByKeyId = signingKeyId
if (authorizedKeys?.[key.id]) {
if (authorizedKeys[key.id]._notAfterHeight == null) {
throw new ChelErrorKeyAlreadyExists(`Cannot set existing unrevoked key: ${key.id}`)
Expand Down Expand Up @@ -1089,7 +1091,7 @@ export default sbp('sbp/selectors/register', {
},
[SPMessage.OP_CONTRACT] (v: SPOpContract) {
state._vm.type = v.type
const keys = keysToMap.call(self, v.keys, height)
const keys = keysToMap.call(self, v.keys, height, signingKeyId)
state._vm.authorizedKeys = keys
// Loop through the keys in the contract and try to decrypt all of the private keys
// Example: in the identity contract you have the IEK, IPK, CSK, and CEK.
Expand Down Expand Up @@ -1533,7 +1535,7 @@ export default sbp('sbp/selectors/register', {
state._vm.props[v.key] = v.value
},
[SPMessage.OP_KEY_ADD] (v: SPOpKeyAdd) {
const keys = keysToMap.call(self, v, height, state._vm.authorizedKeys)
const keys = keysToMap.call(self, v, height, signingKeyId, state._vm.authorizedKeys)
const keysArray = Object.values(v) as SPKey[]
keysArray.forEach((k) => {
if (
Expand Down Expand Up @@ -1790,7 +1792,9 @@ export default sbp('sbp/selectors/register', {
opT === SPMessage.OP_CONTRACT && !state?._vm?.authorizedKeys
? {
_vm: {
authorizedKeys: keysToMap.call(this, (opV as SPOpContract).keys, height)
authorizedKeys: keysToMap.call(this,
(opV as SPOpContract).keys, height, signingKeyId
)
}
}
: state
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,10 @@ export type ChelContractKey = {
ringLevel: number;
permissions: '*' | string[];
allowedActions?: '*' | string[];
permissionsContext?: '*' | string[];
_notBeforeHeight: number;
_notAfterHeight?: number | undefined;
_addedByKeyId: string;
_private?: string;
foreignKey?: string;
meta?: {
Expand Down
25 changes: 23 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,13 @@ export const validateKeyPermissions = (
!Array.isArray(signingKey.purpose) ||
!signingKey.purpose.includes('sig') ||
(signingKey.permissions !== '*' &&
(!Array.isArray(signingKey.permissions) || !signingKey.permissions.includes(opT)))
(!Array.isArray(signingKey.permissions) || (
!signingKey.permissions.includes(opT) &&
!(
opT === SPMessage.OP_KEY_DEL &&
signingKey.permissions.includes(`${opT}#self`)
)
)))
) {
logEvtError(msg, `Signing key ${signingKeyId} is missing permissions for operation ${opT}`)
return false
Expand Down Expand Up @@ -281,7 +287,13 @@ export const validateKeyAddPermissions = function (
if (signingKeyPermissions !== '*') {
if (
!Array.isArray(k.permissions) ||
!k.permissions.reduce((acc, cv) => acc && signingKeyPermissions.has(cv), true)
k.permissions.some((cv) => {
if (cv === `${SPMessage.OP_KEY_DEL}#self`) {
// Granting `kd#self` requires `kd`
cv = SPMessage.OP_KEY_DEL
}
return !signingKeyPermissions.has(cv)
})
) {
throw new Error(
'Unable to add or update a key with more permissions than the signing key. signingKey permissions: ' +
Expand Down Expand Up @@ -324,6 +336,7 @@ export const validateKeyDelPermissions = function (
)
}
const localSigningKey = state._vm.authorizedKeys[signingKey.id]
const selfDeleteOnly = localSigningKey.permissions !== '*' && !localSigningKey.permissions.includes(SPMessage.OP_KEY_DEL)
v.forEach((wid) => {
const data = this.config.unwrapMaybeEncryptedData<string>(wid)
if (!data) return
Expand All @@ -346,6 +359,14 @@ export const validateKeyDelPermissions = function (
k.ringLevel
)
}
if (selfDeleteOnly) {
if (!k._addedByKeyId || !state._vm.authorizedKeys[k._addedByKeyId]) {
throw new Error('Missing or invalid _addedByKeyId')
}
if (state._vm.authorizedKeys[k._addedByKeyId].name !== localSigningKey.name) {
throw new Error('Key was added by a different key')
}
}
})
}

Expand Down