Skip to content

Commit

Permalink
feat: basic support for multiple nullifiers, rotating keys (#3247)
Browse files Browse the repository at this point in the history
* feat: added Nullifier Generators

* feat(identity): added mishti nullifier generator

* chore: fixed existing tests

* test: added mishti nullifier test

* test: fix ceramic integration tests

* feat(identity): added key manager function

* feat(identity): using key rotation in credentials

* chore: updated getIssuerKey => getIssuerInfo, refactored keyManager

* chore: updating ban check to work with new format

* feat(identity): added ignorable errors when generating nullifiers

* feat(identity): ignore mishti errors, rename mishti -> human network

* feat(identity): added execution limiting to nullifiers

* feat: feature flagging multiple nullifiers

* chore: rebase fixes

* fix: fix issues with legacy hash format support, added tests

* feat: update .env-example in iam and embed

---------

Co-authored-by: Gerald Iakobinyi-Pich <[email protected]>
  • Loading branch information
lucianHymer and nutrina authored Feb 24, 2025
1 parent facea14 commit ff9e397
Show file tree
Hide file tree
Showing 32 changed files with 2,472 additions and 881 deletions.
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

0 comments on commit ff9e397

Please sign in to comment.