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

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; + } +}