@keeex/tsr
Version:
Manipulate data related to TSR with pki.js
247 lines • 8.7 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 { asArray } from "@keeex/utils/array.js";
import { getArrayBuffer } from "@keeex/utils/arraybuffer.js";
import * as b64 from "@keeex/utils/base64.js";
import * as uint8Array from "@keeex/utils/uint8array.js";
import * as asn1js from "asn1js";
import * as pkijs from "pkijs";
// #endregion
// #region Functions
/** Return a compatible "crypto" object for pkijs */
const getAvailableCrypto = async () => {
if (typeof crypto === "object" && "subtle" in crypto) return crypto;
return await import("node:crypto");
};
/** Convert either a buffer or an hex input to a buffer */
const hexDataInput = input => {
if (input instanceof Uint8Array) return input;
return uint8Array.hex2buf8(input);
};
const getEcKeyParam = pubkey => ({
name: "ECDSA",
namedCurve: pubkey.namedCurve
});
const getRsaKeyParam = digestAlgorithm => ({
name: "RSA-OAEP",
hash: cryptoDigestAlgToSubtleAlg(digestAlgorithm)
});
const getPrivateKeyLoadParam = (certificate, digestAlgorithm) => {
const pubKey = certificate.subjectPublicKeyInfo.parsedKey;
if (pubKey instanceof pkijs.ECPublicKey) return getEcKeyParam(pubKey);
if (pubKey instanceof pkijs.RSAPublicKey) return getRsaKeyParam(digestAlgorithm);
throw new Error("Unsupported public key type");
};
/** Convert an input from buffer or UTF-8 string to buffer */
const dataInput = data => {
if (data instanceof Uint8Array) return data;
return uint8Array.utf82buf8(data);
};
// #endregion
// #region Service
/** If not already setup otherwise, setup pkijs crypto engine and return subtle implementation */
export const getCryptoImpl = async (tryLoad = true) => {
try {
const impl = pkijs.getCrypto();
if (!impl) throw new Error("No crypto implementation");
return impl.subtle;
} catch (e) {
if (tryLoad) {
// Ideally, bridge with other than node and browser crypto. But that's some work for an
// unneeded feature.
const availableCrypto = await getAvailableCrypto();
pkijs.setEngine("builtin-crypto-object", new pkijs.CryptoEngine({
name: "builtin-crypto-object",
crypto: availableCrypto
}));
return getCryptoImpl(false);
}
throw e;
}
};
/** Convert digest algorithm from `@keeex/crypto` to ASN1 identifier */
export const cryptoDigestAlgToPkiOid = algorithm => {
switch (algorithm) {
case digest.Algorithms.SHA1:
return pkijs.id_sha1;
case digest.Algorithms.SHA256:
return pkijs.id_sha256;
case digest.Algorithms.SHA512:
return pkijs.id_sha512;
default:
throw new Error(`Unsupported algorithm for digest: ${algorithm}`);
}
};
export const pkiOidToCryptoDigestAlg = algorithm => {
if (algorithm === pkijs.id_sha256 || algorithm === digest.Algorithms.SHA256) {
return digest.Algorithms.SHA256;
} else if (algorithm === pkijs.id_sha512 || algorithm === digest.Algorithms.SHA512) {
return digest.Algorithms.SHA512;
} else if (algorithm === pkijs.id_sha1 || algorithm === digest.Algorithms.SHA1) {
return digest.Algorithms.SHA1;
}
throw new Error(`Unsupported algorithm for digest: ${algorithm}`);
};
/** Convert a `@keeex/crypto` digest alg name to a subtlecrypto alg name */
export const cryptoDigestAlgToSubtleAlg = algorithm => {
switch (algorithm) {
case digest.Algorithms.SHA1:
return "SHA1";
case digest.Algorithms.SHA256:
return "SHA-256";
case digest.Algorithms.SHA512:
return "SHA-512";
default:
throw new Error(`Unsupported algorithm for digest: ${algorithm}`);
}
};
/*
* SigningCertificate info from RFC2634 and RFC5035
*
* SigningCertificate ::= SEQUENCE {
* certs SEQUENCE OF ESSCertID,
* policies SEQUENCE OF PolicyInformation OPTIONAL
* }
* ESSCertID ::= SEQUENCE {
* certHash Hash
* issuerSerial IssuerSerial OPTIONAL
* }
* Hash ::= OCTET STRING -- SHA1 of entire certificate
* IssuerSerial ::= SEQUENCE {
* issuer GeneralNames,
* serialNumber CertificateSerialNumber
* }
* SigningCertificateV2 ::= SEQUENCE {
* certs SEQUENCE OF ESSCertIDv2,
* policies SEQUENCE OF PolicyInformation OPTIONAL
* }
* ESSCertIDv2 ::= SEQUENCE {
* hashAlgorithm AlgorithmIdentifier DEFAULT {algorithm id-sha256},
* certHash Hash,
* issuerSerial IssuerSerial OPTIONAL
* }
*/
/**
* Return a SigningCertificate sequence
*
* If multiple certificates are provided, the signing one is expected to be the first one.
*/
export const createSigningCertificate = async certs => {
// TODO Update to V2 if it works
const essCertIDs = await Promise.all(asArray(certs).map(async cert => {
const hash = await digest.digestImmediate(digest.Algorithms.SHA1, uint8Array.getUint8Array(cert.toSchema().toBER()));
// Add issuerSerial if this still works
return new asn1js.Sequence({
value: [new asn1js.OctetString({
valueHex: getArrayBuffer(hash)
})]
});
}));
const certsSequence = new asn1js.Sequence({
value: essCertIDs
});
const signingCertificate = new asn1js.Sequence({
value: [certsSequence]
});
return signingCertificate;
};
/** Return the digest of an OctetString */
export const getContentDigest = async (content, algorithm) => {
const alg = pkiOidToCryptoDigestAlg(algorithm);
const hashBytes = await digest.digestImmediate(alg, uint8Array.getUint8Array(content.getValue()));
return new asn1js.OctetString({
valueHex: hashBytes
});
};
export const loadCertificate = certificatePEM => {
const certData = b64.armorB642Buf8(certificatePEM);
return pkijs.Certificate.fromBER(certData.arrayBuffer);
};
export const loadCertificates = certificatesPEM => {
const certsData = b64.multipleArmorB642Buf8(certificatesPEM);
return certsData.map(c => pkijs.Certificate.fromBER(c.arrayBuffer));
};
/**
* Load a private key from a PEM PKCS#8 file using SubtleCrypto
*
* @param digestAlgorithm - Used only for RSA, because we have to provide something for SubtleCrypto
*/
export const loadPrivateKey = async (privateKeyPEM, certificate, digestAlgorithm = digest.Algorithms.SHA256) => {
const keyData = b64.armorB642Buf8(privateKeyPEM);
const params = getPrivateKeyLoadParam(certificate, digestAlgorithm);
const cryptoImpl = await getCryptoImpl();
const key = await cryptoImpl.importKey("pkcs8", keyData.buf8, params, false, ["sign"]);
return key;
};
/** Return the CN string */
export const getCnString = attr => {
for (const candidate of attr.typesAndValues) {
if (candidate.type === "2.5.4.3") return candidate.value.valueBlock.value;
}
throw new Error("No CN");
};
export const messageImprintFromInput = async input => {
await getCryptoImpl();
if (input.type === "imprint") return input.value;
if (input.type === "digest") {
return new pkijs.MessageImprint({
hashAlgorithm: new pkijs.AlgorithmIdentifier({
algorithmId: cryptoDigestAlgToPkiOid(input.algorithm)
}),
hashedMessage: new asn1js.OctetString({
valueHex: hexDataInput(input.value)
})
});
}
return messageImprintFromInput({
algorithm: input.algorithm,
type: "digest",
value: await digest.digestImmediate(input.algorithm, dataInput(input.value))
});
};
export const digestFromMessageInput = async input => {
if (input.type === "digest") {
return {
algorithm: input.algorithm,
digest: hexDataInput(input.value)
};
}
if (input.type === "data") {
return {
algorithm: input.algorithm,
digest: await digest.digestImmediate(input.algorithm, dataInput(input.value))
};
}
return {
algorithm: pkiOidToCryptoDigestAlg(input.value.hashAlgorithm.algorithmId),
digest: new Uint8Array(input.value.hashedMessage.getValue())
};
};
// #endregion