UNPKG

@keeex/tsr

Version:

Manipulate data related to TSR with pki.js

464 lines 18.1 kB
/** * @license * @preserve * * KeeeX SAS Public code * https://keeex.me * Copyright 2013-2024 KeeeX All Rights Reserved. * * These computer program listings and specifications, herein, * are and remain the property of KeeeX SAS. The intellectual * and technical concepts herein are proprietary to KeeeX SAS * and may be covered by EU and foreign patents, * patents in process, trade secrets and copyright law. * * These listings are published as a way to provide third party * with the ability to process KeeeX data. * As such, support for public inquiries is limited. * They are provided "as-is", without warrany of any kind. * * They shall not be reproduced or copied or used in whole or * in part as the basis for manufacture or sale of items unless * prior written permission is obtained from KeeeX SAS. * * For a license agreement, please contact: * <mailto: contact@keeex.net> * */ // #region Imports import * as digest from "@keeex/crypto/digest.js"; import { randomBytes } from "@keeex/crypto/random.js"; import { asArray } from "@keeex/utils/array.js"; import { abEqual } from "@keeex/utils/arraybuffer.js"; import { asError } from "@keeex/utils/error.js"; import * as uint8Array from "@keeex/utils/uint8array.js"; import * as asn1js from "asn1js"; import * as pkijsLib from "pkijs"; import * as consts from "./consts.js"; import * as pkijsUtils from "./pkijs.js"; // #endregion // #region Constants const MS_DIVIDER = 1000; // #endregion // #region Creation const getNonce = async () => { let tries = 10000; while (--tries > 0) { /* eslint-disable-next-line no-await-in-loop */ const valueHex = await randomBytes(consts.NONCE_LENGTH); // Forbidden prefixes for valid Integer ASN1 values // eslint-disable-next-line @typescript-eslint/no-magic-numbers if (valueHex[0] !== 0x00 && valueHex[0] !== 0xff) return new asn1js.Integer({ valueHex }); } throw new Error("Unexpected monte-carlo error"); }; const genTstInfo = async (serialNumber, genTime, policy, ordering, accuracy, query, tsaCert) => new pkijsLib.TSTInfo({ ...(accuracy ? { accuracy: new pkijsLib.Accuracy(accuracy) } : undefined), genTime, messageImprint: await pkijsUtils.messageImprintFromInput(query), nonce: await getNonce(), ordering, policy, serialNumber: asn1js.Integer.fromBigInt(serialNumber), tsa: new pkijsLib.GeneralName({ type: 4, value: tsaCert.subject }), version: 1 }); const genSignedData = (content, signers, digestAlgorithm, certificatesChain) => new pkijsLib.SignedData({ certificates: certificatesChain, encapContentInfo: new pkijsLib.EncapsulatedContentInfo({ eContentType: pkijsLib.id_eContentType_TSTInfo, eContent: new asn1js.OctetString({ valueHex: content.toSchema().toBER() }) }), signerInfos: signers.map(signerInput => new pkijsLib.SignerInfo({ digestAlgorithm: new pkijsLib.AlgorithmIdentifier({ algorithmId: pkijsUtils.cryptoDigestAlgToPkiOid(digestAlgorithm) }), sid: new pkijsLib.IssuerAndSerialNumber({ issuer: signerInput.certificate.issuer, serialNumber: signerInput.certificate.serialNumber }), version: 1 })), version: 3 }); /** * Helper to get TSTInfo from SignedData * * Digest algorithms are extracted from SignedData itself; if an additional algorithm is required * (for exemple when signing) it can be requested here; pkijs code will add it to the list later on. */ const getTstFromSignedData = async (signedData, extraAlgorithms) => { const eContent = signedData.encapContentInfo.eContent; if (!eContent) throw new Error("Missing encapsulated content"); const tstInfo = pkijsLib.TSTInfo.fromBER(eContent.getValue()); const contentDigests = {}; for (const digestAlgorithm of signedData.digestAlgorithms) { const actualAlgorithm = pkijsUtils.pkiOidToCryptoDigestAlg(digestAlgorithm.algorithmId); if (actualAlgorithm in contentDigests) continue; /* eslint-disable-next-line no-await-in-loop */ contentDigests[actualAlgorithm] = await pkijsUtils.getContentDigest(eContent, actualAlgorithm); } if (extraAlgorithms) { for (const extraAlgorithm of extraAlgorithms) { if (extraAlgorithm in contentDigests) continue; /* eslint-disable-next-line no-await-in-loop */ contentDigests[extraAlgorithm] = await pkijsUtils.getContentDigest(eContent, extraAlgorithm); } } return { tstInfo, contentDigests }; }; /** Get the correct certificate from a list */ const getSignerCertificate = (certificates, signerInfo) => { const info = signerInfo.sid; if (!(info instanceof pkijsLib.IssuerAndSerialNumber)) throw new Error("Unexpected sid value"); for (const candidate of certificates) { if (!(candidate instanceof pkijsLib.Certificate)) { throw new Error("Unexpected certificate type"); } if (candidate.issuer.isEqual(info.issuer) && candidate.serialNumber.isEqual(info.serialNumber)) { return candidate; } } throw new Error("Missing certificate for signerInfo"); }; /** * Sign a SignedData for a TSR * * If multiple signerInfo are provided, the private keys must be provided in the same order */ const signTsrSignedAttrs = async (signedData, privateKey) => { const extraDigests = signedData.signerInfos.map(c => pkijsUtils.pkiOidToCryptoDigestAlg(c.digestAlgorithm.algorithmId)); const { tstInfo, contentDigests } = await getTstFromSignedData(signedData, extraDigests); await Promise.all(signedData.signerInfos.map(async signerInfo => { const digestAlgorithm = pkijsUtils.pkiOidToCryptoDigestAlg(signerInfo.digestAlgorithm.algorithmId); const attributes = []; attributes.push(new pkijsLib.Attribute({ type: consts.OID_CONTENT_TYPE, values: [new asn1js.ObjectIdentifier({ value: pkijsLib.id_eContentType_TSTInfo })] })); attributes.push(new pkijsLib.Attribute({ type: consts.OID_SIGNING_TIME, values: [new asn1js.UTCTime({ valueDate: tstInfo.genTime })] })); const certificate = getSignerCertificate(signedData.certificates ?? [], signerInfo); attributes.push(new pkijsLib.Attribute({ type: consts.OID_SIGNING_CERTIFICATE_V1, values: [await pkijsUtils.createSigningCertificate(certificate)] })); const contentDigest = contentDigests[digestAlgorithm]; if (!contentDigest) throw new Error("Unexpected state"); attributes.push(new pkijsLib.Attribute({ type: consts.OID_DIGEST, values: [contentDigest] })); // eslint-disable-next-line require-atomic-updates signerInfo.signedAttrs = new pkijsLib.SignedAndUnsignedAttributes({ type: 0, attributes }); })); for (let i = 0; i < signedData.signerInfos.length; ++i) { const subtleAlgo = pkijsUtils.cryptoDigestAlgToSubtleAlg(pkijsUtils.pkiOidToCryptoDigestAlg(signedData.signerInfos[i].digestAlgorithm.algorithmId)); const currentPrivateKey = privateKey[i]; /* eslint-disable-next-line no-await-in-loop */ if (currentPrivateKey) await signedData.sign(currentPrivateKey, i, subtleAlgo); } }; // #endregion // #region Verify const getTsrFromInput = input => { if (input instanceof pkijsLib.TimeStampResp) return input; return pkijsLib.TimeStampResp.fromBER(input); }; /** Extract signed data from TimeStampResp */ const getSignedData = input => { const contentInfo = input.timeStampToken; if (!contentInfo) throw new Error("Missing content info"); return new pkijsLib.SignedData({ schema: contentInfo.content }); }; const checkAttrOid = (attribute, expectedOID) => { const oid = attribute.values[0]; if (!(oid instanceof asn1js.ObjectIdentifier) || oid.getValue() !== expectedOID) { throw new Error("Invalid OID"); } }; const checkAttrTime = (attribute, tstInfo) => { const utcTime = attribute.values[0]; if (!(utcTime instanceof asn1js.UTCTime)) throw new Error("Unexpected value"); const expectedTime = Math.floor(tstInfo.genTime.getTime() / MS_DIVIDER); const signedTime = Math.floor(utcTime.toDate().getTime() / MS_DIVIDER); if (signedTime !== expectedTime) throw new Error("Invalid date"); }; const checkAttrCertificateV2 = async (attribute, signedData) => { const seq1 = attribute.values[0]; if (!(seq1 instanceof asn1js.Sequence)) throw new Error("Unexpected value"); const certSeq2 = seq1.valueBlock.value[0]; if (!(certSeq2 instanceof asn1js.Sequence)) throw new Error("Unexpected value"); const essCertID2 = certSeq2.valueBlock.value[0]; if (!(essCertID2 instanceof asn1js.Sequence)) throw new Error("Unexpected value"); const certDigest = essCertID2.valueBlock.value[0]; if (!(certDigest instanceof asn1js.OctetString)) throw new Error("Unexpected value"); if (!signedData.certificates) throw new Error("Missing certificate"); const expectedCert = signedData.certificates[signedData.certificates.length - 1]; const expectedCertDigest = await digest.digestImmediate(digest.Algorithms.SHA256, uint8Array.getUint8Array(expectedCert.toSchema().toBER())); if (!uint8Array.buf8Equal(expectedCertDigest, uint8Array.getUint8Array(certDigest.getValue()))) { throw new Error("Mismatched certificate"); } }; const checkAttrCertificateV1 = async (attribute, signedData) => { const seq1 = attribute.values[0]; if (!(seq1 instanceof asn1js.Sequence)) throw new Error("Unexpected value"); const certSeq1 = seq1.valueBlock.value[0]; if (!(certSeq1 instanceof asn1js.Sequence)) throw new Error("Unexpected value"); const essCertID1 = certSeq1.valueBlock.value[0]; if (!(essCertID1 instanceof asn1js.Sequence)) throw new Error("Unexpected value"); const certDigest = essCertID1.valueBlock.value[0]; if (!(certDigest instanceof asn1js.OctetString)) throw new Error("Unexpected value"); const expectedCert = signedData.certificates?.[0]; if (!expectedCert) throw new Error("Missing certificate"); const expectedCertDigest = await digest.digestImmediate(digest.Algorithms.SHA1, uint8Array.getUint8Array(expectedCert.toSchema().toBER())); if (!uint8Array.buf8Equal(expectedCertDigest, uint8Array.getUint8Array(certDigest.getValue()))) { throw new Error("Mismatched certificate"); } }; const checkAttrDigest = (attribute, contentDigest) => { const attrDigest = attribute.values[0]; if (!(attrDigest instanceof asn1js.OctetString) || !abEqual(attrDigest.getValue(), contentDigest.getValue())) { throw new Error("Mismatched digest"); } }; const getAttributeType = attributeOid => { switch (attributeOid) { case consts.OID_CONTENT_TYPE: return "contentType"; case consts.OID_DIGEST: return "digest"; case consts.OID_SIGNING_TIME: return "signingTime"; case consts.OID_SIGNING_CERTIFICATE_V1: return "signingCertificate"; case consts.OID_SIGNING_CERTIFICATE_V2: return "signingCertificate"; } return null; }; const checkAttribute = async (expectedCheck, attribute, signedData, tstInfo, contentDigest) => { const attrType = getAttributeType(attribute.type); if (attrType === null) return; if (expectedCheck[attrType]) throw new Error("Repeated attribute"); switch (attribute.type) { case consts.OID_CONTENT_TYPE: checkAttrOid(attribute, pkijsLib.id_eContentType_TSTInfo); break; case consts.OID_SIGNING_TIME: checkAttrTime(attribute, tstInfo); break; case consts.OID_SIGNING_CERTIFICATE_V1: await checkAttrCertificateV1(attribute, signedData); break; case consts.OID_SIGNING_CERTIFICATE_V2: await checkAttrCertificateV2(attribute, signedData); break; case consts.OID_DIGEST: checkAttrDigest(attribute, contentDigest); break; } // eslint-disable-next-line require-atomic-updates expectedCheck[attrType] = true; }; /** * Check that SignedAttrs is present and match the TSR content * * @returns * The data that were signed (based on the validated signed attributes) */ const checkTsrSignedAttrs = async signedData => { const { tstInfo, contentDigests } = await getTstFromSignedData(signedData); for (const signerInfo of signedData.signerInfos) { const signedAttrs = signerInfo.signedAttrs; const expectedCheck = { contentType: false, digest: false, signingCertificate: false, signingTime: false }; if (!signedAttrs) throw new Error("Missing signed attributes"); const contentDigest = contentDigests[pkijsUtils.pkiOidToCryptoDigestAlg(signerInfo.digestAlgorithm.algorithmId)]; if (!contentDigest) throw new Error("Missing digest"); for (const attribute of signedAttrs.attributes) { /* eslint-disable-next-line no-await-in-loop */ await checkAttribute(expectedCheck, attribute, signedData, tstInfo, contentDigest); } if (!expectedCheck.contentType || !expectedCheck.signingTime || !expectedCheck.signingCertificate || !expectedCheck.digest) { throw new Error("Invalid signed attributes"); } } return signedData.signerInfos.map(signerInfo => { const data = signerInfo.signedAttrs?.toSchema().toBER(); if (!data) throw new Error("Unexpected state"); const buf8 = uint8Array.getUint8Array(data, undefined, undefined, true); // See RFC on how to handle SignedAttr buf8[0] = 0x31; return buf8; }); }; const isStatusGranted = status => status === pkijsLib.PKIStatus.granted || status === pkijsLib.PKIStatus.grantedWithMods; const getAccuracySeconds = accuracy => { if (accuracy === undefined) return null; if (accuracy.seconds === undefined && accuracy.millis === undefined && accuracy.micros === undefined) { return null; } const seconds = accuracy.seconds ?? 0; const millis = accuracy.millis ?? 0; const micros = accuracy.micros ?? 0; // Conversion from millis and micros to seconds // eslint-disable-next-line @typescript-eslint/no-magic-numbers return seconds + millis / 1000 + micros / 1000000; }; // #endregion // #region Service export const generateTimestampResponse = async ({ serialNumber, policy, query, signers, accuracy, ordering, digestAlgorithm, genTime, tsaCert, certificatesChain, status }) => { const tst = await genTstInfo(serialNumber, genTime, policy, ordering, accuracy, query, tsaCert); const signedData = genSignedData(tst, asArray(signers), digestAlgorithm, certificatesChain); await signTsrSignedAttrs(signedData, asArray(signers).map(c => c.privateKey)); const contentInfo = new pkijsLib.ContentInfo({ contentType: pkijsLib.ContentInfo.SIGNED_DATA, content: signedData.toSchema(true) }); const timestampResp = new pkijsLib.TimeStampResp({ status: new pkijsLib.PKIStatusInfo({ status }), timeStampToken: new pkijsLib.ContentInfo({ schema: contentInfo.toSchema() }) }); return new Uint8Array(timestampResp.toSchema().toBER()); }; export const extractTimestampResponse = input => { const tsr = getTsrFromInput(input); const granted = isStatusGranted(tsr.status.status); if (!granted) return { granted: false }; const signedData = getSignedData(tsr); const signedContent = signedData.encapContentInfo.eContent; if (!signedContent) throw new Error("Missing encapsulated content"); const tstInfo = pkijsLib.TSTInfo.fromBER(signedContent.getValue()); const signerCertificate = getSignerCertificate(signedData.certificates ?? [], signedData.signerInfos[0]); return { accuracyInSeconds: getAccuracySeconds(tstInfo.accuracy), date: tstInfo.genTime, digest: pkijsUtils.pkiOidToCryptoDigestAlg(tstInfo.messageImprint.hashAlgorithm.algorithmId), granted: true, policy: tstInfo.policy, serialNumber: tstInfo.serialNumber.toBigInt(), signatureDigest: pkijsUtils.pkiOidToCryptoDigestAlg(signedData.digestAlgorithms[0].algorithmId), subject: uint8Array.getUint8Array(tstInfo.messageImprint.hashedMessage.getValue()), tsa: pkijsUtils.getCnString(signerCertificate.subject) }; }; /** * This is not a FULL verification, but a simple check that the TSR is locally valid. * * @param messageInput - Validate that the timestamped message match the provided message input. */ export const verifyTimestampResponse = async (input, messageInput) => { await pkijsUtils.getCryptoImpl(); /* eslint-disable-next-line @typescript-eslint/init-declarations */ let tsrInfo; try { const tsr = getTsrFromInput(input); tsrInfo = extractTimestampResponse(tsr); if (!tsrInfo.granted) { return { error: new Error("Not granted"), tsrInfo: { granted: false }, valid: false }; } if (messageInput !== undefined) { const inputDigest = await pkijsUtils.digestFromMessageInput(messageInput); if (inputDigest.algorithm !== tsrInfo.digest || !uint8Array.buf8Equal(inputDigest.digest, tsrInfo.subject)) { return { valid: false, error: new Error("Message mismatch") }; } } const signedData = getSignedData(tsr); const checkedData = await checkTsrSignedAttrs(signedData); const cryptoEngine = pkijsLib.getCrypto(); const signers = []; for (let i = 0; i < signedData.signerInfos.length; ++i) { const signerInfo = signedData.signerInfos[i]; const tbs = checkedData[i]; const cert = getSignerCertificate(signedData.certificates ?? [], signerInfo); if (tsrInfo.date.getTime() < cert.notBefore.value.getTime()) { return { error: new Error("Date mismatch"), tsrInfo, valid: false }; } /* eslint-disable-next-line no-await-in-loop */ const res = await cryptoEngine?.verifyWithPublicKey(tbs, signerInfo.signature, cert.subjectPublicKeyInfo, signerInfo.signatureAlgorithm); if (!res) return { error: new Error("Invalid signature"), tsrInfo, valid: false }; signers.push(cert); } return { signers, tsrInfo, valid: true }; } catch (e) { return { error: asError(e), tsrInfo, valid: false }; } }; // #endregion