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: basic support for multiple nullifiers, rotating keys #3247

Merged
merged 16 commits into from
Feb 24, 2025
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ function App({ Component, pageProps }: AppProps) {

// only continue with the process if a code is returned
if (queryCode) {
channel.postMessage({ target: provider, data: { code: queryCode, state: queryState } });
channel.postMessage({
target: provider,
data: { code: queryCode, state: queryState },
});
}

// always close the redirected window
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ describe("assuming a valid stamp is stored in ceramic", () => {
// Step 1: First, we need to create a valid stamp
const verificationMethod: string = (await DIDKit.keyToVerificationMethod("ethr", eip712Key)) as string;

// TODO temporary workaround until we actually make the new
// nullifier format work with ceramic
const testStampCredentialDocument = stampCredentialDocument(verificationMethod);
delete testStampCredentialDocument.eip712Domain.types.NullifiersContext;
testStampCredentialDocument.eip712Domain.types["@context"][0] = { type: "string", name: "hash" };
testStampCredentialDocument.eip712Domain.types.CredentialSubject[1] = { type: "string", name: "hash" };

const credential = await issueEip712Credential(
DIDKit,
eip712Key,
Expand All @@ -52,7 +59,7 @@ describe("assuming a valid stamp is stored in ceramic", () => {
provider: "Discord",
},
},
stampCredentialDocument(verificationMethod),
testStampCredentialDocument,
["https://w3id.org/vc/status-list/2021/v1"]
);

Expand Down
6 changes: 5 additions & 1 deletion embed/.env-example.env
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,8 @@ SCROLL_BADGE_ATTESTATION_SCHEMA_UID=0xd57de4f41c3d3cc855eadef68f98c0d4edd22d5716

REDIS_URL=redis://localhost:6379/0

EMBED_POPUP_OAUTH_URL=https://embed-popup.review.passport.xyz/
EMBED_POPUP_OAUTH_URL=https://embed-popup.review.passport.xyz/
FF_ROTATING_KEYS=on

# Control the % of stamps that we want to attach a human network hash. This shall be a number between 0 and 100.
# HUMAN_NETWORK_NULLIFIER_PERCENT=
127 changes: 86 additions & 41 deletions embed/src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
groupProviderTypesByPlatform,
verifyProvidersAndIssueCredentials,
getChallenge,
getIssuerKey,
issueChallengeCredential,
getIssuerInfo,
} from "./utils/identityHelper.js";
import {
VerifiableCredential,
Expand All @@ -42,7 +42,7 @@ const apiKey = process.env.SCORER_API_KEY as string;
export class EmbedAxiosError extends Error {
constructor(
public message: string,
public code: number
public code: number,
) {
super(message);
this.name = this.constructor.name;
Expand All @@ -52,11 +52,17 @@ export class EmbedAxiosError extends Error {

// TODO: check if these functions are redundant ... are they also defined in platforms?
// return a JSON error response with a 400 status
export const errorRes = (res: Response, error: string | object, errorCode: number): Response =>
res.status(errorCode).json({ error });
export const errorRes = (
res: Response,
error: string | object,
errorCode: number,
): Response => res.status(errorCode).json({ error });

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const addErrorDetailsToMessage = (message: string, error: any): string => {
export const addErrorDetailsToMessage = (
message: string,
error: any,
): string => {
if (error instanceof EmbedAxiosError || error instanceof Error) {
message += `, ${error.name}: ${error.message}`;
} else if (typeof error === "string") {
Expand All @@ -69,7 +75,9 @@ export const addStampsAndGetScore = async ({
address,
scorerId,
stamps,
}: AutoVerificationFields & { stamps: VerifiableCredential[] }): Promise<PassportScore> => {
}: AutoVerificationFields & {
stamps: VerifiableCredential[];
}): Promise<PassportScore> => {
try {
const scorerResponse: {
data?: {
Expand All @@ -85,7 +93,7 @@ export const addStampsAndGetScore = async ({
headers: {
Authorization: apiKey,
},
}
},
);

if (!scorerResponse.data?.score) {
Expand All @@ -99,8 +107,12 @@ export const addStampsAndGetScore = async ({
};

export const autoVerificationHandler = async (
req: Request<ParamsDictionary, AutoVerificationResponseBodyType, AutoVerificationRequestBodyType>,
res: Response
req: Request<
ParamsDictionary,
AutoVerificationResponseBodyType,
AutoVerificationRequestBodyType
>,
res: Response,
): Promise<void> => {
try {
const { address, scorerId, credentialIds } = req.body;
Expand All @@ -120,7 +132,10 @@ export const autoVerificationHandler = async (
return void errorRes(res, error.message, error.code);
}

const message = addErrorDetailsToMessage("Unexpected error when processing request", error);
const message = addErrorDetailsToMessage(
"Unexpected error when processing request",
error,
);
return void errorRes(res, message, 500);
}
};
Expand All @@ -130,8 +145,12 @@ type EmbedVerifyRequestBody = VerifyRequestBody & {
};

export const verificationHandler = (
req: Request<ParamsDictionary, AutoVerificationResponseBodyType, EmbedVerifyRequestBody>,
res: Response
req: Request<
ParamsDictionary,
AutoVerificationResponseBodyType,
EmbedVerifyRequestBody
>,
res: Response,
): void => {
const requestBody: EmbedVerifyRequestBody = req.body;
// each verify request should be received with a challenge credential detailing a signature contained in the RequestPayload.proofs
Expand All @@ -150,42 +169,54 @@ export const verificationHandler = (
address = await verifyChallengeAndGetAddress(requestBody);
} catch (error) {
if (error instanceof VerifyDidChallengeBaseError) {
return void errorRes(res, `Invalid challenge signature: ${error.name}`, 401);
return void errorRes(
res,
`Invalid challenge signature: ${error.name}`,
401,
);
}
throw error;
}

// Check signer and type
const isSigner = challenge.credentialSubject.id === `did:pkh:eip155:1:${address}`;
const isType = challenge.credentialSubject.provider === `challenge-${payload.type}`;
const isSigner =
challenge.credentialSubject.id === `did:pkh:eip155:1:${address}`;
const isType =
challenge.credentialSubject.provider === `challenge-${payload.type}`;

if (!isSigner || !isType) {
return void errorRes(
res,
"Invalid challenge '" +
[!isSigner && "signer", !isType && "provider"].filter(Boolean).join("' and '") +
[!isSigner && "signer", !isType && "provider"]
.filter(Boolean)
.join("' and '") +
"'",
401
401,
);
}

const types = payload.types?.filter((type) => type) || [];
const providersGroupedByPlatforms = groupProviderTypesByPlatform(types);

const credentialsVerificationResponses = await verifyProvidersAndIssueCredentials(
providersGroupedByPlatforms,
address,
payload
);
const credentialsVerificationResponses =
await verifyProvidersAndIssueCredentials(
providersGroupedByPlatforms,
address,
payload,
);

const stamps = credentialsVerificationResponses.reduce((acc, response) => {
if ("credential" in response && response.credential) {
if (response.credential) {
acc.push(response.credential);
const stamps = credentialsVerificationResponses.reduce(
(acc, response) => {
if ("credential" in response && response.credential) {
if (response.credential) {
acc.push(response.credential);
}
}
}
return acc;
}, [] as VerifiableCredential[]);
return acc;
},
[] as VerifiableCredential[],
);

const score = await addStampsAndGetScore({ address, scorerId, stamps });

Expand All @@ -209,8 +240,12 @@ export const verificationHandler = (
};

export const getChallengeHandler = (
req: Request<ParamsDictionary, AutoVerificationResponseBodyType, EmbedVerifyRequestBody>,
res: Response
req: Request<
ParamsDictionary,
AutoVerificationResponseBodyType,
EmbedVerifyRequestBody
>,
res: Response,
): void => {
// get the payload from the JSON req body
const requestBody: ChallengeRequestBody = req.body as ChallengeRequestBody;
Expand All @@ -235,36 +270,46 @@ export const getChallengeHandler = (
...(challenge?.record || {}),
};

if (!payload.signatureType) {
return void errorRes(res, "Missing signatureType from challenge request body", 400);
}

const currentKey = getIssuerKey(payload.signatureType);
const { issuer } = getIssuerInfo();
// generate a VC for the given payload
return void issueChallengeCredential(DIDKit, currentKey, record, payload.signatureType)
return void issueChallengeCredential(
DIDKit,
issuer.did,
record,
payload.signatureType,
)
.then((credential) => {
// return the verifiable credential
return res.json(credential as CredentialResponseBody);
})
.catch((error): any => {
if (error) {
// return error msg indicating a failure producing VC
return void errorRes(res, "Unable to produce a verifiable credential", 400);
return void errorRes(
res,
"Unable to produce a verifiable credential",
400,
);
}
});
} else {
// return error message if an error present
// limit the error message string to 1000 chars
return void errorRes(
res,
(challenge.error && challenge.error.join(", ").substring(0, 1000)) || "Unable to verify proofs",
403
(challenge.error && challenge.error.join(", ").substring(0, 1000)) ||
"Unable to verify proofs",
403,
);
}
}

if (!payload.address) {
return void errorRes(res, "Missing address from challenge request body", 400);
return void errorRes(
res,
"Missing address from challenge request body",
400,
);
}

if (!payload.type) {
Expand Down
32 changes: 0 additions & 32 deletions embed/src/issuers.ts

This file was deleted.

Loading
Loading