@dfinity/identity
Version:
JavaScript and TypeScript library to manage identity with the Internet Computer
283 lines • 11.1 kB
JavaScript
import { requestIdOf, SignIdentity, IC_REQUEST_DOMAIN_SEPARATOR, IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR, } from '@dfinity/agent';
import { Principal } from '@dfinity/principal';
import { PartialIdentity } from "./partial.js";
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
function _parseBlob(value) {
if (typeof value !== 'string' || value.length < 64) {
throw new Error('Invalid public key.');
}
return hexToBytes(value);
}
/**
* A single delegation object that is signed by a private key. This is constructed by
* `DelegationChain.create()`.
*
* {@see DelegationChain}
*/
export class Delegation {
constructor(pubkey, expiration, targets) {
this.pubkey = pubkey;
this.expiration = expiration;
this.targets = targets;
}
toCborValue() {
return {
pubkey: this.pubkey,
expiration: this.expiration,
...(this.targets && {
targets: this.targets,
}),
};
}
toJSON() {
// every string should be hex and once-de-hexed,
// discoverable what it is (e.g. de-hex to get JSON with a 'type' property, or de-hex to DER
// with an OID). After de-hex, if it's not obvious what it is, it's an ArrayBuffer.
return {
expiration: this.expiration.toString(16),
pubkey: bytesToHex(this.pubkey),
...(this.targets && { targets: this.targets.map(p => p.toHex()) }),
};
}
}
/**
* Sign a single delegation object for a period of time.
* @param from The identity that lends its delegation.
* @param to The identity that receives the delegation.
* @param expiration An expiration date for this delegation.
* @param targets Limit this delegation to the target principals.
*/
async function _createSingleDelegation(from, to, expiration, targets) {
const delegation = new Delegation(to.toDer(), BigInt(+expiration) * BigInt(1000000), // In nanoseconds.
targets);
// The signature is calculated by signing the concatenation of the domain separator
// and the message.
// Note: To ensure Safari treats this as a user gesture, ensure to not use async methods
// besides the actualy webauthn functionality (such as `sign`). Safari will de-register
// a user gesture if you await an async call thats not fetch, xhr, or setTimeout.
const challenge = new Uint8Array([
...IC_REQUEST_AUTH_DELEGATION_DOMAIN_SEPARATOR,
...new Uint8Array(requestIdOf({ ...delegation })),
]);
const signature = await from.sign(challenge);
return {
delegation,
signature,
};
}
/**
* A chain of delegations. This is JSON Serializable.
* This is the object to serialize and pass to a DelegationIdentity. It does not keep any
* private keys.
*/
export class DelegationChain {
/**
* Create a delegation chain between two (or more) keys. By default, the expiration time
* will be very short (15 minutes).
*
* To build a chain of more than 2 identities, this function needs to be called multiple times,
* passing the previous delegation chain into the options argument. For example:
* @example
* const rootKey = createKey();
* const middleKey = createKey();
* const bottomeKey = createKey();
*
* const rootToMiddle = await DelegationChain.create(
* root, middle.getPublicKey(), Date.parse('2100-01-01'),
* );
* const middleToBottom = await DelegationChain.create(
* middle, bottom.getPublicKey(), Date.parse('2100-01-01'), { previous: rootToMiddle },
* );
*
* // We can now use a delegation identity that uses the delegation above:
* const identity = DelegationIdentity.fromDelegation(bottomKey, middleToBottom);
* @param from The identity that will delegate.
* @param to The identity that gets delegated. It can now sign messages as if it was the
* identity above.
* @param expiration The length the delegation is valid. By default, 15 minutes from calling
* this function.
* @param options A set of options for this delegation. expiration and previous
* @param options.previous - Another DelegationChain that this chain should start with.
* @param options.targets - targets that scope the delegation (e.g. Canister Principals)
*/
static async create(from, to, expiration = new Date(Date.now() + 15 * 60 * 1000), options = {}) {
const delegation = await _createSingleDelegation(from, to, expiration, options.targets);
return new DelegationChain([...(options.previous?.delegations || []), delegation], options.previous?.publicKey || from.getPublicKey().toDer());
}
/**
* Creates a DelegationChain object from a JSON string.
* @param json The JSON string to parse.
*/
static fromJSON(json) {
const { publicKey, delegations } = typeof json === 'string' ? JSON.parse(json) : json;
if (!Array.isArray(delegations)) {
throw new Error('Invalid delegations.');
}
const parsedDelegations = delegations.map(signedDelegation => {
const { delegation, signature } = signedDelegation;
const { pubkey, expiration, targets } = delegation;
if (targets !== undefined && !Array.isArray(targets)) {
throw new Error('Invalid targets.');
}
return {
delegation: new Delegation(_parseBlob(pubkey), BigInt('0x' + expiration), // expiration in JSON is an hexa string (See toJSON() below).
targets &&
targets.map((t) => {
if (typeof t !== 'string') {
throw new Error('Invalid target.');
}
return Principal.fromHex(t);
})),
signature: _parseBlob(signature),
};
});
return new this(parsedDelegations, _parseBlob(publicKey));
}
/**
* Creates a DelegationChain object from a list of delegations and a DER-encoded public key.
* @param delegations The list of delegations.
* @param publicKey The DER-encoded public key of the key-pair signing the first delegation.
*/
static fromDelegations(delegations, publicKey) {
return new this(delegations, publicKey);
}
constructor(delegations, publicKey) {
this.delegations = delegations;
this.publicKey = publicKey;
}
toJSON() {
return {
delegations: this.delegations.map(signedDelegation => {
const { delegation, signature } = signedDelegation;
const { targets } = delegation;
return {
delegation: {
expiration: delegation.expiration.toString(16),
pubkey: bytesToHex(delegation.pubkey),
...(targets && {
targets: targets.map(t => t.toHex()),
}),
},
signature: bytesToHex(signature),
};
}),
publicKey: bytesToHex(this.publicKey),
};
}
}
/**
* An Identity that adds delegation to a request. Everywhere in this class, the name
* innerKey refers to the SignIdentity that is being used to sign the requests, while
* originalKey is the identity that is being borrowed. More identities can be used
* in the middle to delegate.
*/
export class DelegationIdentity extends SignIdentity {
/**
* Create a delegation without having access to delegateKey.
* @param key The key used to sign the requests.
* @param delegation A delegation object created using `createDelegation`.
*/
static fromDelegation(key, delegation) {
return new this(key, delegation);
}
constructor(_inner, _delegation) {
super();
this._inner = _inner;
this._delegation = _delegation;
}
getDelegation() {
return this._delegation;
}
getPublicKey() {
return {
derKey: this._delegation.publicKey,
toDer: () => this._delegation.publicKey,
};
}
sign(blob) {
return this._inner.sign(blob);
}
async transformRequest(request) {
const { body, ...fields } = request;
const requestId = await requestIdOf(body);
return {
...fields,
body: {
content: body,
sender_sig: await this.sign(new Uint8Array([...IC_REQUEST_DOMAIN_SEPARATOR, ...new Uint8Array(requestId)])),
sender_delegation: this._delegation.delegations,
sender_pubkey: this._delegation.publicKey,
},
};
}
}
/**
* A partial delegated identity, representing a delegation chain and the public key that it targets
*/
export class PartialDelegationIdentity extends PartialIdentity {
#delegation;
/**
* The Delegation Chain of this identity.
*/
get delegation() {
return this.#delegation;
}
constructor(inner, delegation) {
super(inner);
this.#delegation = delegation;
}
/**
* Create a {@link PartialDelegationIdentity} from a {@link PublicKey} and a {@link DelegationChain}.
* @param key The {@link PublicKey} to delegate to.
* @param delegation a {@link DelegationChain} targeting the inner key.
*/
static fromDelegation(key, delegation) {
return new PartialDelegationIdentity(key, delegation);
}
}
/**
* Analyze a DelegationChain and validate that it's valid, ie. not expired and apply to the
* scope.
* @param chain The chain to validate.
* @param checks Various checks to validate on the chain.
*/
export function isDelegationValid(chain, checks) {
// Verify that the no delegation is expired. If any are in the chain, returns false.
for (const { delegation } of chain.delegations) {
// prettier-ignore
if (+new Date(Number(delegation.expiration / BigInt(1000000))) <= +Date.now()) {
return false;
}
}
// Check the scopes.
const scopes = [];
const maybeScope = checks?.scope;
if (maybeScope) {
if (Array.isArray(maybeScope)) {
scopes.push(...maybeScope.map(s => (typeof s === 'string' ? Principal.fromText(s) : s)));
}
else {
scopes.push(typeof maybeScope === 'string' ? Principal.fromText(maybeScope) : maybeScope);
}
}
for (const s of scopes) {
const scope = s.toText();
for (const { delegation } of chain.delegations) {
if (delegation.targets === undefined) {
continue;
}
let none = true;
for (const target of delegation.targets) {
if (target.toText() === scope) {
none = false;
break;
}
}
if (none) {
return false;
}
}
}
return true;
}
//# sourceMappingURL=delegation.js.map