mirror of
https://github.com/Ylianst/MeshCommander
synced 2025-12-06 06:03:20 +00:00
563 lines
16 KiB
JavaScript
563 lines
16 KiB
JavaScript
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={}]
|
|
* @param {Object} [parameters.schema] asn1js parsed value to initialize the class from
|
|
*/
|
|
constructor(parameters = {})
|
|
{
|
|
//region Internal properties of the object
|
|
/**
|
|
* @type {ResponseData}
|
|
* @desc tbsResponseData
|
|
*/
|
|
this.tbsResponseData = getParametersValue(parameters, "tbsResponseData", BasicOCSPResponse.defaultValues("tbsResponseData"));
|
|
/**
|
|
* @type {AlgorithmIdentifier}
|
|
* @desc signatureAlgorithm
|
|
*/
|
|
this.signatureAlgorithm = getParametersValue(parameters, "signatureAlgorithm", BasicOCSPResponse.defaultValues("signatureAlgorithm"));
|
|
/**
|
|
* @type {BitString}
|
|
* @desc signature
|
|
*/
|
|
this.signature = getParametersValue(parameters, "signature", BasicOCSPResponse.defaultValues("signature"));
|
|
|
|
if("certs" in parameters)
|
|
/**
|
|
* @type {Array.<Certificate>}
|
|
* @desc 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 pre-defined ASN.1 schema for current class
|
|
*
|
|
* ASN.1 schema:
|
|
* ```asn1
|
|
* BasicOCSPResponse ::= SEQUENCE {
|
|
* tbsResponseData ResponseData,
|
|
* signatureAlgorithm AlgorithmIdentifier,
|
|
* signature BIT STRING,
|
|
* certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL }
|
|
* ```
|
|
*
|
|
* @param {Object} parameters Input parameters for the schema
|
|
* @returns {Object} asn1js schema object
|
|
*/
|
|
static schema(parameters = {})
|
|
{
|
|
/**
|
|
* @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;
|
|
}
|
|
//**********************************************************************************
|
|
}
|
|
//**************************************************************************************
|