@keeex/tsr
Version:
Manipulate data related to TSR with pki.js
464 lines • 18.1 kB
JavaScript
/**
* @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