diff --git a/govtool/frontend/package-lock.json b/govtool/frontend/package-lock.json index 0977c4e27..24ffb835c 100644 --- a/govtool/frontend/package-lock.json +++ b/govtool/frontend/package-lock.json @@ -27,6 +27,7 @@ "bech32": "^2.0.0", "blakejs": "^1.2.1", "buffer": "^6.0.3", + "cbor-web": "^10.0.3", "date-fns": "^2.30.0", "esbuild": "^0.25.0", "i18next": "^23.7.19", @@ -10161,6 +10162,15 @@ "node": ">=4" } }, + "node_modules/cbor-web": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/cbor-web/-/cbor-web-10.0.3.tgz", + "integrity": "sha512-IWHUPUDxRlDzT6Saykzpy/0PckZjcPZcS/WQR1pWm+YX5IniAMB4Cih+AdxUss0CNs/XEBXWJsKfJWitx+1zGg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index f25c5a6ce..ea8747291 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -41,6 +41,7 @@ "bech32": "^2.0.0", "blakejs": "^1.2.1", "buffer": "^6.0.3", + "cbor-web": "^10.0.3", "date-fns": "^2.30.0", "esbuild": "^0.25.0", "i18next": "^23.7.19", diff --git a/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx b/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx index c06b9c1c6..d2c4bf83e 100644 --- a/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx +++ b/govtool/frontend/src/components/molecules/GovernanceActionCardElement.tsx @@ -15,13 +15,14 @@ import { useModal } from "@/context"; type BaseProps = { label: string; - text?: React.ReactNode; + text?: string | number; dataTestId?: string; isSliderCard?: boolean; tooltipProps?: Omit; marginBottom?: number; isSemiTransparent?: boolean; isValidating?: boolean; + children?: React.ReactNode; }; type VariantProps = BaseProps & { @@ -44,10 +45,11 @@ export const GovernanceActionCardElement = ({ isMarkdown = false, isSemiTransparent = false, isValidating = false, + children, }: VariantProps) => { const { openModal } = useModal(); - if (!text) return null; + if (text == null && children == null) return null; const renderTooltip = () => tooltipProps && ( @@ -108,11 +110,13 @@ export const GovernanceActionCardElement = ({ ...(isSemiTransparent && { opacity: 0.75 }), }} > - {typeof text === "string" && isMarkdown ? removeMarkdown(text) : text} + {isMarkdown ? removeMarkdown(text) : text} ); - const renderMarkdownText = ({ children }: PropsWithChildren) => ( + const renderMarkdownText = ({ + children: markdownChildren, + }: PropsWithChildren) => ( - {children} + {markdownChildren} ); const markdownComponents = { - p: ({ children }: PropsWithChildren) => renderMarkdownText({ children }), + p: ({ children: markdownChildren }: PropsWithChildren) => + renderMarkdownText({ children: markdownChildren }), br: () =>
, }; - const renderMarkdown = () => { - const formattedText = text + const renderMarkdown = (markdownText: string | number) => { + const formattedText = markdownText .toString() .replace(/\r\n|\r/g, "\n") .replace( @@ -157,14 +162,14 @@ export const GovernanceActionCardElement = ({ ); }; - const renderCopyButton = () => + const renderCopyButton = (copyText: string | number) => isCopyButton && ( - + ); - const renderLinkButton = () => + const renderLinkButton = (linkText: string | number) => isLinkButton && ( openModal({ type: "externalLink", - state: { externalLink: text.toString() }, + state: { externalLink: linkText.toString() }, }) } /> ); + const renderTextOrChildren = () => { + if (text != null) { + return ( + <> + {textVariant === "pill" + ? renderPillText() + : isMarkdown && !isSliderCard + ? renderMarkdown(text) + : renderStandardText()} + {renderCopyButton(text)} + {renderLinkButton(text)} + + ); + } + if (children != null) { + return children; + } + }; + return ( - {textVariant === "pill" - ? renderPillText() - : isMarkdown && !isSliderCard - ? renderMarkdown() - : renderStandardText()} - {renderCopyButton()} - {renderLinkButton()} + {renderTextOrChildren()} )} diff --git a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx index a7982ef73..b38a2b2d0 100644 --- a/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx +++ b/govtool/frontend/src/components/organisms/GovernanceActionDetailsCardData.tsx @@ -374,44 +374,43 @@ export const GovernanceActionDetailsCardData = ({ )} - {(authors ?? []).length <= 0 - ? t("govActions.authors.noDataAvailable") - : (authors ?? []).map((author) => ( - - - {author.name} - - - - - ))} - - } textVariant="longText" dataTestId="authors" - /> + > + + {(authors ?? []).length <= 0 + ? t("govActions.authors.noDataAvailable") + : (authors ?? []).map((author) => ( + + + {author.name} + + + + + ))} + + diff --git a/govtool/frontend/src/types/cbor-web.d.ts b/govtool/frontend/src/types/cbor-web.d.ts new file mode 100644 index 000000000..b89a25e84 --- /dev/null +++ b/govtool/frontend/src/types/cbor-web.d.ts @@ -0,0 +1,20 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +type CBORDecoded = Map | any[]; + +type BufferLike = + | string + | Buffer + | ArrayBuffer + | ArrayBufferView + | DataView + | stream.Readable; + +declare module "cbor-web" { + export function encode(input: any): Buffer; + export function decode(input: BufferLike): CBORDecoded; + + export function encodeAsync(input: any): Promise; + export function decodeAsync( + input: BufferLike, + ): Promise; +} diff --git a/govtool/frontend/src/utils/cip8verification.ts b/govtool/frontend/src/utils/cip8verification.ts new file mode 100644 index 000000000..dda6a69f9 --- /dev/null +++ b/govtool/frontend/src/utils/cip8verification.ts @@ -0,0 +1,184 @@ +// This is slightly refactored version of the CIP-8 verification code from govtool-outcomes-pillar: +// https://github.com/IntersectMBO/govtool-outcomes-pillar/commit/c268532f5ea0edde9aca338f66f1b9f265450db3 + +import * as CardanoASMJS from "@emurgo/cardano-serialization-lib-asmjs"; +import * as cbor from "cbor-web"; +import { Buffer } from "buffer"; + +const isHex = (text: string) => /^[0-9a-fA-F]+$/.test(text); + +const trimString = (s: string) => + s.replace(/(^\s*)|(\s*$)/gi, "").replace(/\n /, "\n"); + +export async function verifyCIP8Signature( + signature: string, + message: string, + vkey: string, +): Promise { + try { + if (!isHex(signature) || !isHex(vkey) || !isHex(message)) { + throw new Error("Signature, vkey, and message must be valid hex strings"); + } + + const publicKey = createPublicKey(vkey); + const signatureStructure = createSignatureStructure(signature); + const sigStructureCborHex = Buffer.from( + cbor.encode(signatureStructure.signature), + ).toString("hex"); + + const payloadHex = signatureStructure.payloadBuffer.toString("hex"); + + if (message !== payloadHex) { + console.error( + "Signature verification failed - Payload in signature does not match hash of provided message", + ); + return false; + } + + const signatureHex = signatureStructure.signatureBuffer.toString("hex"); + + return publicKey.verify( + Buffer.from(sigStructureCborHex, "hex"), + CardanoASMJS.Ed25519Signature.from_hex(signatureHex), + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Signature verification failed: ${errorMessage}`); + } +} + +const createPublicKey = (coseKeyCborHex: string) => { + let publicKey; + let coseKeyStructure; + + coseKeyCborHex = trimString(coseKeyCborHex.toLowerCase()); + try { + coseKeyStructure = cbor.decode(Buffer.from(coseKeyCborHex, "hex")); + } catch { + // If not CBOR, try as raw vkey + try { + publicKey = getPublicKeyFromVkey(coseKeyCborHex); + } catch { + throw new Error( + "Input is neither a valid COSE_Key CBOR nor a valid vkey hex string", + ); + } + } + + if (!publicKey && coseKeyStructure) { + // Validate COSE Key structure + if (!(coseKeyStructure instanceof Map) || coseKeyStructure.size < 4) { + throw new Error( + "COSE_Key is not valid. It must be a map with at least 4 entries: kty,alg,crv,x.", + ); + } + if (coseKeyStructure.get(1) !== 1) { + throw new Error('COSE_Key map label "1" (kty) is not "1" (OKP)'); + } + if (coseKeyStructure.get(3) !== -8) { + throw new Error('COSE_Key map label "3" (alg) is not "-8" (EdDSA)'); + } + if (coseKeyStructure.get(-1) !== 6) { + throw new Error('COSE_Key map label "-1" (crv) is not "6" (Ed25519)'); + } + if (!coseKeyStructure.has(-2)) { + throw new Error('COSE_Key map label "-2" (public key) is missing'); + } + const pubKeyBuffer = coseKeyStructure.get(-2); + if (!Buffer.isBuffer(pubKeyBuffer)) { + throw new Error("PublicKey entry in the COSE_Key is not a bytearray"); + } + const pubKey = pubKeyBuffer.toString("hex"); + publicKey = CardanoASMJS.PublicKey.from_bytes(Buffer.from(pubKey, "hex")); + } + if (!publicKey) { + throw new Error("Public key could not be derived from COSE_Key"); + } + return publicKey; +}; + +const createSignatureStructure = (signature: string) => { + const coseSign1CborHex = trimString(signature.toLowerCase()); + // Decode COSE_Sign1 + let coseSign1Structure; + try { + coseSign1Structure = cbor.decode(Buffer.from(coseSign1CborHex, "hex")); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error( + `Can't cbor decode the given COSE_Sign1 signature: (${errorMessage})`, + ); + } + + // Validate COSE_Sign1 structure + if (!Array.isArray(coseSign1Structure) || coseSign1Structure.length !== 4) { + throw new Error( + "COSE_Sign1 is not a valid signature. It must be an array with 4 entries.", + ); + } + + // Extract content from coseSign1Structure + const [ + protectedHeaderBuffer, + unprotectedHeader, + payloadBuffer, + signatureBuffer, + ] = coseSign1Structure; + + if (!Buffer.isBuffer(signatureBuffer)) { + throw new Error("Signature is not a bytearray"); + } + + // 1) Validate and decode protected header + if (!Buffer.isBuffer(protectedHeaderBuffer)) { + throw new Error("Protected header is not a bytearray (serialized) cbor"); + } + + let protectedHeader; + try { + protectedHeader = ensureMap(cbor.decode(protectedHeaderBuffer)); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Can't cbor decode the protected header (${errorMessage})`); + } + + if (!protectedHeader.has(1)) { + throw new Error('Protected header map label "1" is missing'); + } + if (protectedHeader.get(1) !== -8) { + throw new Error('Protected header map label "1" (alg) is not "-8" (EdDSA)'); + } + if (!protectedHeader.has("address")) { + throw new Error('Protected header map label "address" is missing'); + } + + // 2) Process unprotectedHeader as in executable + ensureMap(unprotectedHeader); + + return { + signature: [ + "Signature1", + protectedHeaderBuffer, + Buffer.from(""), + payloadBuffer, + ], + payloadBuffer, + signatureBuffer, + }; +}; + +function getPublicKeyFromVkey(vkey: string): CardanoASMJS.PublicKey { + try { + return CardanoASMJS.PublicKey.from_bytes(Buffer.from(vkey, "hex")); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(`Invalid vkey format: ${errorMessage}`); + } +} + +function ensureMap(value: unknown): Map { + if (value instanceof Map) return value; + if (typeof value === "object" && value !== null) + return new Map(Object.entries(value)); + throw new Error("Value is not a map"); +} diff --git a/govtool/frontend/src/utils/index.ts b/govtool/frontend/src/utils/index.ts index 84c563c00..f331202b3 100644 --- a/govtool/frontend/src/utils/index.ts +++ b/govtool/frontend/src/utils/index.ts @@ -38,3 +38,4 @@ export * from "./wait"; export * from "./getBase64ImageDetails"; export * from "./parseBoolean"; export * from "./validateSignature"; +export * from "./cip8verification"; diff --git a/govtool/frontend/src/utils/tests/validateSignature.node.test.ts b/govtool/frontend/src/utils/tests/validateSignature.node.test.ts new file mode 100644 index 000000000..ac5cb8e90 --- /dev/null +++ b/govtool/frontend/src/utils/tests/validateSignature.node.test.ts @@ -0,0 +1,148 @@ +/* + * @vitest-environment node + * + * We are switching to the "node" environment just for the Ed25519 algorithm because jsdom does not + * provide a full WebCrypto API, specifically window.crypto.subtle, which is required by library + * @noble/ed25519 for signature verification. Node.js (v16.8+) includes native + * crypto.subtle support, making it suitable for cryptographic tests. Current browsers support + * window.crypto.subtle natively, so this issue only affects the test environment. +*/ + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import { validateSignature } from "../validateSignature"; + +describe("validateSignature NODE", () => { + const validEdInput = { + jsonContent, + publicKey: + "38f01b9b41e7ea4bca5c093e472fa01198ebaf09a55a9e97f7431c3a06df5103", + algorithm: "ed25519", + signature: + "e03643f6f73ebb6edcdedef62e55d132d7a0db11b1ca27b62992ba028f0290e21835898a06fe899746b45682c275ecd82150429401b4a97ebb3119239c70960b", + }; + + beforeEach(() => { + vi.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + vi.restoreAllMocks(); + vi.clearAllMocks(); + }); + + describe("Ed25519 algorithm", () => { + it("accepts 'Ed25519' (case-insensitive)", async () => { + const result = await validateSignature({ + ...validEdInput, + algorithm: "Ed25519", + }); + + expect(result).toBe(true); + }); + + it("accepts 'ed25519' (case-insensitive)", async () => { + const result = await validateSignature({ + ...validEdInput, + algorithm: "ed25519", + }); + expect(result).toBe(true); + }); + + it("returns false for different json content", async () => { + const invaliEdInput = { + ...validEdInput, + jsonContent: { test: "Hello" }, + }; + const result = await validateSignature(invaliEdInput); + expect(result).toBe(false); + }); + }); +}); + +const jsonContent = { + "@context": { + "@language": "en", + CIP100: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#", + CIP108: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#", + authors: { + "@container": "@set", + "@context": { + name: "http://xmlns.com/foaf/0.1/name", + witness: { + "@context": { + publicKey: "CIP100:publicKey", + signature: "CIP100:signature", + witnessAlgorithm: "CIP100:witnessAlgorithm", + }, + "@id": "CIP100:witness", + }, + }, + "@id": "CIP100:authors", + }, + body: { + "@context": { + abstract: "CIP108:abstract", + motivation: "CIP108:motivation", + rationale: "CIP108:rationale", + references: { + "@container": "@set", + "@context": { + GovernanceMetadata: "CIP100:GovernanceMetadataReference", + Other: "CIP100:OtherReference", + label: "CIP100:reference-label", + referenceHash: { + "@context": { + hashAlgorithm: "CIP100:hashAlgorithm", + hashDigest: "CIP108:hashDigest", + }, + "@id": "CIP108:referenceHash", + }, + uri: "CIP100:reference-uri", + }, + "@id": "CIP108:references", + }, + title: "CIP108:title", + }, + "@id": "CIP108:body", + }, + hashAlgorithm: "CIP100:hashAlgorithm", + }, + authors: [ + { + name: "mike", + witness: { + publicKey: + "38f01b9b41e7ea4bca5c093e472fa01198ebaf09a55a9e97f7431c3a06df5103", + signature: + "e03643f6f73ebb6edcdedef62e55d132d7a0db11b1ca27b62992ba028f0290e21835898a06fe899746b45682c275ecd82150429401b4a97ebb3119239c70960b", + witnessAlgorithm: "ed25519", + }, + }, + { + name: "bsok", + witness: { + publicKey: + "29dcd28014660e4c751a2449e7f6c7f8a89517b74a017b1ff4de4bf3b7668225", + signature: + "845829a201276761646472657373581cb567325153c5c0452811069272a8dfa1fe7ff4c9bfd5695059d4f810a166686173686564f45820c14215aed5a285b9d4f476a74c11110c25439a4310cc9688e15df9372c1bc7545840950b9b5c65f5195a877c62d8feb4f420443e822d5b5cd5a263d4045eba9c89d849d8864740f5b28b218964d7479d536416bf36f7517abfea3e5424e599a7d20b", + witnessAlgorithm: "CIP-0008", + }, + }, + ], + body: { + abstract: "My amazing epic metadata", + motivation: "😆😆😆😆😆😆", + rationale: "🚀", + references: [ + { + "@type": "Other", + label: "Ratification Methodology", + uri: "ipfs://bafkreiagfwdg3iejt5wpks5cwm35kibpana7zdbupn2xij4jrc33ugf6gm", + }, + ], + title: "Many authors test", + }, + hashAlgorithm: "blake2b-256", +}; diff --git a/govtool/frontend/src/utils/tests/validateSignature.test.ts b/govtool/frontend/src/utils/tests/validateSignature.test.ts index fed667560..143ec4d5d 100644 --- a/govtool/frontend/src/utils/tests/validateSignature.test.ts +++ b/govtool/frontend/src/utils/tests/validateSignature.test.ts @@ -1,16 +1,16 @@ -/** - * @vitest-environment node - * - * We are switching to the "node" environment because jsdom does not provide a full WebCrypto API, - * specifically window.crypto.subtle, which is required by @noble/ed25519 for signature - * verification. Node.js (v16.8+) includes native crypto.subtle support, making it suitable for - * cryptographic tests. Current browsers support window.crypto.subtle natively, so this issue only - * affects the test environment. */ - import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { validateSignature } from "../validateSignature"; describe("validateSignature", () => { + const validCip8Input = { + jsonContent, + publicKey: + "29dcd28014660e4c751a2449e7f6c7f8a89517b74a017b1ff4de4bf3b7668225", + algorithm: "CIP-8", + signature: + "845829a201276761646472657373581cb567325153c5c0452811069272a8dfa1fe7ff4c9bfd5695059d4f810a166686173686564f45820c14215aed5a285b9d4f476a74c11110c25439a4310cc9688e15df9372c1bc7545840950b9b5c65f5195a877c62d8feb4f420443e822d5b5cd5a263d4045eba9c89d849d8864740f5b28b218964d7479d536416bf36f7517abfea3e5424e599a7d20b", + }; + beforeEach(() => { vi.spyOn(console, "error").mockImplementation(() => {}); }); @@ -22,7 +22,7 @@ describe("validateSignature", () => { it("returns false if signature is missing", async () => { const result = await validateSignature({ - ...validInput, + ...validCip8Input, signature: undefined, }); expect(result).toBe(false); @@ -30,7 +30,7 @@ describe("validateSignature", () => { it("returns false if publicKey is missing", async () => { const result = await validateSignature({ - ...validInput, + ...validCip8Input, publicKey: undefined, }); expect(result).toBe(false); @@ -38,7 +38,7 @@ describe("validateSignature", () => { it("returns false if algorithm is missing", async () => { const result = await validateSignature({ - ...validInput, + ...validCip8Input, algorithm: undefined, }); expect(result).toBe(false); @@ -46,7 +46,7 @@ describe("validateSignature", () => { it("returns false if message is missing", async () => { const result = await validateSignature({ - ...validInput, + ...validCip8Input, jsonContent: undefined, }); expect(result).toBe(false); @@ -55,116 +55,125 @@ describe("validateSignature", () => { it("returns false for unsupported algorithm", async () => { vi.spyOn(console, "error").mockImplementation(() => {}); const result = await validateSignature({ - ...validInput, + ...validCip8Input, algorithm: "rsa", }); expect(result).toBe(false); expect(console.error).toHaveBeenCalledWith("Unsupported algorithm:", "rsa"); }); - describe("Ed25519 algorithm", () => { - it("accepts 'Ed25519' (case-insensitive)", async () => { + describe("CIP-8 algorithm", () => { + it("accepts 'CIP-8' (case-insensitive)", async () => { const result = await validateSignature({ - ...validInput, - algorithm: "Ed25519", + ...validCip8Input, + algorithm: "CIP-8", }); - expect(result).toBe(true); }); - it("accepts 'ed25519' (case-insensitive)", async () => { + it("accepts 'CIP-0008' (case-insensitive)", async () => { const result = await validateSignature({ - ...validInput, - algorithm: "ed25519", + ...validCip8Input, + algorithm: "CIP-0008", }); expect(result).toBe(true); }); + + it("returns false for different json content", async () => { + const invalidCip8Input = { + ...validCip8Input, + jsonContent: { test: "Hello" }, + }; + const result = await validateSignature(invalidCip8Input); + expect(result).toBe(false); + }); }); }); -const validInput = { - jsonContent: { - "@context": { - "@language": "en", - CIP100: - "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#", - CIP108: - "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#", - authors: { - "@container": "@set", - "@context": { - name: "http://xmlns.com/foaf/0.1/name", - witness: { - "@context": { - publicKey: "CIP100:publicKey", - signature: "CIP100:signature", - witnessAlgorithm: "CIP100:witnessAlgorithm", - }, - "@id": "CIP100:witness", +const jsonContent = { + "@context": { + "@language": "en", + CIP100: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#", + CIP108: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#", + authors: { + "@container": "@set", + "@context": { + name: "http://xmlns.com/foaf/0.1/name", + witness: { + "@context": { + publicKey: "CIP100:publicKey", + signature: "CIP100:signature", + witnessAlgorithm: "CIP100:witnessAlgorithm", }, + "@id": "CIP100:witness", }, - "@id": "CIP100:authors", }, - body: { - "@context": { - abstract: "CIP108:abstract", - motivation: "CIP108:motivation", - rationale: "CIP108:rationale", - references: { - "@container": "@set", - "@context": { - GovernanceMetadata: "CIP100:GovernanceMetadataReference", - Other: "CIP100:OtherReference", - label: "CIP100:reference-label", - referenceHash: { - "@context": { - hashAlgorithm: "CIP100:hashAlgorithm", - hashDigest: "CIP108:hashDigest", - }, - "@id": "CIP108:referenceHash", + "@id": "CIP100:authors", + }, + body: { + "@context": { + abstract: "CIP108:abstract", + motivation: "CIP108:motivation", + rationale: "CIP108:rationale", + references: { + "@container": "@set", + "@context": { + GovernanceMetadata: "CIP100:GovernanceMetadataReference", + Other: "CIP100:OtherReference", + label: "CIP100:reference-label", + referenceHash: { + "@context": { + hashAlgorithm: "CIP100:hashAlgorithm", + hashDigest: "CIP108:hashDigest", }, - uri: "CIP100:reference-uri", + "@id": "CIP108:referenceHash", }, - "@id": "CIP108:references", + uri: "CIP100:reference-uri", }, - title: "CIP108:title", + "@id": "CIP108:references", }, - "@id": "CIP108:body", + title: "CIP108:title", }, - hashAlgorithm: "CIP100:hashAlgorithm", + "@id": "CIP108:body", }, - authors: [ + hashAlgorithm: "CIP100:hashAlgorithm", + }, + authors: [ + { + name: "mike", + witness: { + publicKey: + "38f01b9b41e7ea4bca5c093e472fa01198ebaf09a55a9e97f7431c3a06df5103", + signature: + "e03643f6f73ebb6edcdedef62e55d132d7a0db11b1ca27b62992ba028f0290e21835898a06fe899746b45682c275ecd82150429401b4a97ebb3119239c70960b", + witnessAlgorithm: "ed25519", + }, + }, + { + name: "bsok", + witness: { + publicKey: + "29dcd28014660e4c751a2449e7f6c7f8a89517b74a017b1ff4de4bf3b7668225", + signature: + "845829a201276761646472657373581cb567325153c5c0452811069272a8dfa1fe7ff4c9bfd5695059d4f810a166686173686564f45820c14215aed5a285b9d4f476a74c11110c25439a4310cc9688e15df9372c1bc7545840950b9b5c65f5195a877c62d8feb4f420443e822d5b5cd5a263d4045eba9c89d849d8864740f5b28b218964d7479d536416bf36f7517abfea3e5424e599a7d20b", + witnessAlgorithm: "CIP-0008", + }, + }, + ], + body: { + abstract: "My amazing epic metadata", + motivation: "😆😆😆😆😆😆", + rationale: "🚀", + references: [ { - name: "Ryan!", - witness: { - publicKey: - "38f01b9b41e7ea4bca5c093e472fa01198ebaf09a55a9e97f7431c3a06df5103", - signature: - "29796c10ccb180c03d6053dffbbce7b6e9028dcfaf76312ebd161ed0ff40001b906b33f460ef99ed0533250a29a05d6aaab04f96aaf7c6d9acb9dfae5f5f1e0f", - witnessAlgorithm: "ed25519", - }, + "@type": "Other", + label: "Ratification Methodology", + uri: "ipfs://bafkreiagfwdg3iejt5wpks5cwm35kibpana7zdbupn2xij4jrc33ugf6gm", }, ], - body: { - abstract: - "This is Ryan's proposal for a Blockchain Ecosystem Budget, which allocates 100 million ada to various projects and initiatives within the ecosystem.", - motivation: "Without funding Ryan cannot buy an island.", - rationale: "uhhhhhhhhhhhhhhhh", - references: [ - { - "@type": "Other", - label: "Lol", - uri: "ipfs://xd", - }, - ], - title: "Ryan Blockchain Ecosystem Budget - 100M ada", - }, - hashAlgorithm: "blake2b-256", + title: "Many authors test", }, - messageHash: - "64e9e4dcbb15d4c4aff3c47119e1c2ce025e45041c3e5c568530d2613c04f9be", - publicKey: "38f01b9b41e7ea4bca5c093e472fa01198ebaf09a55a9e97f7431c3a06df5103", - algorithm: "ed25519", - signature: - "29796c10ccb180c03d6053dffbbce7b6e9028dcfaf76312ebd161ed0ff40001b906b33f460ef99ed0533250a29a05d6aaab04f96aaf7c6d9acb9dfae5f5f1e0f", + hashAlgorithm: "blake2b-256", }; diff --git a/govtool/frontend/src/utils/validateSignature.ts b/govtool/frontend/src/utils/validateSignature.ts index 1daf5168a..b39d274ea 100644 --- a/govtool/frontend/src/utils/validateSignature.ts +++ b/govtool/frontend/src/utils/validateSignature.ts @@ -1,6 +1,7 @@ -import { verifyAsync } from "@noble/ed25519"; +import { verifyAsync as verifyEd25519Signature } from "@noble/ed25519"; import { blake2bHex } from "blakejs"; import { canonizeJSON } from "./canonizeJSON"; +import { verifyCIP8Signature } from "./cip8verification"; export const validateSignature = async ({ signature, @@ -23,11 +24,11 @@ export const validateSignature = async ({ switch (algorithm) { case "ed25519": case "Ed25519": { - return await verifyAsync(signature, messageHash, publicKey); + return verifyEd25519Signature(signature, messageHash, publicKey); } case "CIP-8": case "CIP-0008": { - throw new Error("CIP-0008 is not supported yet"); + return verifyCIP8Signature(signature, messageHash, publicKey); } default: console.error("Unsupported algorithm:", algorithm);