mirror of
https://github.com/Ylianst/MeshCommander
synced 2025-12-05 21:53:19 +00:00
635 lines
18 KiB
JavaScript
635 lines
18 KiB
JavaScript
import * as asn1js from "asn1js";
|
|
import { getParametersValue, utilFromBase, utilToBase, bufferToHexCodes, toBase64, fromBase64, arrayBufferToString, stringToArrayBuffer } from "pvutils";
|
|
import { ByteStream, SeqStream } from "bytestreamjs";
|
|
import { getCrypto, getEngine } from "./common.js";
|
|
import PublicKeyInfo from "./PublicKeyInfo.js";
|
|
//**************************************************************************************
|
|
export class SignedCertificateTimestamp
|
|
{
|
|
//**********************************************************************************
|
|
/**
|
|
* Constructor for SignedCertificateTimestamp 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 {number}
|
|
* @desc version
|
|
*/
|
|
this.version = getParametersValue(parameters, "version", SignedCertificateTimestamp.defaultValues("version"));
|
|
/**
|
|
* @type {ArrayBuffer}
|
|
* @desc logID
|
|
*/
|
|
this.logID = getParametersValue(parameters, "logID", SignedCertificateTimestamp.defaultValues("logID"));
|
|
/**
|
|
* @type {Date}
|
|
* @desc timestamp
|
|
*/
|
|
this.timestamp = getParametersValue(parameters, "timestamp", SignedCertificateTimestamp.defaultValues("timestamp"));
|
|
/**
|
|
* @type {ArrayBuffer}
|
|
* @desc extensions
|
|
*/
|
|
this.extensions = getParametersValue(parameters, "extensions", SignedCertificateTimestamp.defaultValues("extensions"));
|
|
/**
|
|
* @type {string}
|
|
* @desc hashAlgorithm
|
|
*/
|
|
this.hashAlgorithm = getParametersValue(parameters, "hashAlgorithm", SignedCertificateTimestamp.defaultValues("hashAlgorithm"));
|
|
/**
|
|
* @type {string}
|
|
* @desc signatureAlgorithm
|
|
*/
|
|
this.signatureAlgorithm = getParametersValue(parameters, "signatureAlgorithm", SignedCertificateTimestamp.defaultValues("signatureAlgorithm"));
|
|
/**
|
|
* @type {Object}
|
|
* @desc signature
|
|
*/
|
|
this.signature = getParametersValue(parameters, "signature", SignedCertificateTimestamp.defaultValues("signature"));
|
|
//endregion
|
|
|
|
//region If input argument array contains "schema" for this object
|
|
if("schema" in parameters)
|
|
this.fromSchema(parameters.schema);
|
|
//endregion
|
|
|
|
//region If input argument array contains "stream"
|
|
if("stream" in parameters)
|
|
this.fromStream(parameters.stream);
|
|
//endregion
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Return default values for all class members
|
|
* @param {string} memberName String name for a class member
|
|
*/
|
|
static defaultValues(memberName)
|
|
{
|
|
switch(memberName)
|
|
{
|
|
case "version":
|
|
return 0;
|
|
case "logID":
|
|
case "extensions":
|
|
return new ArrayBuffer(0);
|
|
case "timestamp":
|
|
return new Date(0);
|
|
case "hashAlgorithm":
|
|
case "signatureAlgorithm":
|
|
return "";
|
|
case "signature":
|
|
return new asn1js.Any();
|
|
default:
|
|
throw new Error(`Invalid member name for SignedCertificateTimestamp class: ${memberName}`);
|
|
}
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Convert parsed asn1js object into current class
|
|
* @param {!Object} schema
|
|
*/
|
|
fromSchema(schema)
|
|
{
|
|
if((schema instanceof asn1js.RawData) === false)
|
|
throw new Error("Object's schema was not verified against input data for SignedCertificateTimestamp");
|
|
|
|
const seqStream = new SeqStream({
|
|
stream: new ByteStream({
|
|
buffer: schema.data
|
|
})
|
|
});
|
|
|
|
this.fromStream(seqStream);
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Convert SeqStream data into current class
|
|
* @param {!SeqStream} stream
|
|
*/
|
|
fromStream(stream)
|
|
{
|
|
const blockLength = stream.getUint16();
|
|
|
|
this.version = (stream.getBlock(1))[0];
|
|
|
|
if(this.version === 0)
|
|
{
|
|
this.logID = (new Uint8Array(stream.getBlock(32))).buffer.slice(0);
|
|
this.timestamp = new Date(utilFromBase(new Uint8Array(stream.getBlock(8)), 8));
|
|
|
|
//region Extensions
|
|
const extensionsLength = stream.getUint16();
|
|
this.extensions = (new Uint8Array(stream.getBlock(extensionsLength))).buffer.slice(0);
|
|
//endregion
|
|
|
|
//region Hash algorithm
|
|
switch((stream.getBlock(1))[0])
|
|
{
|
|
case 0:
|
|
this.hashAlgorithm = "none";
|
|
break;
|
|
case 1:
|
|
this.hashAlgorithm = "md5";
|
|
break;
|
|
case 2:
|
|
this.hashAlgorithm = "sha1";
|
|
break;
|
|
case 3:
|
|
this.hashAlgorithm = "sha224";
|
|
break;
|
|
case 4:
|
|
this.hashAlgorithm = "sha256";
|
|
break;
|
|
case 5:
|
|
this.hashAlgorithm = "sha384";
|
|
break;
|
|
case 6:
|
|
this.hashAlgorithm = "sha512";
|
|
break;
|
|
default:
|
|
throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
|
|
}
|
|
//endregion
|
|
|
|
//region Signature algorithm
|
|
switch((stream.getBlock(1))[0])
|
|
{
|
|
case 0:
|
|
this.signatureAlgorithm = "anonymous";
|
|
break;
|
|
case 1:
|
|
this.signatureAlgorithm = "rsa";
|
|
break;
|
|
case 2:
|
|
this.signatureAlgorithm = "dsa";
|
|
break;
|
|
case 3:
|
|
this.signatureAlgorithm = "ecdsa";
|
|
break;
|
|
default:
|
|
throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
|
|
}
|
|
//endregion
|
|
|
|
//region Signature
|
|
const signatureLength = stream.getUint16();
|
|
const signatureData = (new Uint8Array(stream.getBlock(signatureLength))).buffer.slice(0);
|
|
|
|
const asn1 = asn1js.fromBER(signatureData);
|
|
if(asn1.offset === (-1))
|
|
throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
|
|
|
|
this.signature = asn1.result;
|
|
//endregion
|
|
|
|
if(blockLength !== (47 + extensionsLength + signatureLength))
|
|
throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
|
|
}
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Convert current object to asn1js object and set correct values
|
|
* @returns {Object} asn1js object
|
|
*/
|
|
toSchema()
|
|
{
|
|
const stream = this.toStream();
|
|
|
|
return new asn1js.RawData({ data: stream.stream.buffer });
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Convert current object to SeqStream data
|
|
* @returns {SeqStream} SeqStream object
|
|
*/
|
|
toStream()
|
|
{
|
|
const stream = new SeqStream();
|
|
|
|
stream.appendUint16(47 + this.extensions.byteLength + this.signature.valueBeforeDecode.byteLength);
|
|
stream.appendChar(this.version);
|
|
stream.appendView(new Uint8Array(this.logID));
|
|
|
|
const timeBuffer = new ArrayBuffer(8);
|
|
const timeView = new Uint8Array(timeBuffer);
|
|
|
|
const baseArray = utilToBase(this.timestamp.valueOf(), 8);
|
|
timeView.set(new Uint8Array(baseArray), 8 - baseArray.byteLength);
|
|
|
|
stream.appendView(timeView);
|
|
stream.appendUint16(this.extensions.byteLength);
|
|
|
|
if(this.extensions.byteLength)
|
|
stream.appendView(new Uint8Array(this.extensions));
|
|
|
|
let _hashAlgorithm;
|
|
|
|
switch(this.hashAlgorithm.toLowerCase())
|
|
{
|
|
case "none":
|
|
_hashAlgorithm = 0;
|
|
break;
|
|
case "md5":
|
|
_hashAlgorithm = 1;
|
|
break;
|
|
case "sha1":
|
|
_hashAlgorithm = 2;
|
|
break;
|
|
case "sha224":
|
|
_hashAlgorithm = 3;
|
|
break;
|
|
case "sha256":
|
|
_hashAlgorithm = 4;
|
|
break;
|
|
case "sha384":
|
|
_hashAlgorithm = 5;
|
|
break;
|
|
case "sha512":
|
|
_hashAlgorithm = 6;
|
|
break;
|
|
default:
|
|
throw new Error(`Incorrect data for hashAlgorithm: ${this.hashAlgorithm}`);
|
|
}
|
|
|
|
stream.appendChar(_hashAlgorithm);
|
|
|
|
let _signatureAlgorithm;
|
|
|
|
switch(this.signatureAlgorithm.toLowerCase())
|
|
{
|
|
case "anonymous":
|
|
_signatureAlgorithm = 0;
|
|
break;
|
|
case "rsa":
|
|
_signatureAlgorithm = 1;
|
|
break;
|
|
case "dsa":
|
|
_signatureAlgorithm = 2;
|
|
break;
|
|
case "ecdsa":
|
|
_signatureAlgorithm = 3;
|
|
break;
|
|
default:
|
|
throw new Error(`Incorrect data for signatureAlgorithm: ${this.signatureAlgorithm}`);
|
|
}
|
|
|
|
stream.appendChar(_signatureAlgorithm);
|
|
|
|
const _signature = this.signature.toBER(false);
|
|
|
|
stream.appendUint16(_signature.byteLength);
|
|
stream.appendView(new Uint8Array(_signature));
|
|
|
|
return stream;
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Convertion for the class to JSON object
|
|
* @returns {Object}
|
|
*/
|
|
toJSON()
|
|
{
|
|
return {
|
|
version: this.version,
|
|
logID: bufferToHexCodes(this.logID),
|
|
timestamp: this.timestamp,
|
|
extensions: bufferToHexCodes(this.extensions),
|
|
hashAlgorithm: this.hashAlgorithm,
|
|
signatureAlgorithm: this.signatureAlgorithm,
|
|
signature: this.signature.toJSON()
|
|
};
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Verify SignedCertificateTimestamp for specific input data
|
|
* @param {Object[]} logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json)
|
|
* @param {String} logs.log_id Identifier of the CT Log encoded in BASE-64 format
|
|
* @param {String} logs.key Public key of the CT Log encoded in BASE-64 format
|
|
* @param {ArrayBuffer} data Data to verify signature against. Could be encoded Certificate or encoded PreCert
|
|
* @param {Number} [dataType=0] Type = 0 (data is encoded Certificate), type = 1 (data is encoded PreCert)
|
|
* @return {Promise<void>}
|
|
*/
|
|
async verify(logs, data, dataType = 0)
|
|
{
|
|
//region Initial variables
|
|
let logId = toBase64(arrayBufferToString(this.logID));
|
|
|
|
let publicKeyBase64 = null;
|
|
let publicKeyInfo;
|
|
|
|
let stream = new SeqStream();
|
|
//endregion
|
|
|
|
//region Found and init public key
|
|
for(const log of logs)
|
|
{
|
|
if(log.log_id === logId)
|
|
{
|
|
publicKeyBase64 = log.key;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(publicKeyBase64 === null)
|
|
throw new Error(`Public key not found for CT with logId: ${logId}`);
|
|
|
|
const asn1 = asn1js.fromBER(stringToArrayBuffer(fromBase64(publicKeyBase64)));
|
|
if(asn1.offset === (-1))
|
|
throw new Error(`Incorrect key value for CT Log with logId: ${logId}`);
|
|
|
|
publicKeyInfo = new PublicKeyInfo({ schema: asn1.result });
|
|
//endregion
|
|
|
|
//region Initialize signed data block
|
|
stream.appendChar(0x00); // sct_version
|
|
stream.appendChar(0x00); // signature_type = certificate_timestamp
|
|
|
|
const timeBuffer = new ArrayBuffer(8);
|
|
const timeView = new Uint8Array(timeBuffer);
|
|
|
|
const baseArray = utilToBase(this.timestamp.valueOf(), 8);
|
|
timeView.set(new Uint8Array(baseArray), 8 - baseArray.byteLength);
|
|
|
|
stream.appendView(timeView);
|
|
|
|
stream.appendUint16(dataType);
|
|
|
|
if(dataType === 0)
|
|
stream.appendUint24(data.byteLength);
|
|
|
|
stream.appendView(new Uint8Array(data));
|
|
|
|
stream.appendUint16(this.extensions.byteLength);
|
|
|
|
if(this.extensions.byteLength !== 0)
|
|
stream.appendView(new Uint8Array(this.extensions));
|
|
//endregion
|
|
|
|
//region Perform verification
|
|
return getEngine().subtle.verifyWithPublicKey(
|
|
stream._stream._buffer.slice(0, stream._length),
|
|
{ valueBlock: { valueHex: this.signature.toBER(false) } },
|
|
publicKeyInfo,
|
|
{ algorithmId: "" },
|
|
"SHA-256"
|
|
);
|
|
//endregion
|
|
}
|
|
//**********************************************************************************
|
|
}
|
|
//**************************************************************************************
|
|
/**
|
|
* Class from RFC6962
|
|
*/
|
|
export default class SignedCertificateTimestampList
|
|
{
|
|
//**********************************************************************************
|
|
/**
|
|
* Constructor for SignedCertificateTimestampList 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 {Array.<SignedCertificateTimestamp>}
|
|
* @desc timestamps
|
|
*/
|
|
this.timestamps = getParametersValue(parameters, "timestamps", SignedCertificateTimestampList.defaultValues("timestamps"));
|
|
//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 "timestamps":
|
|
return [];
|
|
default:
|
|
throw new Error(`Invalid member name for SignedCertificateTimestampList 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 "timestamps":
|
|
return (memberValue.length === 0);
|
|
default:
|
|
throw new Error(`Invalid member name for SignedCertificateTimestampList class: ${memberName}`);
|
|
}
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Return value of pre-defined ASN.1 schema for current class
|
|
*
|
|
* ASN.1 schema:
|
|
* ```asn1
|
|
* SignedCertificateTimestampList ::= OCTET STRING
|
|
* ```
|
|
*
|
|
* @param {Object} parameters Input parameters for the schema
|
|
* @returns {Object} asn1js schema object
|
|
*/
|
|
static schema(parameters = {})
|
|
{
|
|
/**
|
|
* @type {Object}
|
|
* @property {string} [blockName]
|
|
* @property {string} [optional]
|
|
*/
|
|
const names = getParametersValue(parameters, "names", {});
|
|
|
|
if(("optional" in names) === false)
|
|
names.optional = false;
|
|
|
|
return (new asn1js.OctetString({
|
|
name: (names.blockName || "SignedCertificateTimestampList"),
|
|
optional: names.optional
|
|
}));
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Convert parsed asn1js object into current class
|
|
* @param {!Object} schema
|
|
*/
|
|
fromSchema(schema)
|
|
{
|
|
//region Check the schema is valid
|
|
if((schema instanceof asn1js.OctetString) === false)
|
|
throw new Error("Object's schema was not verified against input data for SignedCertificateTimestampList");
|
|
//endregion
|
|
|
|
//region Get internal properties from parsed schema
|
|
const seqStream = new SeqStream({
|
|
stream: new ByteStream({
|
|
buffer: schema.valueBlock.valueHex
|
|
})
|
|
});
|
|
|
|
let dataLength = seqStream.getUint16();
|
|
if(dataLength !== seqStream.length)
|
|
throw new Error("Object's schema was not verified against input data for SignedCertificateTimestampList");
|
|
|
|
while(seqStream.length)
|
|
this.timestamps.push(new SignedCertificateTimestamp({ stream: seqStream }));
|
|
//endregion
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Convert current object to asn1js object and set correct values
|
|
* @returns {Object} asn1js object
|
|
*/
|
|
toSchema()
|
|
{
|
|
//region Initial variables
|
|
const stream = new SeqStream();
|
|
|
|
let overallLength = 0;
|
|
|
|
const timestampsData = [];
|
|
//endregion
|
|
|
|
//region Get overall length
|
|
for(const timestamp of this.timestamps)
|
|
{
|
|
const timestampStream = timestamp.toStream();
|
|
timestampsData.push(timestampStream);
|
|
overallLength += timestampStream.stream.buffer.byteLength;
|
|
}
|
|
//endregion
|
|
|
|
stream.appendUint16(overallLength);
|
|
|
|
//region Set data from all timestamps
|
|
for(const timestamp of timestampsData)
|
|
stream.appendView(timestamp.stream.view);
|
|
//endregion
|
|
|
|
return new asn1js.OctetString({ valueHex: stream.stream.buffer.slice(0) });
|
|
}
|
|
//**********************************************************************************
|
|
/**
|
|
* Convertion for the class to JSON object
|
|
* @returns {Object}
|
|
*/
|
|
toJSON()
|
|
{
|
|
return {
|
|
timestamps: Array.from(this.timestamps, element => element.toJSON())
|
|
};
|
|
}
|
|
//**********************************************************************************
|
|
}
|
|
//**************************************************************************************
|
|
/**
|
|
* Verify SignedCertificateTimestamp for specific certificate content
|
|
* @param {Certificate} certificate Certificate for which verification would be performed
|
|
* @param {Certificate} issuerCertificate Certificate of the issuer of target certificate
|
|
* @param {Object[]} logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json)
|
|
* @param {String} logs.log_id Identifier of the CT Log encoded in BASE-64 format
|
|
* @param {String} logs.key Public key of the CT Log encoded in BASE-64 format
|
|
* @param {Number} [index=-1] Index of SignedCertificateTimestamp inside SignedCertificateTimestampList (for -1 would verify all)
|
|
* @return {Array} Array of verification results
|
|
*/
|
|
export async function verifySCTsForCertificate(certificate, issuerCertificate, logs, index = (-1))
|
|
{
|
|
//region Initial variables
|
|
let parsedValue = null;
|
|
let tbs;
|
|
let issuerId;
|
|
|
|
const stream = new SeqStream();
|
|
|
|
let preCert;
|
|
//endregion
|
|
|
|
//region Get a "crypto" extension
|
|
const crypto = getCrypto();
|
|
if(typeof crypto === "undefined")
|
|
return Promise.reject("Unable to create WebCrypto object");
|
|
//endregion
|
|
|
|
//region Remove certificate extension
|
|
for(let i = 0; i < certificate.extensions.length; i++)
|
|
{
|
|
switch(certificate.extensions[i].extnID)
|
|
{
|
|
case "1.3.6.1.4.1.11129.2.4.2":
|
|
{
|
|
parsedValue = certificate.extensions[i].parsedValue;
|
|
|
|
if(parsedValue.timestamps.length === 0)
|
|
throw new Error("Nothing to verify in the certificate");
|
|
|
|
certificate.extensions.splice(i, 1);
|
|
}
|
|
break;
|
|
default:
|
|
}
|
|
}
|
|
//endregion
|
|
|
|
//region Check we do have what to verify
|
|
if(parsedValue === null)
|
|
throw new Error("No SignedCertificateTimestampList extension in the specified certificate");
|
|
//endregion
|
|
|
|
//region Prepare modifier TBS value
|
|
tbs = certificate.encodeTBS().toBER(false);
|
|
//endregion
|
|
|
|
//region Initialize "issuer_key_hash" value
|
|
issuerId = await crypto.digest({ name: "SHA-256" }, new Uint8Array(issuerCertificate.subjectPublicKeyInfo.toSchema().toBER(false)));
|
|
//endregion
|
|
|
|
//region Make final "PreCert" value
|
|
stream.appendView(new Uint8Array(issuerId));
|
|
stream.appendUint24(tbs.byteLength);
|
|
stream.appendView(new Uint8Array(tbs));
|
|
|
|
preCert = stream._stream._buffer.slice(0, stream._length);
|
|
//endregion
|
|
|
|
//region Call verification function for specified index
|
|
if(index === (-1))
|
|
{
|
|
const verifyArray = [];
|
|
|
|
for(const timestamp of parsedValue.timestamps)
|
|
{
|
|
const verifyResult = await timestamp.verify(logs, preCert, 1);
|
|
verifyArray.push(verifyResult);
|
|
}
|
|
|
|
return verifyArray;
|
|
}
|
|
|
|
if(index >= parsedValue.timestamps.length)
|
|
index = (parsedValue.timestamps.length - 1);
|
|
|
|
return [await parsedValue.timestamps[index].verify(logs, preCert, 1)];
|
|
//endregion
|
|
}
|
|
//**********************************************************************************
|