480 lines
15 KiB
TypeScript
480 lines
15 KiB
TypeScript
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
|
|
}
|
|
|