Initial commit with translated description
This commit is contained in:
19
scripts/constants.js
Normal file
19
scripts/constants.js
Normal file
@@ -0,0 +1,19 @@
|
||||
export const transactionSender = "0xB3F5d3DD47F6ca17468898291491eBDA69a67797"; // relay sender address
|
||||
export const verifierDid =
|
||||
"did:iden3:privado:main:2SZu1G6YDUtk9AAY6TZic24CcCYcZvtdyp1cQv9cig"; // should be the same as dashboard DID
|
||||
export const callbackBase =
|
||||
"https://attestation-relay.billions.network/api/v1/callback?attestation=";
|
||||
export const walletAddress = "https://wallet.billions.network";
|
||||
export const verificationMessage =
|
||||
"Complete the verification to link your identity to the agent";
|
||||
export const pairingReasonMessage = "agent_pairing:v1";
|
||||
export const accept = [
|
||||
"iden3comm/v1;env=application/iden3-zkp-json;circuitId=authV2,authV3,authV3-8-32;alg=groth16",
|
||||
];
|
||||
export const nullifierSessionId = "240416041207230509012302";
|
||||
export const pouScopeId = 1; // keccak256(nullifierSessionId)
|
||||
export const pouAllowedIssuer = [
|
||||
"did:iden3:billions:main:2VwqkgA2dNEwsnmojaay7C5jJEb8ZygecqCSU3xVfm",
|
||||
];
|
||||
export const authScopeId = 2;
|
||||
export const urlShortener = "https://identity-dashboard.billions.network";
|
||||
80
scripts/createNewEthereumIdentity.js
Normal file
80
scripts/createNewEthereumIdentity.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const { KmsKeyType, hexToBytes } = require("@0xpolygonid/js-sdk");
|
||||
const { DidMethod, Blockchain, NetworkId } = require("@iden3/js-iden3-core");
|
||||
const { SigningKey, Wallet, JsonRpcProvider } = require("ethers");
|
||||
const { getInitializedRuntime } = require("./shared/bootstrap");
|
||||
const {
|
||||
parseArgs,
|
||||
formatError,
|
||||
outputSuccess,
|
||||
addHexPrefix,
|
||||
} = require("./shared/utils");
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseArgs();
|
||||
const {
|
||||
kms,
|
||||
identityWallet,
|
||||
didsStorage,
|
||||
billionsMainnetConfig,
|
||||
revocationOpts,
|
||||
} = await getInitializedRuntime();
|
||||
|
||||
// Use provided key or generate a new one
|
||||
let privateKeyHex = args.key;
|
||||
if (!privateKeyHex) {
|
||||
privateKeyHex = new SigningKey(Wallet.createRandom().privateKey)
|
||||
.privateKey;
|
||||
}
|
||||
|
||||
// Create signer from private key
|
||||
const signer = new SigningKey(addHexPrefix(privateKeyHex));
|
||||
|
||||
// Get the Secp256k1 key provider
|
||||
const keyProvider = kms.getKeyProvider(KmsKeyType.Secp256k1);
|
||||
if (!keyProvider) {
|
||||
console.error("Error: Secp256k1 key provider not found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create wallet with Billions Network provider
|
||||
const wallet = new Wallet(
|
||||
signer,
|
||||
new JsonRpcProvider(billionsMainnetConfig.url),
|
||||
);
|
||||
|
||||
// Create Ethereum-based identity
|
||||
let did;
|
||||
try {
|
||||
const result = await identityWallet.createEthereumBasedIdentity({
|
||||
method: DidMethod.Iden3,
|
||||
blockchain: Blockchain.Billions,
|
||||
networkId: NetworkId.Main,
|
||||
seed: hexToBytes(privateKeyHex),
|
||||
revocationOpts: revocationOpts,
|
||||
ethSigner: wallet,
|
||||
createBjjCredential: false,
|
||||
});
|
||||
did = result.did;
|
||||
} catch (err) {
|
||||
console.error(
|
||||
`Error: Failed to create Ethereum-based identity: ${err.message}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Save DID to storage
|
||||
await didsStorage.save({
|
||||
did: did.string(),
|
||||
publicKeyHex: signer.publicKey,
|
||||
isDefault: true,
|
||||
});
|
||||
|
||||
outputSuccess(did.string());
|
||||
} catch (error) {
|
||||
console.error(formatError(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
30
scripts/generateChallenge.js
Normal file
30
scripts/generateChallenge.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const { randomInt } = require("crypto");
|
||||
const { getInitializedRuntime } = require("./shared/bootstrap");
|
||||
const { parseArgs, formatError, outputSuccess } = require("./shared/utils");
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseArgs();
|
||||
|
||||
if (!args.did) {
|
||||
console.error("Error: --did parameter is required");
|
||||
console.error("Usage: node scripts/generateChallenge.js --did <did>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { challengeStorage } = await getInitializedRuntime();
|
||||
|
||||
// Generate random challenge
|
||||
const challenge = randomInt(0, 10000000000).toString();
|
||||
|
||||
// Save challenge to storage
|
||||
await challengeStorage.save(args.did, challenge);
|
||||
|
||||
outputSuccess(challenge);
|
||||
} catch (error) {
|
||||
console.error(formatError(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
39
scripts/getDidDocument.js
Normal file
39
scripts/getDidDocument.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const { getInitializedRuntime } = require("./shared/bootstrap");
|
||||
const {
|
||||
parseArgs,
|
||||
formatError,
|
||||
outputSuccess,
|
||||
createDidDocument,
|
||||
} = require("./shared/utils");
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseArgs();
|
||||
const { didsStorage } = await getInitializedRuntime();
|
||||
|
||||
// Get DID entry - either specific DID or default
|
||||
const entry = args.did
|
||||
? await didsStorage.find(args.did)
|
||||
: await didsStorage.getDefault();
|
||||
|
||||
if (!entry) {
|
||||
const errorMsg = args.did
|
||||
? `No DID ${args.did} found`
|
||||
: "No default DID found. Create one with createNewEthereumIdentity.js";
|
||||
console.error(errorMsg);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const didDocument = createDidDocument(entry.did, entry.publicKeyHex);
|
||||
|
||||
outputSuccess({
|
||||
didDocument,
|
||||
did: entry.did,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(formatError(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
24
scripts/getIdentities.js
Normal file
24
scripts/getIdentities.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const { getInitializedRuntime } = require("./shared/bootstrap");
|
||||
const { formatError, outputSuccess } = require("./shared/utils");
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const { didsStorage } = await getInitializedRuntime();
|
||||
|
||||
const identities = await didsStorage.list();
|
||||
|
||||
if (identities.length === 0) {
|
||||
console.error(
|
||||
"No identities found. Create one with createNewEthereumIdentity.js",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
outputSuccess(identities);
|
||||
} catch (error) {
|
||||
console.error(formatError(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
153
scripts/linkHumanToAgent.js
Normal file
153
scripts/linkHumanToAgent.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const { auth } = require("@iden3/js-iden3-auth");
|
||||
const { CircuitId } = require("@0xpolygonid/js-sdk");
|
||||
const {
|
||||
buildEthereumAddressFromDid,
|
||||
parseArgs,
|
||||
urlFormating,
|
||||
outputSuccess,
|
||||
formatError,
|
||||
} = require("./shared/utils");
|
||||
const { computeAttestationHash } = require("./shared/attestation");
|
||||
const { getInitializedRuntime } = require("./shared/bootstrap");
|
||||
const { signChallenge } = require("./signChallenge");
|
||||
const {
|
||||
transactionSender,
|
||||
verifierDid,
|
||||
callbackBase,
|
||||
walletAddress,
|
||||
verificationMessage,
|
||||
pairingReasonMessage,
|
||||
accept,
|
||||
nullifierSessionId,
|
||||
pouScopeId,
|
||||
pouAllowedIssuer,
|
||||
authScopeId,
|
||||
urlShortener,
|
||||
} = require("./constants");
|
||||
|
||||
function createPOUScope(transactionSender) {
|
||||
return {
|
||||
id: pouScopeId,
|
||||
circuitId: CircuitId.AtomicQueryV3OnChainStable,
|
||||
params: {
|
||||
sender: transactionSender,
|
||||
nullifierSessionId: nullifierSessionId,
|
||||
},
|
||||
query: {
|
||||
allowedIssuers: pouAllowedIssuer,
|
||||
type: "UniquenessCredential",
|
||||
context: "ipfs://QmcUEDa42Er4nfNFmGQVjiNYFaik6kvNQjfTeBrdSx83At",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createAuthScope(recipientDid) {
|
||||
return {
|
||||
id: authScopeId,
|
||||
circuitId: CircuitId.AuthV3_8_32,
|
||||
params: {
|
||||
challenge: computeAttestationHash({
|
||||
recipientDid: recipientDid,
|
||||
recipientEthAddress: buildEthereumAddressFromDid(recipientDid),
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function createAuthRequestMessage(jws, recipientDid) {
|
||||
const callback = callbackBase + jws;
|
||||
const scope = [
|
||||
createPOUScope(transactionSender),
|
||||
createAuthScope(recipientDid),
|
||||
];
|
||||
|
||||
const message = auth.createAuthorizationRequestWithMessage(
|
||||
pairingReasonMessage,
|
||||
verificationMessage,
|
||||
verifierDid,
|
||||
encodeURI(callback),
|
||||
{
|
||||
scope,
|
||||
accept: accept,
|
||||
},
|
||||
);
|
||||
|
||||
// the code does request to trusted URL shortener service to create
|
||||
// a short link for the wallet deep link.
|
||||
// This is needed to avoid issues with very long URLs in some wallets and to improve user experience.
|
||||
const shortenerResponse = await fetch(`${urlShortener}/shortener`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(message),
|
||||
});
|
||||
|
||||
if (shortenerResponse.status !== 201) {
|
||||
throw new Error(
|
||||
`URL shortener failed with status ${shortenerResponse.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { url } = await shortenerResponse.json();
|
||||
|
||||
return `${walletAddress}#request_uri=${url}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pairing URL for linking a human identity to the agent.
|
||||
* @param {object} challenge - Challenge object with name and description fields.
|
||||
* @param {string} [didOverride] - Optional DID to use instead of the default.
|
||||
* @returns {Promise<string>} The wallet URL the human must open to complete verification.
|
||||
*/
|
||||
async function createPairing(challenge, didOverride) {
|
||||
const { kms, didsStorage } = await getInitializedRuntime();
|
||||
|
||||
const entry = didOverride
|
||||
? await didsStorage.find(didOverride)
|
||||
: await didsStorage.getDefault();
|
||||
|
||||
if (!entry) {
|
||||
const errorMsg = didOverride
|
||||
? `No DID ${didOverride} found`
|
||||
: "No default DID found";
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
const recipientDid = entry.did;
|
||||
const signedChallenge = await signChallenge(challenge, entry, kms);
|
||||
|
||||
return await createAuthRequestMessage(signedChallenge, recipientDid);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseArgs();
|
||||
|
||||
if (!args.challenge) {
|
||||
console.error(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error:
|
||||
"Invalid arguments. Usage: node linkHumanToAgent.js --challenge <json> [--did <did>]",
|
||||
}),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const challenge = JSON.parse(args.challenge);
|
||||
const url = await createPairing(challenge, args.did);
|
||||
|
||||
outputSuccess({
|
||||
success: true,
|
||||
data: urlFormating(verificationMessage, url),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(formatError(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { createPairing };
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
28
scripts/manualLinkHumanToAgent.js
Normal file
28
scripts/manualLinkHumanToAgent.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const { createPairing } = require("./linkHumanToAgent");
|
||||
const { parseArgs, formatError } = require("./shared/utils");
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseArgs();
|
||||
|
||||
if (!args.challenge) {
|
||||
console.error(
|
||||
"Invalid arguments. Usage: node manualLinkHumanToAgent.js --challenge <json> [--did <did>]",
|
||||
);
|
||||
console.error(
|
||||
'Example: node manualLinkHumanToAgent.js --challenge \'{"name": "Agent Name", "description": "Short description of the agent"}\'',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const challenge = JSON.parse(args.challenge);
|
||||
const url = await createPairing(challenge, args.did);
|
||||
|
||||
console.log(url);
|
||||
} catch (error) {
|
||||
console.error(formatError(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
2957
scripts/package-lock.json
generated
Normal file
2957
scripts/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
scripts/package.json
Normal file
23
scripts/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "verified-agent-identity",
|
||||
"version": "0.0.2",
|
||||
"description": "Billions OpenClaw verification skill",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"identity",
|
||||
"openclaw",
|
||||
"zk"
|
||||
],
|
||||
"author": "BillionsNetwork",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@0xpolygonid/js-sdk": "1.42.1",
|
||||
"@iden3/js-iden3-auth": "1.14.0",
|
||||
"@iden3/js-iden3-core": "1.8.0",
|
||||
"@noble/curves": "^1.9.2",
|
||||
"ethers": "^6.13.4",
|
||||
"uuid": "^11.0.3"
|
||||
}
|
||||
}
|
||||
85
scripts/shared/attestation.js
Normal file
85
scripts/shared/attestation.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const { ethers } = require("ethers");
|
||||
const { DID } = require("@iden3/js-iden3-core");
|
||||
|
||||
const ATTESTATION_SCHEMA_ID =
|
||||
"0xca354bee6dc5eded165461d15ccb13aceb6f77ebbb1fd3fe45aca686097f2911"; // bytes32
|
||||
|
||||
const ATTESTER_DID = ""; // string
|
||||
const ATTESTER_IDEN3_ID = 0n; // uint256
|
||||
const ATTESTER_ETH_ADDRESS = "0x0000000000000000000000000000000000000000"; // address
|
||||
|
||||
const EXPIRATION_TIME = 0n; // uint256 — 0 means no expiration
|
||||
const REVOCABLE = false; // bool
|
||||
const REF_ID =
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000"; // bytes32
|
||||
const DATA = "0x"; // bytes
|
||||
|
||||
function extractIdFromDid(did) {
|
||||
return DID.idFromDID(DID.parse(did)).bigInt();
|
||||
}
|
||||
|
||||
function buildJsonAttestation(req) {
|
||||
return {
|
||||
schemaId: ATTESTATION_SCHEMA_ID,
|
||||
attester: {
|
||||
did: ATTESTER_DID,
|
||||
iden3Id: ATTESTER_IDEN3_ID.toString(),
|
||||
ethereumAddress: ATTESTER_ETH_ADDRESS,
|
||||
},
|
||||
recipient: {
|
||||
did: req.recipientDid,
|
||||
iden3Id: extractIdFromDid(req.recipientDid).toString(),
|
||||
ethereumAddress: req.recipientEthAddress,
|
||||
},
|
||||
expirationTime: EXPIRATION_TIME.toString(),
|
||||
revocable: REVOCABLE,
|
||||
refId: REF_ID,
|
||||
data: DATA,
|
||||
};
|
||||
}
|
||||
|
||||
function buildEncodedAttestation(req) {
|
||||
const encoded = ethers.AbiCoder.defaultAbiCoder().encode(
|
||||
[
|
||||
"bytes32", // schemaId
|
||||
"string", // attester.did
|
||||
"uint256", // attester.iden3Id
|
||||
"address", // attester.ethereumAddress
|
||||
"string", // recipient.did
|
||||
"uint256", // recipient.iden3Id
|
||||
"address", // recipient.ethereumAddress
|
||||
"uint256", // expirationTime
|
||||
"bool", // revocable
|
||||
"bytes32", // refId
|
||||
"bytes", // data
|
||||
],
|
||||
[
|
||||
ATTESTATION_SCHEMA_ID,
|
||||
ATTESTER_DID,
|
||||
ATTESTER_IDEN3_ID,
|
||||
ATTESTER_ETH_ADDRESS,
|
||||
req.recipientDid,
|
||||
extractIdFromDid(req.recipientDid),
|
||||
req.recipientEthAddress,
|
||||
EXPIRATION_TIME,
|
||||
REVOCABLE,
|
||||
REF_ID,
|
||||
DATA,
|
||||
],
|
||||
);
|
||||
return encoded;
|
||||
}
|
||||
|
||||
function computeAttestationHash(req) {
|
||||
const hashHex = ethers.keccak256(buildEncodedAttestation(req));
|
||||
return (
|
||||
BigInt(hashHex) &
|
||||
BigInt("0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")
|
||||
).toString();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildEncodedAttestation,
|
||||
computeAttestationHash,
|
||||
buildJsonAttestation,
|
||||
};
|
||||
149
scripts/shared/bootstrap.js
vendored
Normal file
149
scripts/shared/bootstrap.js
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
const {
|
||||
KMS,
|
||||
Sec256k1Provider,
|
||||
KmsKeyType,
|
||||
IdentityWallet,
|
||||
CredentialStatusType,
|
||||
EthStateStorage,
|
||||
CredentialStorage,
|
||||
IdentityStorage,
|
||||
InMemoryMerkleTreeStorage,
|
||||
CredentialStatusResolverRegistry,
|
||||
RHSResolver,
|
||||
CredentialWallet,
|
||||
defaultEthConnectionConfig,
|
||||
BjjProvider,
|
||||
} = require("@0xpolygonid/js-sdk");
|
||||
|
||||
const { KeysFileStorage } = require("./storage/keys");
|
||||
const { IdentitiesFileStorage } = require("./storage/identities");
|
||||
const { DidsFileStorage } = require("./storage/did");
|
||||
const { ChallengeFileStorage } = require("./storage/challenge");
|
||||
|
||||
let cachedRuntime = null;
|
||||
|
||||
/**
|
||||
* Creates and configures the KMS (Key Management System)
|
||||
*/
|
||||
async function newInMemoryKMS() {
|
||||
const memoryKeyStore = new KeysFileStorage("kms.json");
|
||||
const secpProvider = new Sec256k1Provider(
|
||||
KmsKeyType.Secp256k1,
|
||||
memoryKeyStore,
|
||||
);
|
||||
const bjjProvider = new BjjProvider(KmsKeyType.BabyJubJub, memoryKeyStore);
|
||||
const kms = new KMS();
|
||||
kms.registerKeyProvider(KmsKeyType.Secp256k1, secpProvider);
|
||||
kms.registerKeyProvider(KmsKeyType.BabyJubJub, bjjProvider);
|
||||
return kms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Ethereum state storage for Billions Network
|
||||
*/
|
||||
function newEthStateStorage(billionsMainnetConfig) {
|
||||
return new EthStateStorage(billionsMainnetConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates data storage with credential, identity, merkle tree, and state storages
|
||||
*/
|
||||
function newDataStorage(ethStateStorage) {
|
||||
return {
|
||||
credential: new CredentialStorage(
|
||||
new IdentitiesFileStorage("credentials.json"),
|
||||
),
|
||||
identity: new IdentityStorage(
|
||||
new IdentitiesFileStorage("identities.json"),
|
||||
new IdentitiesFileStorage("profiles.json"),
|
||||
),
|
||||
mt: new InMemoryMerkleTreeStorage(40),
|
||||
states: ethStateStorage,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates credential wallet with credential status resolvers
|
||||
*/
|
||||
function newCredentialWallet(dataStorage) {
|
||||
const resolvers = new CredentialStatusResolverRegistry();
|
||||
resolvers.register(
|
||||
CredentialStatusType.Iden3ReverseSparseMerkleTreeProof,
|
||||
new RHSResolver(dataStorage.states),
|
||||
);
|
||||
return new CredentialWallet(dataStorage, resolvers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates identity wallet
|
||||
*/
|
||||
function newIdentityWallet(kms, dataStorage, credentialWallet) {
|
||||
return new IdentityWallet(kms, dataStorage, credentialWallet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Billions Network mainnet configuration
|
||||
*/
|
||||
function getBillionsMainnetConfig() {
|
||||
return {
|
||||
...defaultEthConnectionConfig,
|
||||
url: "https://rpc-mainnet.billions.network",
|
||||
contractAddress: "0x3c9acb2205aa72a05f6d77d708b5cf85fca3a896",
|
||||
chainId: 45056,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets default revocation options
|
||||
*/
|
||||
function getRevocationOpts() {
|
||||
return {
|
||||
type: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof,
|
||||
id: "https://rhs-staging.polygonid.me",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes and returns all runtime dependencies.
|
||||
* Uses caching to avoid re-initialization.
|
||||
*
|
||||
* @returns {Promise<Object>} Runtime object containing:
|
||||
* - kms: Key Management System
|
||||
* - identityWallet: Identity wallet instance
|
||||
* - didsStorage: DID storage
|
||||
* - challengeStorage: Challenge storage
|
||||
* - billionsMainnetConfig: Billions Network configuration
|
||||
* - revocationOpts: Revocation options
|
||||
*/
|
||||
async function getInitializedRuntime() {
|
||||
if (cachedRuntime) {
|
||||
return cachedRuntime;
|
||||
}
|
||||
|
||||
const billionsMainnetConfig = getBillionsMainnetConfig();
|
||||
const revocationOpts = getRevocationOpts();
|
||||
|
||||
const kms = await newInMemoryKMS();
|
||||
const stateStorage = newEthStateStorage(billionsMainnetConfig);
|
||||
const dataStorage = newDataStorage(stateStorage);
|
||||
const credentialWallet = newCredentialWallet(dataStorage);
|
||||
const identityWallet = newIdentityWallet(kms, dataStorage, credentialWallet);
|
||||
|
||||
const didsStorage = new DidsFileStorage("defaultDid.json");
|
||||
const challengeStorage = new ChallengeFileStorage("challenges.json");
|
||||
|
||||
cachedRuntime = {
|
||||
kms,
|
||||
identityWallet,
|
||||
didsStorage,
|
||||
challengeStorage,
|
||||
billionsMainnetConfig,
|
||||
revocationOpts,
|
||||
};
|
||||
|
||||
return cachedRuntime;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getInitializedRuntime,
|
||||
};
|
||||
35
scripts/shared/storage/base.js
Normal file
35
scripts/shared/storage/base.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
|
||||
class FileStorage {
|
||||
constructor(filename, baseDir = `${process.env.HOME}/.openclaw/billions`) {
|
||||
this.filePath = path.join(baseDir, filename);
|
||||
}
|
||||
|
||||
async ensureDirectory() {
|
||||
const dir = path.dirname(this.filePath);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
async readFile() {
|
||||
try {
|
||||
const data = await fs.readFile(this.filePath, "utf-8");
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async writeFile(data) {
|
||||
await this.ensureDirectory();
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
const tempPath = `${this.filePath}.tmp`;
|
||||
await fs.writeFile(tempPath, json, "utf-8");
|
||||
await fs.rename(tempPath, this.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { FileStorage };
|
||||
53
scripts/shared/storage/challenge.js
Normal file
53
scripts/shared/storage/challenge.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const { FileStorage } = require("./base");
|
||||
|
||||
class ChallengeFileStorage extends FileStorage {
|
||||
constructor(filename = "challenges.json") {
|
||||
super(filename);
|
||||
}
|
||||
|
||||
async save(did, challenge) {
|
||||
const entries = await this.readFile();
|
||||
const created_at = new Date();
|
||||
|
||||
const index = entries.findIndex((entry) => entry.did === did);
|
||||
|
||||
if (index >= 0) {
|
||||
// Update existing entry
|
||||
entries[index] = { did, challenge, created_at };
|
||||
} else {
|
||||
// Add new entry
|
||||
entries.push({ did, challenge, created_at });
|
||||
}
|
||||
|
||||
await this.writeFile(entries);
|
||||
}
|
||||
|
||||
async find(did) {
|
||||
const entries = await this.readFile();
|
||||
return entries.find((entry) => entry.did === did);
|
||||
}
|
||||
|
||||
async getChallenge(did) {
|
||||
const entry = await this.find(did);
|
||||
return entry?.challenge;
|
||||
}
|
||||
|
||||
async list() {
|
||||
return this.readFile();
|
||||
}
|
||||
|
||||
async delete(did) {
|
||||
const entries = await this.readFile();
|
||||
const initialLength = entries.length;
|
||||
const filtered = entries.filter((entry) => entry.did !== did);
|
||||
|
||||
if (filtered.length < initialLength) {
|
||||
await this.writeFile(filtered);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ChallengeFileStorage };
|
||||
81
scripts/shared/storage/crypto.js
Normal file
81
scripts/shared/storage/crypto.js
Normal file
@@ -0,0 +1,81 @@
|
||||
"use strict";
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
const ALGORITHM = "aes-256-gcm";
|
||||
const IV_BYTES = 12;
|
||||
const TAG_BYTES = 16;
|
||||
|
||||
function getMasterKey() {
|
||||
const rawKey = process.env.BILLIONS_NETWORK_MASTER_KMS_KEY;
|
||||
|
||||
if (typeof rawKey !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmedKey = rawKey.trim();
|
||||
|
||||
// Reject whitespace-only or too-short keys to avoid weak/blank-looking master keys.
|
||||
// Returning null keeps behavior consistent with the "no key configured" case.
|
||||
const MIN_MASTER_KEY_LENGTH = 16;
|
||||
if (trimmedKey.length < MIN_MASTER_KEY_LENGTH) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmedKey;
|
||||
}
|
||||
|
||||
function deriveAesKey(masterKeyString) {
|
||||
return crypto.createHash("sha256").update(masterKeyString, "utf8").digest();
|
||||
}
|
||||
|
||||
function encryptKey(keyHex, masterKeyString) {
|
||||
const aesKey = deriveAesKey(masterKeyString);
|
||||
const iv = crypto.randomBytes(IV_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, aesKey, iv, {
|
||||
authTagLength: TAG_BYTES,
|
||||
});
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(keyHex, "utf8"),
|
||||
cipher.final(),
|
||||
]);
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
return [
|
||||
iv.toString("hex"),
|
||||
authTag.toString("hex"),
|
||||
encrypted.toString("hex"),
|
||||
].join(":");
|
||||
}
|
||||
|
||||
function decryptKey(encryptedPayload, masterKeyString) {
|
||||
const parts = encryptedPayload.split(":");
|
||||
if (parts.length !== 3) {
|
||||
throw new Error("Invalid encrypted key format in kms.json");
|
||||
}
|
||||
const [ivHex, authTagHex, ciphertextHex] = parts;
|
||||
|
||||
const aesKey = deriveAesKey(masterKeyString);
|
||||
const iv = Buffer.from(ivHex, "hex");
|
||||
const authTag = Buffer.from(authTagHex, "hex");
|
||||
const ciphertext = Buffer.from(ciphertextHex, "hex");
|
||||
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, aesKey, iv, {
|
||||
authTagLength: TAG_BYTES,
|
||||
});
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
try {
|
||||
return Buffer.concat([
|
||||
decipher.update(ciphertext),
|
||||
decipher.final(),
|
||||
]).toString("utf8");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"kms.json decryption failed: wrong BILLIONS_NETWORK_MASTER_KMS_KEY or file has been tampered with",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getMasterKey, encryptKey, decryptKey };
|
||||
47
scripts/shared/storage/did.js
Normal file
47
scripts/shared/storage/did.js
Normal file
@@ -0,0 +1,47 @@
|
||||
const { FileStorage } = require("./base");
|
||||
|
||||
/**
|
||||
* DidsFileStorage manages DID entries with default DID support
|
||||
*/
|
||||
class DidsFileStorage extends FileStorage {
|
||||
constructor(filename = "defaultDid.json") {
|
||||
super(filename);
|
||||
}
|
||||
|
||||
async save({ did, publicKeyHex, isDefault = false }) {
|
||||
const entries = await this.readFile();
|
||||
|
||||
// If setting this as default, unset all other defaults
|
||||
if (isDefault) {
|
||||
entries.forEach((entry) => {
|
||||
entry.isDefault = false;
|
||||
});
|
||||
}
|
||||
|
||||
const index = entries.findIndex((entry) => entry.did === did);
|
||||
|
||||
if (index >= 0) {
|
||||
entries[index] = { did, publicKeyHex, isDefault };
|
||||
} else {
|
||||
entries.push({ did, publicKeyHex, isDefault });
|
||||
}
|
||||
|
||||
await this.writeFile(entries);
|
||||
}
|
||||
|
||||
async find(did) {
|
||||
const entries = await this.readFile();
|
||||
return entries.find((entry) => entry.did === did);
|
||||
}
|
||||
|
||||
async getDefault() {
|
||||
const entries = await this.readFile();
|
||||
return entries.find((entry) => entry.isDefault);
|
||||
}
|
||||
|
||||
async list() {
|
||||
return this.readFile();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DidsFileStorage };
|
||||
44
scripts/shared/storage/identities.js
Normal file
44
scripts/shared/storage/identities.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const { FileStorage } = require("./base");
|
||||
|
||||
/**
|
||||
* IdentitiesFileStorage implements IDataSource<Type> interface from js-sdk
|
||||
*/
|
||||
class IdentitiesFileStorage extends FileStorage {
|
||||
async load() {
|
||||
return await this.readFile();
|
||||
}
|
||||
|
||||
async save(key, value, keyName = "id") {
|
||||
const data = await this.readFile();
|
||||
const index = data.findIndex((item) => item[keyName] === key);
|
||||
|
||||
if (index >= 0) {
|
||||
// Update existing item
|
||||
data[index] = value;
|
||||
} else {
|
||||
// Add new item
|
||||
data.push(value);
|
||||
}
|
||||
|
||||
await this.writeFile(data);
|
||||
}
|
||||
|
||||
async get(key, keyName = "id") {
|
||||
const data = await this.readFile();
|
||||
return data.find((item) => item[keyName] === key);
|
||||
}
|
||||
|
||||
async delete(key, keyName = "id") {
|
||||
const data = await this.readFile();
|
||||
const filtered = data.filter((item) => item[keyName] !== key);
|
||||
|
||||
if (filtered.length === data.length) {
|
||||
// Item not found, throw error to match expected behavior
|
||||
throw new Error(`Item with ${keyName}=${key} not found`);
|
||||
}
|
||||
|
||||
await this.writeFile(filtered);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { IdentitiesFileStorage };
|
||||
117
scripts/shared/storage/keys.js
Normal file
117
scripts/shared/storage/keys.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const { FileStorage } = require("./base");
|
||||
const { getMasterKey, encryptKey, decryptKey } = require("./crypto");
|
||||
|
||||
/**
|
||||
* File-based storage for cryptographic keys.
|
||||
* Implements AbstractPrivateKeyStore interface from js-sdk.
|
||||
* Stores keys in JSON format as an array of per-entry versioned objects.
|
||||
*/
|
||||
class KeysFileStorage extends FileStorage {
|
||||
constructor(filename = "kms.json") {
|
||||
super(filename);
|
||||
// Holds raw on-disk entries that could not be decoded in this session
|
||||
// (e.g. encrypted entries when the master key env var is absent).
|
||||
// They are round-tripped untouched through writeFile so no data is lost.
|
||||
this._opaqueEntries = [];
|
||||
}
|
||||
|
||||
_decodeEntry(entry) {
|
||||
// Legacy format
|
||||
if (Object.prototype.hasOwnProperty.call(entry, "privateKeyHex")) {
|
||||
return { alias: entry.alias, privateKeyHex: entry.privateKeyHex };
|
||||
}
|
||||
|
||||
if (entry.version === 1) {
|
||||
const { alias, key } = entry.data;
|
||||
|
||||
const { createdAt } = entry.data;
|
||||
|
||||
if (entry.provider === "plain") {
|
||||
return { alias, privateKeyHex: key, createdAt };
|
||||
}
|
||||
|
||||
if (entry.provider === "encrypted") {
|
||||
const masterKey = getMasterKey();
|
||||
if (!masterKey) {
|
||||
return { alias, _opaque: true, _raw: entry };
|
||||
}
|
||||
return { alias, privateKeyHex: decryptKey(key, masterKey), createdAt };
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unrecognised kms.json entry format: ${entry.alias || entry.data.alias || "unknown alias"}}`,
|
||||
);
|
||||
}
|
||||
|
||||
_encodeEntry({ alias, privateKeyHex, createdAt }) {
|
||||
const masterKey = getMasterKey();
|
||||
if (masterKey) {
|
||||
return {
|
||||
version: 1,
|
||||
provider: "encrypted",
|
||||
data: { alias, key: encryptKey(privateKeyHex, masterKey), createdAt },
|
||||
};
|
||||
}
|
||||
return {
|
||||
version: 1,
|
||||
provider: "plain",
|
||||
data: { alias, key: privateKeyHex, createdAt },
|
||||
};
|
||||
}
|
||||
|
||||
async readFile() {
|
||||
const raw = await super.readFile();
|
||||
if (!Array.isArray(raw)) {
|
||||
throw new Error("kms.json root must be an array");
|
||||
}
|
||||
const decoded = raw.map((entry) => this._decodeEntry(entry));
|
||||
// Stash raw on-disk objects for entries we cannot decode right now so
|
||||
// writeFile can round-trip them untouched.
|
||||
this._opaqueEntries = decoded.filter((e) => e._opaque).map((e) => e._raw);
|
||||
return decoded.filter((e) => !e._opaque);
|
||||
}
|
||||
|
||||
async writeFile(keys) {
|
||||
const encoded = keys.map((entry) => this._encodeEntry(entry));
|
||||
await super.writeFile([...encoded, ...this._opaqueEntries]);
|
||||
}
|
||||
|
||||
async importKey(args) {
|
||||
const keys = await this.readFile();
|
||||
const index = keys.findIndex((entry) => entry.alias === args.alias);
|
||||
|
||||
if (index >= 0) {
|
||||
keys[index].privateKeyHex = args.key;
|
||||
} else {
|
||||
keys.push({
|
||||
alias: args.alias,
|
||||
privateKeyHex: args.key,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// update key under alias
|
||||
this._opaqueEntries = this._opaqueEntries.filter(
|
||||
(raw) => raw.data?.alias !== args.alias,
|
||||
);
|
||||
|
||||
await this.writeFile(keys);
|
||||
}
|
||||
|
||||
async get(args) {
|
||||
const keys = await this.readFile();
|
||||
const entry = keys.find((entry) => entry.alias === args.alias);
|
||||
return entry ? entry.privateKeyHex : "";
|
||||
}
|
||||
|
||||
async list() {
|
||||
const keys = await this.readFile();
|
||||
return keys.map((entry) => ({
|
||||
alias: entry.alias,
|
||||
key: entry.privateKeyHex,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { KeysFileStorage };
|
||||
131
scripts/shared/utils.js
Normal file
131
scripts/shared/utils.js
Normal file
@@ -0,0 +1,131 @@
|
||||
const { bytesToHex, keyPath } = require("@0xpolygonid/js-sdk");
|
||||
const { DID, Id } = require("@iden3/js-iden3-core");
|
||||
const { v7: uuid } = require("uuid");
|
||||
const { secp256k1 } = require("@noble/curves/secp256k1");
|
||||
|
||||
/**
|
||||
* Removes the "0x" prefix from a hexadecimal string if it exists
|
||||
*/
|
||||
function normalizeKey(keyId) {
|
||||
return keyId.startsWith("0x") ? keyId.slice(2) : keyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add hex prefix if missing
|
||||
*/
|
||||
function addHexPrefix(keyId) {
|
||||
return keyId.startsWith("0x") ? keyId : `0x${keyId}`;
|
||||
}
|
||||
|
||||
function buildEthereumAddressFromDid(did) {
|
||||
const ethereumAddress = Id.ethAddressFromId(DID.idFromDID(DID.parse(did)));
|
||||
return `0x${bytesToHex(ethereumAddress)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a W3C DID document for an Ethereum-based identity
|
||||
*/
|
||||
function createDidDocument(did, publicKeyHex) {
|
||||
return {
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/did/v1",
|
||||
"https://w3id.org/security/suites/secp256k1recovery-2020/v2",
|
||||
],
|
||||
id: did,
|
||||
verificationMethod: [
|
||||
{
|
||||
id: `${did}#ethereum-based-id`,
|
||||
controller: did,
|
||||
type: "EcdsaSecp256k1RecoveryMethod2020",
|
||||
ethereumAddress: buildEthereumAddressFromDid(did),
|
||||
publicKeyHex: secp256k1.Point.fromHex(publicKeyHex.slice(2)).toHex(
|
||||
true,
|
||||
),
|
||||
},
|
||||
],
|
||||
authentication: [`${did}#ethereum-based-id`],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a normalized key path for storage
|
||||
*/
|
||||
function normalizedKeyPath(keyType, keyID) {
|
||||
return keyPath(keyType, normalizeKey(keyID));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Authorization Response Message for challenge signing
|
||||
*/
|
||||
function getAuthResponseMessage(did, challenge) {
|
||||
const { PROTOCOL_CONSTANTS } = require("@0xpolygonid/js-sdk");
|
||||
return {
|
||||
id: uuid(),
|
||||
thid: uuid(),
|
||||
from: did,
|
||||
to: "",
|
||||
type: PROTOCOL_CONSTANTS.PROTOCOL_MESSAGE_TYPE
|
||||
.AUTHORIZATION_RESPONSE_MESSAGE_TYPE,
|
||||
body: {
|
||||
message: challenge,
|
||||
scope: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses command line arguments into an object
|
||||
* Example: --did abc --key 123 => { did: 'abc', key: '123' }
|
||||
*/
|
||||
function parseArgs() {
|
||||
const args = {};
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
if (process.argv[i].startsWith("--")) {
|
||||
const key = process.argv[i].slice(2);
|
||||
const value = process.argv[i + 1];
|
||||
args[key] = value;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an error message for CLI output
|
||||
*/
|
||||
function formatError(error) {
|
||||
return `Error: ${error.message}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs success message to stdout
|
||||
*/
|
||||
function outputSuccess(data) {
|
||||
if (typeof data === "string") {
|
||||
console.log(data);
|
||||
} else {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function urlFormating(title, url) {
|
||||
return `[${title}](${url})`;
|
||||
}
|
||||
|
||||
function codeFormating(data) {
|
||||
return `\\\`\\\`\\\`${data}\\\`\\\`\\\``;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeKey,
|
||||
addHexPrefix,
|
||||
createDidDocument,
|
||||
normalizedKeyPath,
|
||||
getAuthResponseMessage,
|
||||
parseArgs,
|
||||
formatError,
|
||||
outputSuccess,
|
||||
buildEthereumAddressFromDid,
|
||||
urlFormating,
|
||||
codeFormating,
|
||||
};
|
||||
92
scripts/signChallenge.js
Normal file
92
scripts/signChallenge.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const {
|
||||
JWSPacker,
|
||||
byteEncoder,
|
||||
byteDecoder,
|
||||
KmsKeyType,
|
||||
} = require("@0xpolygonid/js-sdk");
|
||||
const { getInitializedRuntime } = require("./shared/bootstrap");
|
||||
const {
|
||||
parseArgs,
|
||||
formatError,
|
||||
outputSuccess,
|
||||
createDidDocument,
|
||||
getAuthResponseMessage,
|
||||
buildEthereumAddressFromDid,
|
||||
} = require("./shared/utils");
|
||||
const { buildJsonAttestation } = require("./shared/attestation");
|
||||
|
||||
async function signChallenge(challenge, entry, kms) {
|
||||
const didDocument = createDidDocument(entry.did, entry.publicKeyHex);
|
||||
|
||||
const resolveDIDDocument = {
|
||||
resolve: () => Promise.resolve({ didDocument }),
|
||||
};
|
||||
|
||||
const jwsPacker = new JWSPacker(kms, resolveDIDDocument);
|
||||
|
||||
challenge.attestationInfo = buildJsonAttestation({
|
||||
recipientDid: entry.did,
|
||||
recipientEthAddress: buildEthereumAddressFromDid(entry.did),
|
||||
});
|
||||
|
||||
const authMessage = getAuthResponseMessage(entry.did, challenge);
|
||||
const msgBytes = byteEncoder.encode(JSON.stringify(authMessage));
|
||||
|
||||
let token;
|
||||
try {
|
||||
token = await jwsPacker.pack(msgBytes, {
|
||||
alg: "ES256K-R",
|
||||
issuer: entry.did,
|
||||
did: entry.did,
|
||||
keyType: KmsKeyType.Secp256k1,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to sign challenge: ${err.message}`);
|
||||
}
|
||||
|
||||
return byteDecoder.decode(token);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseArgs();
|
||||
|
||||
if (!args.challenge) {
|
||||
console.error("Error: --challenge are required");
|
||||
console.error(
|
||||
"Usage: node scripts/signChallenge.js --challenge <challenge> [--did <did>]",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { kms, didsStorage } = await getInitializedRuntime();
|
||||
|
||||
// Get DID entry - either specific DID or default
|
||||
const entry = args.did
|
||||
? await didsStorage.find(args.did)
|
||||
: await didsStorage.getDefault();
|
||||
|
||||
if (!entry) {
|
||||
const errorMsg = args.did
|
||||
? `No DID ${args.did} found`
|
||||
: "No default DID found";
|
||||
console.error(errorMsg);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const challenge = JSON.parse(args.challenge);
|
||||
const tokenString = await signChallenge(challenge, entry, kms);
|
||||
|
||||
outputSuccess({ success: true, data: { token: tokenString } });
|
||||
} catch (error) {
|
||||
console.error(formatError(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { signChallenge };
|
||||
|
||||
// Run main if this script is executed directly (not imported as a module)
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
66
scripts/verifySignature.js
Normal file
66
scripts/verifySignature.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const { JWSPacker, byteEncoder } = require("@0xpolygonid/js-sdk");
|
||||
const { getInitializedRuntime } = require("./shared/bootstrap");
|
||||
const { parseArgs, formatError, outputSuccess } = require("./shared/utils");
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const args = parseArgs();
|
||||
|
||||
if (!args.token) {
|
||||
console.error("Error: --token parameters is required");
|
||||
console.error(
|
||||
"Usage: node scripts/verifySignature.js --did <did> --token <token>",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { kms, challengeStorage } = await getInitializedRuntime();
|
||||
|
||||
// Get the stored challenge
|
||||
const challenge = await challengeStorage.getChallenge(args.did);
|
||||
if (!challenge) {
|
||||
console.error(`Error: No challenge found for DID: ${args.did}`);
|
||||
console.error("Generate a challenge first with generateChallenge.js");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create DID resolver that fetches from remote resolver
|
||||
const resolveDIDDocument = {
|
||||
resolve: async (did) => {
|
||||
const resp = await fetch(
|
||||
`https://resolver.privado.id/1.0/identifiers/${did}`,
|
||||
);
|
||||
const didResolutionRes = await resp.json();
|
||||
return didResolutionRes;
|
||||
},
|
||||
};
|
||||
|
||||
// Create JWS packer and unpack token
|
||||
const jws = new JWSPacker(kms, resolveDIDDocument);
|
||||
const basicMessage = await jws.unpack(byteEncoder.encode(args.token));
|
||||
|
||||
// Verify the sender
|
||||
if (basicMessage.from !== args.did) {
|
||||
console.error(
|
||||
`Error: Invalid from: expected from ${args.did}, got ${basicMessage.from}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Verify the challenge matches
|
||||
const payload = basicMessage.body;
|
||||
if (payload.message !== challenge) {
|
||||
console.error(
|
||||
`Error: Invalid signature: challenge mismatch ${payload.message} !== ${challenge}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
outputSuccess("Signature verified successfully");
|
||||
} catch (error) {
|
||||
console.error(formatError(error));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user