import * as asn1js from "asn1js"; import * as pvutils from "pvutils"; import { ResponseBytes, ResponseBytesJson, ResponseBytesSchema } from "./ResponseBytes"; import { BasicOCSPResponse } from "./BasicOCSPResponse"; import * as Schema from "./Schema"; import { Certificate } from "./Certificate"; import { id_PKIX_OCSP_Basic } from "./ObjectIdentifiers"; import { AsnError } from "./errors"; import { PkiObject, PkiObjectParameters } from "./PkiObject"; import * as common from "./common"; const RESPONSE_STATUS = "responseStatus"; const RESPONSE_BYTES = "responseBytes"; export interface IOCSPResponse { responseStatus: asn1js.Enumerated; responseBytes?: ResponseBytes; } export interface OCSPResponseJson { responseStatus: asn1js.EnumeratedJson; responseBytes?: ResponseBytesJson; } export type OCSPResponseParameters = PkiObjectParameters & Partial; /** * Represents an OCSP response described in [RFC6960 Section 4.2](https://datatracker.ietf.org/doc/html/rfc6960#section-4.2) * * @example The following example demonstrates how to verify OCSP response * ```js * const asnOcspResp = asn1js.fromBER(ocspRespRaw); * const ocspResp = new pkijs.OCSPResponse({ schema: asnOcspResp.result }); * * if (!ocspResp.responseBytes) { * throw new Error("No \"ResponseBytes\" in the OCSP Response - nothing to verify"); * } * * const asnOcspRespBasic = asn1js.fromBER(ocspResp.responseBytes.response.valueBlock.valueHex); * const ocspBasicResp = new pkijs.BasicOCSPResponse({ schema: asnOcspRespBasic.result }); * const ok = await ocspBasicResp.verify({ trustedCerts: [cert] }); * ``` * * @example The following example demonstrates how to create OCSP response * ```js * const ocspBasicResp = new pkijs.BasicOCSPResponse(); * * // Create specific TST info structure to sign * ocspBasicResp.tbsResponseData.responderID = issuerCert.subject; * ocspBasicResp.tbsResponseData.producedAt = new Date(); * * const certID = new pkijs.CertID(); * await certID.createForCertificate(cert, { * hashAlgorithm: "SHA-256", * issuerCertificate: issuerCert, * }); * const response = new pkijs.SingleResponse({ * certID, * }); * response.certStatus = new asn1js.Primitive({ * idBlock: { * tagClass: 3, // CONTEXT-SPECIFIC * tagNumber: 0 // [0] * }, * lenBlockLength: 1 // The length contains one byte 0x00 * }); // status - success * response.thisUpdate = new Date(); * * ocspBasicResp.tbsResponseData.responses.push(response); * * // Add certificates for chain OCSP response validation * ocspBasicResp.certs = [issuerCert]; * * await ocspBasicResp.sign(keys.privateKey, "SHA-256"); * * // Finally create completed OCSP response structure * const ocspBasicRespRaw = ocspBasicResp.toSchema().toBER(false); * * const ocspResp = new pkijs.OCSPResponse({ * responseStatus: new asn1js.Enumerated({ value: 0 }), // success * responseBytes: new pkijs.ResponseBytes({ * responseType: pkijs.id_PKIX_OCSP_Basic, * response: new asn1js.OctetString({ valueHex: ocspBasicRespRaw }), * }), * }); * * const ocspRespRaw = ocspResp.toSchema().toBER(); * ``` */ export class OCSPResponse extends PkiObject implements IOCSPResponse { public static override CLASS_NAME = "OCSPResponse"; public responseStatus!: asn1js.Enumerated; public responseBytes?: ResponseBytes; /** * Initializes a new instance of the {@link OCSPResponse} class * @param parameters Initialization parameters */ constructor(parameters: OCSPResponseParameters = {}) { super(); this.responseStatus = pvutils.getParametersValue(parameters, RESPONSE_STATUS, OCSPResponse.defaultValues(RESPONSE_STATUS)); if (RESPONSE_BYTES in parameters) { this.responseBytes = pvutils.getParametersValue(parameters, RESPONSE_BYTES, OCSPResponse.defaultValues(RESPONSE_BYTES)); } 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 RESPONSE_STATUS): asn1js.Enumerated; public static override defaultValues(memberName: typeof RESPONSE_BYTES): ResponseBytes; public static override defaultValues(memberName: string): any { switch (memberName) { case RESPONSE_STATUS: return new asn1js.Enumerated(); case RESPONSE_BYTES: return new ResponseBytes(); 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 RESPONSE_STATUS: return (memberValue.isEqual(OCSPResponse.defaultValues(memberName))); case RESPONSE_BYTES: return ((ResponseBytes.compareWithDefault("responseType", memberValue.responseType)) && (ResponseBytes.compareWithDefault("response", memberValue.response))); default: return super.defaultValues(memberName); } } /** * @inheritdoc * @asn ASN.1 schema * ```asn * OCSPResponse ::= SEQUENCE { * responseStatus OCSPResponseStatus, * responseBytes [0] EXPLICIT ResponseBytes OPTIONAL } * * OCSPResponseStatus ::= ENUMERATED { * successful (0), -- Response has valid confirmations * malformedRequest (1), -- Illegal confirmation request * internalError (2), -- Internal error in issuer * tryLater (3), -- Try again later * -- (4) is not used * sigRequired (5), -- Must sign the request * unauthorized (6) -- Request unauthorized * } *``` */ public static override schema(parameters: Schema.SchemaParameters<{ responseStatus?: string; responseBytes?: ResponseBytesSchema; }> = {}): Schema.SchemaType { const names = pvutils.getParametersValue>(parameters, "names", {}); return (new asn1js.Sequence({ name: (names.blockName || "OCSPResponse"), value: [ new asn1js.Enumerated({ name: (names.responseStatus || RESPONSE_STATUS) }), new asn1js.Constructed({ optional: true, idBlock: { tagClass: 3, // CONTEXT-SPECIFIC tagNumber: 0 // [0] }, value: [ ResponseBytes.schema(names.responseBytes || { names: { blockName: RESPONSE_BYTES } }) ] }) ] })); } public fromSchema(schema: Schema.SchemaType): void { // Clear input data first pvutils.clearProps(schema, [ RESPONSE_STATUS, RESPONSE_BYTES ]); // Check the schema is valid const asn1 = asn1js.compareSchema(schema, schema, OCSPResponse.schema() ); AsnError.assertSchema(asn1, this.className); // Get internal properties from parsed schema this.responseStatus = asn1.result.responseStatus; if (RESPONSE_BYTES in asn1.result) this.responseBytes = new ResponseBytes({ schema: asn1.result.responseBytes }); } public toSchema(): asn1js.Sequence { //#region Create array for output sequence const outputArray = []; outputArray.push(this.responseStatus); if (this.responseBytes) { outputArray.push(new asn1js.Constructed({ idBlock: { tagClass: 3, // CONTEXT-SPECIFIC tagNumber: 0 // [0] }, value: [this.responseBytes.toSchema()] })); } //#endregion //#region Construct and return new ASN.1 schema for this object return (new asn1js.Sequence({ value: outputArray })); //#endregion } public toJSON(): OCSPResponseJson { const res: OCSPResponseJson = { responseStatus: this.responseStatus.toJSON() }; if (this.responseBytes) { res.responseBytes = this.responseBytes.toJSON(); } return res; } /** * Get OCSP response status for specific certificate * @param certificate * @param issuerCertificate * @param crypto Crypto engine */ public async getCertificateStatus(certificate: Certificate, issuerCertificate: Certificate, crypto = common.getCrypto(true)) { //#region Initial variables let basicResponse; const result = { isForCertificate: false, status: 2 // 0 = good, 1 = revoked, 2 = unknown }; //#endregion //#region Check that RESPONSE_BYTES contain "OCSP_BASIC_RESPONSE" if (!this.responseBytes) return result; if (this.responseBytes.responseType !== id_PKIX_OCSP_Basic) // id-pkix-ocsp-basic return result; try { const asn1Basic = asn1js.fromBER(this.responseBytes.response.valueBlock.valueHexView); AsnError.assert(asn1Basic, "Basic OCSP response"); basicResponse = new BasicOCSPResponse({ schema: asn1Basic.result }); } catch (ex) { return result; } //#endregion return basicResponse.getCertificateStatus(certificate, issuerCertificate, crypto); } /** * Make a signature for current OCSP Response * @param privateKey Private key for "subjectPublicKeyInfo" structure * @param hashAlgorithm Hashing algorithm. Default SHA-1 */ public async sign(privateKey: CryptoKey, hashAlgorithm?: string, crypto = common.getCrypto(true)) { //#region Check that ResponseData has type BasicOCSPResponse and sign it if (this.responseBytes && this.responseBytes.responseType === id_PKIX_OCSP_Basic) { const basicResponse = BasicOCSPResponse.fromBER(this.responseBytes.response.valueBlock.valueHexView); return basicResponse.sign(privateKey, hashAlgorithm, crypto); } throw new Error(`Unknown ResponseBytes type: ${this.responseBytes?.responseType || "Unknown"}`); //#endregion } /** * Verify current OCSP Response * @param issuerCertificate In order to decrease size of resp issuer cert could be omitted. In such case you need manually provide it. * @param crypto Crypto engine */ public async verify(issuerCertificate: Certificate | null = null, crypto = common.getCrypto(true)): Promise { //#region Check that ResponseBytes exists in the object if ((RESPONSE_BYTES in this) === false) throw new Error("Empty ResponseBytes field"); //#endregion //#region Check that ResponseData has type BasicOCSPResponse and verify it if (this.responseBytes && this.responseBytes.responseType === id_PKIX_OCSP_Basic) { const basicResponse = BasicOCSPResponse.fromBER(this.responseBytes.response.valueBlock.valueHexView); if (issuerCertificate !== null) { if (!basicResponse.certs) { basicResponse.certs = []; } basicResponse.certs.push(issuerCertificate); } return basicResponse.verify({}, crypto); } throw new Error(`Unknown ResponseBytes type: ${this.responseBytes?.responseType || "Unknown"}`); //#endregion } }