src/BasicOCSPResponse.js
import * as asn1js from "asn1js";
import { getParametersValue, isEqualBuffer, clearProps } from "pvutils";
import { getAlgorithmByOID, getCrypto, getEngine } from "./common.js";
import ResponseData from "./ResponseData.js";
import AlgorithmIdentifier from "./AlgorithmIdentifier.js";
import Certificate from "./Certificate.js";
import CertID from "./CertID.js";
import RelativeDistinguishedNames from "./RelativeDistinguishedNames.js";
import CertificateChainValidationEngine from "./CertificateChainValidationEngine.js";
//**************************************************************************************
/**
* Class from RFC6960
*/
export default class BasicOCSPResponse
{
//**********************************************************************************
/**
* Constructor for BasicOCSPResponse class
* @param {Object} [parameters={}]
* @property {Object} [schema] asn1js parsed value
*/
constructor(parameters = {})
{
//region Internal properties of the object
/**
* @type {ResponseData}
* @description tbsResponseData
*/
this.tbsResponseData = getParametersValue(parameters, "tbsResponseData", BasicOCSPResponse.defaultValues("tbsResponseData"));
/**
* @type {AlgorithmIdentifier}
* @description signatureAlgorithm
*/
this.signatureAlgorithm = getParametersValue(parameters, "signatureAlgorithm", BasicOCSPResponse.defaultValues("signatureAlgorithm"));
/**
* @type {BitString}
* @description signature
*/
this.signature = getParametersValue(parameters, "signature", BasicOCSPResponse.defaultValues("signature"));
if("certs" in parameters)
/**
* @type {Array.<Certificate>}
* @description certs
*/
this.certs = getParametersValue(parameters, "certs", BasicOCSPResponse.defaultValues("certs"));
//endregion
//region If input argument array contains "schema" for this object
if("schema" in parameters)
this.fromSchema(parameters.schema);
//endregion
}
//**********************************************************************************
/**
* Return default values for all class members
* @param {string} memberName String name for a class member
*/
static defaultValues(memberName)
{
switch(memberName)
{
case "tbsResponseData":
return new ResponseData();
case "signatureAlgorithm":
return new AlgorithmIdentifier();
case "signature":
return new asn1js.BitString();
case "certs":
return [];
default:
throw new Error(`Invalid member name for BasicOCSPResponse class: ${memberName}`);
}
}
//**********************************************************************************
/**
* Compare values with default values for all class members
* @param {string} memberName String name for a class member
* @param {*} memberValue Value to compare with default value
*/
static compareWithDefault(memberName, memberValue)
{
switch(memberName)
{
case "type":
{
// noinspection OverlyComplexBooleanExpressionJS
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 "signatureAlgorithm":
return ((memberValue.algorithmId === "") && (("algorithmParams" in memberValue) === false));
case "signature":
return (memberValue.isEqual(BasicOCSPResponse.defaultValues(memberName)));
case "certs":
return (memberValue.length === 0);
default:
throw new Error(`Invalid member name for BasicOCSPResponse class: ${memberName}`);
}
}
//**********************************************************************************
/**
* Return value of asn1js schema for current class
* @param {Object} parameters Input parameters for the schema
* @returns {Object} asn1js schema object
*/
static schema(parameters = {})
{
//BasicOCSPResponse ::= SEQUENCE {
// tbsResponseData ResponseData,
// signatureAlgorithm AlgorithmIdentifier,
// signature BIT STRING,
// certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL }
/**
* @type {Object}
* @property {string} [blockName]
* @property {string} [tbsResponseData]
* @property {string} [signatureAlgorithm]
* @property {string} [signature]
* @property {string} [certs]
*/
const names = getParametersValue(parameters, "names", {});
return (new asn1js.Sequence({
name: (names.blockName || "BasicOCSPResponse"),
value: [
ResponseData.schema(names.tbsResponseData || {
names: {
blockName: "BasicOCSPResponse.tbsResponseData"
}
}),
AlgorithmIdentifier.schema(names.signatureAlgorithm || {
names: {
blockName: "BasicOCSPResponse.signatureAlgorithm"
}
}),
new asn1js.BitString({ name: (names.signature || "BasicOCSPResponse.signature") }),
new asn1js.Constructed({
optional: true,
idBlock: {
tagClass: 3, // CONTEXT-SPECIFIC
tagNumber: 0 // [0]
},
value: [
new asn1js.Sequence({
value: [new asn1js.Repeated({
name: "BasicOCSPResponse.certs",
value: Certificate.schema(names.certs || {})
})]
})
]
})
]
}));
}
//**********************************************************************************
/**
* Convert parsed asn1js object into current class
* @param {!Object} schema
*/
fromSchema(schema)
{
//region Clear input data first
clearProps(schema, [
"BasicOCSPResponse.tbsResponseData",
"BasicOCSPResponse.signatureAlgorithm",
"BasicOCSPResponse.signature",
"BasicOCSPResponse.certs"
]);
//endregion
//region Check the schema is valid
const asn1 = asn1js.compareSchema(schema,
schema,
BasicOCSPResponse.schema()
);
if(asn1.verified === false)
throw new Error("Object's schema was not verified against input data for BasicOCSPResponse");
//endregion
//region Get internal properties from parsed schema
this.tbsResponseData = new ResponseData({ schema: asn1.result["BasicOCSPResponse.tbsResponseData"] });
this.signatureAlgorithm = new AlgorithmIdentifier({ schema: asn1.result["BasicOCSPResponse.signatureAlgorithm"] });
this.signature = asn1.result["BasicOCSPResponse.signature"];
if("BasicOCSPResponse.certs" in asn1.result)
this.certs = Array.from(asn1.result["BasicOCSPResponse.certs"], element => new Certificate({ schema: element }));
//endregion
}
//**********************************************************************************
/**
* Convert current object to asn1js object and set correct values
* @returns {Object} asn1js object
*/
toSchema()
{
//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("certs" in this)
{
outputArray.push(new asn1js.Constructed({
idBlock: {
tagClass: 3, // CONTEXT-SPECIFIC
tagNumber: 0 // [0]
},
value: [
new asn1js.Sequence({
value: Array.from(this.certs, element => element.toSchema())
})
]
}));
}
//endregion
//endregion
//region Construct and return new ASN.1 schema for this object
return (new asn1js.Sequence({
value: outputArray
}));
//endregion
}
//**********************************************************************************
/**
* Convertion for the class to JSON object
* @returns {Object}
*/
toJSON()
{
const _object = {
tbsResponseData: this.tbsResponseData.toJSON(),
signatureAlgorithm: this.signatureAlgorithm.toJSON(),
signature: this.signature.toJSON()
};
if("certs" in this)
_object.certs = Array.from(this.certs, element => element.toJSON());
return _object;
}
//**********************************************************************************
/**
* Get OCSP response status for specific certificate
* @param {Certificate} certificate Certificate to be checked
* @param {Certificate} issuerCertificate Certificate of issuer for certificate to be checked
* @returns {Promise}
*/
getCertificateStatus(certificate, issuerCertificate)
{
//region Initial variables
let sequence = Promise.resolve();
const result = {
isForCertificate: false,
status: 2 // 0 = good, 1 = revoked, 2 = unknown
};
const hashesObject = {};
const certIDs = [];
const certIDPromises = [];
//endregion
//region Create all "certIDs" for input certificates
for(const response of this.tbsResponseData.responses)
{
const hashAlgorithm = getAlgorithmByOID(response.certID.hashAlgorithm.algorithmId);
if(("name" in hashAlgorithm) === false)
return Promise.reject(`Wrong CertID hashing algorithm: ${response.certID.hashAlgorithm.algorithmId}`);
if((hashAlgorithm.name in hashesObject) === false)
{
hashesObject[hashAlgorithm.name] = 1;
const certID = new CertID();
certIDs.push(certID);
certIDPromises.push(certID.createForCertificate(certificate, {
hashAlgorithm: hashAlgorithm.name,
issuerCertificate
}));
}
}
sequence = sequence.then(() =>
Promise.all(certIDPromises)
);
//endregion
//region Compare all response's "certIDs" with identifiers for input certificate
sequence = sequence.then(() =>
{
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)
{
}
return result;
}
}
}
return result;
});
//endregion
return sequence;
}
//**********************************************************************************
/**
* Make signature for current OCSP Basic Response
* @param {Object} privateKey Private key for "subjectPublicKeyInfo" structure
* @param {string} [hashAlgorithm="SHA-1"] Hashing algorithm. Default SHA-1
* @returns {Promise}
*/
sign(privateKey, hashAlgorithm = "SHA-1")
{
//region Initial checking
//region Get a private key from function parameter
if(typeof privateKey === "undefined")
return Promise.reject("Need to provide a private key for signing");
//endregion
//endregion
//region Initial variables
let sequence = Promise.resolve();
let parameters;
const engine = getEngine();
//endregion
//region Get a "default parameters" for current algorithm and set correct signature algorithm
sequence = sequence.then(() => engine.subtle.getSignatureParameters(privateKey, hashAlgorithm));
sequence = sequence.then(result =>
{
parameters = result.parameters;
this.signatureAlgorithm = result.signatureAlgorithm;
});
//endregion
//region Create TBS data for signing
sequence = sequence.then(() =>
{
this.tbsResponseData.tbs = this.tbsResponseData.toSchema(true).toBER(false);
});
//endregion
//region Signing TBS data on provided private key
sequence = sequence.then(() => engine.subtle.signWithPrivateKey(this.tbsResponseData.tbs, privateKey, parameters));
sequence = sequence.then(result =>
{
this.signature = new asn1js.BitString({ valueHex: result });
});
//endregion
return sequence;
}
//**********************************************************************************
/**
* Verify existing OCSP Basic Response
* @param {Object} parameters Additional parameters
* @returns {Promise}
*/
verify(parameters = {})
{
//region Initial variables
let signerCert = null;
let certIndex = -1;
let sequence = Promise.resolve();
let trustedCerts = [];
const _this = this;
const engine = getEngine();
//endregion
//region Check amount of certificates
if(("certs" in this) === false)
return Promise.reject("No certificates attached to the BasicOCSPResponce");
//endregion
//region Get input values
if("trustedCerts" in parameters)
trustedCerts = parameters.trustedCerts;
//endregion
//region Aux functions
/**
* Check CA flag for the certificate
* @param {Certificate} cert Certificate to find CA flag for
* @returns {*}
*/
function checkCA(cert)
{
//region Do not include signer's certificate
if((cert.issuer.isEqual(signerCert.issuer) === true) && (cert.serialNumber.isEqual(signerCert.serialNumber) === true))
return null;
//endregion
let isCA = false;
for(const extension of cert.extensions)
{
if(extension.extnID === "2.5.29.19") // BasicConstraints
{
if("cA" in extension.parsedValue)
{
if(extension.parsedValue.cA === true)
isCA = true;
}
}
}
if(isCA)
return cert;
return null;
}
//endregion
//region Get a "crypto" extension
const crypto = getCrypto();
if(typeof crypto === "undefined")
return Promise.reject("Unable to create WebCrypto object");
//endregion
//region Find correct value for "responderID"
switch(true)
{
case (this.tbsResponseData.responderID instanceof RelativeDistinguishedNames): // [1] Name
sequence = sequence.then(() =>
{
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
sequence = sequence.then(() => Promise.all(Array.from(_this.certs, element =>
crypto.digest({ name: "sha-1" }, new Uint8Array(element.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHex)))).then(results =>
{
for(const [index, ] of _this.certs.entries())
{
if(isEqualBuffer(results[index], _this.tbsResponseData.responderID.valueBlock.valueHex))
{
certIndex = index;
break;
}
}
}));
break;
default:
return Promise.reject("Wrong value for responderID");
}
//endregion
//region Make additional verification for signer's certificate
sequence = sequence.then(() =>
{
if(certIndex === (-1))
return Promise.reject("Correct certificate was not found in OCSP response");
signerCert = this.certs[certIndex];
return Promise.all(Array.from(_this.certs, element => checkCA(element))).then(promiseResults =>
{
const additionalCerts = [];
additionalCerts.push(signerCert);
for(const promiseResult of promiseResults)
{
if(promiseResult !== null)
additionalCerts.push(promiseResult);
}
const certChain = new CertificateChainValidationEngine({
certs: additionalCerts,
trustedCerts
});
return certChain.verify().then(verificationResult =>
{
if(verificationResult.result === true)
return Promise.resolve();
return Promise.reject("Validation of signer's certificate failed");
}, error =>
Promise.reject(`Validation of signer's certificate failed with error: ${((error instanceof Object) ? error.resultMessage : error)}`)
);
}, promiseError =>
Promise.reject(`Error during checking certificates for CA flag: ${promiseError}`)
);
});
//endregion
sequence = sequence.then(() => engine.subtle.verifyWithPublicKey(this.tbsResponseData.tbs, this.signature, this.certs[certIndex].subjectPublicKeyInfo, this.signatureAlgorithm));
return sequence;
}
//**********************************************************************************
}
//**************************************************************************************