@tangle-js/ld-proofs
Version:
Linked Data Proofs on the Tangle. Powered by IOTA Identity & IOTA Streams
255 lines • 19.8 kB
JavaScript
/* eslint-disable jsdoc/require-jsdoc */
import { IotaAnchoringChannel, AnchoringChannelErrorNames, SeedHelper } from "@tangle-js/anchors";
import LdProofError from "./errors/ldProofError.mjs";
import LdProofErrorNames from "./errors/ldProofErrorNames.mjs";
import JsonHelper from "./helpers/jsonHelper.mjs";
import ValidationHelper from "./helpers/validationHelper.mjs";
import { IotaVerifier } from "./iotaVerifier.mjs";
/**
* Linked Data Proof Verifier.
*
* In the future it will also need to verify.
*
*/
export class IotaLdProofVerifier {
/**
* Verifies a JSON(-LD) document.
*
* @param doc The JSON(-LD) document.
* @param options The verification options.
* @returns True or false with the verification result.
*/
static async verifyJson(doc, options) {
let document;
try {
document = JsonHelper.getAnchoredDocument(doc);
}
catch (error) {
if (error.name === LdProofErrorNames.JSON_DOC_NOT_SIGNED) {
return false;
}
throw error;
}
return this.doVerifyDoc(document, undefined, options);
}
/**
* Verifies a chain of JSON(-LD) documents ensuring that are anchored to the same channel.
* And in the order implicit in the list.
* @param docs The chain of documents to verify.
* @param options The verification options.
* @returns The global verification result.
*/
static async verifyJsonChain(docs, options) {
return this.doVerifyChain(docs, options);
}
/**
* Verifies a list of JSON(-LD) documents using the proof passed as parameter.
* The individual proofs of the events shall be found on the Channel specified.
*
* @param docs The documents.
* @param proof The proof that points to the Channel used for verification.
* @param options The verification options.
* @returns The global result of the verification.
*/
static async verifyJsonChainSingleProof(docs, proof, options) {
return this.doVerifyChainSingleProof(docs, proof, options);
}
/**
* Verifies a chain of JSON(LD) documents ensuring that are anchored to the same channel.
* And in the order implicit in the list.
* @param docs The chain of documents to verify.
* @param options The verification options.
* @returns The global verification result.
*/
static async doVerifyChain(docs, options) {
const documents = [];
// The anchored documents are obtained
for (const document of docs) {
let doc;
try {
doc = JsonHelper.getAnchoredDocument(document);
}
catch (error) {
if (error.name === LdProofErrorNames.JSON_DOC_NOT_SIGNED) {
return false;
}
throw error;
}
documents.push(doc);
}
const node = options?.node;
// The Channel will be used to verify the proofs
const channelID = documents[0].proof.proofValue.channelID;
let channel;
// If channel cannot be bound the proof will fail
try {
channel = await IotaAnchoringChannel.fromID(channelID, { node }).bind(SeedHelper.generateSeed());
}
catch (error) {
if (error.name === AnchoringChannelErrorNames.CHANNEL_BINDING_ERROR) {
return false;
}
throw error;
}
let index = 0;
const verificationOptions = {
...options
};
for (const document of documents) {
const proofValue = document.proof.proofValue;
// If the channel is not the expected the verification fails
if (proofValue.channelID !== channelID) {
return false;
}
// The first needs to properly position on the channel
if (index === 0) {
verificationOptions.strict = false;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
}
else if (options && options.strict === false) {
verificationOptions.strict = false;
}
else {
verificationOptions.strict = true;
}
const verificationResult = await this.doVerifyChainedDoc(document, channel, verificationOptions);
if (!verificationResult.result) {
return false;
}
index++;
}
return true;
}
static async doVerifyChainSingleProof(docs, proof, options) {
const proofDetails = proof.proofValue;
const documents = [];
for (const document of docs) {
const doc = JsonHelper.getDocument(document);
documents.push(doc);
}
let isStrict = true;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (options?.strict === false) {
isStrict = false;
}
const channelID = proofDetails.channelID;
let channel;
try {
channel = await IotaAnchoringChannel.fromID(channelID, options).bind(SeedHelper.generateSeed());
}
catch (error) {
if (error.name === AnchoringChannelErrorNames.CHANNEL_BINDING_ERROR) {
return false;
}
throw error;
}
// Clone it to use it locally
const docProof = JSON.parse(JSON.stringify(proof));
const doc = documents[0];
doc.proof = docProof;
// First document is verified as single document
const verificationResult = await this.doVerifyDoc(doc, channel, options);
// Restore the original document
delete doc.proof;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (verificationResult === false) {
return false;
}
// In strict mode we should test the anchorageID but unfortunately that is a IOTA Streams limitation
// let currentAnchorageID = verificationResult.fetchResult.msgID;
// Verification of the rest of documents
for (let index = 1; index < documents.length; index++) {
const aDoc = documents[index];
let fetchResult = await channel.fetchNext();
let verified = false;
while (!verified && fetchResult) {
const linkedDataSignature = JSON.parse(fetchResult.message.toString());
// now assign the Linked Data Signature as proof
aDoc.proof = linkedDataSignature;
verified = await IotaVerifier.verifyJson(aDoc, { node: options?.node });
if (!verified) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (isStrict === false) {
fetchResult = await channel.fetchNext();
}
else {
return false;
}
}
}
if (!verified || !fetchResult) {
return false;
}
delete aDoc.proof;
}
return true;
}
static async doVerifyDoc(document, channel, options) {
if (options?.node && !ValidationHelper.url(options.node)) {
throw new LdProofError(LdProofErrorNames.INVALID_NODE, "The node has to be a URL");
}
const proofDetails = document.proof.proofValue;
let fetchResult;
let targetChannel = channel;
try {
if (!channel) {
targetChannel = await IotaAnchoringChannel.fromID(proofDetails.channelID, options).bind(SeedHelper.generateSeed());
}
fetchResult = await targetChannel.fetch(proofDetails.anchorageID, proofDetails.msgID);
}
catch (error) {
if (error.name === AnchoringChannelErrorNames.MSG_NOT_FOUND ||
error.name === AnchoringChannelErrorNames.ANCHORAGE_NOT_FOUND ||
error.name === AnchoringChannelErrorNames.CHANNEL_BINDING_ERROR) {
return false;
}
// If it is not the controlled error the error is thrown
throw error;
}
const linkedDataSignature = JSON.parse(fetchResult.message.toString());
// now assign the Linked Data Signature as proof
document.proof = linkedDataSignature;
const result = await IotaVerifier.verifyJson(document, { node: options?.node });
return result;
}
static async doVerifyChainedDoc(document, channel, options) {
if (options?.node && !ValidationHelper.url(options.node)) {
throw new LdProofError(LdProofErrorNames.INVALID_NODE, "The node has to be a URL");
}
const linkedDataProof = document.proof;
const proofDetails = document.proof.proofValue;
let fetchResult;
const targetMsgID = proofDetails.msgID;
try {
// In strict mode we just fetch the next message
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
if (!options || options.strict === undefined || options.strict === true) {
fetchResult = await channel.fetchNext();
}
else {
fetchResult = await channel.fetchNext();
while (fetchResult && fetchResult.msgID !== targetMsgID) {
fetchResult = await channel.fetchNext();
}
}
}
catch (error) {
if (error.name === AnchoringChannelErrorNames.MSG_NOT_FOUND) {
return { result: false };
}
throw error;
}
// If this is not the message expected the verification has failed
if (!fetchResult || fetchResult.msgID !== targetMsgID) {
return { result: false };
}
const linkedDataSignature = JSON.parse(fetchResult.message.toString());
// now assign the Linked Data Signature as proof
document.proof = linkedDataSignature;
const result = await IotaVerifier.verifyJson(document, { node: options?.node });
// Restore the original document
document.proof = linkedDataProof;
return { result, fetchResult };
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW90YUxkUHJvb2ZWZXJpZmllci5tanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvaW90YUxkUHJvb2ZWZXJpZmllci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSx3Q0FBd0M7QUFFeEMsT0FBTyxFQUFxQixvQkFBb0IsRUFBRSwwQkFBMEIsRUFBRSxVQUFVLEVBQUUsTUFDakYsb0JBQW9CLENBQUM7QUFDOUIsT0FBTyxZQUFZLE1BQU0sdUJBQXVCLENBQUM7QUFDakQsT0FBTyxpQkFBaUIsTUFBTSw0QkFBNEIsQ0FBQztBQUMzRCxPQUFPLFVBQVUsTUFBTSxzQkFBc0IsQ0FBQztBQUM5QyxPQUFPLGdCQUFnQixNQUFNLDRCQUE0QixDQUFDO0FBQzFELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQU85Qzs7Ozs7R0FLRztBQUNILE1BQU0sT0FBTyxtQkFBbUI7SUFDNUI7Ozs7OztPQU1HO0lBQ0ksTUFBTSxDQUFDLEtBQUssQ0FBQyxVQUFVLENBQUMsR0FBbUMsRUFDOUQsT0FBcUM7UUFDckMsSUFBSSxRQUErQixDQUFDO1FBRXBDLElBQUk7WUFDQSxRQUFRLEdBQUcsVUFBVSxDQUFDLG1CQUFtQixDQUFDLEdBQUcsQ0FBQyxDQUFDO1NBQ2xEO1FBQUMsT0FBTyxLQUFLLEVBQUU7WUFDWixJQUFJLEtBQUssQ0FBQyxJQUFJLEtBQUssaUJBQWlCLENBQUMsbUJBQW1CLEVBQUU7Z0JBQ3RELE9BQU8sS0FBSyxDQUFDO2FBQ2hCO1lBRUQsTUFBTSxLQUFLLENBQUM7U0FDZjtRQUVELE9BQU8sSUFBSSxDQUFDLFdBQVcsQ0FBQyxRQUFRLEVBQUUsU0FBUyxFQUFFLE9BQU8sQ0FBQyxDQUFDO0lBQzFELENBQUM7SUFFRDs7Ozs7O09BTUc7SUFDSSxNQUFNLENBQUMsS0FBSyxDQUFDLGVBQWUsQ0FBQyxJQUF3QyxFQUN4RSxPQUFxQztRQUNyQyxPQUFPLElBQUksQ0FBQyxhQUFhLENBQUMsSUFBSSxFQUFF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