forked from mirrors/gecko-dev
335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
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<IOCSPResponse>;
|
|
|
|
/**
|
|
* 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<NonNullable<typeof parameters.names>>(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<boolean> {
|
|
//#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
|
|
}
|
|
|
|
}
|
|
|