Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduces partial identity #812

Merged
merged 4 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}
}