UNPKG

@keeex/tsr

Version:

Manipulate data related to TSR with pki.js

243 lines (242 loc) 9.14 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 { 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