Skip to content

Instantly share code, notes, and snippets.

@Harasz
Created May 12, 2025 14:13
Show Gist options
  • Save Harasz/e3c041c19b85a1f03e1637243554e47b to your computer and use it in GitHub Desktop.
Save Harasz/e3c041c19b85a1f03e1637243554e47b to your computer and use it in GitHub Desktop.
import {
Client,
PrivateKey,
PublicKey,
TopicCreateTransaction,
TopicMessageSubmitTransaction,
} from "@hashgraph/sdk";
import {
JsonLdDIDDocument,
KeysUtility,
VerificationMethod,
} from "@swiss-digital-assets-institute/core";
import { TopicReaderHederaClient } from "@swiss-digital-assets-institute/resolver";
import { inspect } from "util";
import { canonicalize } from "@tufjs/canonical-json";
import { createHash } from "crypto";
// https://www.rfc-editor.org/rfc/rfc8037.html
interface Ed25519JWK {
kty: "OKP";
crv: "Ed25519";
alg?: "EdDSA";
// Public key
x: string;
// Private key
d?: string;
kid?: string;
key_ops?: Array<"sign" | "verify">;
use?: "sig";
}
type ProofPurpose =
| "authentication"
| "assertionMethod"
| "keyAgreement"
| "capabilityDelegation"
| "capabilityInvocation";
interface Proof {
type: string;
created: string;
proofPurpose: ProofPurpose;
verificationMethod: string;
proofValue: string;
cryptosuite?: string;
expires?: string;
domain?: string;
challenge?: string;
[key: string]: unknown;
}
type LDContext = string | string[];
interface InputDocument {
"@context"?: LDContext;
[key: string]: unknown;
}
interface SecuredDataDocument extends InputDocument {
proof: Proof;
}
interface ProofOptions {
proofPurpose: ProofPurpose;
verificationMethod: string;
expires?: string;
domain?: string;
challenge?: string;
}
interface ProofConfig extends ProofOptions {
type: string;
cryptosuite?: string;
created: string;
}
type UnsecuredDocument = InputDocument;
const canonizeJsonDocument = async (document: object): Promise<string> => {
return canonicalize(document);
};
const hashData = async (data: string): Promise<Uint8Array> => {
return createHash("sha256").update(data).digest();
};
interface VerificationResult {
verified: boolean;
verifiedDocument: UnsecuredDocument;
}
class InternalEd25519Verifier {
private publicKey: PublicKey;
constructor(publicKey: string | PublicKey | Ed25519JWK) {
if (typeof publicKey === "string") {
this.publicKey = PublicKey.fromStringED25519(publicKey);
} else if (publicKey instanceof PublicKey) {
this.publicKey = publicKey;
} else {
this.publicKey = PublicKey.fromBytesED25519(
Buffer.from(publicKey.x, "base64url")
);
}
}
async verifyProof(
securedDocument: SecuredDataDocument
): Promise<VerificationResult> {
const unsecuredDocument = structuredClone(securedDocument);
delete unsecuredDocument.proof;
const proofOptions = structuredClone(securedDocument.proof);
delete proofOptions.proofValue;
const proofBytes = KeysUtility.fromMultibase(
securedDocument.proof.proofValue
).toBytes();
if (proofOptions["@context"]) {
const proofContext = Array.isArray(proofOptions["@context"])
? proofOptions["@context"]
: [proofOptions["@context"]];
const docContext = Array.isArray(securedDocument["@context"])
? securedDocument["@context"]
: [securedDocument["@context"] || []];
const isValidContext = proofContext.every(
(ctx, index) => docContext[index] === ctx
);
if (!isValidContext) {
return {
verified: false,
verifiedDocument: null,
};
}
unsecuredDocument["@context"] = proofOptions["@context"] as LDContext;
}
const canonicalProofConfig = await canonizeJsonDocument(proofOptions);
const canonicalDocument = await canonizeJsonDocument(unsecuredDocument);
const canonicalProofConfigHash = await hashData(canonicalProofConfig);
const canonicalDocumentHash = await hashData(canonicalDocument);
const hashedData = Buffer.concat([
canonicalDocumentHash,
canonicalProofConfigHash,
]);
const verified = this.publicKey.verify(hashedData, proofBytes);
return {
verified,
verifiedDocument: verified ? unsecuredDocument : null,
};
}
}
/**
* InternalEd25519Signer is a class that implements the Data Integrity EdDSA Cryptosuites v1.0 for the Ed25519 algorithm.
*
* @see https://www.w3.org/TR/vc-di-eddsa/
*/
class InternalEd25519Signer extends InternalEd25519Verifier {
private key: PrivateKey;
constructor(privateKey: string | PrivateKey | Ed25519JWK) {
let key: PrivateKey;
if (typeof privateKey === "string") {
key = PrivateKey.fromStringED25519(privateKey);
} else if (privateKey instanceof PrivateKey) {
key = privateKey;
} else {
key = PrivateKey.fromBytesED25519(Buffer.from(privateKey.d, "base64url"));
}
super(key.publicKey);
this.key = key;
}
async createProof(
inputDocument: InputDocument,
proofOptions: ProofOptions
): Promise<SecuredDataDocument> {
const proofConfig: ProofConfig = {
type: "DataIntegrityProof",
cryptosuite: "eddsa-jcs-2022",
created: new Date().toISOString(),
proofPurpose: proofOptions.proofPurpose,
verificationMethod: proofOptions.verificationMethod,
};
if (inputDocument["@context"]) {
proofConfig["@context"] = inputDocument["@context"];
}
if (proofOptions.expires) {
proofConfig.expires = proofOptions.expires;
}
if (proofOptions.domain) {
proofConfig.domain = proofOptions.domain;
}
if (proofOptions.challenge) {
proofConfig.challenge = proofOptions.challenge;
}
const canonicalProofConfig = await canonizeJsonDocument(proofConfig);
const canonicalDocument = await canonizeJsonDocument(inputDocument);
const canonicalProofConfigHash = await hashData(canonicalProofConfig);
const canonicalDocumentHash = await hashData(canonicalDocument);
const hashedData = Buffer.concat([
canonicalDocumentHash,
canonicalProofConfigHash,
]);
const proofBytes = this.key.sign(hashedData);
const proofValue =
KeysUtility.fromBytes(proofBytes).toMultibase("base58btc");
const proof: Proof = {
...proofConfig,
proofValue,
};
return {
...inputDocument,
proof,
};
}
}
// Create a new topic and publish a 'create' message
async function createDidAndPublish(client: Client) {
// 1. Generate Ed25519 keypair
const privateKey = PrivateKey.generateED25519();
const publicKey = privateKey.publicKey;
const pubKeyBytes = publicKey.toBytesRaw();
const base58btcKey = KeysUtility.fromBytes(pubKeyBytes).toBase58();
const publicKeyMultibase = KeysUtility.fromBytes(pubKeyBytes).toMultibase();
// 2. Create topic
const topicTx = await new TopicCreateTransaction()
.setTopicMemo("DID v2 PoC")
.execute(client);
const topicId = (await topicTx.getReceipt(client)).topicId.toString();
// 3. Build DID
const network = "testnet";
const did = `did:hedera:${network}:${base58btcKey}_${topicId}`;
const verificationMethodId = `${did}#key-1`;
// 4. Build DID document
const didDocument = {
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/ed25519-2020/v1",
],
id: did,
controller: [did],
verificationMethod: [
{
id: verificationMethodId,
type: "Ed25519VerificationKey2020",
controller: did,
publicKeyMultibase,
},
],
capabilityInvocation: [verificationMethodId],
};
// 5. Build and sign create message
const createPayload = {
version: "2.0",
operation: "create",
did,
didDocument,
};
const signer = new InternalEd25519Signer(privateKey);
const signedCreatePayload = await signer.createProof(createPayload, {
proofPurpose: "capabilityInvocation",
verificationMethod: verificationMethodId,
});
// 6. Publish to topic
await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage(Buffer.from(JSON.stringify(signedCreatePayload)))
.execute(client);
console.log("\n--- DID Created ---");
console.log("DID:", did);
console.log(`Topic: ${topicId}`);
return {
did,
topicId,
privateKey,
publicKey,
verificationMethodId,
didDocument,
};
}
// Publish an update message (e.g., add a service)
async function updateDidAndPublish({
client,
did,
topicId,
privateKey,
verificationMethodId,
didDocument,
updateFn,
}) {
// Apply update
const updatedDidDocument = updateFn({ ...didDocument });
const updatePayload = {
version: "2.0",
operation: "update",
did,
didDocument: updatedDidDocument,
};
const signer = new InternalEd25519Signer(privateKey);
const signedUpdatePayload = await signer.createProof(updatePayload, {
proofPurpose: "capabilityInvocation",
verificationMethod: verificationMethodId,
});
await new TopicMessageSubmitTransaction()
.setTopicId(topicId)
.setMessage(Buffer.from(JSON.stringify(signedUpdatePayload)))
.execute(client);
return updatedDidDocument;
}
// Resolve: fetch all messages, reconstruct latest valid DID document
async function resolveDid({ did }) {
const topicId = did.split("_")[1];
const topicReader = new TopicReaderHederaClient();
// wait for 10 seconds
await new Promise((resolve) => setTimeout(resolve, 10000));
const messages = await topicReader.fetchAllToDate(topicId, "testnet");
let previousDidDocument: JsonLdDIDDocument | null = null;
let currentDidDocument: JsonLdDIDDocument | null = null;
for (const message of messages) {
const parsed = JSON.parse(message);
const { version, operation, did, didDocument, proof } = parsed;
if (version !== "2.0" || !proof) continue;
let verificationMethodEntry: VerificationMethod | null = null;
let publicKeyMultibase: string | null = null;
// For 'create', use the verification method in the current didDocument
// For 'update', use the verification method in the previous didDocument
let docToCheck = operation === "create" ? didDocument : previousDidDocument;
if (!docToCheck) throw new Error("No document to check");
// console.log(inspect(docToCheck, { depth: null }));
// Find the verification method entry
if (
docToCheck.verificationMethod &&
Array.isArray(docToCheck.verificationMethod)
) {
verificationMethodEntry = docToCheck.verificationMethod.find(
(vm: VerificationMethod) => {
return vm.id === proof.verificationMethod;
}
);
}
if (!verificationMethodEntry)
throw new Error("No verification method entry");
if (
verificationMethodEntry.type === "Ed25519VerificationKey2020" &&
verificationMethodEntry.publicKeyMultibase
) {
publicKeyMultibase = verificationMethodEntry.publicKeyMultibase;
} else {
throw new Error("Invalid verification method");
}
// Build the verifier
const publicKey =
KeysUtility.fromMultibase(publicKeyMultibase).toDerString();
let verifier = new InternalEd25519Verifier(publicKey);
// Verify the proof
const { verified, verifiedDocument } = await verifier.verifyProof(parsed);
if (!verified) throw new Error("Proof verification failed");
console.log(inspect(verifiedDocument, { depth: null }));
// If valid, update the current state
previousDidDocument =
verifiedDocument.didDocument as unknown as JsonLdDIDDocument;
currentDidDocument = didDocument;
}
return currentDidDocument;
}
// Main flow
(async () => {
const client = Client.forTestnet().setOperator(
"",
""
);
// 1. Create DID
const { did, topicId, privateKey, verificationMethodId, didDocument } =
await createDidAndPublish(client);
const resolvedDidDocument0 = await resolveDid({ did });
console.log(inspect(resolvedDidDocument0, { depth: null }));
const privateKey2 = PrivateKey.generateED25519();
const publicKey2 = privateKey2.publicKey;
const pubKeyBytes2 = publicKey2.toBytesRaw();
const publicKeyMultibase2 = KeysUtility.fromBytes(pubKeyBytes2).toMultibase();
// 2. First update: add a service
const updatedDidDocument1 = await updateDidAndPublish({
client,
did,
topicId,
privateKey,
verificationMethodId,
didDocument,
updateFn: (doc) => {
doc.verificationMethod.push({
id: `${did}#key-2`,
type: "Ed25519VerificationKey2020",
controller: did,
publicKeyMultibase: publicKeyMultibase2,
});
return doc;
},
});
const resolvedDidDocument1 = await resolveDid({ did });
console.log(inspect(resolvedDidDocument1, { depth: null }));
// 3. Second update: add another service
await updateDidAndPublish({
client,
did,
topicId,
privateKey: privateKey2,
verificationMethodId: `${did}#key-2`,
didDocument: updatedDidDocument1,
updateFn: (doc) => {
doc.service = [
{
id: `${did}#service-2`,
type: "LinkedDomains",
serviceEndpoint: "https://example.com/did",
},
];
return doc;
},
});
const resolvedDidDocument2 = await resolveDid({ did });
console.log(inspect(resolvedDidDocument2, { depth: null }));
client.close();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment