BasicOCSPResponse.ts (15483B)
1 import * as asn1js from "asn1js"; 2 import * as pvutils from "pvutils"; 3 import * as common from "./common"; 4 import { ResponseData, ResponseDataJson, ResponseDataSchema } from "./ResponseData"; 5 import { AlgorithmIdentifier, AlgorithmIdentifierJson, AlgorithmIdentifierSchema } from "./AlgorithmIdentifier"; 6 import { Certificate, CertificateJson, CertificateSchema, checkCA } from "./Certificate"; 7 import { CertID } from "./CertID"; 8 import { RelativeDistinguishedNames } from "./RelativeDistinguishedNames"; 9 import { CertificateChainValidationEngine } from "./CertificateChainValidationEngine"; 10 import * as Schema from "./Schema"; 11 import { PkiObject, PkiObjectParameters } from "./PkiObject"; 12 import { AsnError } from "./errors"; 13 import { EMPTY_STRING } from "./constants"; 14 15 const TBS_RESPONSE_DATA = "tbsResponseData"; 16 const SIGNATURE_ALGORITHM = "signatureAlgorithm"; 17 const SIGNATURE = "signature"; 18 const CERTS = "certs"; 19 const BASIC_OCSP_RESPONSE = "BasicOCSPResponse"; 20 const BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA = `${BASIC_OCSP_RESPONSE}.${TBS_RESPONSE_DATA}`; 21 const BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM = `${BASIC_OCSP_RESPONSE}.${SIGNATURE_ALGORITHM}`; 22 const BASIC_OCSP_RESPONSE_SIGNATURE = `${BASIC_OCSP_RESPONSE}.${SIGNATURE}`; 23 const BASIC_OCSP_RESPONSE_CERTS = `${BASIC_OCSP_RESPONSE}.${CERTS}`; 24 const CLEAR_PROPS = [ 25 BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA, 26 BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM, 27 BASIC_OCSP_RESPONSE_SIGNATURE, 28 BASIC_OCSP_RESPONSE_CERTS 29 ]; 30 31 export interface IBasicOCSPResponse { 32 tbsResponseData: ResponseData; 33 signatureAlgorithm: AlgorithmIdentifier; 34 signature: asn1js.BitString; 35 certs?: Certificate[]; 36 } 37 38 export interface CertificateStatus { 39 isForCertificate: boolean; 40 /** 41 * 0 = good, 1 = revoked, 2 = unknown 42 */ 43 status: number; 44 } 45 46 export type BasicOCSPResponseParameters = PkiObjectParameters & Partial<IBasicOCSPResponse>; 47 48 export interface BasicOCSPResponseVerifyParams { 49 trustedCerts?: Certificate[]; 50 } 51 52 export interface BasicOCSPResponseJson { 53 tbsResponseData: ResponseDataJson; 54 signatureAlgorithm: AlgorithmIdentifierJson; 55 signature: asn1js.BitStringJson; 56 certs?: CertificateJson[]; 57 } 58 59 /** 60 * Represents the BasicOCSPResponse structure described in [RFC6960](https://datatracker.ietf.org/doc/html/rfc6960) 61 */ 62 export class BasicOCSPResponse extends PkiObject implements IBasicOCSPResponse { 63 64 public static override CLASS_NAME = "BasicOCSPResponse"; 65 66 public tbsResponseData!: ResponseData; 67 public signatureAlgorithm!: AlgorithmIdentifier; 68 public signature!: asn1js.BitString; 69 public certs?: Certificate[]; 70 71 /** 72 * Initializes a new instance of the {@link BasicOCSPResponse} class 73 * @param parameters Initialization parameters 74 */ 75 constructor(parameters: BasicOCSPResponseParameters = {}) { 76 super(); 77 78 this.tbsResponseData = pvutils.getParametersValue(parameters, TBS_RESPONSE_DATA, BasicOCSPResponse.defaultValues(TBS_RESPONSE_DATA)); 79 this.signatureAlgorithm = pvutils.getParametersValue(parameters, SIGNATURE_ALGORITHM, BasicOCSPResponse.defaultValues(SIGNATURE_ALGORITHM)); 80 this.signature = pvutils.getParametersValue(parameters, SIGNATURE, BasicOCSPResponse.defaultValues(SIGNATURE)); 81 if (CERTS in parameters) { 82 this.certs = pvutils.getParametersValue(parameters, CERTS, BasicOCSPResponse.defaultValues(CERTS)); 83 } 84 85 if (parameters.schema) { 86 this.fromSchema(parameters.schema); 87 } 88 } 89 90 /** 91 * Returns default values for all class members 92 * @param memberName String name for a class member 93 * @returns Default value 94 */ 95 public static override defaultValues(memberName: typeof TBS_RESPONSE_DATA): ResponseData; 96 public static override defaultValues(memberName: typeof SIGNATURE_ALGORITHM): AlgorithmIdentifier; 97 public static override defaultValues(memberName: typeof SIGNATURE): asn1js.BitString; 98 public static override defaultValues(memberName: typeof CERTS): Certificate[]; 99 public static override defaultValues(memberName: string): any { 100 switch (memberName) { 101 case TBS_RESPONSE_DATA: 102 return new ResponseData(); 103 case SIGNATURE_ALGORITHM: 104 return new AlgorithmIdentifier(); 105 case SIGNATURE: 106 return new asn1js.BitString(); 107 case CERTS: 108 return []; 109 default: 110 return super.defaultValues(memberName); 111 } 112 } 113 114 /** 115 * Compare values with default values for all class members 116 * @param memberName String name for a class member 117 * @param memberValue Value to compare with default value 118 */ 119 public static compareWithDefault(memberName: string, memberValue: any): boolean { 120 switch (memberName) { 121 case "type": 122 { 123 let comparisonResult = ((ResponseData.compareWithDefault("tbs", memberValue.tbs)) && 124 (ResponseData.compareWithDefault("responderID", memberValue.responderID)) && 125 (ResponseData.compareWithDefault("producedAt", memberValue.producedAt)) && 126 (ResponseData.compareWithDefault("responses", memberValue.responses))); 127 128 if ("responseExtensions" in memberValue) 129 comparisonResult = comparisonResult && (ResponseData.compareWithDefault("responseExtensions", memberValue.responseExtensions)); 130 131 return comparisonResult; 132 } 133 case SIGNATURE_ALGORITHM: 134 return ((memberValue.algorithmId === EMPTY_STRING) && (("algorithmParams" in memberValue) === false)); 135 case SIGNATURE: 136 return (memberValue.isEqual(BasicOCSPResponse.defaultValues(memberName))); 137 case CERTS: 138 return (memberValue.length === 0); 139 default: 140 return super.defaultValues(memberName); 141 } 142 } 143 144 /** 145 * @inheritdoc 146 * @asn ASN.1 schema 147 * ```asn 148 * BasicOCSPResponse ::= SEQUENCE { 149 * tbsResponseData ResponseData, 150 * signatureAlgorithm AlgorithmIdentifier, 151 * signature BIT STRING, 152 * certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL } 153 *``` 154 */ 155 public static override schema(parameters: Schema.SchemaParameters<{ 156 tbsResponseData?: ResponseDataSchema; 157 signatureAlgorithm?: AlgorithmIdentifierSchema; 158 signature?: string; 159 certs?: CertificateSchema; 160 }> = {}): Schema.SchemaType { 161 const names = pvutils.getParametersValue<NonNullable<typeof parameters.names>>(parameters, "names", {}); 162 163 return (new asn1js.Sequence({ 164 name: (names.blockName || BASIC_OCSP_RESPONSE), 165 value: [ 166 ResponseData.schema(names.tbsResponseData || { 167 names: { 168 blockName: BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA 169 } 170 }), 171 AlgorithmIdentifier.schema(names.signatureAlgorithm || { 172 names: { 173 blockName: BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM 174 } 175 }), 176 new asn1js.BitString({ name: (names.signature || BASIC_OCSP_RESPONSE_SIGNATURE) }), 177 new asn1js.Constructed({ 178 optional: true, 179 idBlock: { 180 tagClass: 3, // CONTEXT-SPECIFIC 181 tagNumber: 0 // [0] 182 }, 183 value: [ 184 new asn1js.Sequence({ 185 value: [new asn1js.Repeated({ 186 name: BASIC_OCSP_RESPONSE_CERTS, 187 value: Certificate.schema(names.certs || {}) 188 })] 189 }) 190 ] 191 }) 192 ] 193 })); 194 } 195 196 public fromSchema(schema: Schema.SchemaType): void { 197 // Clear input data first 198 pvutils.clearProps(schema, CLEAR_PROPS); 199 //#endregion 200 201 // Check the schema is valid 202 const asn1 = asn1js.compareSchema(schema, 203 schema, 204 BasicOCSPResponse.schema() 205 ); 206 AsnError.assertSchema(asn1, this.className); 207 208 //#region Get internal properties from parsed schema 209 this.tbsResponseData = new ResponseData({ schema: asn1.result[BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA] }); 210 this.signatureAlgorithm = new AlgorithmIdentifier({ schema: asn1.result[BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM] }); 211 this.signature = asn1.result[BASIC_OCSP_RESPONSE_SIGNATURE]; 212 213 if (BASIC_OCSP_RESPONSE_CERTS in asn1.result) { 214 this.certs = Array.from(asn1.result[BASIC_OCSP_RESPONSE_CERTS], element => new Certificate({ schema: element })); 215 } 216 //#endregion 217 } 218 219 public toSchema(): asn1js.Sequence { 220 //#region Create array for output sequence 221 const outputArray = []; 222 223 outputArray.push(this.tbsResponseData.toSchema()); 224 outputArray.push(this.signatureAlgorithm.toSchema()); 225 outputArray.push(this.signature); 226 227 //#region Create array of certificates 228 if (this.certs) { 229 outputArray.push(new asn1js.Constructed({ 230 idBlock: { 231 tagClass: 3, // CONTEXT-SPECIFIC 232 tagNumber: 0 // [0] 233 }, 234 value: [ 235 new asn1js.Sequence({ 236 value: Array.from(this.certs, o => o.toSchema()) 237 }) 238 ] 239 })); 240 } 241 //#endregion 242 //#endregion 243 244 //#region Construct and return new ASN.1 schema for this object 245 return (new asn1js.Sequence({ 246 value: outputArray 247 })); 248 //#endregion 249 } 250 251 public toJSON(): BasicOCSPResponseJson { 252 const res: BasicOCSPResponseJson = { 253 tbsResponseData: this.tbsResponseData.toJSON(), 254 signatureAlgorithm: this.signatureAlgorithm.toJSON(), 255 signature: this.signature.toJSON(), 256 }; 257 258 if (this.certs) { 259 res.certs = Array.from(this.certs, o => o.toJSON()); 260 } 261 262 return res; 263 } 264 265 /** 266 * Get OCSP response status for specific certificate 267 * @param certificate Certificate to be checked 268 * @param issuerCertificate Certificate of issuer for certificate to be checked 269 * @param crypto Crypto engine 270 */ 271 public async getCertificateStatus(certificate: Certificate, issuerCertificate: Certificate, crypto = common.getCrypto(true)): Promise<CertificateStatus> { 272 //#region Initial variables 273 const result = { 274 isForCertificate: false, 275 status: 2 // 0 = good, 1 = revoked, 2 = unknown 276 }; 277 278 const hashesObject: Record<string, number> = {}; 279 280 const certIDs: CertID[] = []; 281 //#endregion 282 283 //#region Create all "certIDs" for input certificates 284 for (const response of this.tbsResponseData.responses) { 285 const hashAlgorithm = crypto.getAlgorithmByOID(response.certID.hashAlgorithm.algorithmId, true, "CertID.hashAlgorithm"); 286 287 if (!hashesObject[hashAlgorithm.name]) { 288 hashesObject[hashAlgorithm.name] = 1; 289 290 const certID = new CertID(); 291 292 certIDs.push(certID); 293 await certID.createForCertificate(certificate, { 294 hashAlgorithm: hashAlgorithm.name, 295 issuerCertificate 296 }, crypto); 297 } 298 } 299 //#endregion 300 301 //#region Compare all response's "certIDs" with identifiers for input certificate 302 for (const response of this.tbsResponseData.responses) { 303 for (const id of certIDs) { 304 if (response.certID.isEqual(id)) { 305 result.isForCertificate = true; 306 307 try { 308 switch (response.certStatus.idBlock.isConstructed) { 309 case true: 310 if (response.certStatus.idBlock.tagNumber === 1) 311 result.status = 1; // revoked 312 313 break; 314 case false: 315 switch (response.certStatus.idBlock.tagNumber) { 316 case 0: // good 317 result.status = 0; 318 break; 319 case 2: // unknown 320 result.status = 2; 321 break; 322 default: 323 } 324 325 break; 326 default: 327 } 328 } 329 catch { 330 // nothing 331 } 332 333 return result; 334 } 335 } 336 } 337 338 return result; 339 //#endregion 340 } 341 342 /** 343 * Make signature for current OCSP Basic Response 344 * @param privateKey Private key for "subjectPublicKeyInfo" structure 345 * @param hashAlgorithm Hashing algorithm. Default SHA-1 346 * @param crypto Crypto engine 347 */ 348 async sign(privateKey: CryptoKey, hashAlgorithm = "SHA-1", crypto = common.getCrypto(true)): Promise<void> { 349 // Get a private key from function parameter 350 if (!privateKey) { 351 throw new Error("Need to provide a private key for signing"); 352 } 353 354 //#region Get a "default parameters" for current algorithm and set correct signature algorithm 355 const signatureParams = await crypto.getSignatureParameters(privateKey, hashAlgorithm); 356 357 const algorithm = signatureParams.parameters.algorithm; 358 if (!("name" in algorithm)) { 359 throw new Error("Empty algorithm"); 360 } 361 this.signatureAlgorithm = signatureParams.signatureAlgorithm; 362 //#endregion 363 364 //#region Create TBS data for signing 365 this.tbsResponseData.tbsView = new Uint8Array(this.tbsResponseData.toSchema(true).toBER()); 366 //#endregion 367 368 //#region Signing TBS data on provided private key 369 const signature = await crypto.signWithPrivateKey(this.tbsResponseData.tbsView as BufferSource, privateKey, { algorithm }); 370 this.signature = new asn1js.BitString({ valueHex: signature }); 371 //#endregion 372 } 373 374 /** 375 * Verify existing OCSP Basic Response 376 * @param params Additional parameters 377 * @param crypto Crypto engine 378 */ 379 public async verify(params: BasicOCSPResponseVerifyParams = {}, crypto = common.getCrypto(true)): Promise<boolean> { 380 //#region Initial variables 381 let signerCert: Certificate | null = null; 382 let certIndex = -1; 383 const trustedCerts: Certificate[] = params.trustedCerts || []; 384 //#endregion 385 386 //#region Check amount of certificates 387 if (!this.certs) { 388 throw new Error("No certificates attached to the BasicOCSPResponse"); 389 } 390 //#endregion 391 392 //#region Find correct value for "responderID" 393 switch (true) { 394 case (this.tbsResponseData.responderID instanceof RelativeDistinguishedNames): // [1] Name 395 for (const [index, certificate] of this.certs.entries()) { 396 if (certificate.subject.isEqual(this.tbsResponseData.responderID)) { 397 certIndex = index; 398 break; 399 } 400 } 401 break; 402 case (this.tbsResponseData.responderID instanceof asn1js.OctetString): // [2] KeyHash 403 for (const [index, cert] of this.certs.entries()) { 404 const hash = await crypto.digest({ name: "sha-1" }, cert.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHexView as BufferSource); 405 if (pvutils.isEqualBuffer(hash, this.tbsResponseData.responderID.valueBlock.valueHex)) { 406 certIndex = index; 407 break; 408 } 409 } 410 break; 411 default: 412 throw new Error("Wrong value for responderID"); 413 } 414 //#endregion 415 416 //#region Make additional verification for signer's certificate 417 if (certIndex === (-1)) 418 throw new Error("Correct certificate was not found in OCSP response"); 419 420 signerCert = this.certs[certIndex]; 421 422 const additionalCerts: Certificate[] = [signerCert]; 423 for (const cert of this.certs) { 424 const caCert = await checkCA(cert, signerCert); 425 if (caCert) { 426 additionalCerts.push(caCert); 427 } 428 } 429 const certChain = new CertificateChainValidationEngine({ 430 certs: additionalCerts, 431 trustedCerts, 432 }); 433 434 const verificationResult = await certChain.verify({}, crypto); 435 if (!verificationResult.result) { 436 throw new Error("Validation of signer's certificate failed"); 437 } 438 439 return crypto.verifyWithPublicKey(this.tbsResponseData.tbsView as BufferSource, this.signature, this.certs[certIndex].subjectPublicKeyInfo, this.signatureAlgorithm); 440 } 441 442 }