From 269de0acedda4ab79b55f9a11f15d2c5faf9cdda Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Tue, 12 Sep 2023 20:36:31 -0600 Subject: [PATCH 1/7] feat(contracts): wip verify didkit signed credential --- contracts/GitcoinStampVerifier.sol | 140 +++++++++++++++++++++++ test/GitcoinStampVerifier.ts | 171 +++++++++++++++++++++++++++++ 2 files changed, 311 insertions(+) create mode 100644 contracts/GitcoinStampVerifier.sol create mode 100644 test/GitcoinStampVerifier.ts diff --git a/contracts/GitcoinStampVerifier.sol b/contracts/GitcoinStampVerifier.sol new file mode 100644 index 0000000..2284f11 --- /dev/null +++ b/contracts/GitcoinStampVerifier.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.4; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { VcVerifier } from "./VCVerifier.sol"; +import { DIDpkhAdapter } from "./DIDpkhAdapter.sol"; +import { AttestationStation } from "./AttestationStation.sol"; + +struct CredentialSubject { + // underscored since hash is a reserved keyword + string _hash; + string id; + string provider; +} + +struct Proof { + // underscored since @ is not valid for struct member + string _context; + string created; + string proofPurpose; + // underscored since typoe is a reserved keyword + string _type; + string verificationMethod; +} + +struct Document { + // underscored since @ is not valid for struct member + string _context; + CredentialSubject credentialSubject; + string expirationDate; + string issuanceDate; + string issuer; + Proof proof; + // underscored since @ is not valid for struct member + string[] _type; +} + +contract DIDStampVcVerifier is VcVerifier, DIDpkhAdapter { + bytes32 private constant PROOF_TYPE_HASH = + keccak256("Proof(string @context,string created,string proofPurpose,string type,string verificationMethod)"); + + bytes32 private constant CREDENTIAL_SUBJECT_TYPEHASH = + keccak256("CredentialSubject(string hash,string id,string provider)"); + + bytes32 private constant DOCUMENT_TYPEHASH = + keccak256( + "Document(string @context,CredentialSubject credentialSubject,string expirationDate,string issuanceDate,string issuer,Proof proof,string[] type)CredentialSubject(string hash,string id,string provider)Proof(string @context,string created,string proofPurpose,string type,string verificationMethod)" + ); + + address public _verifier; + address public _attestationStation; + + AttestationStation.AttestationData[] public _attestations; + + event Verified(string indexed id, string iamHash, string provider); + + mapping(string => string) public verifiedStamps; + + constructor(string memory domainName, address verifier, address attestationStation) VcVerifier(domainName) { + _verifier = verifier; + _attestationStation = attestationStation; + } + + function hashCredentialSubject(CredentialSubject calldata subject) public pure returns (bytes32) { + return + keccak256( + abi.encode( + CREDENTIAL_SUBJECT_TYPEHASH, + keccak256(bytes(subject._hash)), + keccak256(bytes(subject.id)), + keccak256(bytes(subject.provider)) + ) + ); + } + + function hashCredentialProof(Proof calldata proof) public pure returns (bytes32) { + return + keccak256( + abi.encode( + PROOF_TYPE_HASH, + keccak256(bytes(proof._context)), + keccak256(bytes(proof.created)), + keccak256(bytes(proof.proofPurpose)), + keccak256(bytes(proof._type)), + keccak256(bytes(proof.verificationMethod)) + ) + ); + } + + function hashDocument(Document calldata document) public pure returns (bytes32) { + bytes32 credentialSubjectHash = hashCredentialSubject(document.credentialSubject); + bytes32 proofHash = hashCredentialProof(document.proof); + + return + keccak256( + abi.encode( + DOCUMENT_TYPEHASH, + keccak256(bytes(document._context)), + credentialSubjectHash, + keccak256(bytes(document.expirationDate)), + keccak256(bytes(document.issuanceDate)), + keccak256(bytes(document.issuer)), + proofHash, + _hashArray(document._type) + ) + ); + } + + function verifyStampVc(Document calldata document, uint8 v, bytes32 r, bytes32 s) public returns (bool) { + bytes32 vcHash = hashDocument(document); + bytes32 digest = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR, vcHash); + + address issuerAddress = DIDpkhAdapter.pseudoResolveDidIssuer(document.issuer); + + address recoveredAddress = ECDSA.recover(digest, v, r, s); + + // Here we could check the issuer's address against an on-chain registry. + // We could provide a verifying contract address when signing the credential which could correspond to this contract + require(recoveredAddress == issuerAddress, "VC verification failed issuer does not match signature"); + + verifiedStamps[document.credentialSubject.id] = document.credentialSubject._hash; + + emit Verified( + document.credentialSubject.id, + document.credentialSubject._hash, + document.credentialSubject.provider + ); + + AttestationStation attestationStation = AttestationStation(_attestationStation); + AttestationStation.AttestationData memory attestation = AttestationStation.AttestationData( + msg.sender, + "Verified", + "yes" + ); + _attestations.push(attestation); + + attestationStation.attest(_attestations); + return true; + } +} diff --git a/test/GitcoinStampVerifier.ts b/test/GitcoinStampVerifier.ts new file mode 100644 index 0000000..b8aa6f5 --- /dev/null +++ b/test/GitcoinStampVerifier.ts @@ -0,0 +1,171 @@ +import { Signature } from "ethers"; +import { ethers } from "hardhat"; +import { PromiseOrValue } from "../typechain-types/common"; +import { expect } from "chai"; + +export type ProofStruct = { + _context: PromiseOrValue; + created: PromiseOrValue; + proofPurpose: PromiseOrValue; + _type: PromiseOrValue; + verificationMethod: PromiseOrValue; +}; + +export type CredentialSubjectStruct = { + _hash: PromiseOrValue; + id: PromiseOrValue; + provider: PromiseOrValue; +}; + +export type DocumentStruct = { + _context: string[]; + credentialSubject: CredentialSubjectStruct; + expirationDate: PromiseOrValue; + issuanceDate: PromiseOrValue; + issuer: PromiseOrValue; + proof: ProofStruct; + _type: PromiseOrValue[]; +}; + +export interface DIDCredential { + "@context": string[]; + type?: string[]; + credentialSubject: CredentialSubject; + issuer: string; + issuanceDate: string; + proof: Proof; + expirationDate: string; +} +export interface CredentialSubject { + id: string; + provider: string; + hash: string; + customInfo: { + [key: string]: string; + }; + "@context": { + customInfo: string; + hash: string; + metaPointer: string; + provider: string; + }; +} +export interface Proof { + type: string; + created: string; + "@context": string; + proofValue: string; + proofPurpose: string; + verificationMethod: string; +} +export interface Eip712Domain { + domain: Domain; + primaryType: string; + types: Types; +} +export interface Domain { + name: string; +} +export interface Types { + CredentialSubject?: + | CredentialSubjectEntityOrDocumentEntityOrEIP712DomainEntityOrProofEntity[] + | null; + Document?: + | CredentialSubjectEntityOrDocumentEntityOrEIP712DomainEntityOrProofEntity[] + | null; + EIP712Domain?: + | CredentialSubjectEntityOrDocumentEntityOrEIP712DomainEntityOrProofEntity[] + | null; + Proof?: + | CredentialSubjectEntityOrDocumentEntityOrEIP712DomainEntityOrProofEntity[] + | null; +} +export interface CredentialSubjectEntityOrDocumentEntityOrEIP712DomainEntityOrProofEntity { + name: string; + type: string; +} + +export const normalizeDIDCredential = (credential: DIDCredential) => { + const normalizedCredential = {} as DocumentStruct; + const normalizedSubject = {} as CredentialSubjectStruct; + const normalizedProof = {} as ProofStruct; + + normalizedSubject["id"] = credential.credentialSubject.id; + normalizedSubject["provider"] = credential.credentialSubject.provider; + normalizedSubject["_hash"] = credential.credentialSubject.hash; + + normalizedProof["_context"] = credential.proof["@context"]; + normalizedProof["created"] = credential.proof.created; + normalizedProof["proofPurpose"] = credential.proof.proofPurpose; + normalizedProof["_type"] = credential.proof.type; + normalizedProof["verificationMethod"] = credential.proof.verificationMethod; + + normalizedCredential["_context"] = credential["@context"]; + normalizedCredential["credentialSubject"] = normalizedSubject; + normalizedCredential["expirationDate"] = credential.expirationDate; + normalizedCredential["issuanceDate"] = credential.issuanceDate; + normalizedCredential["issuer"] = credential.issuer; + normalizedCredential["proof"] = normalizedProof; + + if (credential.type) { + normalizedCredential["_type"] = credential.type; + } + + return normalizedCredential; +}; + +const sampleCredential = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1", + ], + type: ["VerifiableCredential"], + credentialSubject: { + id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", + "@context": { + customInfo: "https://schema.org/Thing", + hash: "https://schema.org/Text", + metaPointer: "https://schema.org/URL", + provider: "https://schema.org/Text", + }, + provider: "githubAccountCreationGte#90", + customInfo: {}, + hash: "v0.0.0:KkYaKn2GaF55a3y/n6myG9kfrpQPHW5DnhhzO9APTGI=", + }, + issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", + issuanceDate: "2023-09-13T02:12:37.417Z", + proof: { + type: "EthereumEip712Signature2021", + created: "2023-09-13T02:12:37.422Z", + "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", + proofValue: + "0x17d27bfcd590ce7bd26b1c6ebd953d9f390f2089279403733acf874366f768697dab73a4fdb1ea380a0feea129b521245d1b2ffcd69b4e2586cda64b3c7c770a1c", + proofPurpose: "assertionMethod", + verificationMethod: + "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", + }, + expirationDate: "2023-12-12T03:12:37.417Z", +}; + +describe.only("Signature for GitcoinStampVerifier", () => { + it("should split the signature into r, s, v", () => { + const signature = Signature.from(sampleCredential.proof.proofValue); + const { r, s, v } = signature; + expect(r).to.be.equal( + "0x17d27bfcd590ce7bd26b1c6ebd953d9f390f2089279403733acf874366f76869" + ); + expect(s).to.be.equal( + "0x7dab73a4fdb1ea380a0feea129b521245d1b2ffcd69b4e2586cda64b3c7c770a" + ); + + expect(v).to.be.equal(28); + }); + it("should normalize the credential for writing on chain", () => { + const normalizedCredential = normalizeDIDCredential(sampleCredential); + debugger; + expect(normalizedCredential._context).to.be.deep.equal([ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1", + ]); + }); +}); From dd3c628247d878cb778c71a0522b2685e1f3a287 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Wed, 13 Sep 2023 17:11:53 -0600 Subject: [PATCH 2/7] feat(contracts): seemingly correct hashing functions --- contracts/GitcoinStampVerifier.sol | 198 ++++++++++++++-------- test/GitcoinStampVerifier.ts | 264 +++++++++++++++++++++++++---- 2 files changed, 356 insertions(+), 106 deletions(-) diff --git a/contracts/GitcoinStampVerifier.sol b/contracts/GitcoinStampVerifier.sol index 2284f11..e413170 100644 --- a/contracts/GitcoinStampVerifier.sol +++ b/contracts/GitcoinStampVerifier.sol @@ -1,74 +1,132 @@ -// SPDX-License-Identifier: MIT -pragma solidity >=0.8.4; +// SPDX-License-Identifier: GPL +pragma solidity ^0.8.9; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { VcVerifier } from "./VCVerifier.sol"; -import { DIDpkhAdapter } from "./DIDpkhAdapter.sol"; -import { AttestationStation } from "./AttestationStation.sol"; - -struct CredentialSubject { - // underscored since hash is a reserved keyword - string _hash; - string id; - string provider; -} -struct Proof { - // underscored since @ is not valid for struct member - string _context; - string created; - string proofPurpose; - // underscored since typoe is a reserved keyword - string _type; - string verificationMethod; -} +import "hardhat/console.sol"; -struct Document { - // underscored since @ is not valid for struct member - string _context; - CredentialSubject credentialSubject; - string expirationDate; - string issuanceDate; - string issuer; - Proof proof; - // underscored since @ is not valid for struct member - string[] _type; -} +import "./GitcoinAttester.sol"; + +/** + * @title GitcoinVerifier + * @notice This contract is used to verify a passport's authenticity and to add a passport to the GitcoinAttester contract using the verifyAndAttest() function. + */ +contract GitcoinStampVerifier { + using ECDSA for bytes32; + + // Address of the issuer of the passport + address public issuer; + + // Domain Separator, as defined by EIP-712 (`hashstruct(eip712Domain)`) + bytes32 private DOMAIN_SEPARATOR; + + // Name of the contract + string public name; + + struct Proof { + // underscored since @ is not valid for struct member + string _context; + // underscored since typoe is a reserved keyword + string _type; + string proofPurpose; + string verificationMethod; + string created; + } + + struct CredentialSubjectContext { + string customInfo; + string _hash; + string metaPointer; + string provider; + } + + struct Customifo { + string description; + } + + struct CredentialSubject { + string id; + CredentialSubjectContext _context; + string provider; + // Customifo customInfo; + // underscored since hash is a reserved keyword + string _hash; + } + + + struct Document { + // underscored since @ is not valid for struct member + string[] _context; + // underscored since @ is not valid for struct member + string[] _type; + CredentialSubject credentialSubject; + string issuer; + string issuanceDate; + Proof proof; + string expirationDate; + } -contract DIDStampVcVerifier is VcVerifier, DIDpkhAdapter { bytes32 private constant PROOF_TYPE_HASH = - keccak256("Proof(string @context,string created,string proofPurpose,string type,string verificationMethod)"); + keccak256("Proof(string @context,string type,string proofPurpose,string verificationMethod,string created)"); + + bytes32 private constant CREDENTIAL_SUBJECT_CONTEXT_TYPEHASH = + keccak256("CredentialSubjectContext(string customInfo,string hash,string metaPointer,string provider)"); + + // bytes32 private constant CUSTOMINFO_TYPEHASH = keccak256("CustomInfo(string description)"); bytes32 private constant CREDENTIAL_SUBJECT_TYPEHASH = - keccak256("CredentialSubject(string hash,string id,string provider)"); + keccak256( + "CredentialSubject(string id,CredentialSubjectContext @context,string provider,string hash)" + ); bytes32 private constant DOCUMENT_TYPEHASH = keccak256( - "Document(string @context,CredentialSubject credentialSubject,string expirationDate,string issuanceDate,string issuer,Proof proof,string[] type)CredentialSubject(string hash,string id,string provider)Proof(string @context,string created,string proofPurpose,string type,string verificationMethod)" + "Document(string[] @context,string[] type,CredentialSubject credentialSubject,string issuer,string issuanceDate,Proof proof,string expirationDate)" ); - address public _verifier; - address public _attestationStation; + bytes32 private constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version)"); - AttestationStation.AttestationData[] public _attestations; + /** + * @notice Initializer function responsible for setting up the contract's initial state. + * @param _issuer The address of the issuer of the passport. + */ + function initialize(address _issuer) public { - event Verified(string indexed id, string iamHash, string provider); + issuer = _issuer; + name = "Gitcoin Passport Verifiable Credential of Stamp data"; - mapping(string => string) public verifiedStamps; + DOMAIN_SEPARATOR = keccak256( + abi.encode( + EIP712DOMAIN_TYPEHASH, + keccak256(bytes(name)), + keccak256(bytes("0")) + ) + ); + } - constructor(string memory domainName, address verifier, address attestationStation) VcVerifier(domainName) { - _verifier = verifier; - _attestationStation = attestationStation; + function hashCredentialSubjectContext(CredentialSubjectContext calldata context) public pure returns (bytes32) { + return keccak256( + abi.encode( + CREDENTIAL_SUBJECT_CONTEXT_TYPEHASH, + keccak256(bytes(context.customInfo)), + keccak256(bytes(context._hash)), + keccak256(bytes(context.metaPointer)), + keccak256(bytes(context.provider)) + ) + ); } + function hashCredentialSubject(CredentialSubject calldata subject) public pure returns (bytes32) { + bytes32 credentialSubjectContext = hashCredentialSubjectContext(subject._context); return keccak256( abi.encode( CREDENTIAL_SUBJECT_TYPEHASH, - keccak256(bytes(subject._hash)), keccak256(bytes(subject.id)), - keccak256(bytes(subject.provider)) + credentialSubjectContext, + keccak256(bytes(subject.provider)), + keccak256(bytes(subject._hash)) ) ); } @@ -79,62 +137,54 @@ contract DIDStampVcVerifier is VcVerifier, DIDpkhAdapter { abi.encode( PROOF_TYPE_HASH, keccak256(bytes(proof._context)), - keccak256(bytes(proof.created)), - keccak256(bytes(proof.proofPurpose)), keccak256(bytes(proof._type)), - keccak256(bytes(proof.verificationMethod)) + keccak256(bytes(proof.proofPurpose)), + keccak256(bytes(proof.verificationMethod)), + keccak256(bytes(proof.created)) ) ); } + function _hashArray(string[] calldata array) internal pure returns (bytes32 result) { + bytes32[] memory _array = new bytes32[](array.length); + for (uint256 i = 0; i < array.length; ++i) { + _array[i] = keccak256(bytes(array[i])); + } + result = keccak256(abi.encodePacked(_array)); + } + function hashDocument(Document calldata document) public pure returns (bytes32) { bytes32 credentialSubjectHash = hashCredentialSubject(document.credentialSubject); bytes32 proofHash = hashCredentialProof(document.proof); + console.log(document._context, "document._context"); return keccak256( abi.encode( DOCUMENT_TYPEHASH, - keccak256(bytes(document._context)), + _hashArray(document._context), + _hashArray(document._type), credentialSubjectHash, - keccak256(bytes(document.expirationDate)), - keccak256(bytes(document.issuanceDate)), keccak256(bytes(document.issuer)), + keccak256(bytes(document.issuanceDate)), proofHash, - _hashArray(document._type) + keccak256(bytes(document.expirationDate)) ) ); } - function verifyStampVc(Document calldata document, uint8 v, bytes32 r, bytes32 s) public returns (bool) { + function verifyStampVc(Document calldata document, uint8 v, bytes32 r, bytes32 s) public view returns (bool) { bytes32 vcHash = hashDocument(document); bytes32 digest = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR, vcHash); - address issuerAddress = DIDpkhAdapter.pseudoResolveDidIssuer(document.issuer); - address recoveredAddress = ECDSA.recover(digest, v, r, s); + console.log(recoveredAddress, issuer, "recoveredAddress, issuer, "); + // Here we could check the issuer's address against an on-chain registry. // We could provide a verifying contract address when signing the credential which could correspond to this contract - require(recoveredAddress == issuerAddress, "VC verification failed issuer does not match signature"); - - verifiedStamps[document.credentialSubject.id] = document.credentialSubject._hash; - - emit Verified( - document.credentialSubject.id, - document.credentialSubject._hash, - document.credentialSubject.provider - ); - - AttestationStation attestationStation = AttestationStation(_attestationStation); - AttestationStation.AttestationData memory attestation = AttestationStation.AttestationData( - msg.sender, - "Verified", - "yes" - ); - _attestations.push(attestation); + require(recoveredAddress == issuer, "VC verification failed issuer does not match signature"); - attestationStation.attest(_attestations); return true; } } diff --git a/test/GitcoinStampVerifier.ts b/test/GitcoinStampVerifier.ts index b8aa6f5..4667838 100644 --- a/test/GitcoinStampVerifier.ts +++ b/test/GitcoinStampVerifier.ts @@ -2,29 +2,44 @@ import { Signature } from "ethers"; import { ethers } from "hardhat"; import { PromiseOrValue } from "../typechain-types/common"; import { expect } from "chai"; +import { GitcoinStampVerifier__factory } from "../typechain-types"; export type ProofStruct = { _context: PromiseOrValue; - created: PromiseOrValue; - proofPurpose: PromiseOrValue; _type: PromiseOrValue; + proofPurpose: PromiseOrValue; + // proofValue?: PromiseOrValue; verificationMethod: PromiseOrValue; + created: PromiseOrValue; }; +export type CredentialSubjectContext = { + customInfo: string; + _hash: string; + metaPointer: string; + provider: string; +}; + +// export type CustomInfoStruct = { +// description: string; +// }; + export type CredentialSubjectStruct = { - _hash: PromiseOrValue; id: PromiseOrValue; + _context: CredentialSubjectContext; provider: PromiseOrValue; + // customInfo: CustomInfoStruct; + _hash: PromiseOrValue; }; export type DocumentStruct = { _context: string[]; + _type: PromiseOrValue[]; credentialSubject: CredentialSubjectStruct; - expirationDate: PromiseOrValue; - issuanceDate: PromiseOrValue; issuer: PromiseOrValue; + issuanceDate: PromiseOrValue; proof: ProofStruct; - _type: PromiseOrValue[]; + expirationDate: PromiseOrValue; }; export interface DIDCredential { @@ -40,9 +55,9 @@ export interface CredentialSubject { id: string; provider: string; hash: string; - customInfo: { - [key: string]: string; - }; + // customInfo: { + // description: string; + // }; "@context": { customInfo: string; hash: string; @@ -89,11 +104,28 @@ export const normalizeDIDCredential = (credential: DIDCredential) => { const normalizedCredential = {} as DocumentStruct; const normalizedSubject = {} as CredentialSubjectStruct; const normalizedProof = {} as ProofStruct; + const normalizedCredentialContext = {} as { + customInfo: string; + _hash: string; + metaPointer: string; + provider: string; + }; normalizedSubject["id"] = credential.credentialSubject.id; normalizedSubject["provider"] = credential.credentialSubject.provider; normalizedSubject["_hash"] = credential.credentialSubject.hash; + normalizedCredentialContext["customInfo"] = + credential.credentialSubject["@context"]["customInfo"]; + normalizedCredentialContext["_hash"] = + credential.credentialSubject["@context"]["hash"]; + normalizedCredentialContext["metaPointer"] = + credential.credentialSubject["@context"]["metaPointer"]; + normalizedCredentialContext["provider"] = + credential.credentialSubject["@context"]["provider"]; + + normalizedSubject["_context"] = normalizedCredentialContext; + normalizedProof["_context"] = credential.proof["@context"]; normalizedProof["created"] = credential.proof.created; normalizedProof["proofPurpose"] = credential.proof.proofPurpose; @@ -115,36 +147,176 @@ export const normalizeDIDCredential = (credential: DIDCredential) => { }; const sampleCredential = { + type: ["VerifiableCredential"], + proof: { + type: "EthereumEip712Signature2021", + created: "2023-09-13T21:25:13.163Z", + "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", + proofValue: + "0x1139ee60eab7323a7ec73427ece720642e93b1e29ab854c6942fc04d1b08aeaa16cd6e158f721eec74d115fb8e3ea29b0dd1f4b37167325e67a905bd5abc72431b", + eip712Domain: { + types: { + Proof: [ + { + name: "@context", + type: "string", + }, + { + name: "type", + type: "string", + }, + { + name: "proofPurpose", + type: "string", + }, + { + name: "proofValue", + type: "string", + }, + { + name: "verificationMethod", + type: "string", + }, + { + name: "created", + type: "string", + }, + ], + Context: [ + { + name: "customInfo", + type: "string", + }, + { + name: "hash", + type: "string", + }, + { + name: "metaPointer", + type: "string", + }, + { + name: "provider", + type: "string", + }, + ], + Document: [ + { + name: "@context", + type: "string[]", + }, + { + name: "type", + type: "string[]", + }, + { + name: "issuer", + type: "string", + }, + { + name: "issuanceDate", + type: "string", + }, + { + name: "expirationDate", + type: "string", + }, + { + name: "credentialSubject", + type: "CredentialSubject", + }, + { + name: "proof", + type: "Proof", + }, + { + name: "credentialStatus", + type: "CredentialStatus", + }, + ], + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, + ], + CredentialStatus: [ + { + name: "id", + type: "string", + }, + { + name: "type", + type: "string", + }, + { + name: "statusPurpose", + type: "string", + }, + { + name: "statusListIndex", + type: "string", + }, + { + name: "statusListCredential", + type: "string", + }, + ], + CredentialSubject: [ + { + name: "id", + type: "string", + }, + { + name: "provider", + type: "string", + }, + { + name: "metaPointer", + type: "string", + }, + { + name: "hash", + type: "string", + }, + { + name: "@context", + type: "Context", + }, + ], + }, + domain: { + name: "Gitcoin Passport Verifiable Credential of Stamp data", + version: "0", + }, + primaryType: "Document", + }, + proofPurpose: "assertionMethod", + verificationMethod: + "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", + }, + issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", "@context": [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/vc/status-list/2021/v1", ], - type: ["VerifiableCredential"], + issuanceDate: "2023-09-13T21:25:13.150Z", + expirationDate: "2023-12-12T22:25:13.150Z", credentialSubject: { id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", + hash: "v0.0.0:0xRiw2BMLTItJg8iicfNTLlzQumcF37sKIifVthEx9Y=", "@context": { - customInfo: "https://schema.org/Thing", hash: "https://schema.org/Text", - metaPointer: "https://schema.org/URL", provider: "https://schema.org/Text", + customInfo: "https://schema.org/Thing", + metaPointer: "https://schema.org/URL", }, - provider: "githubAccountCreationGte#90", - customInfo: {}, - hash: "v0.0.0:KkYaKn2GaF55a3y/n6myG9kfrpQPHW5DnhhzO9APTGI=", + provider: "EthGTEOneTxnProvider", }, - issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", - issuanceDate: "2023-09-13T02:12:37.417Z", - proof: { - type: "EthereumEip712Signature2021", - created: "2023-09-13T02:12:37.422Z", - "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", - proofValue: - "0x17d27bfcd590ce7bd26b1c6ebd953d9f390f2089279403733acf874366f768697dab73a4fdb1ea380a0feea129b521245d1b2ffcd69b4e2586cda64b3c7c770a1c", - proofPurpose: "assertionMethod", - verificationMethod: - "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", - }, - expirationDate: "2023-12-12T03:12:37.417Z", }; describe.only("Signature for GitcoinStampVerifier", () => { @@ -152,20 +324,48 @@ describe.only("Signature for GitcoinStampVerifier", () => { const signature = Signature.from(sampleCredential.proof.proofValue); const { r, s, v } = signature; expect(r).to.be.equal( - "0x17d27bfcd590ce7bd26b1c6ebd953d9f390f2089279403733acf874366f76869" + "0x1139ee60eab7323a7ec73427ece720642e93b1e29ab854c6942fc04d1b08aeaa" ); expect(s).to.be.equal( - "0x7dab73a4fdb1ea380a0feea129b521245d1b2ffcd69b4e2586cda64b3c7c770a" + "0x16cd6e158f721eec74d115fb8e3ea29b0dd1f4b37167325e67a905bd5abc7243" ); - expect(v).to.be.equal(28); + expect(v).to.be.equal(27); }); it("should normalize the credential for writing on chain", () => { const normalizedCredential = normalizeDIDCredential(sampleCredential); - debugger; + expect(normalizedCredential._context).to.be.deep.equal([ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/vc/status-list/2021/v1", ]); }); + it("should verify the VC signature on chain", async () => { + const normalizedCredential = normalizeDIDCredential( + sampleCredential + ) as DocumentStruct; + const [signer] = await ethers.getSigners(); + + const GitcoinStampVerifier = await ethers.getContractFactory( + "GitcoinStampVerifier" + ); + + const gitcoinStampVerifier = await GitcoinStampVerifier.deploy(); + + await gitcoinStampVerifier.initialize( + "0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e" + ); + const signature = Signature.from(sampleCredential.proof.proofValue); + const { r, s, v } = signature; + + debugger; + const tx = await gitcoinStampVerifier.verifyStampVc( + normalizedCredential, + v, + r, + s + ); + + expect(tx).to.be.true; + }); }); From 158a65fd47ea8b30d8a0d587e36cdf1b9f4ee022 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Wed, 13 Sep 2023 20:46:00 -0600 Subject: [PATCH 3/7] feat(contracts): wip stamp verification --- contracts/GitcoinStampVerifier.sol | 22 +- test/GitcoinStampVerifier.ts | 335 ++++++++++++++++++++++------- 2 files changed, 274 insertions(+), 83 deletions(-) diff --git a/contracts/GitcoinStampVerifier.sol b/contracts/GitcoinStampVerifier.sol index e413170..0852d86 100644 --- a/contracts/GitcoinStampVerifier.sol +++ b/contracts/GitcoinStampVerifier.sol @@ -104,7 +104,11 @@ contract GitcoinStampVerifier { ); } - function hashCredentialSubjectContext(CredentialSubjectContext calldata context) public pure returns (bytes32) { + function hashCredentialSubjectContext(CredentialSubjectContext calldata context) public view returns (bytes32) { + console.log(context.customInfo); + console.log(context._hash); + console.log(context.metaPointer); + console.log(context.provider); return keccak256( abi.encode( CREDENTIAL_SUBJECT_CONTEXT_TYPEHASH, @@ -117,8 +121,12 @@ contract GitcoinStampVerifier { } - function hashCredentialSubject(CredentialSubject calldata subject) public pure returns (bytes32) { + function hashCredentialSubject(CredentialSubject calldata subject) public view returns (bytes32) { + console.log(subject.id); + console.log(subject.provider); + console.log(subject._hash); bytes32 credentialSubjectContext = hashCredentialSubjectContext(subject._context); + return keccak256( abi.encode( @@ -131,7 +139,12 @@ contract GitcoinStampVerifier { ); } - function hashCredentialProof(Proof calldata proof) public pure returns (bytes32) { + function hashCredentialProof(Proof calldata proof) public view returns (bytes32) { + console.log(proof._context); + console.log(proof._type); + console.log(proof.proofPurpose); + console.log(proof.verificationMethod); + console.log(proof.created); return keccak256( abi.encode( @@ -153,11 +166,10 @@ contract GitcoinStampVerifier { result = keccak256(abi.encodePacked(_array)); } - function hashDocument(Document calldata document) public pure returns (bytes32) { + function hashDocument(Document calldata document) public view returns (bytes32) { bytes32 credentialSubjectHash = hashCredentialSubject(document.credentialSubject); bytes32 proofHash = hashCredentialProof(document.proof); - console.log(document._context, "document._context"); return keccak256( abi.encode( diff --git a/test/GitcoinStampVerifier.ts b/test/GitcoinStampVerifier.ts index 4667838..ea43db1 100644 --- a/test/GitcoinStampVerifier.ts +++ b/test/GitcoinStampVerifier.ts @@ -114,6 +114,7 @@ export const normalizeDIDCredential = (credential: DIDCredential) => { normalizedSubject["id"] = credential.credentialSubject.id; normalizedSubject["provider"] = credential.credentialSubject.provider; normalizedSubject["_hash"] = credential.credentialSubject.hash; + normalizedSubject["_context"] = normalizedCredentialContext; normalizedCredentialContext["customInfo"] = credential.credentialSubject["@context"]["customInfo"]; @@ -124,8 +125,6 @@ export const normalizeDIDCredential = (credential: DIDCredential) => { normalizedCredentialContext["provider"] = credential.credentialSubject["@context"]["provider"]; - normalizedSubject["_context"] = normalizedCredentialContext; - normalizedProof["_context"] = credential.proof["@context"]; normalizedProof["created"] = credential.proof.created; normalizedProof["proofPurpose"] = credential.proof.proofPurpose; @@ -147,57 +146,128 @@ export const normalizeDIDCredential = (credential: DIDCredential) => { }; const sampleCredential = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1", + ], type: ["VerifiableCredential"], + credentialSubject: { + id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", + provider: "githubAccountCreationGte#90", + hash: "v0.0.0:KkYaKn2GaF55a3y/n6myG9kfrpQPHW5DnhhzO9APTGI=", + "@context": { + customInfo: "https://schema.org/Thing", + hash: "https://schema.org/Text", + metaPointer: "https://schema.org/URL", + provider: "https://schema.org/Text", + }, + }, + issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", + issuanceDate: "2023-09-14T02:25:53.645Z", proof: { - type: "EthereumEip712Signature2021", - created: "2023-09-13T21:25:13.163Z", "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", + type: "EthereumEip712Signature2021", + proofPurpose: "assertionMethod", proofValue: - "0x1139ee60eab7323a7ec73427ece720642e93b1e29ab854c6942fc04d1b08aeaa16cd6e158f721eec74d115fb8e3ea29b0dd1f4b37167325e67a905bd5abc72431b", + "0x08527479a9e8e4cfcabb0266bdd48b306999a354ad861663e8057c4ed6690c1a1797ae960cca559184988d841c86571a737a6a36938408c7b9d1231f8ced141a1b", + verificationMethod: + "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", + created: "2023-09-14T02:25:53.650Z", eip712Domain: { + domain: { + name: "Gitcoin Passport Verifiable Credential of Stamp data", + version: "0", + }, + primaryType: "Document", types: { + Context: [ + { name: "customInfo", type: "string" }, + { name: "hash", type: "string" }, + { name: "metaPointer", type: "string" }, + { name: "provider", type: "string" }, + ], + CredentialSubject: [ + { name: "id", type: "string" }, + { name: "provider", type: "string" }, + { name: "hash", type: "string" }, + { name: "@context", type: "Context" }, + ], + Document: [ + { name: "@context", type: "string[]" }, + { name: "type", type: "string[]" }, + { name: "issuer", type: "string" }, + { name: "issuanceDate", type: "string" }, + { name: "expirationDate", type: "string" }, + { name: "credentialSubject", type: "CredentialSubject" }, + { name: "proof", type: "Proof" }, + ], + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + ], Proof: [ + { name: "@context", type: "string" }, + { name: "type", type: "string" }, + { name: "proofPurpose", type: "string" }, + { name: "proofValue", type: "string" }, + { name: "verificationMethod", type: "string" }, + { name: "created", type: "string" }, + ], + }, + }, + }, + expirationDate: "2023-12-13T03:25:53.645Z", +}; + +const prepareCredential = { + proof: { + "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", + type: "EthereumEip712Signature2021", + proofPurpose: "assertionMethod", + verificationMethod: + "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", + created: "2023-09-14T02:25:53.667Z", + eip712Domain: { + domain: { + name: "Gitcoin Passport Verifiable Credential of Stamp data", + version: "0", + }, + primaryType: "Document", + types: { + Context: [ { - name: "@context", - type: "string", - }, - { - name: "type", - type: "string", - }, - { - name: "proofPurpose", + name: "customInfo", type: "string", }, { - name: "proofValue", + name: "hash", type: "string", }, { - name: "verificationMethod", + name: "metaPointer", type: "string", }, { - name: "created", + name: "provider", type: "string", }, ], - Context: [ + CredentialSubject: [ { - name: "customInfo", + name: "id", type: "string", }, { - name: "hash", + name: "provider", type: "string", }, { - name: "metaPointer", + name: "hash", type: "string", }, { - name: "provider", - type: "string", + name: "@context", + type: "Context", }, ], Document: [ @@ -229,10 +299,6 @@ const sampleCredential = { name: "proof", type: "Proof", }, - { - name: "credentialStatus", - type: "CredentialStatus", - }, ], EIP712Domain: [ { @@ -244,9 +310,9 @@ const sampleCredential = { type: "string", }, ], - CredentialStatus: [ + Proof: [ { - name: "id", + name: "@context", type: "string", }, { @@ -254,68 +320,165 @@ const sampleCredential = { type: "string", }, { - name: "statusPurpose", - type: "string", - }, - { - name: "statusListIndex", - type: "string", - }, - { - name: "statusListCredential", - type: "string", - }, - ], - CredentialSubject: [ - { - name: "id", + name: "proofPurpose", type: "string", }, { - name: "provider", + name: "proofValue", type: "string", }, { - name: "metaPointer", + name: "verificationMethod", type: "string", }, { - name: "hash", + name: "created", type: "string", }, - { - name: "@context", - type: "Context", - }, ], }, - domain: { - name: "Gitcoin Passport Verifiable Credential of Stamp data", - version: "0", - }, - primaryType: "Document", }, - proofPurpose: "assertionMethod", - verificationMethod: - "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", }, - issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/2021/v1", - ], - issuanceDate: "2023-09-13T21:25:13.150Z", - expirationDate: "2023-12-12T22:25:13.150Z", - credentialSubject: { - id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", - hash: "v0.0.0:0xRiw2BMLTItJg8iicfNTLlzQumcF37sKIifVthEx9Y=", - "@context": { - hash: "https://schema.org/Text", - provider: "https://schema.org/Text", - customInfo: "https://schema.org/Thing", - metaPointer: "https://schema.org/URL", + jwsHeader: null, + signingInput: { + types: { + EIP712Domain: [ + { + type: "string", + name: "name", + }, + { + type: "string", + name: "version", + }, + ], + CredentialSubject: [ + { + type: "string", + name: "id", + }, + { + type: "string", + name: "provider", + }, + { + type: "string", + name: "hash", + }, + { + type: "Context", + name: "@context", + }, + ], + Document: [ + { + type: "string[]", + name: "@context", + }, + { + type: "string[]", + name: "type", + }, + { + type: "string", + name: "issuer", + }, + { + type: "string", + name: "issuanceDate", + }, + { + type: "string", + name: "expirationDate", + }, + { + type: "CredentialSubject", + name: "credentialSubject", + }, + { + type: "Proof", + name: "proof", + }, + ], + Proof: [ + { + type: "string", + name: "@context", + }, + { + type: "string", + name: "type", + }, + { + type: "string", + name: "proofPurpose", + }, + { + type: "string", + name: "proofValue", + }, + { + type: "string", + name: "verificationMethod", + }, + { + type: "string", + name: "created", + }, + ], + Context: [ + { + type: "string", + name: "customInfo", + }, + { + type: "string", + name: "hash", + }, + { + type: "string", + name: "metaPointer", + }, + { + type: "string", + name: "provider", + }, + ], + }, + primaryType: "Document", + domain: { + name: "Gitcoin Passport Verifiable Credential of Stamp data", + version: "0", + }, + message: { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1", + ], + credentialSubject: { + "@context": { + customInfo: "https://schema.org/Thing", + hash: "https://schema.org/Text", + metaPointer: "https://schema.org/URL", + provider: "https://schema.org/Text", + }, + hash: "v0.0.0:KkYaKn2GaF55a3y/n6myG9kfrpQPHW5DnhhzO9APTGI=", + id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", + provider: "githubAccountCreationGte#90", + }, + expirationDate: "2023-12-13T03:25:53.645Z", + issuanceDate: "2023-09-14T02:25:53.645Z", + issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", + proof: { + "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", + created: "2023-09-14T02:25:53.667Z", + proofPurpose: "assertionMethod", + type: "EthereumEip712Signature2021", + verificationMethod: + "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", + }, + type: ["VerifiableCredential"], }, - provider: "EthGTEOneTxnProvider", }, }; @@ -324,13 +487,13 @@ describe.only("Signature for GitcoinStampVerifier", () => { const signature = Signature.from(sampleCredential.proof.proofValue); const { r, s, v } = signature; expect(r).to.be.equal( - "0x1139ee60eab7323a7ec73427ece720642e93b1e29ab854c6942fc04d1b08aeaa" + "0xd4952bda469284acffc0d8b7980a73894d1420b148f61af4e25cec43f4127ec8" ); expect(s).to.be.equal( - "0x16cd6e158f721eec74d115fb8e3ea29b0dd1f4b37167325e67a905bd5abc7243" + "0x773bb203302d75ea5e8248b58a073a8cc2bb616fec1dbd817a074db1436466c0" ); - expect(v).to.be.equal(27); + expect(v).to.be.equal(28); }); it("should normalize the credential for writing on chain", () => { const normalizedCredential = normalizeDIDCredential(sampleCredential); @@ -355,6 +518,22 @@ describe.only("Signature for GitcoinStampVerifier", () => { await gitcoinStampVerifier.initialize( "0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e" ); + + const standardizedTypes = prepareCredential.signingInput.types; + // @ts-ignore + delete standardizedTypes.EIP712Domain; + + const signerAddress = ethers.verifyTypedData( + prepareCredential.proof.eip712Domain.domain, + standardizedTypes, + sampleCredential, + sampleCredential.proof.proofValue + ); + + const signerIssuedCredential = + signerAddress.toLowerCase() === sampleCredential.issuer.split(":").pop(); + + expect(signerIssuedCredential).to.be.true; const signature = Signature.from(sampleCredential.proof.proofValue); const { r, s, v } = signature; From 0e8e3478e52a4bc6e2880e97c5cbce54eb09cd07 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Thu, 14 Sep 2023 10:04:47 -0600 Subject: [PATCH 4/7] wip signature verification --- test/GitcoinStampVerifier.ts | 463 +++++++++++++++++++++++++++-------- 1 file changed, 359 insertions(+), 104 deletions(-) diff --git a/test/GitcoinStampVerifier.ts b/test/GitcoinStampVerifier.ts index ea43db1..b18c319 100644 --- a/test/GitcoinStampVerifier.ts +++ b/test/GitcoinStampVerifier.ts @@ -3,6 +3,10 @@ import { ethers } from "hardhat"; import { PromiseOrValue } from "../typechain-types/common"; import { expect } from "chai"; import { GitcoinStampVerifier__factory } from "../typechain-types"; +import * as DIDKit from "@spruceid/didkit-wasm-node"; + +const key = + '{"kty":"EC","crv":"secp256k1","x":"PdB2nS-knyAxc6KPuxBr65vRpW-duAXwpeXlwGJ03eU","y":"MwoGZ08hF5uv-_UEC9BKsYdJVSbJNHcFhR1BZWer5RQ","d":"z9VrSNNZXf9ywUx3v_8cLDhSw8-pvAT9qu_WZmqqfWM"}'; export type ProofStruct = { _context: PromiseOrValue; @@ -153,8 +157,8 @@ const sampleCredential = { type: ["VerifiableCredential"], credentialSubject: { id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", - provider: "githubAccountCreationGte#90", - hash: "v0.0.0:KkYaKn2GaF55a3y/n6myG9kfrpQPHW5DnhhzO9APTGI=", + provider: "FirstEthTxnProvider", + hash: "v0.0.0:HLEH/2c+EIFqSwYbiYiCK1dGVH17mVbA23Ez5c7kr/Q=", "@context": { customInfo: "https://schema.org/Thing", hash: "https://schema.org/Text", @@ -163,16 +167,16 @@ const sampleCredential = { }, }, issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", - issuanceDate: "2023-09-14T02:25:53.645Z", + issuanceDate: "2023-09-14T11:29:41.676Z", proof: { "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", type: "EthereumEip712Signature2021", proofPurpose: "assertionMethod", proofValue: - "0x08527479a9e8e4cfcabb0266bdd48b306999a354ad861663e8057c4ed6690c1a1797ae960cca559184988d841c86571a737a6a36938408c7b9d1231f8ced141a1b", + "0x95a8ec9b47a747be0ebdc11787783a4830d5ac27d9601dd81dc8d933527f74ce70013dc6a3632783acd7608351405eb7fe5638892e217d3be76adc7ec4c296001c", verificationMethod: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", - created: "2023-09-14T02:25:53.650Z", + created: "2023-09-14T11:29:41.679Z", eip712Domain: { domain: { name: "Gitcoin Passport Verifiable Credential of Stamp data", @@ -181,42 +185,111 @@ const sampleCredential = { primaryType: "Document", types: { Context: [ - { name: "customInfo", type: "string" }, - { name: "hash", type: "string" }, - { name: "metaPointer", type: "string" }, - { name: "provider", type: "string" }, + { + name: "customInfo", + type: "string", + }, + { + name: "hash", + type: "string", + }, + { + name: "metaPointer", + type: "string", + }, + { + name: "provider", + type: "string", + }, ], CredentialSubject: [ - { name: "id", type: "string" }, - { name: "provider", type: "string" }, - { name: "hash", type: "string" }, - { name: "@context", type: "Context" }, + { + name: "id", + type: "string", + }, + { + name: "provider", + type: "string", + }, + { + name: "hash", + type: "string", + }, + { + name: "@context", + type: "Context", + }, ], Document: [ - { name: "@context", type: "string[]" }, - { name: "type", type: "string[]" }, - { name: "issuer", type: "string" }, - { name: "issuanceDate", type: "string" }, - { name: "expirationDate", type: "string" }, - { name: "credentialSubject", type: "CredentialSubject" }, - { name: "proof", type: "Proof" }, + { + name: "@context", + type: "string[]", + }, + { + name: "type", + type: "string[]", + }, + { + name: "issuer", + type: "string", + }, + { + name: "issuanceDate", + type: "string", + }, + { + name: "expirationDate", + type: "string", + }, + { + name: "credentialSubject", + type: "CredentialSubject", + }, + { + name: "proof", + type: "Proof", + }, ], EIP712Domain: [ - { name: "name", type: "string" }, - { name: "version", type: "string" }, + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, ], Proof: [ - { name: "@context", type: "string" }, - { name: "type", type: "string" }, - { name: "proofPurpose", type: "string" }, - { name: "proofValue", type: "string" }, - { name: "verificationMethod", type: "string" }, - { name: "created", type: "string" }, + { + name: "@context", + type: "string", + }, + { + name: "type", + type: "string", + }, + { + name: "proofPurpose", + type: "string", + }, + { + name: "proofValue", + type: "string", + }, + { + name: "verificationMethod", + type: "string", + }, + { + name: "created", + type: "string", + }, ], }, }, }, - expirationDate: "2023-12-13T03:25:53.645Z", + expirationDate: "2023-12-13T12:29:41.675Z", }; const prepareCredential = { @@ -226,7 +299,7 @@ const prepareCredential = { proofPurpose: "assertionMethod", verificationMethod: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", - created: "2023-09-14T02:25:53.667Z", + created: "2023-09-14T11:29:41.690Z", eip712Domain: { domain: { name: "Gitcoin Passport Verifiable Credential of Stamp data", @@ -370,34 +443,22 @@ const prepareCredential = { name: "@context", }, ], - Document: [ - { - type: "string[]", - name: "@context", - }, - { - type: "string[]", - name: "type", - }, + Context: [ { type: "string", - name: "issuer", + name: "customInfo", }, { type: "string", - name: "issuanceDate", + name: "hash", }, { type: "string", - name: "expirationDate", - }, - { - type: "CredentialSubject", - name: "credentialSubject", + name: "metaPointer", }, { - type: "Proof", - name: "proof", + type: "string", + name: "provider", }, ], Proof: [ @@ -426,22 +487,34 @@ const prepareCredential = { name: "created", }, ], - Context: [ + Document: [ { - type: "string", - name: "customInfo", + type: "string[]", + name: "@context", + }, + { + type: "string[]", + name: "type", }, { type: "string", - name: "hash", + name: "issuer", }, { type: "string", - name: "metaPointer", + name: "issuanceDate", }, { type: "string", - name: "provider", + name: "expirationDate", + }, + { + type: "CredentialSubject", + name: "credentialSubject", + }, + { + type: "Proof", + name: "proof", }, ], }, @@ -462,16 +535,16 @@ const prepareCredential = { metaPointer: "https://schema.org/URL", provider: "https://schema.org/Text", }, - hash: "v0.0.0:KkYaKn2GaF55a3y/n6myG9kfrpQPHW5DnhhzO9APTGI=", + hash: "v0.0.0:HLEH/2c+EIFqSwYbiYiCK1dGVH17mVbA23Ez5c7kr/Q=", id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", - provider: "githubAccountCreationGte#90", + provider: "FirstEthTxnProvider", }, - expirationDate: "2023-12-13T03:25:53.645Z", - issuanceDate: "2023-09-14T02:25:53.645Z", + expirationDate: "2023-12-13T12:29:41.675Z", + issuanceDate: "2023-09-14T11:29:41.676Z", issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", proof: { "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", - created: "2023-09-14T02:25:53.667Z", + created: "2023-09-14T11:29:41.690Z", proofPurpose: "assertionMethod", type: "EthereumEip712Signature2021", verificationMethod: @@ -483,68 +556,250 @@ const prepareCredential = { }; describe.only("Signature for GitcoinStampVerifier", () => { - it("should split the signature into r, s, v", () => { - const signature = Signature.from(sampleCredential.proof.proofValue); - const { r, s, v } = signature; - expect(r).to.be.equal( - "0xd4952bda469284acffc0d8b7980a73894d1420b148f61af4e25cec43f4127ec8" - ); - expect(s).to.be.equal( - "0x773bb203302d75ea5e8248b58a073a8cc2bb616fec1dbd817a074db1436466c0" - ); + // it("should split the signature into r, s, v", () => { + // const signature = Signature.from(sampleCredential.proof.proofValue); + // const { r, s, v } = signature; + // expect(r).to.be.equal( + // "0xd4952bda469284acffc0d8b7980a73894d1420b148f61af4e25cec43f4127ec8" + // ); + // expect(s).to.be.equal( + // "0x773bb203302d75ea5e8248b58a073a8cc2bb616fec1dbd817a074db1436466c0" + // ); - expect(v).to.be.equal(28); - }); - it("should normalize the credential for writing on chain", () => { - const normalizedCredential = normalizeDIDCredential(sampleCredential); + // expect(v).to.be.equal(28); + // }); + // it("should normalize the credential for writing on chain", () => { + // const normalizedCredential = normalizeDIDCredential(sampleCredential); - expect(normalizedCredential._context).to.be.deep.equal([ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/2021/v1", - ]); - }); - it("should verify the VC signature on chain", async () => { - const normalizedCredential = normalizeDIDCredential( - sampleCredential - ) as DocumentStruct; - const [signer] = await ethers.getSigners(); - - const GitcoinStampVerifier = await ethers.getContractFactory( - "GitcoinStampVerifier" - ); + // expect(normalizedCredential._context).to.be.deep.equal([ + // "https://www.w3.org/2018/credentials/v1", + // "https://w3id.org/vc/status-list/2021/v1", + // ]); + // }); + // it("should verify the VC signature on chain", async () => { + // const normalizedCredential = normalizeDIDCredential( + // sampleCredential + // ) as DocumentStruct; + // const [signer] = await ethers.getSigners(); - const gitcoinStampVerifier = await GitcoinStampVerifier.deploy(); + // const GitcoinStampVerifier = await ethers.getContractFactory( + // "GitcoinStampVerifier" + // ); - await gitcoinStampVerifier.initialize( - "0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e" + // const gitcoinStampVerifier = await GitcoinStampVerifier.deploy(); + + // await gitcoinStampVerifier.initialize( + // "0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e" + // ); + + // const standardizedTypes = prepareCredential.signingInput.types; + // // @ts-ignore + // // delete standardizedTypes.EIP712Domain; + + // const recoveredAddress = ethers.verifyTypedData( + // prepareCredential.proof.eip712Domain.domain, + // standardizedTypes, + // sampleCredential, + // sampleCredential.proof.proofValue + // ); + + // const issuerAddress = sampleCredential.issuer.split(":").pop(); + + // debugger; + // const signerIssuedCredential = + // recoveredAddress.toLowerCase() === issuerAddress; + + // expect(signerIssuedCredential).to.be.true; + // const signature = Signature.from(sampleCredential.proof.proofValue); + // const { r, s, v } = signature; + + // debugger; + // const tx = await gitcoinStampVerifier.verifyStampVc( + // normalizedCredential, + // v, + // r, + // s + // ); + + // expect(tx).to.be.true; + // }); + it("sign and verify the VC using ethers", async () => { + const issuer = DIDKit.keyToDID("ethr", key); + const credentialInput = { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1", + ], + type: ["VerifiableCredential"], + issuer, + issuanceDate: "2023-09-14T11:59:35.821Z", + expirationDate: "2023-12-13T12:59:35.821Z", + credentialSubject: { + "@context": { + customInfo: "https://schema.org/Thing", + hash: "https://schema.org/Text", + metaPointer: "https://schema.org/URL", + provider: "https://schema.org/Text", + }, + id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", + provider: "FirstEthTxnProvider", + metaPointer: undefined, + hash: "v0.0.0:HLEH/2c+EIFqSwYbiYiCK1dGVH17mVbA23Ez5c7kr/Q=", + }, + }; + + const options = { + type: "EthereumEip712Signature2021", + verificationMethod: + "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#Eip712Method2021", + eip712Domain: { + domain: { + name: "GitcoinPassportStampVerifiableCredential", + version: "0", + }, + types: { + Document: [ + { + name: "@context", + type: "string[]", + }, + { + name: "type", + type: "string[]", + }, + { + name: "issuer", + type: "string", + }, + { + name: "issuanceDate", + type: "string", + }, + { + name: "expirationDate", + type: "string", + }, + { + name: "credentialSubject", + type: "CredentialSubject", + }, + { + name: "proof", + type: "Proof", + }, + ], + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, + ], + CredentialSubject: [ + { + name: "id", + type: "string", + }, + { + name: "provider", + type: "string", + }, + { + name: "hash", + type: "string", + }, + { + name: "@context", + type: "Context", + }, + ], + Context: [ + { + name: "customInfo", + type: "string", + }, + { + name: "hash", + type: "string", + }, + { + name: "metaPointer", + type: "string", + }, + { + name: "provider", + type: "string", + }, + ], + Proof: [ + { + name: "@context", + type: "string", + }, + { + name: "type", + type: "string", + }, + { + name: "proofPurpose", + type: "string", + }, + { + name: "proofValue", + type: "string", + }, + { + name: "verificationMethod", + type: "string", + }, + { + name: "created", + type: "string", + }, + ], + }, + primaryType: "Document", + }, + }; + + const preparedCredential = await DIDKit.prepareIssueCredential( + JSON.stringify(credentialInput, undefined, 2), + JSON.stringify(options, undefined, 2), + key ); - const standardizedTypes = prepareCredential.signingInput.types; - // @ts-ignore + const issuedCredential = await DIDKit.issueCredential( + JSON.stringify(credentialInput, undefined, 2), + JSON.stringify(options, undefined, 2), + key + ); + + const preppedCredential = JSON.parse(preparedCredential); + const signedCredential = JSON.parse(issuedCredential); + + const standardizedTypes = preppedCredential.signingInput.types; delete standardizedTypes.EIP712Domain; + debugger; const signerAddress = ethers.verifyTypedData( - prepareCredential.proof.eip712Domain.domain, + preppedCredential.proof.eip712Domain.domain, standardizedTypes, - sampleCredential, - sampleCredential.proof.proofValue + signedCredential, + signedCredential.proof.proofValue ); + // last run signerAddress 0x6a24210b3403B66f1D3A78f4563FdECC7616e151 + const signedCredIssuer = signedCredential.issuer.split(":").pop(); const signerIssuedCredential = - signerAddress.toLowerCase() === sampleCredential.issuer.split(":").pop(); + signerAddress.toLowerCase() === signedCredIssuer; - expect(signerIssuedCredential).to.be.true; const signature = Signature.from(sampleCredential.proof.proofValue); const { r, s, v } = signature; - debugger; - const tx = await gitcoinStampVerifier.verifyStampVc( - normalizedCredential, - v, - r, - s - ); - expect(tx).to.be.true; + console.log({ issuedCredential, preparedCredential }); }); }); From a30aea579900f1916ce85a2fe5b3696ebc1ffd13 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Thu, 14 Sep 2023 12:09:09 -0600 Subject: [PATCH 5/7] feat(tests): verifying signature using ethers --- contracts/GitcoinStampVerifier.sol | 7 +- package.json | 1 + test/GitcoinStampVerifier.ts | 659 +++++------------------------ yarn.lock | 5 + 4 files changed, 124 insertions(+), 548 deletions(-) diff --git a/contracts/GitcoinStampVerifier.sol b/contracts/GitcoinStampVerifier.sol index 0852d86..22540a5 100644 --- a/contracts/GitcoinStampVerifier.sol +++ b/contracts/GitcoinStampVerifier.sol @@ -84,7 +84,7 @@ contract GitcoinStampVerifier { "Document(string[] @context,string[] type,CredentialSubject credentialSubject,string issuer,string issuanceDate,Proof proof,string expirationDate)" ); - bytes32 private constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version)"); + bytes32 private constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name)"); /** * @notice Initializer function responsible for setting up the contract's initial state. @@ -93,13 +93,12 @@ contract GitcoinStampVerifier { function initialize(address _issuer) public { issuer = _issuer; - name = "Gitcoin Passport Verifiable Credential of Stamp data"; + name = "GitcoinPassportStampVerifiableCredential"; DOMAIN_SEPARATOR = keccak256( abi.encode( EIP712DOMAIN_TYPEHASH, - keccak256(bytes(name)), - keccak256(bytes("0")) + keccak256(bytes(name)) ) ); } diff --git a/package.json b/package.json index c6a87e9..478415e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@openzeppelin/contracts": "^4.8.2", "@openzeppelin/contracts-upgradeable": "^4.9.2", "@openzeppelin/upgrades-core": "^1.27.3", + "@spruceid/didkit-wasm-node": "^0.2.1", "@typechain/ethers-v6": "^0.4.0", "ethers": "^6.6.3" } diff --git a/test/GitcoinStampVerifier.ts b/test/GitcoinStampVerifier.ts index b18c319..bee1431 100644 --- a/test/GitcoinStampVerifier.ts +++ b/test/GitcoinStampVerifier.ts @@ -149,436 +149,203 @@ export const normalizeDIDCredential = (credential: DIDCredential) => { return normalizedCredential; }; -const sampleCredential = { +const issuer = DIDKit.keyToDID("ethr", key); +const credentialInput = { + type: ["VerifiableCredential"], "@context": [ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/vc/status-list/2021/v1", ], - type: ["VerifiableCredential"], + issuer, + issuanceDate: "2023-09-14T11:59:35.821Z", + expirationDate: "2023-12-13T12:59:35.821Z", credentialSubject: { - id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", - provider: "FirstEthTxnProvider", - hash: "v0.0.0:HLEH/2c+EIFqSwYbiYiCK1dGVH17mVbA23Ez5c7kr/Q=", "@context": { customInfo: "https://schema.org/Thing", hash: "https://schema.org/Text", metaPointer: "https://schema.org/URL", provider: "https://schema.org/Text", }, + id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", + provider: "FirstEthTxnProvider", + // metaPointer: "undefined", + hash: "v0.0.0:HLEH/2c+EIFqSwYbiYiCK1dGVH17mVbA23Ez5c7kr/Q=", }, - issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", - issuanceDate: "2023-09-14T11:29:41.676Z", - proof: { - "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", - type: "EthereumEip712Signature2021", - proofPurpose: "assertionMethod", - proofValue: - "0x95a8ec9b47a747be0ebdc11787783a4830d5ac27d9601dd81dc8d933527f74ce70013dc6a3632783acd7608351405eb7fe5638892e217d3be76adc7ec4c296001c", - verificationMethod: - "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", - created: "2023-09-14T11:29:41.679Z", - eip712Domain: { - domain: { - name: "Gitcoin Passport Verifiable Credential of Stamp data", - version: "0", - }, - primaryType: "Document", - types: { - Context: [ - { - name: "customInfo", - type: "string", - }, - { - name: "hash", - type: "string", - }, - { - name: "metaPointer", - type: "string", - }, - { - name: "provider", - type: "string", - }, - ], - CredentialSubject: [ - { - name: "id", - type: "string", - }, - { - name: "provider", - type: "string", - }, - { - name: "hash", - type: "string", - }, - { - name: "@context", - type: "Context", - }, - ], - Document: [ - { - name: "@context", - type: "string[]", - }, - { - name: "type", - type: "string[]", - }, - { - name: "issuer", - type: "string", - }, - { - name: "issuanceDate", - type: "string", - }, - { - name: "expirationDate", - type: "string", - }, - { - name: "credentialSubject", - type: "CredentialSubject", - }, - { - name: "proof", - type: "Proof", - }, - ], - EIP712Domain: [ - { - name: "name", - type: "string", - }, - { - name: "version", - type: "string", - }, - ], - Proof: [ - { - name: "@context", - type: "string", - }, - { - name: "type", - type: "string", - }, - { - name: "proofPurpose", - type: "string", - }, - { - name: "proofValue", - type: "string", - }, - { - name: "verificationMethod", - type: "string", - }, - { - name: "created", - type: "string", - }, - ], - }, - }, - }, - expirationDate: "2023-12-13T12:29:41.675Z", }; -const prepareCredential = { - proof: { - "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", - type: "EthereumEip712Signature2021", - proofPurpose: "assertionMethod", - verificationMethod: - "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", - created: "2023-09-14T11:29:41.690Z", - eip712Domain: { - domain: { - name: "Gitcoin Passport Verifiable Credential of Stamp data", - version: "0", - }, - primaryType: "Document", - types: { - Context: [ - { - name: "customInfo", - type: "string", - }, - { - name: "hash", - type: "string", - }, - { - name: "metaPointer", - type: "string", - }, - { - name: "provider", - type: "string", - }, - ], - CredentialSubject: [ - { - name: "id", - type: "string", - }, - { - name: "provider", - type: "string", - }, - { - name: "hash", - type: "string", - }, - { - name: "@context", - type: "Context", - }, - ], - Document: [ - { - name: "@context", - type: "string[]", - }, - { - name: "type", - type: "string[]", - }, - { - name: "issuer", - type: "string", - }, - { - name: "issuanceDate", - type: "string", - }, - { - name: "expirationDate", - type: "string", - }, - { - name: "credentialSubject", - type: "CredentialSubject", - }, - { - name: "proof", - type: "Proof", - }, - ], - EIP712Domain: [ - { - name: "name", - type: "string", - }, - { - name: "version", - type: "string", - }, - ], - Proof: [ - { - name: "@context", - type: "string", - }, - { - name: "type", - type: "string", - }, - { - name: "proofPurpose", - type: "string", - }, - { - name: "proofValue", - type: "string", - }, - { - name: "verificationMethod", - type: "string", - }, - { - name: "created", - type: "string", - }, - ], - }, +const options = { + type: "EthereumEip712Signature2021", + eip712Domain: { + domain: { + name: "GitcoinPassportStampVerifiableCredential", }, - }, - jwsHeader: null, - signingInput: { types: { - EIP712Domain: [ + Document: [ { - type: "string", - name: "name", + name: "@context", + type: "string[]", }, { - type: "string", - name: "version", + name: "type", + type: "string[]", }, - ], - CredentialSubject: [ { + name: "issuer", type: "string", - name: "id", }, { + name: "issuanceDate", type: "string", - name: "provider", }, { + name: "expirationDate", type: "string", - name: "hash", }, { - type: "Context", - name: "@context", + name: "credentialSubject", + type: "CredentialSubject", }, - ], - Context: [ { - type: "string", - name: "customInfo", + name: "proof", + type: "Proof", }, + ], + CredentialSubject: [ { + name: "id", type: "string", - name: "hash", }, { + name: "provider", type: "string", - name: "metaPointer", }, { + name: "hash", type: "string", - name: "provider", }, - ], - Proof: [ { - type: "string", name: "@context", + type: "CredentialSubjectContext", }, + ], + CredentialSubjectContext: [ { + name: "customInfo", type: "string", - name: "type", - }, - { - type: "string", - name: "proofPurpose", }, { + name: "hash", type: "string", - name: "proofValue", }, { + name: "metaPointer", type: "string", - name: "verificationMethod", }, { + name: "provider", type: "string", - name: "created", }, ], - Document: [ + Proof: [ { - type: "string[]", name: "@context", + type: "string", }, { - type: "string[]", name: "type", - }, - { type: "string", - name: "issuer", }, { + name: "proofPurpose", type: "string", - name: "issuanceDate", }, { + name: "verificationMethod", type: "string", - name: "expirationDate", }, { - type: "CredentialSubject", - name: "credentialSubject", - }, - { - type: "Proof", - name: "proof", + name: "created", + type: "string", }, ], - }, - primaryType: "Document", - domain: { - name: "Gitcoin Passport Verifiable Credential of Stamp data", - version: "0", - }, - message: { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/2021/v1", + EIP712Domain: [ + { name: "name", type: "string" }, + // { name: "version", type: "string" }, ], - credentialSubject: { - "@context": { - customInfo: "https://schema.org/Thing", - hash: "https://schema.org/Text", - metaPointer: "https://schema.org/URL", - provider: "https://schema.org/Text", - }, - hash: "v0.0.0:HLEH/2c+EIFqSwYbiYiCK1dGVH17mVbA23Ez5c7kr/Q=", - id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", - provider: "FirstEthTxnProvider", - }, - expirationDate: "2023-12-13T12:29:41.675Z", - issuanceDate: "2023-09-14T11:29:41.676Z", - issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", - proof: { - "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", - created: "2023-09-14T11:29:41.690Z", - proofPurpose: "assertionMethod", - type: "EthereumEip712Signature2021", - verificationMethod: - "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#controller", - }, - type: ["VerifiableCredential"], }, + primaryType: "Document", }, }; +// rough outline of a VerifiableCredential +export type VerifiableCredential = { + "@context": string[]; + type: string[]; + credentialSubject: { + id: string; + "@context": { [key: string]: string }[]; + hash?: string; + provider?: string; + address?: string; + challenge?: string; + metaPointer?: string; + }; + issuer: string; + issuanceDate: string; + expirationDate: string; + proof: { + type: string; + proofPurpose: string; + verificationMethod: string; + created: string; + jws: string; + }; +}; + describe.only("Signature for GitcoinStampVerifier", () => { - // it("should split the signature into r, s, v", () => { - // const signature = Signature.from(sampleCredential.proof.proofValue); - // const { r, s, v } = signature; - // expect(r).to.be.equal( - // "0xd4952bda469284acffc0d8b7980a73894d1420b148f61af4e25cec43f4127ec8" - // ); - // expect(s).to.be.equal( - // "0x773bb203302d75ea5e8248b58a073a8cc2bb616fec1dbd817a074db1436466c0" - // ); + let preppedCredential: any, signedCredential: any; + beforeEach(async () => { + const preparedCredential = await DIDKit.prepareIssueCredential( + JSON.stringify(credentialInput, undefined, 2), + JSON.stringify(options, undefined, 2), + key + ); - // expect(v).to.be.equal(28); - // }); - // it("should normalize the credential for writing on chain", () => { - // const normalizedCredential = normalizeDIDCredential(sampleCredential); + const issuedCredential = await DIDKit.issueCredential( + JSON.stringify(credentialInput, undefined, 2), + JSON.stringify(options, undefined, 2), + key + ); - // expect(normalizedCredential._context).to.be.deep.equal([ - // "https://www.w3.org/2018/credentials/v1", - // "https://w3id.org/vc/status-list/2021/v1", - // ]); - // }); + preppedCredential = JSON.parse(preparedCredential) as any; + signedCredential = JSON.parse(issuedCredential) as any; + }); + it("should normalize the credential for writing on chain", () => { + const normalizedCredential = normalizeDIDCredential(signedCredential); + + expect(normalizedCredential._context).to.be.deep.equal([ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1", + ]); + }); + it("sign and verify the VC using ethers", async () => { + const standardizedTypes = preppedCredential.signingInput.types; + delete standardizedTypes.EIP712Domain; + + const signerAddress = ethers.verifyTypedData( + preppedCredential.signingInput.domain, + standardizedTypes, + signedCredential, + signedCredential.proof.proofValue + ); + + const signedCredIssuer = signedCredential.issuer.split(":").pop(); + + expect(signerAddress.toLowerCase()).to.be.equal(signedCredIssuer); + }); // it("should verify the VC signature on chain", async () => { // const normalizedCredential = normalizeDIDCredential( - // sampleCredential + // signedCredential // ) as DocumentStruct; // const [signer] = await ethers.getSigners(); @@ -592,28 +359,11 @@ describe.only("Signature for GitcoinStampVerifier", () => { // "0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e" // ); - // const standardizedTypes = prepareCredential.signingInput.types; - // // @ts-ignore - // // delete standardizedTypes.EIP712Domain; - - // const recoveredAddress = ethers.verifyTypedData( - // prepareCredential.proof.eip712Domain.domain, - // standardizedTypes, - // sampleCredential, - // sampleCredential.proof.proofValue - // ); + // const standardizedTypes = preppedCredential.signingInput.types; - // const issuerAddress = sampleCredential.issuer.split(":").pop(); - - // debugger; - // const signerIssuedCredential = - // recoveredAddress.toLowerCase() === issuerAddress; - - // expect(signerIssuedCredential).to.be.true; - // const signature = Signature.from(sampleCredential.proof.proofValue); + // const signature = Signature.from(signedCredential.proof.proofValue); // const { r, s, v } = signature; - // debugger; // const tx = await gitcoinStampVerifier.verifyStampVc( // normalizedCredential, // v, @@ -623,183 +373,4 @@ describe.only("Signature for GitcoinStampVerifier", () => { // expect(tx).to.be.true; // }); - it("sign and verify the VC using ethers", async () => { - const issuer = DIDKit.keyToDID("ethr", key); - const credentialInput = { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/2021/v1", - ], - type: ["VerifiableCredential"], - issuer, - issuanceDate: "2023-09-14T11:59:35.821Z", - expirationDate: "2023-12-13T12:59:35.821Z", - credentialSubject: { - "@context": { - customInfo: "https://schema.org/Thing", - hash: "https://schema.org/Text", - metaPointer: "https://schema.org/URL", - provider: "https://schema.org/Text", - }, - id: "did:pkh:eip155:1:0xC79ABB54e4824Cdb65C71f2eeb2D7f2db5dA1fB8", - provider: "FirstEthTxnProvider", - metaPointer: undefined, - hash: "v0.0.0:HLEH/2c+EIFqSwYbiYiCK1dGVH17mVbA23Ez5c7kr/Q=", - }, - }; - - const options = { - type: "EthereumEip712Signature2021", - verificationMethod: - "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e#Eip712Method2021", - eip712Domain: { - domain: { - name: "GitcoinPassportStampVerifiableCredential", - version: "0", - }, - types: { - Document: [ - { - name: "@context", - type: "string[]", - }, - { - name: "type", - type: "string[]", - }, - { - name: "issuer", - type: "string", - }, - { - name: "issuanceDate", - type: "string", - }, - { - name: "expirationDate", - type: "string", - }, - { - name: "credentialSubject", - type: "CredentialSubject", - }, - { - name: "proof", - type: "Proof", - }, - ], - EIP712Domain: [ - { - name: "name", - type: "string", - }, - { - name: "version", - type: "string", - }, - ], - CredentialSubject: [ - { - name: "id", - type: "string", - }, - { - name: "provider", - type: "string", - }, - { - name: "hash", - type: "string", - }, - { - name: "@context", - type: "Context", - }, - ], - Context: [ - { - name: "customInfo", - type: "string", - }, - { - name: "hash", - type: "string", - }, - { - name: "metaPointer", - type: "string", - }, - { - name: "provider", - type: "string", - }, - ], - Proof: [ - { - name: "@context", - type: "string", - }, - { - name: "type", - type: "string", - }, - { - name: "proofPurpose", - type: "string", - }, - { - name: "proofValue", - type: "string", - }, - { - name: "verificationMethod", - type: "string", - }, - { - name: "created", - type: "string", - }, - ], - }, - primaryType: "Document", - }, - }; - - const preparedCredential = await DIDKit.prepareIssueCredential( - JSON.stringify(credentialInput, undefined, 2), - JSON.stringify(options, undefined, 2), - key - ); - - const issuedCredential = await DIDKit.issueCredential( - JSON.stringify(credentialInput, undefined, 2), - JSON.stringify(options, undefined, 2), - key - ); - - const preppedCredential = JSON.parse(preparedCredential); - const signedCredential = JSON.parse(issuedCredential); - - const standardizedTypes = preppedCredential.signingInput.types; - delete standardizedTypes.EIP712Domain; - - debugger; - const signerAddress = ethers.verifyTypedData( - preppedCredential.proof.eip712Domain.domain, - standardizedTypes, - signedCredential, - signedCredential.proof.proofValue - ); - - // last run signerAddress 0x6a24210b3403B66f1D3A78f4563FdECC7616e151 - const signedCredIssuer = signedCredential.issuer.split(":").pop(); - const signerIssuedCredential = - signerAddress.toLowerCase() === signedCredIssuer; - - const signature = Signature.from(sampleCredential.proof.proofValue); - const { r, s, v } = signature; - debugger; - - console.log({ issuedCredential, preparedCredential }); - }); }); diff --git a/yarn.lock b/yarn.lock index b16eb2a..3c8602e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1241,6 +1241,11 @@ dependencies: antlr4ts "^0.5.0-alpha.4" +"@spruceid/didkit-wasm-node@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@spruceid/didkit-wasm-node/-/didkit-wasm-node-0.2.1.tgz#85f7023979b4ef84bd6de16c79c67b6e9a08e1db" + integrity sha512-c8e3u5FIRS/2Gf6UHnRPnfRozgKgby4avZzlvIiaJRDVLl1LaX1SE13vEvKV2rAq6NMfZrV4YG908M4uVlT90Q== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" From 9d96f68b409d4301c1fe5be26b4dd72673f7e8e8 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Thu, 14 Sep 2023 17:54:50 -0600 Subject: [PATCH 6/7] fix(contracts): verifying signed credentials --- contracts/GitcoinStampVerifier.sol | 24 +++--------- test/GitcoinStampVerifier.ts | 59 +++++++++++++----------------- 2 files changed, 32 insertions(+), 51 deletions(-) diff --git a/contracts/GitcoinStampVerifier.sol b/contracts/GitcoinStampVerifier.sol index 22540a5..d2022e6 100644 --- a/contracts/GitcoinStampVerifier.sol +++ b/contracts/GitcoinStampVerifier.sol @@ -103,11 +103,7 @@ contract GitcoinStampVerifier { ); } - function hashCredentialSubjectContext(CredentialSubjectContext calldata context) public view returns (bytes32) { - console.log(context.customInfo); - console.log(context._hash); - console.log(context.metaPointer); - console.log(context.provider); + function hashCredentialSubjectContext(CredentialSubjectContext calldata context) public pure returns (bytes32) { return keccak256( abi.encode( CREDENTIAL_SUBJECT_CONTEXT_TYPEHASH, @@ -120,10 +116,7 @@ contract GitcoinStampVerifier { } - function hashCredentialSubject(CredentialSubject calldata subject) public view returns (bytes32) { - console.log(subject.id); - console.log(subject.provider); - console.log(subject._hash); + function hashCredentialSubject(CredentialSubject calldata subject) public pure returns (bytes32) { bytes32 credentialSubjectContext = hashCredentialSubjectContext(subject._context); return @@ -138,12 +131,7 @@ contract GitcoinStampVerifier { ); } - function hashCredentialProof(Proof calldata proof) public view returns (bytes32) { - console.log(proof._context); - console.log(proof._type); - console.log(proof.proofPurpose); - console.log(proof.verificationMethod); - console.log(proof.created); + function hashCredentialProof(Proof calldata proof) public pure returns (bytes32) { return keccak256( abi.encode( @@ -165,7 +153,7 @@ contract GitcoinStampVerifier { result = keccak256(abi.encodePacked(_array)); } - function hashDocument(Document calldata document) public view returns (bytes32) { + function hashDocument(Document calldata document) public pure returns (bytes32) { bytes32 credentialSubjectHash = hashCredentialSubject(document.credentialSubject); bytes32 proofHash = hashCredentialProof(document.proof); @@ -184,11 +172,11 @@ contract GitcoinStampVerifier { ); } - function verifyStampVc(Document calldata document, uint8 v, bytes32 r, bytes32 s) public view returns (bool) { + function verifyStampVc(Document calldata document, bytes memory signature) public view returns (bool) { bytes32 vcHash = hashDocument(document); bytes32 digest = ECDSA.toTypedDataHash(DOMAIN_SEPARATOR, vcHash); - address recoveredAddress = ECDSA.recover(digest, v, r, s); + address recoveredAddress = ECDSA.recover(digest, signature); console.log(recoveredAddress, issuer, "recoveredAddress, issuer, "); diff --git a/test/GitcoinStampVerifier.ts b/test/GitcoinStampVerifier.ts index bee1431..efc55f8 100644 --- a/test/GitcoinStampVerifier.ts +++ b/test/GitcoinStampVerifier.ts @@ -303,7 +303,7 @@ export type VerifiableCredential = { }; describe.only("Signature for GitcoinStampVerifier", () => { - let preppedCredential: any, signedCredential: any; + let preppedCredential: any, signedCredential: any, normalizedCredential: any; beforeEach(async () => { const preparedCredential = await DIDKit.prepareIssueCredential( JSON.stringify(credentialInput, undefined, 2), @@ -319,15 +319,38 @@ describe.only("Signature for GitcoinStampVerifier", () => { preppedCredential = JSON.parse(preparedCredential) as any; signedCredential = JSON.parse(issuedCredential) as any; + normalizedCredential = normalizeDIDCredential(signedCredential) as any; }); it("should normalize the credential for writing on chain", () => { - const normalizedCredential = normalizeDIDCredential(signedCredential); - expect(normalizedCredential._context).to.be.deep.equal([ "https://www.w3.org/2018/credentials/v1", "https://w3id.org/vc/status-list/2021/v1", ]); }); + it("should verify the VC signature on chain", async () => { + const [signer] = await ethers.getSigners(); + + const GitcoinStampVerifier = await ethers.getContractFactory( + "GitcoinStampVerifier" + ); + + const gitcoinStampVerifier = await GitcoinStampVerifier.deploy(); + + await gitcoinStampVerifier.initialize( + "0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e" + ); + + const standardizedTypes = preppedCredential.signingInput.types; + + const signature = Signature.from(signedCredential.proof.proofValue); + + const tx = await gitcoinStampVerifier.verifyStampVc( + normalizedCredential, + signedCredential.proof.proofValue + ); + + expect(tx).to.be.true; + }); it("sign and verify the VC using ethers", async () => { const standardizedTypes = preppedCredential.signingInput.types; delete standardizedTypes.EIP712Domain; @@ -343,34 +366,4 @@ describe.only("Signature for GitcoinStampVerifier", () => { expect(signerAddress.toLowerCase()).to.be.equal(signedCredIssuer); }); - // it("should verify the VC signature on chain", async () => { - // const normalizedCredential = normalizeDIDCredential( - // signedCredential - // ) as DocumentStruct; - // const [signer] = await ethers.getSigners(); - - // const GitcoinStampVerifier = await ethers.getContractFactory( - // "GitcoinStampVerifier" - // ); - - // const gitcoinStampVerifier = await GitcoinStampVerifier.deploy(); - - // await gitcoinStampVerifier.initialize( - // "0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e" - // ); - - // const standardizedTypes = preppedCredential.signingInput.types; - - // const signature = Signature.from(signedCredential.proof.proofValue); - // const { r, s, v } = signature; - - // const tx = await gitcoinStampVerifier.verifyStampVc( - // normalizedCredential, - // v, - // r, - // s - // ); - - // expect(tx).to.be.true; - // }); }); From 37a9bfa73240fd5d3048eeef86d38faa38905362 Mon Sep 17 00:00:00 2001 From: schultztimothy Date: Fri, 15 Sep 2023 13:32:17 -0600 Subject: [PATCH 7/7] no domain example --- test/GitcoinStampVerifierNoDomain.ts | 83 ++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 test/GitcoinStampVerifierNoDomain.ts diff --git a/test/GitcoinStampVerifierNoDomain.ts b/test/GitcoinStampVerifierNoDomain.ts new file mode 100644 index 0000000..07e23e6 --- /dev/null +++ b/test/GitcoinStampVerifierNoDomain.ts @@ -0,0 +1,83 @@ +import { verifyTypedData } from "ethers"; +// import { ethers } from "hardhat"; +// import { PromiseOrValue } from "../typechain-types/common"; +import { expect } from "chai"; +// import { GitcoinStampVerifier__factory } from "../typechain-types"; +import * as DIDKit from "@spruceid/didkit-wasm-node"; + +const key = + '{"kty":"EC","crv":"secp256k1","x":"PdB2nS-knyAxc6KPuxBr65vRpW-duAXwpeXlwGJ03eU","y":"MwoGZ08hF5uv-_UEC9BKsYdJVSbJNHcFhR1BZWer5RQ","d":"z9VrSNNZXf9ywUx3v_8cLDhSw8-pvAT9qu_WZmqqfWM"}'; +const credentialInput = { + type: ["VerifiableCredential"], + issuer: "did:ethr:0xd6fc34345bc8c8e5659a35bed9629d5558d48c4e", + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/vc/status-list/2021/v1", + ], + issuanceDate: "2023-09-15T16:16:20.527Z", + expirationDate: "2023-12-14T17:16:20.527Z", + credentialSubject: { + id: "did:pkh:eip155:1:0x0636F974D29d947d4946b2091d769ec6D2d415DE", + hash: "v0.0.0:VC1vOVLxD3X29DYfyyT58AA+j0rC5vkXNflCA94iApA=", + "@context": { + hash: "https://schema.org/Text", + provider: "https://schema.org/Text", + customInfo: "https://schema.org/Thing", + metaPointer: "https://schema.org/URL", + }, + provider: "FirstEthTxnProvider", + customInfo: {}, + }, +}; + +async function generateCredentials() { + const verificationMethod = await DIDKit.keyToVerificationMethod("ethr", key); + const options = { + verificationMethod, + type: "EthereumEip712Signature2021", + // domain: { + // name: "Eip712Method2021", + // }, + }; + + const preparedCredential = await DIDKit.prepareIssueCredential( + JSON.stringify(credentialInput, undefined, 2), + JSON.stringify(options, undefined, 2), + key + ); + + const issuedCredential = await DIDKit.issueCredential( + JSON.stringify(credentialInput, undefined, 2), + JSON.stringify(options, undefined, 2), + key + ); + + const preppedCredential = JSON.parse(preparedCredential) as any; + const signedCredential = JSON.parse(issuedCredential) as any; + return { preppedCredential, signedCredential }; +} + +describe.only("Verifying EIP712 Credentials", () => { + it("should verify a credential using ethers", async () => { + const { preppedCredential, signedCredential } = await generateCredentials(); + const standardizedTypes = preppedCredential.signingInput.types; + delete standardizedTypes.EIP712Domain; + + const didkitVerification = await DIDKit.verifyCredential( + JSON.stringify(signedCredential), + JSON.stringify({ + proofPurpose: "assertionMethod", + }) + ); + + const signerAddress = verifyTypedData( + preppedCredential.signingInput.domain, + standardizedTypes, + signedCredential, + signedCredential.proof.proofValue + ); + const signedCredIssuer = signedCredential.issuer.split(":").pop(); + + expect(signerAddress.toLowerCase()).to.be.equal(signedCredIssuer); + }); +});