diff options
Diffstat (limited to 'third_party/js/PKI.js/src/SignedCertificateTimestamp.ts')
-rw-r--r-- | third_party/js/PKI.js/src/SignedCertificateTimestamp.ts | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/third_party/js/PKI.js/src/SignedCertificateTimestamp.ts b/third_party/js/PKI.js/src/SignedCertificateTimestamp.ts new file mode 100644 index 0000000000..fa9e01b5d1 --- /dev/null +++ b/third_party/js/PKI.js/src/SignedCertificateTimestamp.ts @@ -0,0 +1,480 @@ +import * as asn1js from "asn1js"; +import * as pvutils from "pvutils"; +import * as bs from "bytestreamjs"; +import * as common from "./common"; +import { PublicKeyInfo } from "./PublicKeyInfo"; +import * as Schema from "./Schema"; +import { AlgorithmIdentifier } from "./AlgorithmIdentifier"; +import { Certificate } from "./Certificate"; +import { AsnError } from "./errors"; +import { PkiObject, PkiObjectParameters } from "./PkiObject"; +import { EMPTY_BUFFER, EMPTY_STRING } from "./constants"; +import { SignedCertificateTimestampList } from "./SignedCertificateTimestampList"; +import { id_SignedCertificateTimestampList } from "./ObjectIdentifiers"; + +const VERSION = "version"; +const LOG_ID = "logID"; +const EXTENSIONS = "extensions"; +const TIMESTAMP = "timestamp"; +const HASH_ALGORITHM = "hashAlgorithm"; +const SIGNATURE_ALGORITHM = "signatureAlgorithm"; +const SIGNATURE = "signature"; + +const NONE = "none"; +const MD5 = "md5"; +const SHA1 = "sha1"; +const SHA224 = "sha224"; +const SHA256 = "sha256"; +const SHA384 = "sha384"; +const SHA512 = "sha512"; +const ANONYMOUS = "anonymous"; +const RSA = "rsa"; +const DSA = "dsa"; +const ECDSA = "ecdsa"; + +export interface ISignedCertificateTimestamp { + version: number; + logID: ArrayBuffer; + timestamp: Date; + extensions: ArrayBuffer; + hashAlgorithm: string; + signatureAlgorithm: string; + signature: Schema.SchemaType; +} + +export interface SignedCertificateTimestampJson { + version: number; + logID: string; + timestamp: Date; + extensions: string; + hashAlgorithm: string; + signatureAlgorithm: string; + signature: Schema.SchemaType; +} + +export type SignedCertificateTimestampParameters = PkiObjectParameters & Partial<ISignedCertificateTimestamp> & { stream?: bs.SeqStream; }; + +export interface Log { + /** + * Identifier of the CT Log encoded in BASE-64 format + */ + log_id: string; + /** + * Public key of the CT Log encoded in BASE-64 format + */ + key: string; +} + +export class SignedCertificateTimestamp extends PkiObject implements ISignedCertificateTimestamp { + + public static override CLASS_NAME = "SignedCertificateTimestamp"; + + public version!: number; + public logID!: ArrayBuffer; + public timestamp!: Date; + public extensions!: ArrayBuffer; + public hashAlgorithm!: string; + public signatureAlgorithm!: string; + public signature: asn1js.BaseBlock; + + /** + * Initializes a new instance of the {@link SignedCertificateTimestamp} class + * @param parameters Initialization parameters + */ + constructor(parameters: SignedCertificateTimestampParameters = {}) { + super(); + + this.version = pvutils.getParametersValue(parameters, VERSION, SignedCertificateTimestamp.defaultValues(VERSION)); + this.logID = pvutils.getParametersValue(parameters, LOG_ID, SignedCertificateTimestamp.defaultValues(LOG_ID)); + this.timestamp = pvutils.getParametersValue(parameters, TIMESTAMP, SignedCertificateTimestamp.defaultValues(TIMESTAMP)); + this.extensions = pvutils.getParametersValue(parameters, EXTENSIONS, SignedCertificateTimestamp.defaultValues(EXTENSIONS)); + this.hashAlgorithm = pvutils.getParametersValue(parameters, HASH_ALGORITHM, SignedCertificateTimestamp.defaultValues(HASH_ALGORITHM)); + this.signatureAlgorithm = pvutils.getParametersValue(parameters, SIGNATURE_ALGORITHM, SignedCertificateTimestamp.defaultValues(SIGNATURE_ALGORITHM)); + this.signature = pvutils.getParametersValue(parameters, SIGNATURE, SignedCertificateTimestamp.defaultValues(SIGNATURE)); + + if ("stream" in parameters && parameters.stream) { + this.fromStream(parameters.stream); + } + + if (parameters.schema) { + this.fromSchema(parameters.schema); + } + } + + /** + * Returns default values for all class members + * @param memberName String name for a class member + * @returns Default value + */ + public static override defaultValues(memberName: typeof VERSION): number; + public static override defaultValues(memberName: typeof LOG_ID): ArrayBuffer; + public static override defaultValues(memberName: typeof EXTENSIONS): ArrayBuffer; + public static override defaultValues(memberName: typeof TIMESTAMP): Date; + public static override defaultValues(memberName: typeof HASH_ALGORITHM): string; + public static override defaultValues(memberName: typeof SIGNATURE_ALGORITHM): string; + public static override defaultValues(memberName: typeof SIGNATURE): Schema.SchemaType; + public static override defaultValues(memberName: string): any { + switch (memberName) { + case VERSION: + return 0; + case LOG_ID: + case EXTENSIONS: + return EMPTY_BUFFER; + case TIMESTAMP: + return new Date(0); + case HASH_ALGORITHM: + case SIGNATURE_ALGORITHM: + return EMPTY_STRING; + case SIGNATURE: + return new asn1js.Any(); + default: + return super.defaultValues(memberName); + } + } + + public fromSchema(schema: Schema.SchemaType): void { + if ((schema instanceof asn1js.RawData) === false) + throw new Error("Object's schema was not verified against input data for SignedCertificateTimestamp"); + + const seqStream = new bs.SeqStream({ + stream: new bs.ByteStream({ + buffer: schema.data + }) + }); + + this.fromStream(seqStream); + } + + /** + * Converts SeqStream data into current class + * @param stream + */ + public fromStream(stream: bs.SeqStream): void { + 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(pvutils.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); + AsnError.assert(asn1, "SignedCertificateTimestamp"); + this.signature = asn1.result; + //#endregion + + if (blockLength !== (47 + extensionsLength + signatureLength)) { + throw new Error("Object's stream was not correct for SignedCertificateTimestamp"); + } + } + } + + public toSchema(): asn1js.RawData { + const stream = this.toStream(); + + return new asn1js.RawData({ data: stream.stream.buffer }); + } + + /** + * Converts current object to SeqStream data + * @returns SeqStream object + */ + public toStream(): bs.SeqStream { + const stream = new bs.SeqStream(); + + stream.appendUint16(47 + this.extensions.byteLength + this.signature.valueBeforeDecodeView.byteLength); + stream.appendChar(this.version); + stream.appendView(new Uint8Array(this.logID)); + + const timeBuffer = new ArrayBuffer(8); + const timeView = new Uint8Array(timeBuffer); + + const baseArray = pvutils.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; + } + + public toJSON(): SignedCertificateTimestampJson { + return { + version: this.version, + logID: pvutils.bufferToHexCodes(this.logID), + timestamp: this.timestamp, + extensions: pvutils.bufferToHexCodes(this.extensions), + hashAlgorithm: this.hashAlgorithm, + signatureAlgorithm: this.signatureAlgorithm, + signature: this.signature.toJSON() + }; + } + + /** + * Verify SignedCertificateTimestamp for specific input data + * @param logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json) + * @param data Data to verify signature against. Could be encoded Certificate or encoded PreCert + * @param dataType Type = 0 (data is encoded Certificate), type = 1 (data is encoded PreCert) + * @param crypto Crypto engine + */ + async verify(logs: Log[], data: ArrayBuffer, dataType = 0, crypto = common.getCrypto(true)): Promise<boolean> { + //#region Initial variables + const logId = pvutils.toBase64(pvutils.arrayBufferToString(this.logID)); + + let publicKeyBase64 = null; + + const stream = new bs.SeqStream(); + //#endregion + + //#region Found and init public key + for (const log of logs) { + if (log.log_id === logId) { + publicKeyBase64 = log.key; + break; + } + } + + if (!publicKeyBase64) { + throw new Error(`Public key not found for CT with logId: ${logId}`); + } + + const pki = pvutils.stringToArrayBuffer(pvutils.fromBase64(publicKeyBase64)); + const publicKeyInfo = PublicKeyInfo.fromBER(pki); + //#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 = pvutils.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 crypto.verifyWithPublicKey( + stream.buffer.slice(0, stream.length), + new asn1js.OctetString({ valueHex: this.signature.toBER(false) }), + publicKeyInfo, + { algorithmId: EMPTY_STRING } as AlgorithmIdentifier, + "SHA-256" + ); + //#endregion + } + +} + +export interface Log { + /** + * Identifier of the CT Log encoded in BASE-64 format + */ + log_id: string; + /** + * Public key of the CT Log encoded in BASE-64 format + */ + key: string; +} + +/** + * Verify SignedCertificateTimestamp for specific certificate content + * @param certificate Certificate for which verification would be performed + * @param issuerCertificate Certificate of the issuer of target certificate + * @param logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json) + * @param index Index of SignedCertificateTimestamp inside SignedCertificateTimestampList (for -1 would verify all) + * @param crypto Crypto engine + * @return Array of verification results + */ +export async function verifySCTsForCertificate(certificate: Certificate, issuerCertificate: Certificate, logs: Log[], index = (-1), crypto = common.getCrypto(true)) { + let parsedValue: SignedCertificateTimestampList | null = null; + + const stream = new bs.SeqStream(); + + //#region Remove certificate extension + for (let i = 0; certificate.extensions && i < certificate.extensions.length; i++) { + switch (certificate.extensions[i].extnID) { + case id_SignedCertificateTimestampList: + { + parsedValue = certificate.extensions[i].parsedValue; + + if (!parsedValue || 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 + const tbs = certificate.encodeTBS().toBER(); + //#endregion + + //#region Initialize "issuer_key_hash" value + const 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)); + + const preCert = stream.stream.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.buffer, 1, crypto); + verifyArray.push(verifyResult); + } + + return verifyArray; + } + + if (index >= parsedValue.timestamps.length) + index = (parsedValue.timestamps.length - 1); + + return [await parsedValue.timestamps[index].verify(logs, preCert.buffer, 1, crypto)]; + //#endregion +} + |