From 95f7b7c0389a1fe576796b420b8147b410e3430c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Wed, 30 Oct 2024 18:50:15 +0100 Subject: [PATCH 1/5] fix: ritual initialization doesn't belong to encryption --- packages/taco/src/taco.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/taco/src/taco.ts b/packages/taco/src/taco.ts index da1fc9ce..15314b77 100644 --- a/packages/taco/src/taco.ts +++ b/packages/taco/src/taco.ts @@ -49,18 +49,6 @@ export const encrypt = async ( ritualId: number, authSigner: ethers.Signer, ): Promise => { - // TODO(#264): Enable ritual initialization - // if (ritualId === undefined) { - // ritualId = await DkgClient.initializeRitual( - // provider, - // this.cohort.ursulaAddresses, - // true - // ); - // } - // if (ritualId === undefined) { - // // Given that we just initialized the ritual, this should never happen - // throw new Error('Ritual ID is undefined'); - // } const dkgRitual = await DkgClient.getActiveRitual(provider, domain, ritualId); return await encryptWithPublicKey( From e548f9aa1679141bb26d2aa0c7665fb4a935990c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Fri, 1 Nov 2024 21:44:30 +0100 Subject: [PATCH 2/5] feat: PoC of encryptor signature self-delegation --- packages/taco-auth/src/auth-sig.ts | 9 ++- .../src/providers/encryptor/self-delegate.ts | 47 +++++++++++++++ packages/taco-auth/src/providers/index.ts | 1 + packages/taco-auth/test/auth-provider.test.ts | 59 ++++++++++++++++++- 4 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 packages/taco-auth/src/providers/encryptor/self-delegate.ts diff --git a/packages/taco-auth/src/auth-sig.ts b/packages/taco-auth/src/auth-sig.ts index 03727842..064d2868 100644 --- a/packages/taco-auth/src/auth-sig.ts +++ b/packages/taco-auth/src/auth-sig.ts @@ -5,12 +5,17 @@ import { EIP4361_AUTH_METHOD, EIP4361TypedDataSchema, } from './providers/eip4361/common'; +import { + ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD, + SelfDelegateTypedDataSchema, +} from './providers/encryptor/self-delegate'; +// TODO: Create two different schemas, rather than one with different field schemas export const authSignatureSchema = z.object({ signature: z.string(), address: EthAddressSchema, - scheme: z.enum([EIP4361_AUTH_METHOD]), - typedData: EIP4361TypedDataSchema, + scheme: z.enum([EIP4361_AUTH_METHOD, ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD]), + typedData: z.union([EIP4361TypedDataSchema, SelfDelegateTypedDataSchema]), }); export type AuthSignature = z.infer; diff --git a/packages/taco-auth/src/providers/encryptor/self-delegate.ts b/packages/taco-auth/src/providers/encryptor/self-delegate.ts new file mode 100644 index 00000000..b9f7a61d --- /dev/null +++ b/packages/taco-auth/src/providers/encryptor/self-delegate.ts @@ -0,0 +1,47 @@ +import { ethers } from 'ethers'; +import { z } from 'zod'; + +import { AuthSignature } from '../../auth-sig'; +import { LocalStorage } from '../../storage'; + +export const ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD = 'EncryptorSelfDelegate' + +export const SelfDelegateTypedDataSchema = z.string(); + +export class SelfDelegateProvider { + private readonly storage: LocalStorage; + + constructor(private readonly signer: ethers.Signer) { + this.storage = new LocalStorage(); + } + + public async getOrCreateAuthSignature( + ephemeralPublicKey: string + ): Promise { + const address = await this.signer.getAddress(); + const storageKey = `eth-${ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD}-${address}-${ephemeralPublicKey}`; + + // If we have a signature in localStorage, return it + const maybeSignature = this.storage.getAuthSignature(storageKey); + if (maybeSignature) { + // TODO: Consider design that includes freshness validation (see SIWE) + return maybeSignature; + } + + // If at this point we didn't return, we need to create a new message + const authMessage = await this.createAuthMessage(ephemeralPublicKey); + this.storage.setAuthSignature(storageKey, authMessage); + return authMessage; + } + + private async createAuthMessage( + ephemeralPublicKey: string + ): Promise { + // TODO: Consider adding domain, uri, version, chainId (see SIWE signature) + const address = await this.signer.getAddress(); + const scheme = ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD; + const message = ephemeralPublicKey; + const signature = await this.signer.signMessage(message); + return { signature, address, scheme, typedData: message }; + } +} diff --git a/packages/taco-auth/src/providers/index.ts b/packages/taco-auth/src/providers/index.ts index 82912650..2e30944f 100644 --- a/packages/taco-auth/src/providers/index.ts +++ b/packages/taco-auth/src/providers/index.ts @@ -1,2 +1,3 @@ export * from './eip4361/eip4361'; export * from './eip4361/external-eip4361'; +export * from './encryptor/self-delegate'; diff --git a/packages/taco-auth/test/auth-provider.test.ts b/packages/taco-auth/test/auth-provider.test.ts index 611cbb71..2b84b17b 100644 --- a/packages/taco-auth/test/auth-provider.test.ts +++ b/packages/taco-auth/test/auth-provider.test.ts @@ -1,4 +1,5 @@ import { + aliceSecretKeyBytes, bobSecretKeyBytes, fakeProvider, TEST_SIWE_PARAMS, @@ -8,11 +9,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { EIP4361AuthProvider, + SelfDelegateProvider, SingleSignOnEIP4361AuthProvider, } from '../src/providers'; import { EIP4361TypedDataSchema } from '../src/providers/eip4361/common'; -describe('auth provider', () => { +describe('siwe auth provider', () => { const provider = fakeProvider(bobSecretKeyBytes); const signer = provider.getSigner(); const eip4361Provider = new EIP4361AuthProvider( @@ -55,7 +57,7 @@ describe('auth provider', () => { }); }); -describe('auth provider caching', () => { +describe('siwe auth provider caching', () => { beforeEach(() => { // tell vitest we use mocked time vi.useFakeTimers(); @@ -103,7 +105,7 @@ describe('auth provider caching', () => { }); }); -describe('single sign-on auth provider', async () => { +describe('single sign-on siwe auth provider', async () => { const provider = fakeProvider(bobSecretKeyBytes); const signer = provider.getSigner(); @@ -133,3 +135,54 @@ describe('single sign-on auth provider', async () => { expect(typedSignature.scheme).toEqual('EIP4361'); }); }); + +describe('encryptor self-delegate provider authorization', () => { + const provider = fakeProvider(bobSecretKeyBytes); + const signer = provider.getSigner(); + const selfDelegateProvider = new SelfDelegateProvider(signer); + + const applicationSideProvider = fakeProvider(aliceSecretKeyBytes); + const applicationSideSigner = applicationSideProvider.getSigner(); + + it('creates a new message', async () => { + const appSideSignerAddress = await applicationSideSigner.getAddress(); + const typedSignature = await selfDelegateProvider.getOrCreateAuthSignature(appSideSignerAddress); + expect(typedSignature.signature).toBeDefined(); + expect(typedSignature.address).toEqual(await signer.getAddress()); + expect(typedSignature.scheme).toEqual('EncryptorSelfDelegate'); + expect(typedSignature.typedData).toEqual(appSideSignerAddress); + + }); + +}); + +describe('encryptor self-delegate provider caching', () => { + const provider = fakeProvider(bobSecretKeyBytes); + const signer = provider.getSigner(); + const selfDelegateProvider = new SelfDelegateProvider(signer); + + const applicationSideProvider = fakeProvider(aliceSecretKeyBytes); + const applicationSideSigner = applicationSideProvider.getSigner(); + + it('caches signature', async () => { + const appSideSignerAddress = await applicationSideSigner.getAddress(); + + const createSignatureSpy = vi.spyOn( + selfDelegateProvider, + // @ts-expect-error -- spying on private function + 'createAuthMessage', + ); + + expect(createSignatureSpy).toHaveBeenCalledTimes(0); + + const typedSignature = await selfDelegateProvider.getOrCreateAuthSignature(appSideSignerAddress); + expect(createSignatureSpy).toHaveBeenCalledTimes(1); + + const typedSignatureSecondCall = + await selfDelegateProvider.getOrCreateAuthSignature(appSideSignerAddress); + // auth signature is cached, so spy is not called a 2nd time + expect(createSignatureSpy).toHaveBeenCalledTimes(1); + expect(typedSignatureSecondCall).toEqual(typedSignature); + }); + +}); From 409aa5e077edb212969d7a7b1a9159059eb2f5ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 4 Nov 2024 13:39:14 +0100 Subject: [PATCH 3/5] feat: rename parameter to hint that we support different signer types Normally, we would use ETH-based signers, but in principle anything that's a PK signer should work. --- .../taco-auth/src/providers/encryptor/self-delegate.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/taco-auth/src/providers/encryptor/self-delegate.ts b/packages/taco-auth/src/providers/encryptor/self-delegate.ts index b9f7a61d..2f2cc11b 100644 --- a/packages/taco-auth/src/providers/encryptor/self-delegate.ts +++ b/packages/taco-auth/src/providers/encryptor/self-delegate.ts @@ -16,10 +16,10 @@ export class SelfDelegateProvider { } public async getOrCreateAuthSignature( - ephemeralPublicKey: string + ephemeralPublicKeyOrAddress: string ): Promise { const address = await this.signer.getAddress(); - const storageKey = `eth-${ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD}-${address}-${ephemeralPublicKey}`; + const storageKey = `eth-${ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD}-${address}-${ephemeralPublicKeyOrAddress}`; // If we have a signature in localStorage, return it const maybeSignature = this.storage.getAuthSignature(storageKey); @@ -29,18 +29,18 @@ export class SelfDelegateProvider { } // If at this point we didn't return, we need to create a new message - const authMessage = await this.createAuthMessage(ephemeralPublicKey); + const authMessage = await this.createAuthMessage(ephemeralPublicKeyOrAddress); this.storage.setAuthSignature(storageKey, authMessage); return authMessage; } private async createAuthMessage( - ephemeralPublicKey: string + ephemeralPublicKeyOrAddress: string ): Promise { // TODO: Consider adding domain, uri, version, chainId (see SIWE signature) const address = await this.signer.getAddress(); const scheme = ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD; - const message = ephemeralPublicKey; + const message = ephemeralPublicKeyOrAddress; const signature = await this.signer.signMessage(message); return { signature, address, scheme, typedData: message }; } From fa6633c66e2be988cfdd7247eeef8c2e9fe5bb03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 4 Nov 2024 13:40:48 +0100 Subject: [PATCH 4/5] feat: function to create self-delegated app-side signers This means, they already include the self-delegation authorization from the encryptor --- .../src/providers/encryptor/self-delegate.ts | 35 ++++++++++++++++++- packages/taco-auth/test/auth-provider.test.ts | 9 ++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/taco-auth/src/providers/encryptor/self-delegate.ts b/packages/taco-auth/src/providers/encryptor/self-delegate.ts index 2f2cc11b..e2a9f614 100644 --- a/packages/taco-auth/src/providers/encryptor/self-delegate.ts +++ b/packages/taco-auth/src/providers/encryptor/self-delegate.ts @@ -1,6 +1,8 @@ -import { ethers } from 'ethers'; +import { ethers, Wallet } from 'ethers'; import { z } from 'zod'; +// import { Bytes } from "@ethersproject/bytes"; + import { AuthSignature } from '../../auth-sig'; import { LocalStorage } from '../../storage'; @@ -8,6 +10,29 @@ export const ENCRYPTOR_SELF_DELEGATE_AUTH_METHOD = 'EncryptorSelfDelegate' export const SelfDelegateTypedDataSchema = z.string(); + +// TODO: Create generic EncryptorSigner class/interface, which can be +// instantiated with ethers' Signers and Wallets, but also with our custom +// classes +export class DelegatedSigner extends Wallet { // TODO: extend from generic Signer + + authSignature?: AuthSignature; + + async authenticate(selfDelegateProvider: SelfDelegateProvider){ + const appSideSignerAddress = await this.getAddress(); + this.authSignature = await selfDelegateProvider.getOrCreateAuthSignature(appSideSignerAddress); + } + + override async signMessage(message: any): Promise { // TODO: Restrict input type to Bytes | string + if (typeof this.authSignature === 'undefined'){ + throw new Error('Encryptor must authenticate app signer first'); + } + const appSignature = await super.signMessage(message); + return appSignature.concat(this.authSignature.signature) + } +} + + export class SelfDelegateProvider { private readonly storage: LocalStorage; @@ -15,6 +40,14 @@ export class SelfDelegateProvider { this.storage = new LocalStorage(); } + public async createSelfDelegatedAppSideSigner( + ephemeralPrivateKey: any // TODO: Find a stricter type + ): Promise { + const appSideSigner = new DelegatedSigner(ephemeralPrivateKey); + await appSideSigner.authenticate(this); + return appSideSigner; + } + public async getOrCreateAuthSignature( ephemeralPublicKeyOrAddress: string ): Promise { diff --git a/packages/taco-auth/test/auth-provider.test.ts b/packages/taco-auth/test/auth-provider.test.ts index 2b84b17b..2329f373 100644 --- a/packages/taco-auth/test/auth-provider.test.ts +++ b/packages/taco-auth/test/auth-provider.test.ts @@ -140,11 +140,18 @@ describe('encryptor self-delegate provider authorization', () => { const provider = fakeProvider(bobSecretKeyBytes); const signer = provider.getSigner(); const selfDelegateProvider = new SelfDelegateProvider(signer); + + it('creates a new self-delegated app signer', async () => { + const appSideSignerAddress = await applicationSideSigner.getAddress(); + const [newSigner, newAuthSignature] = await selfDelegateProvider.createSelfDelegatedAppSideSigner(aliceSecretKeyBytes); + expect(await newSigner.getAddress()).toEqual(appSideSignerAddress); + expect(newAuthSignature.typedData).toEqual(appSideSignerAddress); + }); const applicationSideProvider = fakeProvider(aliceSecretKeyBytes); const applicationSideSigner = applicationSideProvider.getSigner(); - it('creates a new message', async () => { + it('creates a new auth signature', async () => { const appSideSignerAddress = await applicationSideSigner.getAddress(); const typedSignature = await selfDelegateProvider.getOrCreateAuthSignature(appSideSignerAddress); expect(typedSignature.signature).toBeDefined(); From d8b249d948bb9f8d328280ff1122de2952e47fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20N=C3=BA=C3=B1ez?= Date: Mon, 4 Nov 2024 17:35:06 +0100 Subject: [PATCH 5/5] [TEMP COMMIT] Show how self-delegation works in an example This currently works with tapir ritual 6, but only because it uses OpenAccessAuthorizer. In reality, this will fail with GlobalAllowList since this needs proper validation of evidence authorization. This commit should be deleted before merging the PR, it's just illustrative --- examples/taco/nodejs/src/index.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/taco/nodejs/src/index.ts b/examples/taco/nodejs/src/index.ts index ef4550ab..a2c12726 100644 --- a/examples/taco/nodejs/src/index.ts +++ b/examples/taco/nodejs/src/index.ts @@ -13,10 +13,11 @@ import { } from '@nucypher/taco'; import { EIP4361AuthProvider, + SelfDelegateProvider, USER_ADDRESS_PARAM_DEFAULT, } from '@nucypher/taco-auth'; import * as dotenv from 'dotenv'; -import { ethers } from 'ethers'; +import { Wallet, ethers } from 'ethers'; dotenv.config(); @@ -73,13 +74,18 @@ const encryptToBytes = async (messageString: string) => { 'Condition requires authentication', ); + const ephemeralPrivateKey = Wallet.createRandom().privateKey; + const selfDelegateProvider = new SelfDelegateProvider(encryptorSigner); + + const appSideSigner = await selfDelegateProvider.createSelfDelegatedAppSideSigner(ephemeralPrivateKey); + const messageKit = await encrypt( provider, domain, message, hasPositiveBalance, ritualId, - encryptorSigner, + appSideSigner, ); return messageKit.toBytes();