Skip to content

Commit

Permalink
feat: introduces partial identity (#812)
Browse files Browse the repository at this point in the history
* feat: introduces partial identities from public keys for authentication flows

* changelog

* partial identity tests

* partial delegation tests
  • Loading branch information
krpeacock committed Dec 18, 2023
1 parent 0104098 commit ca161c8
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 11 deletions.
1 change: 1 addition & 0 deletions docs/generated/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ <h1>Agent-JS Changelog</h1>
<section>
<h2>Version x.x.x</h2>
<ul>
<li>feat: introduces partial identities from public keys for authentication flows</li>
<li>fix: honor disableIdle flag</li>
</ul>
<h2>Version 0.20.2</h2>
Expand Down
32 changes: 22 additions & 10 deletions packages/auth-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
DelegationIdentity,
Ed25519KeyIdentity,
ECDSAKeyIdentity,
PartialDelegationIdentity,
} from '@dfinity/identity';
import { Principal } from '@dfinity/principal';
import { IdleManager, IdleManagerOptions } from './idleManager';
Expand All @@ -25,6 +26,7 @@ import {
KEY_VECTOR,
LocalStorage,
} from './storage';
import { PartialIdentity } from '@dfinity/identity/lib/cjs/identity/partial';

export { IdbStorage, LocalStorage, KEY_STORAGE_DELEGATION, KEY_STORAGE_KEY } from './storage';
export { IdbKeyVal, DBCreateOptions } from './db';
Expand All @@ -47,7 +49,7 @@ export interface AuthClientCreateOptions {
/**
* An identity to use as the base
*/
identity?: SignIdentity | ECDSAKeyIdentity;
identity?: SignIdentity | PartialIdentity;
/**
* Optional storage with get, set, and remove. Uses {@link IdbStorage} by default
*/
Expand Down Expand Up @@ -187,10 +189,9 @@ export class AuthClient {
public static async create(
options: {
/**
* An {@link Identity} to use as the base.
* By default, a new {@link AnonymousIdentity}
* An {@link SignIdentity} or {@link PartialIdentity} to authenticate via delegation.
*/
identity?: SignIdentity;
identity?: SignIdentity | PartialIdentity;
/**
* {@link AuthClientStorage}
* @description Optional storage with get, set, and remove. Uses {@link IdbStorage} by default
Expand All @@ -213,7 +214,7 @@ export class AuthClient {
const storage = options.storage ?? new IdbStorage();
const keyType = options.keyType ?? ECDSA_KEY_LABEL;

let key: null | SignIdentity | ECDSAKeyIdentity = null;
let key: null | SignIdentity | PartialIdentity = null;
if (options.identity) {
key = options.identity;
} else {
Expand Down Expand Up @@ -258,7 +259,7 @@ export class AuthClient {
}
}

let identity = new AnonymousIdentity();
let identity: SignIdentity | PartialIdentity = new AnonymousIdentity() as PartialIdentity;
let chain: null | DelegationChain = null;
if (key) {
try {
Expand All @@ -279,7 +280,13 @@ export class AuthClient {
await _deleteStorage(storage);
key = null;
} else {
identity = DelegationIdentity.fromDelegation(key, chain);
// If the key is a public key, then we create a PartialDelegationIdentity.
if ('toDer' in key) {
identity = PartialDelegationIdentity.fromDelegation(key, chain);
// otherwise, we create a DelegationIdentity.
} else {
identity = DelegationIdentity.fromDelegation(key, chain);
}
}
}
} catch (e) {
Expand Down Expand Up @@ -318,8 +325,8 @@ export class AuthClient {
}

protected constructor(
private _identity: Identity,
private _key: SignIdentity,
private _identity: Identity | PartialIdentity,
private _key: SignIdentity | PartialIdentity,
private _chain: DelegationChain | null,
private _storage: AuthClientStorage,
public idleManager: IdleManager | undefined,
Expand Down Expand Up @@ -372,7 +379,12 @@ export class AuthClient {
}

this._chain = delegationChain;
this._identity = DelegationIdentity.fromDelegation(key, this._chain);

if ('toDer' in key) {
this._identity = PartialDelegationIdentity.fromDelegation(key, this._chain);
} else {
this._identity = DelegationIdentity.fromDelegation(key, this._chain);
}

this._idpWindow?.close();
const idleOptions = this._createOptions?.idleOptions;
Expand Down
35 changes: 34 additions & 1 deletion packages/identity/src/identity/delegation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Principal } from '@dfinity/principal';
import { DelegationChain, DelegationIdentity } from './delegation';
import { DelegationChain, DelegationIdentity, PartialDelegationIdentity } from './delegation';
import { Ed25519KeyIdentity } from './ed25519';
import { Ed25519PublicKey } from '@dfinity/agent';

function createIdentity(seed: number): Ed25519KeyIdentity {
const s = new Uint8Array([seed, ...new Array(31).fill(0)]);
Expand Down Expand Up @@ -122,3 +123,35 @@ test('Delegation Chain can sign', async () => {
expect(isValid).toBe(true);
expect(middle.toJSON()[1].length).toBe(64);
});

describe('PartialDelegationIdentity', () => {
it('should create a partial identity from a public key and a delegation chain', async () => {
const key = Ed25519PublicKey.fromRaw(new Uint8Array(32).fill(0));
const signingIdentity = Ed25519KeyIdentity.generate(new Uint8Array(32).fill(1));
const chain = await DelegationChain.create(signingIdentity, key, new Date(1609459200000));

const partial = PartialDelegationIdentity.fromDelegation(key, chain);

const partialDelegation = partial.delegation;
expect(partialDelegation).toBeDefined();

const rawKey = partial.rawKey;
expect(rawKey).toBeDefined();

const principal = partial.getPrincipal();
expect(principal).toBeDefined();
expect(principal.toText()).toEqual(
'deffl-liaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaa',
);
});
it('should throw an error if one attempts to sign', async () => {
const key = Ed25519PublicKey.fromRaw(new Uint8Array(32).fill(0));
const signingIdentity = Ed25519KeyIdentity.generate(new Uint8Array(32).fill(1));
const chain = await DelegationChain.create(signingIdentity, key, new Date(1609459200000));

const partial = PartialDelegationIdentity.fromDelegation(key, chain);
await partial.transformRequest().catch(e => {
expect(e).toContain('Not implemented.');
});
});
});
30 changes: 30 additions & 0 deletions packages/identity/src/identity/delegation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from '@dfinity/agent';
import { Principal } from '@dfinity/principal';
import * as cbor from 'simple-cbor';
import { PartialIdentity } from './partial';

const domainSeparator = new TextEncoder().encode('\x1Aic-request-auth-delegation');
const requestDomainSeparator = new TextEncoder().encode('\x0Aic-request');
Expand Down Expand Up @@ -313,6 +314,35 @@ export class DelegationIdentity extends SignIdentity {
}
}

/**
* A partial delegated identity, representing a delegation chain and the public key that it targets
*/
export class PartialDelegationIdentity extends PartialIdentity {
#delegation: DelegationChain;

/**
* The Delegation Chain of this identity.
*/
get delegation(): DelegationChain {
return this.#delegation;
}

private constructor(inner: PublicKey, delegation: DelegationChain) {
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.
* @constructs PartialDelegationIdentity
*/
public static fromDelegation(key: PublicKey, delegation: DelegationChain) {
return new PartialDelegationIdentity(key, delegation);
}
}

/**
* List of things to check for a delegation chain validity.
*/
Expand Down
35 changes: 35 additions & 0 deletions packages/identity/src/identity/partial.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { HttpAgent } from '@dfinity/agent';
import { Ed25519PublicKey } from '../identity/ed25519';
import { PartialIdentity } from './partial';
describe('Partial Identity', () => {
it('should create a partial identity from a public key', async () => {
const key = Ed25519PublicKey.fromRaw(new Uint8Array(32).fill(0));
const partial = new PartialIdentity(key);

const agent = new HttpAgent({ identity: partial });
expect((await agent.getPrincipal()).toText()).toBe(
'deffl-liaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaaaa-aaa',
);

const rawKey = partial.rawKey;
expect(rawKey).toBeDefined();

const derKey = partial.derKey;
expect(derKey).toBeDefined();

const toDer = partial.toDer();
expect(toDer).toBeDefined();
expect(toDer).toEqual(derKey);

const publicKey = partial.getPublicKey();
expect(publicKey).toBeDefined();
expect(publicKey).toEqual(key);
});
it('should throw an error when attempting to sign', async () => {
const key = Ed25519PublicKey.fromRaw(new Uint8Array(32).fill(0));
const partial = new PartialIdentity(key);
await partial.transformRequest().catch(e => {
expect(e).toContain('Not implemented.');
});
});
});
57 changes: 57 additions & 0 deletions packages/identity/src/identity/partial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Identity, PublicKey } from '@dfinity/agent';
import { Principal } from '@dfinity/principal';

/**
* A partial delegated identity, representing a delegation chain and the public key that it targets
*/
export class PartialIdentity implements Identity {
#inner: PublicKey;

/**
* The raw public key of this identity.
*/
get rawKey(): ArrayBuffer | undefined {
return this.#inner.rawKey;
}

/**
* The DER-encoded public key of this identity.
*/
get derKey(): ArrayBuffer | undefined {
return this.#inner.derKey;
}

/**
* The DER-encoded public key of this identity.
*/
public toDer(): ArrayBuffer {
return this.#inner.toDer();
}

/**
* The inner {@link PublicKey} used by this identity.
*/
public getPublicKey(): PublicKey {
return this.#inner;
}

/**
* The {@link Principal} of this identity.
*/
public getPrincipal(): Principal {
return Principal.from(this.#inner.rawKey);
}

/**
* Required for the Identity interface, but cannot implemented for just a public key.
*/
public transformRequest(): Promise<never> {
return Promise.reject(
'Not implemented. You are attempting to use a partial identity to sign calls, but this identity only has access to the public key.To sign calls, use a DelegationIdentity instead.',
);
}

constructor(inner: PublicKey) {
this.#inner = inner;
}
}

0 comments on commit ca161c8

Please sign in to comment.