import * as asn1js from "asn1js"; import * as pvutils from "pvutils"; import * as common from "./common"; import { ResponseData, ResponseDataJson, ResponseDataSchema } from "./ResponseData"; import { AlgorithmIdentifier, AlgorithmIdentifierJson, AlgorithmIdentifierSchema } from "./AlgorithmIdentifier"; import { Certificate, CertificateJson, CertificateSchema, checkCA } from "./Certificate"; import { CertID } from "./CertID"; import { RelativeDistinguishedNames } from "./RelativeDistinguishedNames"; import { CertificateChainValidationEngine } from "./CertificateChainValidationEngine"; import * as Schema from "./Schema"; import { PkiObject, PkiObjectParameters } from "./PkiObject"; import { AsnError } from "./errors"; import { EMPTY_STRING } from "./constants"; const TBS_RESPONSE_DATA = "tbsResponseData"; const SIGNATURE_ALGORITHM = "signatureAlgorithm"; const SIGNATURE = "signature"; const CERTS = "certs"; const BASIC_OCSP_RESPONSE = "BasicOCSPResponse"; const BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA = `${BASIC_OCSP_RESPONSE}.${TBS_RESPONSE_DATA}`; const BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM = `${BASIC_OCSP_RESPONSE}.${SIGNATURE_ALGORITHM}`; const BASIC_OCSP_RESPONSE_SIGNATURE = `${BASIC_OCSP_RESPONSE}.${SIGNATURE}`; const BASIC_OCSP_RESPONSE_CERTS = `${BASIC_OCSP_RESPONSE}.${CERTS}`; const CLEAR_PROPS = [ BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA, BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM, BASIC_OCSP_RESPONSE_SIGNATURE, BASIC_OCSP_RESPONSE_CERTS ]; export interface IBasicOCSPResponse { tbsResponseData: ResponseData; signatureAlgorithm: AlgorithmIdentifier; signature: asn1js.BitString; certs?: Certificate[]; } export interface CertificateStatus { isForCertificate: boolean; /** * 0 = good, 1 = revoked, 2 = unknown */ status: number; } export type BasicOCSPResponseParameters = PkiObjectParameters & Partial; export interface BasicOCSPResponseVerifyParams { trustedCerts?: Certificate[]; } export interface BasicOCSPResponseJson { tbsResponseData: ResponseDataJson; signatureAlgorithm: AlgorithmIdentifierJson; signature: asn1js.BitStringJson; certs?: CertificateJson[]; } /** * Represents the BasicOCSPResponse structure described in [RFC6960](https://datatracker.ietf.org/doc/html/rfc6960) */ export class BasicOCSPResponse extends PkiObject implements IBasicOCSPResponse { public static override CLASS_NAME = "BasicOCSPResponse"; public tbsResponseData!: ResponseData; public signatureAlgorithm!: AlgorithmIdentifier; public signature!: asn1js.BitString; public certs?: Certificate[]; /** * Initializes a new instance of the {@link BasicOCSPResponse} class * @param parameters Initialization parameters */ constructor(parameters: BasicOCSPResponseParameters = {}) { super(); this.tbsResponseData = pvutils.getParametersValue(parameters, TBS_RESPONSE_DATA, BasicOCSPResponse.defaultValues(TBS_RESPONSE_DATA)); this.signatureAlgorithm = pvutils.getParametersValue(parameters, SIGNATURE_ALGORITHM, BasicOCSPResponse.defaultValues(SIGNATURE_ALGORITHM)); this.signature = pvutils.getParametersValue(parameters, SIGNATURE, BasicOCSPResponse.defaultValues(SIGNATURE)); if (CERTS in parameters) { this.certs = pvutils.getParametersValue(parameters, CERTS, BasicOCSPResponse.defaultValues(CERTS)); } if (parameters.schema) { this.fromSchema(parameters.schema); } } /** * Returns default values for all class members * @param memberName String name for a class member * @returns Default value */ public static override defaultValues(memberName: typeof TBS_RESPONSE_DATA): ResponseData; public static override defaultValues(memberName: typeof SIGNATURE_ALGORITHM): AlgorithmIdentifier; public static override defaultValues(memberName: typeof SIGNATURE): asn1js.BitString; public static override defaultValues(memberName: typeof CERTS): Certificate[]; public static override defaultValues(memberName: string): any { switch (memberName) { case TBS_RESPONSE_DATA: return new ResponseData(); case SIGNATURE_ALGORITHM: return new AlgorithmIdentifier(); case SIGNATURE: return new asn1js.BitString(); case CERTS: return []; default: return super.defaultValues(memberName); } } /** * Compare values with default values for all class members * @param memberName String name for a class member * @param memberValue Value to compare with default value */ public static compareWithDefault(memberName: string, memberValue: any): boolean { switch (memberName) { case "type": { let comparisonResult = ((ResponseData.compareWithDefault("tbs", memberValue.tbs)) && (ResponseData.compareWithDefault("responderID", memberValue.responderID)) && (ResponseData.compareWithDefault("producedAt", memberValue.producedAt)) && (ResponseData.compareWithDefault("responses", memberValue.responses))); if ("responseExtensions" in memberValue) comparisonResult = comparisonResult && (ResponseData.compareWithDefault("responseExtensions", memberValue.responseExtensions)); return comparisonResult; } case SIGNATURE_ALGORITHM: return ((memberValue.algorithmId === EMPTY_STRING) && (("algorithmParams" in memberValue) === false)); case SIGNATURE: return (memberValue.isEqual(BasicOCSPResponse.defaultValues(memberName))); case CERTS: return (memberValue.length === 0); default: return super.defaultValues(memberName); } } /** * @inheritdoc * @asn ASN.1 schema * ```asn * BasicOCSPResponse ::= SEQUENCE { * tbsResponseData ResponseData, * signatureAlgorithm AlgorithmIdentifier, * signature BIT STRING, * certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL } *``` */ public static override schema(parameters: Schema.SchemaParameters<{ tbsResponseData?: ResponseDataSchema; signatureAlgorithm?: AlgorithmIdentifierSchema; signature?: string; certs?: CertificateSchema; }> = {}): Schema.SchemaType { const names = pvutils.getParametersValue>(parameters, "names", {}); return (new asn1js.Sequence({ name: (names.blockName || BASIC_OCSP_RESPONSE), value: [ ResponseData.schema(names.tbsResponseData || { names: { blockName: BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA } }), AlgorithmIdentifier.schema(names.signatureAlgorithm || { names: { blockName: BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM } }), new asn1js.BitString({ name: (names.signature || BASIC_OCSP_RESPONSE_SIGNATURE) }), new asn1js.Constructed({ optional: true, idBlock: { tagClass: 3, // CONTEXT-SPECIFIC tagNumber: 0 // [0] }, value: [ new asn1js.Sequence({ value: [new asn1js.Repeated({ name: BASIC_OCSP_RESPONSE_CERTS, value: Certificate.schema(names.certs || {}) })] }) ] }) ] })); } public fromSchema(schema: Schema.SchemaType): void { // Clear input data first pvutils.clearProps(schema, CLEAR_PROPS); //#endregion // Check the schema is valid const asn1 = asn1js.compareSchema(schema, schema, BasicOCSPResponse.schema() ); AsnError.assertSchema(asn1, this.className); //#region Get internal properties from parsed schema this.tbsResponseData = new ResponseData({ schema: asn1.result[BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA] }); this.signatureAlgorithm = new AlgorithmIdentifier({ schema: asn1.result[BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM] }); this.signature = asn1.result[BASIC_OCSP_RESPONSE_SIGNATURE]; if (BASIC_OCSP_RESPONSE_CERTS in asn1.result) { this.certs = Array.from(asn1.result[BASIC_OCSP_RESPONSE_CERTS], element => new Certificate({ schema: element })); } //#endregion } public toSchema(): asn1js.Sequence { //#region Create array for output sequence const outputArray = []; outputArray.push(this.tbsResponseData.toSchema()); outputArray.push(this.signatureAlgorithm.toSchema()); outputArray.push(this.signature); //#region Create array of certificates if (this.certs) { outputArray.push(new asn1js.Constructed({ idBlock: { tagClass: 3, // CONTEXT-SPECIFIC tagNumber: 0 // [0] }, value: [ new asn1js.Sequence({ value: Array.from(this.certs, o => o.toSchema()) }) ] })); } //#endregion //#endregion //#region Construct and return new ASN.1 schema for this object return (new asn1js.Sequence({ value: outputArray })); //#endregion } public toJSON(): BasicOCSPResponseJson { const res: BasicOCSPResponseJson = { tbsResponseData: this.tbsResponseData.toJSON(), signatureAlgorithm: this.signatureAlgorithm.toJSON(), signature: this.signature.toJSON(), }; if (this.certs) { res.certs = Array.from(this.certs, o => o.toJSON()); } return res; } /** * Get OCSP response status for specific certificate * @param certificate Certificate to be checked * @param issuerCertificate Certificate of issuer for certificate to be checked * @param crypto Crypto engine */ public async getCertificateStatus(certificate: Certificate, issuerCertificate: Certificate, crypto = common.getCrypto(true)): Promise { //#region Initial variables const result = { isForCertificate: false, status: 2 // 0 = good, 1 = revoked, 2 = unknown }; const hashesObject: Record = {}; const certIDs: CertID[] = []; //#endregion //#region Create all "certIDs" for input certificates for (const response of this.tbsResponseData.responses) { const hashAlgorithm = crypto.getAlgorithmByOID(response.certID.hashAlgorithm.algorithmId, true, "CertID.hashAlgorithm"); if (!hashesObject[hashAlgorithm.name]) { hashesObject[hashAlgorithm.name] = 1; const certID = new CertID(); certIDs.push(certID); await certID.createForCertificate(certificate, { hashAlgorithm: hashAlgorithm.name, issuerCertificate }, crypto); } } //#endregion //#region Compare all response's "certIDs" with identifiers for input certificate for (const response of this.tbsResponseData.responses) { for (const id of certIDs) { if (response.certID.isEqual(id)) { result.isForCertificate = true; try { switch (response.certStatus.idBlock.isConstructed) { case true: if (response.certStatus.idBlock.tagNumber === 1) result.status = 1; // revoked break; case false: switch (response.certStatus.idBlock.tagNumber) { case 0: // good result.status = 0; break; case 2: // unknown result.status = 2; break; default: } break; default: } } catch (ex) { // nothing } return result; } } } return result; //#endregion } /** * Make signature for current OCSP Basic Response * @param privateKey Private key for "subjectPublicKeyInfo" structure * @param hashAlgorithm Hashing algorithm. Default SHA-1 * @param crypto Crypto engine */ async sign(privateKey: CryptoKey, hashAlgorithm = "SHA-1", crypto = common.getCrypto(true)): Promise { // Get a private key from function parameter if (!privateKey) { throw new Error("Need to provide a private key for signing"); } //#region Get a "default parameters" for current algorithm and set correct signature algorithm const signatureParams = await crypto.getSignatureParameters(privateKey, hashAlgorithm); const algorithm = signatureParams.parameters.algorithm; if (!("name" in algorithm)) { throw new Error("Empty algorithm"); } this.signatureAlgorithm = signatureParams.signatureAlgorithm; //#endregion //#region Create TBS data for signing this.tbsResponseData.tbsView = new Uint8Array(this.tbsResponseData.toSchema(true).toBER()); //#endregion //#region Signing TBS data on provided private key const signature = await crypto.signWithPrivateKey(this.tbsResponseData.tbsView, privateKey, { algorithm }); this.signature = new asn1js.BitString({ valueHex: signature }); //#endregion } /** * Verify existing OCSP Basic Response * @param params Additional parameters * @param crypto Crypto engine */ public async verify(params: BasicOCSPResponseVerifyParams = {}, crypto = common.getCrypto(true)): Promise { //#region Initial variables let signerCert: Certificate | null = null; let certIndex = -1; const trustedCerts: Certificate[] = params.trustedCerts || []; //#endregion //#region Check amount of certificates if (!this.certs) { throw new Error("No certificates attached to the BasicOCSPResponse"); } //#endregion //#region Find correct value for "responderID" switch (true) { case (this.tbsResponseData.responderID instanceof RelativeDistinguishedNames): // [1] Name for (const [index, certificate] of this.certs.entries()) { if (certificate.subject.isEqual(this.tbsResponseData.responderID)) { certIndex = index; break; } } break; case (this.tbsResponseData.responderID instanceof asn1js.OctetString): // [2] KeyHash for (const [index, cert] of this.certs.entries()) { const hash = await crypto.digest({ name: "sha-1" }, cert.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHexView); if (pvutils.isEqualBuffer(hash, this.tbsResponseData.responderID.valueBlock.valueHex)) { certIndex = index; break; } } break; default: throw new Error("Wrong value for responderID"); } //#endregion //#region Make additional verification for signer's certificate if (certIndex === (-1)) throw new Error("Correct certificate was not found in OCSP response"); signerCert = this.certs[certIndex]; const additionalCerts: Certificate[] = [signerCert]; for (const cert of this.certs) { const caCert = await checkCA(cert, signerCert); if (caCert) { additionalCerts.push(caCert); } } const certChain = new CertificateChainValidationEngine({ certs: additionalCerts, trustedCerts, }); const verificationResult = await certChain.verify({}, crypto); if (!verificationResult.result) { throw new Error("Validation of signer's certificate failed"); } return crypto.verifyWithPublicKey(this.tbsResponseData.tbsView, this.signature, this.certs[certIndex].subjectPublicKeyInfo, this.signatureAlgorithm); } }