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

EIP1271 Support #617

Open
wants to merge 10 commits into
base: epic-v0.6.x
Choose a base branch
from
14 changes: 6 additions & 8 deletions packages/taco-auth/src/auth-sig.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { EthAddressSchema } from '@nucypher/shared';
import { z } from 'zod';

import {
EIP4361_AUTH_METHOD,
EIP4361TypedDataSchema,
} from './providers/eip4361/common';
import { EIP1271AuthSignature } from './providers/eip1271/auth';
import { EIP4361AuthSignature } from './providers/eip4361/auth';

export const authSignatureSchema = z.object({
export const baseAuthSignatureSchema = z.object({
signature: z.string(),
address: EthAddressSchema,
scheme: z.enum([EIP4361_AUTH_METHOD]),
typedData: EIP4361TypedDataSchema,
scheme: z.string(),
typedData: z.unknown(),
});

export type AuthSignature = z.infer<typeof authSignatureSchema>;
export type AuthSignature = EIP4361AuthSignature | EIP1271AuthSignature;
17 changes: 17 additions & 0 deletions packages/taco-auth/src/providers/eip1271/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { z } from 'zod';

import { baseAuthSignatureSchema } from '../../auth-sig';

export const EIP1271_AUTH_METHOD = 'EIP1271';

export const EIP1271TypedDataSchema = z.object({
chain: z.number().int().nonnegative(),
dataHash: z.string().startsWith('0x'), // hex string
});

export const eip1271AuthSignatureSchema = baseAuthSignatureSchema.extend({
scheme: z.literal(EIP1271_AUTH_METHOD),
typedData: EIP1271TypedDataSchema,
});

export type EIP1271AuthSignature = z.infer<typeof eip1271AuthSignatureSchema>;
22 changes: 22 additions & 0 deletions packages/taco-auth/src/providers/eip1271/eip1271.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { EIP1271_AUTH_METHOD, EIP1271AuthSignature } from './auth';

export class EIP1271AuthProvider {
constructor(
public readonly contractAddress: string,
public readonly chain: number,
public readonly dataHash: string,
public readonly signature: string,
) {}

public async getOrCreateAuthSignature(): Promise<EIP1271AuthSignature> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm missing something, but the function name (getOrCREATEAuthSignature()) makes me think that the function is able to create a new EIP1271AuthProvider object, but this is not correct, right? This function will be called only for already created objects. Should we rename it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes me think that the function is able to create a new EIP1271AuthProvider object

It's the flip. AuthProviders implement the function getOrCreateAuthSignature(). The AuthProvider is specified as a parameter to the ConditionContext, which then uses it to generate an AuthSignature via the getOrCreateAuthSignature() function.

So for EIP1271 as an example the code looks like the following:

const authProvider = new EIP1271AuthProvider(...);
conditionContext.addAuthProvider(':userAddressEIP1271', authProvider)
...
const decryptedMessage = await decrypt(
      ...
      messageKit,
      conditionContext,
    );

This conditionContext can then provide the auth signature data needed for proving ownership using the auth provider - see https://github.com/derekpierre/taco-web/blob/eip1271-support/packages/taco/src/conditions/context/context.ts#L116.

This function will be called only for already created objects. Should we rename it?

The getOrCreateAuthSignature() was named (perhaps incorrectly so) as such because it may not actually create the object because the auth signature object could be cached. For example, EIP4361 caches the auth signature after first creating it for 2 hours and doesn't need to create it again within that 2 hours i.e. subsequet calls to getOrCreateAuthSignature simply uses the cached object if called again within that 2-hour window. That being said, perhaps the name is too tied to implementation. we can always rename it to getAuthSignature() or something else ... although let's do that in a different PR ...?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it!
|
Yes, something to consider, but in a different PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #621

return {
signature: this.signature,
address: this.contractAddress,
scheme: EIP1271_AUTH_METHOD,
typedData: {
chain: this.chain,
dataHash: this.dataHash,
},
};
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { SiweMessage } from 'siwe';
import { z } from 'zod';

import { baseAuthSignatureSchema } from '../../auth-sig';

export const EIP4361_AUTH_METHOD = 'EIP4361';

const isSiweMessage = (message: string): boolean => {
Expand All @@ -15,3 +17,10 @@ const isSiweMessage = (message: string): boolean => {
export const EIP4361TypedDataSchema = z
.string()
.refine(isSiweMessage, { message: 'Invalid SIWE message' });

export const eip4361AuthSignatureSchema = baseAuthSignatureSchema.extend({
scheme: z.literal(EIP4361_AUTH_METHOD),
typedData: EIP4361TypedDataSchema,
});

export type EIP4361AuthSignature = z.infer<typeof eip4361AuthSignatureSchema>;
15 changes: 9 additions & 6 deletions packages/taco-auth/src/providers/eip4361/eip4361.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { ethers } from 'ethers';
import { generateNonce, SiweMessage } from 'siwe';

import { AuthSignature } from '../../auth-sig';
import { LocalStorage } from '../../storage';

import { EIP4361_AUTH_METHOD } from './common';
import {
EIP4361_AUTH_METHOD,
EIP4361AuthSignature,
eip4361AuthSignatureSchema,
} from './auth';

export const USER_ADDRESS_PARAM_DEFAULT = ':userAddress';

Expand All @@ -17,7 +20,7 @@ const TACO_DEFAULT_DOMAIN = 'taco.build';
const TACO_DEFAULT_URI = 'https://taco.build';

export class EIP4361AuthProvider {
private readonly storage: LocalStorage;
private readonly storage: LocalStorage<EIP4361AuthSignature>;
private readonly providerParams: EIP4361AuthProviderParams;

constructor(
Expand All @@ -26,7 +29,7 @@ export class EIP4361AuthProvider {
private readonly signer: ethers.Signer,
providerParams?: EIP4361AuthProviderParams,
) {
this.storage = new LocalStorage();
this.storage = new LocalStorage(eip4361AuthSignatureSchema);
if (providerParams) {
this.providerParams = providerParams;
} else {
Expand All @@ -50,7 +53,7 @@ export class EIP4361AuthProvider {
};
}

public async getOrCreateAuthSignature(): Promise<AuthSignature> {
public async getOrCreateAuthSignature(): Promise<EIP4361AuthSignature> {
const address = await this.signer.getAddress();
const storageKey = `eth-${EIP4361_AUTH_METHOD}-message-${address}`;

Expand Down Expand Up @@ -85,7 +88,7 @@ export class EIP4361AuthProvider {
return twoHourWindow < now;
}

private async createSIWEAuthMessage(): Promise<AuthSignature> {
private async createSIWEAuthMessage(): Promise<EIP4361AuthSignature> {
const address = await this.signer.getAddress();
const { domain, uri } = this.providerParams;
const version = '1';
Expand Down
6 changes: 2 additions & 4 deletions packages/taco-auth/src/providers/eip4361/external-eip4361.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { SiweMessage } from 'siwe';

import { AuthSignature } from '../../auth-sig';

import { EIP4361_AUTH_METHOD } from './common';
import { EIP4361_AUTH_METHOD, EIP4361AuthSignature } from './auth';

export const USER_ADDRESS_PARAM_EXTERNAL_EIP4361 =
':userAddressExternalEIP4361';
Expand Down Expand Up @@ -30,7 +28,7 @@ export class SingleSignOnEIP4361AuthProvider {
private readonly signature: string,
) {}

public async getOrCreateAuthSignature(): Promise<AuthSignature> {
public async getOrCreateAuthSignature(): Promise<EIP4361AuthSignature> {
const scheme = EIP4361_AUTH_METHOD;
return {
signature: this.signature,
Expand Down
4 changes: 4 additions & 0 deletions packages/taco-auth/src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
// TODO: should we export with package names?
export { EIP1271AuthSignature } from './eip1271/auth';
export * from './eip1271/eip1271';
export { EIP4361AuthSignature } from './eip4361/auth';
export * from './eip4361/eip4361';
export * from './eip4361/external-eip4361';
16 changes: 10 additions & 6 deletions packages/taco-auth/src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { AuthSignature, authSignatureSchema } from './index';
import { z } from 'zod';

import { AuthSignature } from './index';

interface IStorage {
getItem(key: string): string | null;
Expand Down Expand Up @@ -38,25 +40,27 @@ class NodeStorage implements IStorage {
}
}

export class LocalStorage {
export class LocalStorage<T extends AuthSignature> {
private storage: IStorage;
private signatureSchema: z.ZodSchema;

constructor() {
constructor(signatureSchema: z.ZodSchema) {
this.storage =
typeof localStorage === 'undefined'
? new NodeStorage()
: new BrowserStorage();
this.signatureSchema = signatureSchema;
}

public getAuthSignature(key: string): AuthSignature | null {
public getAuthSignature(key: string): T | null {
const asJson = this.storage.getItem(key);
if (!asJson) {
return null;
}
return authSignatureSchema.parse(JSON.parse(asJson));
return this.signatureSchema.parse(JSON.parse(asJson));
}

public setAuthSignature(key: string, authSignature: AuthSignature): void {
public setAuthSignature(key: string, authSignature: T): void {
const asJson = JSON.stringify(authSignature);
this.storage.setItem(key, asJson);
}
Expand Down
34 changes: 30 additions & 4 deletions packages/taco-auth/test/auth-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import { SiweMessage } from 'siwe';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import {
EIP1271AuthProvider,
EIP4361AuthProvider,
SingleSignOnEIP4361AuthProvider,
} from '../src/providers';
import { EIP4361TypedDataSchema } from '../src/providers/eip4361/common';
import { EIP4361TypedDataSchema } from '../src/providers/eip4361/auth';

describe('auth provider', () => {
describe('eip4361 auth provider', () => {
const provider = fakeProvider(bobSecretKeyBytes);
const signer = provider.getSigner();
const eip4361Provider = new EIP4361AuthProvider(
Expand Down Expand Up @@ -77,7 +78,7 @@ describe('auth provider', () => {
});
});

describe('auth provider caching', () => {
describe('eip4361 single sign-on auth provider', () => {
beforeEach(() => {
// tell vitest we use mocked time
vi.useFakeTimers();
Expand Down Expand Up @@ -125,7 +126,7 @@ describe('auth provider caching', () => {
});
});

describe('single sign-on auth provider', async () => {
describe('eip4361 single sign-on auth provider', async () => {
const provider = fakeProvider(bobSecretKeyBytes);
const signer = provider.getSigner();

Expand Down Expand Up @@ -155,3 +156,28 @@ describe('single sign-on auth provider', async () => {
expect(typedSignature.scheme).toEqual('EIP4361');
});
});

describe('eip1271 auth provider', () => {
const dataHash = '0xdeadbeef';
const contractAddress = '0x100000000000000000000000000000000000000';
const chainId = 1234;
const signature = '0xabc123';

const eip1271Provider = new EIP1271AuthProvider(
contractAddress,
chainId,
dataHash,
signature,
);

it('creates a new EIP1271 auth signature', async () => {
const typedSignature = await eip1271Provider.getOrCreateAuthSignature();
expect(typedSignature.signature).toEqual(signature);
expect(typedSignature.address).toEqual(contractAddress);
expect(typedSignature.scheme).toEqual('EIP1271');
expect(typedSignature.typedData).toEqual({
chain: chainId,
dataHash: dataHash,
});
});
});
49 changes: 45 additions & 4 deletions packages/taco-auth/test/auth-sig.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';

import { authSignatureSchema } from '../src';
import { eip1271AuthSignatureSchema } from '../src/providers/eip1271/auth';
import { eip4361AuthSignatureSchema } from '../src/providers/eip4361/auth';

const eip4361AuthSignature = {
signature: 'fake-signature',
Expand All @@ -10,17 +11,57 @@ const eip4361AuthSignature = {
'localhost wants you to sign in with your Ethereum account:\n0x0000000000000000000000000000000000000000\n\nlocalhost wants you to sign in with your Ethereum account: 0x0000000000000000000000000000000000000000\n\nURI: http://localhost:3000\nVersion: 1\nChain ID: 1234\nNonce: 5ixAg1odyfDnrbfGa\nIssued At: 2024-07-01T10:32:39.631Z',
};

const eip1271AuthSignature = {
signature: '0xdeadbeef',
address: '0x0000000000000000000000000000000000000000',
scheme: 'EIP1271',
typedData: {
chain: 23,
dataHash: '0xdeadbeef',
},
};

describe('auth signature', () => {
it('accepts a well-formed EIP4361 auth signature', async () => {
authSignatureSchema.parse(eip4361AuthSignature);
eip4361AuthSignatureSchema.parse(eip4361AuthSignature);
});

it('rejects an EIP4361 auth signature with missing fields', async () => {
it('rejects an EIP4361 auth signature with missing/incorrect fields', async () => {
expect(() =>
authSignatureSchema.parse({
eip4361AuthSignatureSchema.parse({
...eip4361AuthSignature,
signature: undefined,
}),
).toThrow();

expect(() =>
eip4361AuthSignatureSchema.parse({
...eip4361AuthSignature,
scheme: 'EIP1271',
}),
).toThrow();
});

it('accepts a well-formed EIP1271 auth signature', async () => {
eip1271AuthSignatureSchema.parse(eip1271AuthSignature);
});

it('rejects an EIP1271 auth signature with missing/incorrect fields', async () => {
expect(() =>
eip1271AuthSignatureSchema.parse({
...eip1271AuthSignature,
scheme: 'EIP4361',
}),
).toThrow();

expect(() =>
eip1271AuthSignatureSchema.parse({
...eip1271AuthSignature,
typedData: {
chain: 21,
dataHash: undefined,
},
}),
).toThrow();
});
});
18 changes: 12 additions & 6 deletions packages/taco/src/conditions/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { toJSON } from '@nucypher/shared';
import {
AuthProvider,
AuthSignature,
EIP1271AuthProvider,
EIP4361AuthProvider,
SingleSignOnEIP4361AuthProvider,
USER_ADDRESS_PARAM_DEFAULT,
Expand Down Expand Up @@ -39,10 +40,16 @@ const ERR_AUTH_PROVIDER_NOT_NEEDED_FOR_CONTEXT_PARAM = (param: string) =>

type AuthProviderType =
| typeof EIP4361AuthProvider
| typeof EIP1271AuthProvider
| typeof SingleSignOnEIP4361AuthProvider;
const EXPECTED_AUTH_PROVIDER_TYPES: Record<string, AuthProviderType> = {
[USER_ADDRESS_PARAM_DEFAULT]: EIP4361AuthProvider,
[USER_ADDRESS_PARAM_EXTERNAL_EIP4361]: SingleSignOnEIP4361AuthProvider,

const EXPECTED_AUTH_PROVIDER_TYPES: Record<string, AuthProviderType[]> = {
[USER_ADDRESS_PARAM_DEFAULT]: [
EIP4361AuthProvider,
EIP1271AuthProvider,
SingleSignOnEIP4361AuthProvider,
],
[USER_ADDRESS_PARAM_EXTERNAL_EIP4361]: [SingleSignOnEIP4361AuthProvider],
};

export const RESERVED_CONTEXT_PARAMS = [
Expand Down Expand Up @@ -216,16 +223,15 @@ export class ConditionContext {
ERR_AUTH_PROVIDER_NOT_NEEDED_FOR_CONTEXT_PARAM(contextParam),
);
}

if (!(authProvider instanceof EXPECTED_AUTH_PROVIDER_TYPES[contextParam])) {
const expectedTypes = EXPECTED_AUTH_PROVIDER_TYPES[contextParam];
if (!expectedTypes.some((type) => authProvider instanceof type)) {
throw new Error(
ERR_INVALID_AUTH_PROVIDER_TYPE(contextParam, typeof authProvider),
);
}

this.authProviders[contextParam] = authProvider;
}

public async toJson(): Promise<string> {
const parameters = await this.toContextParameters();
return toJSON(parameters);
Expand Down
2 changes: 1 addition & 1 deletion packages/taco/test/conditions/base/contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ describe('supports custom function abi', async () => {

conditionContext.addAuthProvider(
USER_ADDRESS_PARAM_DEFAULT,
authProviders[USER_ADDRESS_PARAM_DEFAULT],
authProviders['EIP4361'],
);

const asJson = await conditionContext.toJson();
Expand Down
Loading