CertificateRevocationList.ts (20174B)
1 import * as asn1js from "asn1js"; 2 import * as pvtsutils from "pvtsutils"; 3 import * as pvutils from "pvutils"; 4 import * as common from "./common"; 5 import { AlgorithmIdentifier, AlgorithmIdentifierJson, AlgorithmIdentifierSchema } from "./AlgorithmIdentifier"; 6 import { RelativeDistinguishedNames, RelativeDistinguishedNamesJson, RelativeDistinguishedNamesSchema } from "./RelativeDistinguishedNames"; 7 import { Time, TimeJson, TimeSchema } from "./Time"; 8 import { RevokedCertificate, RevokedCertificateJson } from "./RevokedCertificate"; 9 import { Extensions, ExtensionsJson, ExtensionsSchema } from "./Extensions"; 10 import * as Schema from "./Schema"; 11 import { Certificate } from "./Certificate"; 12 import { PublicKeyInfo } from "./PublicKeyInfo"; 13 import { id_AuthorityInfoAccess, id_AuthorityKeyIdentifier, id_BaseCRLNumber, id_CertificateIssuer, id_CRLNumber, id_CRLReason, id_FreshestCRL, id_InvalidityDate, id_IssuerAltName, id_IssuingDistributionPoint } from "./ObjectIdentifiers"; 14 import { AsnError } from "./errors"; 15 import { PkiObject, PkiObjectParameters } from "./PkiObject"; 16 import { EMPTY_BUFFER } from "./constants"; 17 18 const TBS = "tbs"; 19 const VERSION = "version"; 20 const SIGNATURE = "signature"; 21 const ISSUER = "issuer"; 22 const THIS_UPDATE = "thisUpdate"; 23 const NEXT_UPDATE = "nextUpdate"; 24 const REVOKED_CERTIFICATES = "revokedCertificates"; 25 const CRL_EXTENSIONS = "crlExtensions"; 26 const SIGNATURE_ALGORITHM = "signatureAlgorithm"; 27 const SIGNATURE_VALUE = "signatureValue"; 28 const TBS_CERT_LIST = "tbsCertList"; 29 const TBS_CERT_LIST_VERSION = `${TBS_CERT_LIST}.version`; 30 const TBS_CERT_LIST_SIGNATURE = `${TBS_CERT_LIST}.signature`; 31 const TBS_CERT_LIST_ISSUER = `${TBS_CERT_LIST}.issuer`; 32 const TBS_CERT_LIST_THIS_UPDATE = `${TBS_CERT_LIST}.thisUpdate`; 33 const TBS_CERT_LIST_NEXT_UPDATE = `${TBS_CERT_LIST}.nextUpdate`; 34 const TBS_CERT_LIST_REVOKED_CERTIFICATES = `${TBS_CERT_LIST}.revokedCertificates`; 35 const TBS_CERT_LIST_EXTENSIONS = `${TBS_CERT_LIST}.extensions`; 36 const CLEAR_PROPS = [ 37 TBS_CERT_LIST, 38 TBS_CERT_LIST_VERSION, 39 TBS_CERT_LIST_SIGNATURE, 40 TBS_CERT_LIST_ISSUER, 41 TBS_CERT_LIST_THIS_UPDATE, 42 TBS_CERT_LIST_NEXT_UPDATE, 43 TBS_CERT_LIST_REVOKED_CERTIFICATES, 44 TBS_CERT_LIST_EXTENSIONS, 45 SIGNATURE_ALGORITHM, 46 SIGNATURE_VALUE 47 ]; 48 49 export interface ICertificateRevocationList { 50 tbs: ArrayBuffer; 51 version: number; 52 signature: AlgorithmIdentifier; 53 issuer: RelativeDistinguishedNames; 54 thisUpdate: Time; 55 nextUpdate?: Time; 56 revokedCertificates?: RevokedCertificate[]; 57 crlExtensions?: Extensions; 58 signatureAlgorithm: AlgorithmIdentifier; 59 signatureValue: asn1js.BitString; 60 } 61 62 export type TBSCertListSchema = Schema.SchemaParameters<{ 63 tbsCertListVersion?: string; 64 signature?: AlgorithmIdentifierSchema; 65 issuer?: RelativeDistinguishedNamesSchema; 66 tbsCertListThisUpdate?: TimeSchema; 67 tbsCertListNextUpdate?: TimeSchema; 68 tbsCertListRevokedCertificates?: string; 69 crlExtensions?: ExtensionsSchema; 70 }>; 71 72 export interface CertificateRevocationListJson { 73 tbs: string; 74 version: number; 75 signature: AlgorithmIdentifierJson; 76 issuer: RelativeDistinguishedNamesJson; 77 thisUpdate: TimeJson; 78 nextUpdate?: TimeJson; 79 revokedCertificates?: RevokedCertificateJson[]; 80 crlExtensions?: ExtensionsJson; 81 signatureAlgorithm: AlgorithmIdentifierJson; 82 signatureValue: asn1js.BitStringJson; 83 } 84 85 function tbsCertList(parameters: TBSCertListSchema = {}): Schema.SchemaType { 86 //TBSCertList ::= SEQUENCE { 87 // version Version OPTIONAL, 88 // -- if present, MUST be v2 89 // signature AlgorithmIdentifier, 90 // issuer Name, 91 // thisUpdate Time, 92 // nextUpdate Time OPTIONAL, 93 // revokedCertificates SEQUENCE OF SEQUENCE { 94 // userCertificate CertificateSerialNumber, 95 // revocationDate Time, 96 // crlEntryExtensions Extensions OPTIONAL 97 // -- if present, version MUST be v2 98 // } OPTIONAL, 99 // crlExtensions [0] EXPLICIT Extensions OPTIONAL 100 // -- if present, version MUST be v2 101 //} 102 const names = pvutils.getParametersValue<NonNullable<typeof parameters.names>>(parameters, "names", {}); 103 104 return (new asn1js.Sequence({ 105 name: (names.blockName || TBS_CERT_LIST), 106 value: [ 107 new asn1js.Integer({ 108 optional: true, 109 name: (names.tbsCertListVersion || TBS_CERT_LIST_VERSION), 110 value: 2 111 }), // EXPLICIT integer value (v2) 112 AlgorithmIdentifier.schema(names.signature || { 113 names: { 114 blockName: TBS_CERT_LIST_SIGNATURE 115 } 116 }), 117 RelativeDistinguishedNames.schema(names.issuer || { 118 names: { 119 blockName: TBS_CERT_LIST_ISSUER 120 } 121 }), 122 Time.schema(names.tbsCertListThisUpdate || { 123 names: { 124 utcTimeName: TBS_CERT_LIST_THIS_UPDATE, 125 generalTimeName: TBS_CERT_LIST_THIS_UPDATE 126 } 127 }), 128 Time.schema(names.tbsCertListNextUpdate || { 129 names: { 130 utcTimeName: TBS_CERT_LIST_NEXT_UPDATE, 131 generalTimeName: TBS_CERT_LIST_NEXT_UPDATE 132 } 133 }, true), 134 new asn1js.Sequence({ 135 optional: true, 136 value: [ 137 new asn1js.Repeated({ 138 name: (names.tbsCertListRevokedCertificates || TBS_CERT_LIST_REVOKED_CERTIFICATES), 139 value: new asn1js.Sequence({ 140 value: [ 141 new asn1js.Integer(), 142 Time.schema(), 143 Extensions.schema({}, true) 144 ] 145 }) 146 }) 147 ] 148 }), 149 new asn1js.Constructed({ 150 optional: true, 151 idBlock: { 152 tagClass: 3, // CONTEXT-SPECIFIC 153 tagNumber: 0 // [0] 154 }, 155 value: [Extensions.schema(names.crlExtensions || { 156 names: { 157 blockName: TBS_CERT_LIST_EXTENSIONS 158 } 159 })] 160 }) // EXPLICIT SEQUENCE value 161 ] 162 })); 163 } 164 165 export type CertificateRevocationListParameters = PkiObjectParameters & Partial<ICertificateRevocationList>; 166 167 export interface CertificateRevocationListVerifyParams { 168 issuerCertificate?: Certificate; 169 publicKeyInfo?: PublicKeyInfo; 170 } 171 172 const WELL_KNOWN_EXTENSIONS = [ 173 id_AuthorityKeyIdentifier, 174 id_IssuerAltName, 175 id_CRLNumber, 176 id_BaseCRLNumber, 177 id_IssuingDistributionPoint, 178 id_FreshestCRL, 179 id_AuthorityInfoAccess, 180 id_CRLReason, 181 id_InvalidityDate, 182 id_CertificateIssuer, 183 ]; 184 /** 185 * Represents the CertificateRevocationList structure described in [RFC5280](https://datatracker.ietf.org/doc/html/rfc5280) 186 */ 187 export class CertificateRevocationList extends PkiObject implements ICertificateRevocationList { 188 189 public static override CLASS_NAME = "CertificateRevocationList"; 190 191 public tbsView!: Uint8Array; 192 /** 193 * @deprecated Since version 3.0.0 194 */ 195 public get tbs(): ArrayBuffer { 196 return pvtsutils.BufferSourceConverter.toArrayBuffer(this.tbsView); 197 } 198 199 /** 200 * @deprecated Since version 3.0.0 201 */ 202 public set tbs(value: ArrayBuffer) { 203 this.tbsView = new Uint8Array(value); 204 } 205 public version!: number; 206 public signature!: AlgorithmIdentifier; 207 public issuer!: RelativeDistinguishedNames; 208 public thisUpdate!: Time; 209 public nextUpdate?: Time; 210 public revokedCertificates?: RevokedCertificate[]; 211 public crlExtensions?: Extensions; 212 public signatureAlgorithm!: AlgorithmIdentifier; 213 public signatureValue!: asn1js.BitString; 214 215 /** 216 * Initializes a new instance of the {@link CertificateRevocationList} class 217 * @param parameters Initialization parameters 218 */ 219 constructor(parameters: CertificateRevocationListParameters = {}) { 220 super(); 221 222 this.tbsView = new Uint8Array(pvutils.getParametersValue(parameters, TBS, CertificateRevocationList.defaultValues(TBS))); 223 this.version = pvutils.getParametersValue(parameters, VERSION, CertificateRevocationList.defaultValues(VERSION)); 224 this.signature = pvutils.getParametersValue(parameters, SIGNATURE, CertificateRevocationList.defaultValues(SIGNATURE)); 225 this.issuer = pvutils.getParametersValue(parameters, ISSUER, CertificateRevocationList.defaultValues(ISSUER)); 226 this.thisUpdate = pvutils.getParametersValue(parameters, THIS_UPDATE, CertificateRevocationList.defaultValues(THIS_UPDATE)); 227 if (NEXT_UPDATE in parameters) { 228 this.nextUpdate = pvutils.getParametersValue(parameters, NEXT_UPDATE, CertificateRevocationList.defaultValues(NEXT_UPDATE)); 229 } 230 if (REVOKED_CERTIFICATES in parameters) { 231 this.revokedCertificates = pvutils.getParametersValue(parameters, REVOKED_CERTIFICATES, CertificateRevocationList.defaultValues(REVOKED_CERTIFICATES)); 232 } 233 if (CRL_EXTENSIONS in parameters) { 234 this.crlExtensions = pvutils.getParametersValue(parameters, CRL_EXTENSIONS, CertificateRevocationList.defaultValues(CRL_EXTENSIONS)); 235 } 236 this.signatureAlgorithm = pvutils.getParametersValue(parameters, SIGNATURE_ALGORITHM, CertificateRevocationList.defaultValues(SIGNATURE_ALGORITHM)); 237 this.signatureValue = pvutils.getParametersValue(parameters, SIGNATURE_VALUE, CertificateRevocationList.defaultValues(SIGNATURE_VALUE)); 238 239 if (parameters.schema) { 240 this.fromSchema(parameters.schema); 241 } 242 } 243 244 /** 245 * Returns default values for all class members 246 * @param memberName String name for a class member 247 * @returns Default value 248 */ 249 public static override defaultValues(memberName: typeof TBS): ArrayBuffer; 250 public static override defaultValues(memberName: typeof VERSION): number; 251 public static override defaultValues(memberName: typeof SIGNATURE): AlgorithmIdentifier; 252 public static override defaultValues(memberName: typeof ISSUER): RelativeDistinguishedNames; 253 public static override defaultValues(memberName: typeof THIS_UPDATE): Time; 254 public static override defaultValues(memberName: typeof NEXT_UPDATE): Time; 255 public static override defaultValues(memberName: typeof REVOKED_CERTIFICATES): RevokedCertificate[]; 256 public static override defaultValues(memberName: typeof CRL_EXTENSIONS): Extensions; 257 public static override defaultValues(memberName: typeof SIGNATURE_ALGORITHM): AlgorithmIdentifier; 258 public static override defaultValues(memberName: typeof SIGNATURE_VALUE): asn1js.BitString; 259 public static override defaultValues(memberName: string): any { 260 switch (memberName) { 261 case TBS: 262 return EMPTY_BUFFER; 263 case VERSION: 264 return 0; 265 case SIGNATURE: 266 return new AlgorithmIdentifier(); 267 case ISSUER: 268 return new RelativeDistinguishedNames(); 269 case THIS_UPDATE: 270 return new Time(); 271 case NEXT_UPDATE: 272 return new Time(); 273 case REVOKED_CERTIFICATES: 274 return []; 275 case CRL_EXTENSIONS: 276 return new Extensions(); 277 case SIGNATURE_ALGORITHM: 278 return new AlgorithmIdentifier(); 279 case SIGNATURE_VALUE: 280 return new asn1js.BitString(); 281 default: 282 return super.defaultValues(memberName); 283 } 284 } 285 286 /** 287 * @inheritdoc 288 * @asn ASN.1 schema 289 * ```asn 290 * CertificateList ::= SEQUENCE { 291 * tbsCertList TBSCertList, 292 * signatureAlgorithm AlgorithmIdentifier, 293 * signatureValue BIT STRING } 294 *``` 295 */ 296 public static override schema(parameters: Schema.SchemaParameters<{ 297 tbsCertListVersion?: string; 298 signature?: AlgorithmIdentifierSchema; 299 issuer?: RelativeDistinguishedNamesSchema; 300 tbsCertListThisUpdate?: TimeSchema; 301 tbsCertListNextUpdate?: TimeSchema; 302 tbsCertListRevokedCertificates?: string; 303 crlExtensions?: ExtensionsSchema; 304 signatureAlgorithm?: AlgorithmIdentifierSchema; 305 signatureValue?: string; 306 }> = {}): Schema.SchemaType { 307 const names = pvutils.getParametersValue<NonNullable<typeof parameters.names>>(parameters, "names", {}); 308 309 return (new asn1js.Sequence({ 310 name: (names.blockName || "CertificateList"), 311 value: [ 312 tbsCertList(parameters), 313 AlgorithmIdentifier.schema(names.signatureAlgorithm || { 314 names: { 315 blockName: SIGNATURE_ALGORITHM 316 } 317 }), 318 new asn1js.BitString({ name: (names.signatureValue || SIGNATURE_VALUE) }) 319 ] 320 })); 321 } 322 323 public fromSchema(schema: Schema.SchemaType): void { 324 // Clear input data first 325 pvutils.clearProps(schema, CLEAR_PROPS); 326 327 // Check the schema is valid 328 const asn1 = asn1js.compareSchema(schema, 329 schema, 330 CertificateRevocationList.schema() 331 ); 332 AsnError.assertSchema(asn1, this.className); 333 334 //#region Get internal properties from parsed schema 335 this.tbsView = (asn1.result.tbsCertList as asn1js.Sequence).valueBeforeDecodeView; 336 337 if (TBS_CERT_LIST_VERSION in asn1.result) { 338 this.version = asn1.result[TBS_CERT_LIST_VERSION].valueBlock.valueDec; 339 } 340 this.signature = new AlgorithmIdentifier({ schema: asn1.result[TBS_CERT_LIST_SIGNATURE] }); 341 this.issuer = new RelativeDistinguishedNames({ schema: asn1.result[TBS_CERT_LIST_ISSUER] }); 342 this.thisUpdate = new Time({ schema: asn1.result[TBS_CERT_LIST_THIS_UPDATE] }); 343 if (TBS_CERT_LIST_NEXT_UPDATE in asn1.result) { 344 this.nextUpdate = new Time({ schema: asn1.result[TBS_CERT_LIST_NEXT_UPDATE] }); 345 } 346 if (TBS_CERT_LIST_REVOKED_CERTIFICATES in asn1.result) { 347 this.revokedCertificates = Array.from(asn1.result[TBS_CERT_LIST_REVOKED_CERTIFICATES], element => new RevokedCertificate({ schema: element })); 348 } 349 if (TBS_CERT_LIST_EXTENSIONS in asn1.result) { 350 this.crlExtensions = new Extensions({ schema: asn1.result[TBS_CERT_LIST_EXTENSIONS] }); 351 } 352 353 this.signatureAlgorithm = new AlgorithmIdentifier({ schema: asn1.result.signatureAlgorithm }); 354 this.signatureValue = asn1.result.signatureValue; 355 //#endregion 356 } 357 358 protected encodeTBS() { 359 //#region Create array for output sequence 360 const outputArray: any[] = []; 361 362 if (this.version !== CertificateRevocationList.defaultValues(VERSION)) { 363 outputArray.push(new asn1js.Integer({ value: this.version })); 364 } 365 366 outputArray.push(this.signature.toSchema()); 367 outputArray.push(this.issuer.toSchema()); 368 outputArray.push(this.thisUpdate.toSchema()); 369 370 if (this.nextUpdate) { 371 outputArray.push(this.nextUpdate.toSchema()); 372 } 373 374 if (this.revokedCertificates) { 375 outputArray.push(new asn1js.Sequence({ 376 value: Array.from(this.revokedCertificates, o => o.toSchema()) 377 })); 378 } 379 380 if (this.crlExtensions) { 381 outputArray.push(new asn1js.Constructed({ 382 optional: true, 383 idBlock: { 384 tagClass: 3, // CONTEXT-SPECIFIC 385 tagNumber: 0 // [0] 386 }, 387 value: [ 388 this.crlExtensions.toSchema() 389 ] 390 })); 391 } 392 //#endregion 393 394 return (new asn1js.Sequence({ 395 value: outputArray 396 })); 397 } 398 399 /** 400 * Convert current object to asn1js object and set correct values 401 * @returns asn1js object 402 */ 403 public toSchema(encodeFlag = false) { 404 //#region Decode stored TBS value 405 let tbsSchema; 406 407 if (!encodeFlag) { 408 if (!this.tbsView.byteLength) { // No stored TBS part 409 return CertificateRevocationList.schema(); 410 } 411 412 const asn1 = asn1js.fromBER(this.tbsView); 413 AsnError.assert(asn1, "TBS Certificate Revocation List"); 414 tbsSchema = asn1.result; 415 } 416 //#endregion 417 //#region Create TBS schema via assembling from TBS parts 418 else { 419 tbsSchema = this.encodeTBS(); 420 } 421 //#endregion 422 423 //#region Construct and return new ASN.1 schema for this object 424 return (new asn1js.Sequence({ 425 value: [ 426 tbsSchema, 427 this.signatureAlgorithm.toSchema(), 428 this.signatureValue 429 ] 430 })); 431 //#endregion 432 } 433 434 public toJSON(): CertificateRevocationListJson { 435 const res: CertificateRevocationListJson = { 436 tbs: pvtsutils.Convert.ToHex(this.tbsView), 437 version: this.version, 438 signature: this.signature.toJSON(), 439 issuer: this.issuer.toJSON(), 440 thisUpdate: this.thisUpdate.toJSON(), 441 signatureAlgorithm: this.signatureAlgorithm.toJSON(), 442 signatureValue: this.signatureValue.toJSON() 443 }; 444 445 if (this.version !== CertificateRevocationList.defaultValues(VERSION)) 446 res.version = this.version; 447 448 if (this.nextUpdate) { 449 res.nextUpdate = this.nextUpdate.toJSON(); 450 } 451 452 if (this.revokedCertificates) { 453 res.revokedCertificates = Array.from(this.revokedCertificates, o => o.toJSON()); 454 } 455 456 if (this.crlExtensions) { 457 res.crlExtensions = this.crlExtensions.toJSON(); 458 } 459 460 return res; 461 } 462 463 /** 464 * Returns `true` if supplied certificate is revoked, otherwise `false` 465 * @param certificate 466 */ 467 public isCertificateRevoked(certificate: Certificate): boolean { 468 // Check that issuer of the input certificate is the same with issuer of this CRL 469 if (!this.issuer.isEqual(certificate.issuer)) { 470 return false; 471 } 472 473 // Check that there are revoked certificates in this CRL 474 if (!this.revokedCertificates) { 475 return false; 476 } 477 478 // Search for input certificate in revoked certificates array 479 for (const revokedCertificate of this.revokedCertificates) { 480 if (revokedCertificate.userCertificate.isEqual(certificate.serialNumber)) { 481 return true; 482 } 483 } 484 485 return false; 486 } 487 488 /** 489 * Make a signature for existing CRL data 490 * @param privateKey Private key for "subjectPublicKeyInfo" structure 491 * @param hashAlgorithm Hashing algorithm. Default SHA-1 492 * @param crypto Crypto engine 493 */ 494 public async sign(privateKey: CryptoKey, hashAlgorithm = "SHA-1", crypto = common.getCrypto(true)): Promise<void> { 495 // Get a private key from function parameter 496 if (!privateKey) { 497 throw new Error("Need to provide a private key for signing"); 498 } 499 500 //#region Get a "default parameters" for current algorithm and set correct signature algorithm 501 const signatureParameters = await crypto.getSignatureParameters(privateKey, hashAlgorithm); 502 const { parameters } = signatureParameters; 503 this.signature = signatureParameters.signatureAlgorithm; 504 this.signatureAlgorithm = signatureParameters.signatureAlgorithm; 505 //#endregion 506 507 //#region Create TBS data for signing 508 this.tbsView = new Uint8Array(this.encodeTBS().toBER()); 509 //#endregion 510 511 //#region Signing TBS data on provided private key 512 const signature = await crypto.signWithPrivateKey(this.tbsView as BufferSource, privateKey, parameters as any); 513 this.signatureValue = new asn1js.BitString({ valueHex: signature }); 514 //#endregion 515 } 516 517 /** 518 * Verify existing signature 519 * @param parameters 520 * @param crypto Crypto engine 521 */ 522 public async verify(parameters: CertificateRevocationListVerifyParams = {}, crypto = common.getCrypto(true)): Promise<boolean> { 523 let subjectPublicKeyInfo: PublicKeyInfo | undefined; 524 525 //#region Get information about CRL issuer certificate 526 if (parameters.issuerCertificate) { // "issuerCertificate" must be of type "Certificate" 527 subjectPublicKeyInfo = parameters.issuerCertificate.subjectPublicKeyInfo; 528 529 // The CRL issuer name and "issuerCertificate" subject name are not equal 530 if (!this.issuer.isEqual(parameters.issuerCertificate.subject)) { 531 return false; 532 } 533 } 534 535 //#region In case if there is only public key during verification 536 if (parameters.publicKeyInfo) { 537 subjectPublicKeyInfo = parameters.publicKeyInfo; // Must be of type "PublicKeyInfo" 538 } 539 //#endregion 540 541 if (!subjectPublicKeyInfo) { 542 throw new Error("Issuer's certificate must be provided as an input parameter"); 543 } 544 //#endregion 545 546 //#region Check the CRL for unknown critical extensions 547 if (this.crlExtensions) { 548 for (const extension of this.crlExtensions.extensions) { 549 if (extension.critical) { 550 // We can not be sure that unknown extension has no value for CRL signature 551 if (!WELL_KNOWN_EXTENSIONS.includes(extension.extnID)) 552 return false; 553 } 554 } 555 } 556 //#endregion 557 558 return crypto.verifyWithPublicKey(this.tbsView as BufferSource, this.signatureValue, subjectPublicKeyInfo, this.signatureAlgorithm); 559 } 560 561 }