diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html
index 1e4259db5..6f4c0c57a 100644
--- a/docs/generated/changelog.html
+++ b/docs/generated/changelog.html
@@ -12,6 +12,7 @@
Agent-JS Changelog
Version x.x.x
+ - feat: introduces partial identities from public keys for authentication flows
- fix: honor disableIdle flag
Version 0.20.2
diff --git a/packages/auth-client/src/index.ts b/packages/auth-client/src/index.ts
index dd9a89152..d9fbff5b8 100644
--- a/packages/auth-client/src/index.ts
+++ b/packages/auth-client/src/index.ts
@@ -13,6 +13,7 @@ import {
DelegationIdentity,
Ed25519KeyIdentity,
ECDSAKeyIdentity,
+ PartialDelegationIdentity,
} from '@dfinity/identity';
import { Principal } from '@dfinity/principal';
import { IdleManager, IdleManagerOptions } from './idleManager';
@@ -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';
@@ -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
*/
@@ -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
@@ -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 {
@@ -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 {
@@ -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) {
@@ -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,
@@ -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;
diff --git a/packages/identity/src/identity/delegation.test.ts b/packages/identity/src/identity/delegation.test.ts
index 1131d8ca0..c6b0b9716 100644
--- a/packages/identity/src/identity/delegation.test.ts
+++ b/packages/identity/src/identity/delegation.test.ts
@@ -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)]);
@@ -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.');
+ });
+ });
+});
diff --git a/packages/identity/src/identity/delegation.ts b/packages/identity/src/identity/delegation.ts
index d5477bf62..99d296e37 100644
--- a/packages/identity/src/identity/delegation.ts
+++ b/packages/identity/src/identity/delegation.ts
@@ -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');
@@ -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.
*/
diff --git a/packages/identity/src/identity/partial.test.ts b/packages/identity/src/identity/partial.test.ts
new file mode 100644
index 000000000..35bba4c52
--- /dev/null
+++ b/packages/identity/src/identity/partial.test.ts
@@ -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.');
+ });
+ });
+});
diff --git a/packages/identity/src/identity/partial.ts b/packages/identity/src/identity/partial.ts
new file mode 100644
index 000000000..1a97e878e
--- /dev/null
+++ b/packages/identity/src/identity/partial.ts
@@ -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 {
+ 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;
+ }
+}