├── README.md ├── .gitignore ├── lib ├── helpers │ ├── interfaces │ │ ├── Api.ts │ │ ├── utils.ts │ │ ├── SignedKeyList.ts │ │ ├── Key.ts │ │ └── Address.ts │ ├── api │ │ ├── interface.ts │ │ ├── keys.ts │ │ ├── keyTransparency.ts │ │ └── canonicalEmailMap.ts │ ├── encoding.ts │ └── storage.ts ├── typings.d.ts ├── index.ts ├── constants.ts ├── interfaces.ts ├── merkleTree.ts ├── vrf.ts ├── fetchHelper.ts ├── certTransparency.ts ├── utils.ts ├── keyTransparency.ts └── certificates.ts ├── tsconfig.json ├── test ├── index.spec.js ├── setup.js ├── karma.conf.js ├── keyTransparency.spec.js ├── certTransparency.spec.js ├── merkleTree.spec.js └── keyTransparency.data.js ├── .prettierrc ├── .eslintrc.json ├── tsconfig.base.json ├── LICENSE └── package.json /README.md: -------------------------------------------------------------------------------- 1 | # key-transparency-web-client 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .eslintcache -------------------------------------------------------------------------------- /lib/helpers/interfaces/Api.ts: -------------------------------------------------------------------------------- 1 | export type Api = (arg: object) => Promise; 2 | -------------------------------------------------------------------------------- /lib/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'pkijs/src/SignedCertificateTimestampList'; 2 | -------------------------------------------------------------------------------- /lib/helpers/interfaces/utils.ts: -------------------------------------------------------------------------------- 1 | export type SimpleMap = { [key: string]: T | undefined }; 2 | -------------------------------------------------------------------------------- /lib/helpers/api/interface.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationParams { 2 | Page: number; 3 | PageSize: number; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "maxNodeModuleJsDepth": 0 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import './setup'; 2 | 3 | const testsContext = require.context('.', true, /.spec.(js|tsx?)$/); 4 | testsContext.keys().forEach(testsContext); 5 | -------------------------------------------------------------------------------- /lib/helpers/api/keys.ts: -------------------------------------------------------------------------------- 1 | export const getSignedKeyLists = (params: { SinceEpochID: number; Email: string }) => ({ 2 | url: 'keys/signedkeylists', 3 | method: 'get', 4 | params, 5 | }); 6 | -------------------------------------------------------------------------------- /lib/helpers/interfaces/SignedKeyList.ts: -------------------------------------------------------------------------------- 1 | export interface SignedKeyList { 2 | Data: string; 3 | Signature: string; 4 | } 5 | 6 | export interface SignedKeyListEpochs extends SignedKeyList { 7 | MinEpochID?: number | null; 8 | MaxEpochID?: number | null; 9 | } 10 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export { ktSelfAudit, verifySelfAuditResult, verifyPublicKeys, ktSaveToLS } from './keyTransparency'; 2 | export { KT_STATUS, MAX_EPOCH_INTERVAL, EXP_EPOCH_INTERVAL, KTError } from './constants'; 3 | export { Epoch, EpochExtended, KTInfo, KTInfoSelfAudit, KTInfoToLS } from './interfaces'; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "arrowParens": "always", 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "proseWrap": "never", 7 | "overrides": [ 8 | { 9 | "files": "*.scss", 10 | "options": { 11 | "useTabs": true 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "proton-lint" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": 12, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint" 16 | ], 17 | "rules": { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/helpers/encoding.ts: -------------------------------------------------------------------------------- 1 | const decodeBase64 = (input: string) => atob(input); 2 | 3 | export const stringToUint8Array = (str: string) => { 4 | const result = new Uint8Array(str.length); 5 | for (let i = 0; i < str.length; i++) { 6 | result[i] = str.charCodeAt(i); 7 | } 8 | return result; 9 | }; 10 | 11 | export const base64StringToUint8Array = (string: string) => stringToUint8Array(decodeBase64(string) || ''); 12 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { init } from 'pmcrypto'; 2 | import * as openpgp from 'openpgp'; 3 | 4 | const staticRandom = new Uint32Array(255); 5 | for (let i = 0; i < 255; ++i) { 6 | staticRandom[i] = i; 7 | } 8 | 9 | const mockRandomValues = (buf) => { 10 | for (let i = 0; i < buf.length; ++i) { 11 | // eslint-disable-next-line 12 | buf[i] = staticRandom[i]; 13 | } 14 | return buf; 15 | }; 16 | 17 | window.crypto.getRandomValues = mockRandomValues; 18 | 19 | init(openpgp); 20 | -------------------------------------------------------------------------------- /lib/helpers/interfaces/Key.ts: -------------------------------------------------------------------------------- 1 | import { OpenPGPKey } from 'pmcrypto'; 2 | 3 | export interface Key { 4 | ID: string; 5 | Primary: 1 | 0; 6 | Flags?: number; // undefined for user keys 7 | Fingerprint: string; 8 | Fingerprints: string[]; 9 | PublicKey: string; // armored key 10 | Version: number; 11 | Activation?: string; 12 | PrivateKey: string; // armored key 13 | Token?: string; 14 | Signature: string; 15 | } 16 | 17 | export interface KeyPair { 18 | privateKey: OpenPGPKey; 19 | publicKey: OpenPGPKey; 20 | } 21 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const MAX_EPOCH_INTERVAL = 24 * 60 * 60 * 1000; 2 | export const EXP_EPOCH_INTERVAL = 4 * 60 * 60 * 1000; 3 | 4 | export enum KT_STATUS { 5 | KT_FAILED, 6 | KT_PASSED, 7 | KT_WARNING, 8 | KTERROR_ADDRESS_NOT_IN_KT, 9 | KTERROR_MINEPOCHID_NULL, 10 | } 11 | 12 | export const vrfHexKey = '2d7688feb429f714f102f758412cd4b81337b307122770f620ad9e4ac898a2eb'; 13 | 14 | export class KTError extends Error { 15 | constructor(message: string) { 16 | super(message); 17 | this.name = 'KTError'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "esModuleInterop": true, 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "noUnusedLocals": true, 13 | "noEmit": true, 14 | "jsx": "preserve", 15 | "allowJs": true, 16 | "maxNodeModuleJsDepth": 10 17 | }, 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | -------------------------------------------------------------------------------- /lib/helpers/interfaces/Address.ts: -------------------------------------------------------------------------------- 1 | import { Key } from './Key'; 2 | import { SignedKeyListEpochs } from './SignedKeyList'; 3 | 4 | enum ADDRESS_TYPE { 5 | TYPE_ORIGINAL = 1, 6 | TYPE_ALIAS = 2, 7 | TYPE_CUSTOM_DOMAIN = 3, 8 | TYPE_PREMIUM = 4, 9 | TYPE_EXTERNAL = 5, 10 | } 11 | 12 | export interface Address { 13 | DisplayName: string; 14 | DomainID: string; 15 | Email: string; 16 | HasKeys: number; 17 | ID: string; 18 | Keys: Key[]; 19 | SignedKeyList: SignedKeyListEpochs | null; 20 | Order: number; 21 | Priority: number; 22 | Receive: number; 23 | Send: number; 24 | Signature: string; 25 | Status: number; 26 | Type: ADDRESS_TYPE; 27 | } 28 | -------------------------------------------------------------------------------- /lib/helpers/storage.ts: -------------------------------------------------------------------------------- 1 | export const getItem = (key: string, defaultValue?: string) => { 2 | try { 3 | const value = window.localStorage.getItem(key); 4 | return value === undefined ? defaultValue : value; 5 | } catch (e) { 6 | return defaultValue; 7 | } 8 | }; 9 | 10 | export const setItem = (key: string, value: string) => { 11 | try { 12 | window.localStorage.setItem(key, value); 13 | } catch (e) { 14 | return undefined; 15 | } 16 | }; 17 | 18 | export const removeItem = (key: string) => { 19 | try { 20 | window.localStorage.removeItem(key); 21 | } catch (e) { 22 | return undefined; 23 | } 24 | }; 25 | 26 | export const hasStorage = (key = 'test') => { 27 | try { 28 | window.localStorage.setItem(key, key); 29 | window.localStorage.removeItem(key); 30 | return true; 31 | } catch (e) { 32 | return false; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /lib/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { KT_STATUS } from './constants'; 2 | 3 | export interface Epoch { 4 | EpochID: number; 5 | TreeHash: string; 6 | ChainHash: string; 7 | PrevChainHash: string; 8 | Certificate: string; 9 | IssuerKeyHash: string; 10 | } 11 | 12 | export interface EpochExtended extends Epoch { 13 | Revision: number; 14 | CertificateDate: number; 15 | } 16 | 17 | export interface KeyInfo { 18 | Fingerprint: string; 19 | SHA256Fingerprints: string[]; 20 | Primary: number; 21 | Flags: number; 22 | } 23 | 24 | export interface Proof { 25 | Neighbors: string[]; 26 | Proof: string; 27 | Revision: number; 28 | Name: string; 29 | } 30 | 31 | export interface KTInfo { 32 | code: KT_STATUS; 33 | error: string; 34 | } 35 | 36 | export interface KTInfoSelfAudit extends KTInfo { 37 | verifiedEpoch?: EpochExtended; 38 | } 39 | 40 | export interface KTInfoToLS { 41 | message: string; 42 | addressID: string; 43 | } 44 | -------------------------------------------------------------------------------- /lib/helpers/api/keyTransparency.ts: -------------------------------------------------------------------------------- 1 | export const getEpochs = (params: { SinceEpochID?: number; Email?: string }) => ({ 2 | url: 'kt/epochs', 3 | method: 'get', 4 | params, 5 | }); 6 | 7 | export const getCertificate = ({ EpochID }: { EpochID: number }) => ({ 8 | url: `kt/epochs/${EpochID}`, 9 | method: 'get', 10 | }); 11 | 12 | export const getProof = ({ EpochID, Email }: { EpochID: number; Email: string }) => ({ 13 | url: `kt/epochs/${EpochID}/proof/${Email}`, 14 | method: 'get', 15 | }); 16 | 17 | export const getLatestVerifiedEpoch = ({ AddressID }: { AddressID: string }) => ({ 18 | url: `kt/verifiedepoch/${AddressID}`, 19 | method: 'get', 20 | }); 21 | 22 | export const uploadVerifiedEpoch = ({ 23 | AddressID, 24 | Data, 25 | Signature, 26 | }: { 27 | AddressID: string; 28 | Data: string; 29 | Signature: string; 30 | }) => ({ 31 | url: `kt/verifiedepoch/${AddressID}`, 32 | method: 'put', 33 | data: { 34 | Data, 35 | Signature, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2013-2019 Proton Technologies A.G. (Switzerland) Email: contact@protonmail.ch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/helpers/api/canonicalEmailMap.ts: -------------------------------------------------------------------------------- 1 | import { Api } from '../interfaces/Api'; 2 | import { SimpleMap } from '../interfaces/utils'; 3 | 4 | enum API_CODES { 5 | GLOBAL_SUCCESS = 1001, 6 | SINGLE_SUCCESS = 1000, 7 | } 8 | 9 | const getCanonicalAddresses = (Emails: string[]) => ({ 10 | // params doesn't work correctly so 11 | url: `addresses/canonical?${Emails.map((email) => `Emails[]=${email}`).join('&')}`, 12 | method: 'get', 13 | // params: { Emails }, 14 | }); 15 | 16 | interface GetCanonicalAddressesResponses { 17 | Email: string; 18 | Response: { 19 | Code: number; 20 | CanonicalEmail: string; 21 | }; 22 | } 23 | 24 | interface GetCanonicalAddressesResponse { 25 | Code: number; 26 | Responses: GetCanonicalAddressesResponses[]; 27 | } 28 | 29 | export const getCanonicalEmailMap = async (emails: string[] = [], api: Api) => { 30 | const map: SimpleMap = {}; 31 | if (emails.length) { 32 | const encodedEmails = emails.map((email) => encodeURIComponent(email)); 33 | const { Responses, Code } = await api(getCanonicalAddresses(encodedEmails)); 34 | if (Code !== API_CODES.GLOBAL_SUCCESS) { 35 | throw new Error('Canonize operation failed'); 36 | } 37 | Responses.forEach(({ Email, Response: { Code, CanonicalEmail } }) => { 38 | if (Code !== API_CODES.SINGLE_SUCCESS) { 39 | throw new Error('Canonize operation failed'); 40 | } 41 | map[Email] = CanonicalEmail; 42 | }); 43 | } 44 | return map; 45 | }; 46 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = (config) => { 2 | config.set({ 3 | basePath: '..', 4 | frameworks: ['jasmine'], 5 | files: ['test/index.spec.js'], 6 | preprocessors: { 7 | 'test/index.spec.js': ['webpack'], 8 | }, 9 | webpack: { 10 | mode: 'development', 11 | resolve: { 12 | extensions: ['.js', '.ts', '.tsx'], 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: [ 19 | { 20 | loader: 'ts-loader', 21 | options: { transpileOnly: true }, 22 | }, 23 | ], 24 | exclude: /node_modules/, 25 | }, 26 | ], 27 | }, 28 | devtool: 'inline-source-map', 29 | }, 30 | webpackMiddleware: { 31 | stats: 'minimal', 32 | }, 33 | mime: { 34 | 'text/x-typescript': ['ts', 'tsx'], 35 | }, 36 | reporters: ['progress'], 37 | port: 9876, 38 | colors: true, 39 | logLevel: config.LOG_INFO, 40 | autoWatch: false, 41 | customLaunchers: { 42 | ChromeHeadlessCI: { 43 | base: 'ChromeHeadless', 44 | flags: ['--no-sandbox'], 45 | }, 46 | }, 47 | browsers: ['ChromeHeadlessCI'], 48 | singleRun: true, 49 | concurrency: Infinity, 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "key-transparency-web-client", 3 | "version": "1.0.0", 4 | "description": "Key Transparency Web Client", 5 | "main": "lib/index.ts", 6 | "scripts": { 7 | "test": "NODE_ENV=test karma start test/karma.conf.js", 8 | "lint": "eslint lib test --ext .js,.ts,tsx --quiet --cache", 9 | "pretty": "prettier --write $(find lib test -type f -name '*.js' -o -name '*.ts' -o -name '*.tsx')", 10 | "check-types": "tsc" 11 | }, 12 | "husky": { 13 | "hooks": { 14 | "pre-commit": "lint-staged" 15 | } 16 | }, 17 | "lint-staged": { 18 | "(*.ts|*.tsx|*.js)": [ 19 | "prettier --write", 20 | "eslint" 21 | ] 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/ProtonMail/key-transparency-web-client.git" 26 | }, 27 | "author": "ProtonMail", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/ProtonMail/key-transparency-web-client/issues" 31 | }, 32 | "homepage": "https://github.com/ProtonMail/key-transparency-web-client#readme", 33 | "dependencies": { 34 | "@types/elliptic": "^6.4.12", 35 | "@types/pkijs": "0.0.6", 36 | "elliptic": "^6.5.3", 37 | "pkijs": "^2.1.90", 38 | "pmcrypto": "github:ProtonMail/pmcrypto.git#semver:~6.3.0" 39 | }, 40 | "devDependencies": { 41 | "@types/jasmine": "^3.4.6", 42 | "eslint": "^7.3.1", 43 | "eslint-config-proton-lint": "github:ProtonMail/proton-lint#semver:^0.0.4", 44 | "husky": "^4.2.5", 45 | "jasmine": "3.5.0", 46 | "jasmine-core": "3.5.0", 47 | "karma": "^4.1.0", 48 | "karma-chrome-launcher": "^2.2.0", 49 | "karma-jasmine": "^2.0.1", 50 | "karma-webpack": "^4.0.2", 51 | "lint-staged": "^10.4.0", 52 | "prettier": "^2.0.5", 53 | "ts-loader": "^6.2.0", 54 | "typescript": "^4.0.3", 55 | "webpack": "^4.33.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/merkleTree.ts: -------------------------------------------------------------------------------- 1 | import { SHA256, arrayToHexString, concatArrays, binaryStringToArray } from 'pmcrypto'; 2 | import { vrfVerify } from './vrf'; 3 | import { vrfHexKey } from './constants'; 4 | import { Proof } from './interfaces'; 5 | 6 | const LEFT_N = 1; // left neighbor 7 | 8 | function hexStringToArray(hex: string): Uint8Array { 9 | const result = new Uint8Array(hex.length >> 1); 10 | for (let k = 0; k < hex.length >> 1; k++) { 11 | result[k] = parseInt(hex.substr(k << 1, 2), 16); 12 | } 13 | return result; 14 | } 15 | 16 | export async function verifyChainHash(TreeHash: string, PreviousChainHash: string, ChainHash: string) { 17 | if (ChainHash !== arrayToHexString(await SHA256(hexStringToArray(`${PreviousChainHash}${TreeHash}`)))) { 18 | throw new Error('Chain hash of fetched epoch is not consistent'); 19 | } 20 | } 21 | 22 | export async function verifyProof(proof: Proof, TreeHash: string, sklData: string, email: string) { 23 | // Verify proof 24 | const pkBuffer = Buffer.from(hexStringToArray(vrfHexKey)); 25 | const emailBuffer = Buffer.from(binaryStringToArray(email)); 26 | const proofBuffer = Buffer.from(hexStringToArray(proof.Proof)); 27 | const valueBuffer = Buffer.from(hexStringToArray(proof.Name)); 28 | 29 | try { 30 | await vrfVerify(pkBuffer, emailBuffer, proofBuffer, valueBuffer); 31 | } catch (err) { 32 | throw new Error(`VRF verification failed with error "${err.message}"`); 33 | } 34 | 35 | // Parse proof and verify epoch against proof 36 | let val = await SHA256( 37 | concatArrays([ 38 | await SHA256(binaryStringToArray(sklData)), 39 | new Uint8Array([proof.Revision >>> 24, proof.Revision >>> 16, proof.Revision >>> 8, proof.Revision]), 40 | ]) 41 | ); 42 | const emptyNode = new Uint8Array(32); 43 | const key = hexStringToArray(proof.Name); 44 | 45 | for (let i = proof.Neighbors.length - 1; i >= 0; i--) { 46 | const bit = (key[Math.floor(i / 8) % 32] >>> (8 - (i % 8) - 1)) & 1; 47 | const neighbor = proof.Neighbors[i] === null ? emptyNode : hexStringToArray(proof.Neighbors[i]); 48 | const toHash = bit === LEFT_N ? concatArrays([neighbor, val]) : concatArrays([val, neighbor]); 49 | val = await SHA256(toHash); 50 | } 51 | 52 | if (arrayToHexString(val) !== TreeHash) { 53 | throw new Error('Hash chain does not result in TreeHash'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/vrf.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import * as elliptic from 'elliptic'; 3 | import { SHA256, concatArrays } from 'pmcrypto'; 4 | 5 | type Point = elliptic.curve.base.BasePoint; 6 | /* eslint-disable new-cap */ 7 | const EDDSA = new elliptic.eddsa('ed25519'); 8 | /* eslint-enable new-cap */ 9 | const N2 = 32; 10 | const PROOF_SIZE = 48; 11 | const N = N2 / 2; 12 | const G = EDDSA.curve.g as Point; 13 | const LIMIT = 100; 14 | const CO_FACTOR = 8; 15 | 16 | function OS2ECP(os: Buffer) { 17 | try { 18 | return EDDSA.decodePoint(elliptic.utils.toArray(os, 16)) as Point; 19 | } catch (e) { 20 | return null; 21 | } 22 | } 23 | 24 | function S2OS(os: number[]) { 25 | const sign = os[31] >>> 7; 26 | os.unshift(sign + 2); 27 | return Buffer.from(os); 28 | } 29 | 30 | function ECP2OS(P: Point) { 31 | return S2OS([...EDDSA.encodePoint(P)]); 32 | } 33 | 34 | function OS2IP(os: Buffer) { 35 | return new BN(os); 36 | } 37 | 38 | function I2OSP(i: BN, len?: number) { 39 | return Buffer.from(i.toArray('be', len)); 40 | } 41 | 42 | function decodeProof(proof: Buffer) { 43 | let pos = 0; 44 | const sign = proof[pos++]; 45 | if (sign !== 2 && sign !== 3) { 46 | return; 47 | } 48 | const r = OS2ECP(proof.slice(pos, pos + N2)); 49 | if (!r) { 50 | return; 51 | } 52 | pos += N2; 53 | const c = proof.slice(pos, pos + N); 54 | pos += N; 55 | const s = proof.slice(pos, pos + N2); 56 | return { r, c: OS2IP(c), s: OS2IP(s) }; 57 | } 58 | 59 | async function hashToCurve(email: Buffer, publicKey: Buffer): Promise { 60 | for (let i = 0; i < LIMIT; i++) { 61 | const ctr = I2OSP(new BN(i), 4); 62 | const digest = Buffer.from( 63 | await SHA256(concatArrays([new Uint8Array(email), new Uint8Array(publicKey), new Uint8Array(ctr)])) 64 | ); 65 | 66 | let point = OS2ECP(digest); 67 | if (point) { 68 | for (let j = 1; j < CO_FACTOR; j *= 2) { 69 | point = point.add(point); 70 | } 71 | return point; 72 | } 73 | } 74 | return null; 75 | } 76 | 77 | async function hashPoints(...args: Point[]) { 78 | let hash = new Uint8Array(); 79 | for (let i = 0; i < args.length; i++) { 80 | hash = concatArrays([hash, new Uint8Array(ECP2OS(args[i]))]); 81 | } 82 | const digest = Buffer.from(await SHA256(hash)); 83 | return OS2IP(digest.slice(0, N)); 84 | } 85 | 86 | export async function vrfVerify(publicKey: Buffer, email: Buffer, proof: Buffer, value: Buffer) { 87 | if (proof.length !== N2 + PROOF_SIZE + 1 || value.length !== N2 || publicKey.length !== N2) { 88 | throw new Error('Length mismatch found'); 89 | } 90 | if (!value.equals(proof.slice(1, N2 + 1))) { 91 | throw new Error('Fetched name is different than name in proof'); 92 | } 93 | const o = decodeProof(proof); 94 | if (!o) { 95 | throw new Error('Proof decoding failed'); 96 | } 97 | const P1 = OS2ECP(publicKey); 98 | if (!P1) { 99 | throw new Error('VRF public key parsing failed'); 100 | } 101 | const u = P1.mul(o.c).add(G.mul(o.s)); 102 | const h = await hashToCurve(email, publicKey); 103 | if (!h) { 104 | throw new Error('Point generation failed'); 105 | } 106 | const v = o.r.mul(o.c).add(h.mul(o.s)); 107 | const c = await hashPoints(G, h, P1, o.r, u, v); 108 | if (!c.eq(o.c)) { 109 | throw new Error('Verification went through but failed'); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/keyTransparency.spec.js: -------------------------------------------------------------------------------- 1 | import { testEmail, keyList, skl, epoch, proof } from './keyTransparency.data'; 2 | import { verifyPublicKeys } from '../lib/keyTransparency'; 3 | import { KT_STATUS } from '../lib/constants'; 4 | 5 | describe('key transparency', () => { 6 | const mockAddress = { 7 | Responses: [ 8 | { 9 | Email: testEmail, 10 | Response: { Code: 1000, CanonicalEmail: testEmail }, 11 | }, 12 | ], 13 | Code: 1001, 14 | }; 15 | 16 | const mockApi = (returnedEpoch, returnedProof, returnedAddress) => (call) => { 17 | const splitCall = call.url.split('/'); 18 | if (splitCall[0] === 'addresses') { 19 | return returnedAddress; 20 | } 21 | if (splitCall[0] === 'kt') { 22 | if (splitCall.length > 3) { 23 | return returnedProof; 24 | } 25 | return returnedEpoch; 26 | } 27 | }; 28 | 29 | it('should verify public keys and fail when it checks the certificate returnedDate', async () => { 30 | const result = await verifyPublicKeys(keyList, testEmail, skl, mockApi(epoch, proof, mockAddress)); 31 | expect(result.code).toEqual(KT_STATUS.KT_FAILED); 32 | expect(result.error).toEqual('Returned date is older than MAX_EPOCH_INTERVAL'); 33 | }); 34 | 35 | it('should warn that public keys are too young to be verified', async () => { 36 | const result = await verifyPublicKeys( 37 | keyList, 38 | testEmail, 39 | { ...skl, MinEpochID: null, MaxEpochID: null }, 40 | mockApi(epoch, proof, mockAddress) 41 | ); 42 | expect(result.code).toEqual(KT_STATUS.KTERROR_MINEPOCHID_NULL); 43 | expect(result.error).toEqual('The keys were generated too recently to be included in key transparency'); 44 | }); 45 | 46 | it('should fail with undefined canonizeEmail', async () => { 47 | const corruptAddress = JSON.parse(JSON.stringify(mockAddress)); 48 | corruptAddress.Responses[0].Response.CanonicalEmail = undefined; 49 | 50 | const result = await verifyPublicKeys(keyList, testEmail, skl, mockApi(epoch, proof, corruptAddress)); 51 | expect(result.code).toEqual(KT_STATUS.KT_FAILED); 52 | expect(result.error).toEqual(`Failed to canonize email "${testEmail}"`); 53 | }); 54 | 55 | it('should fail with no signed key list given', async () => { 56 | const result = await verifyPublicKeys(keyList, testEmail, null, mockApi(epoch, proof, mockAddress)); 57 | expect(result.code).toEqual(KT_STATUS.KTERROR_ADDRESS_NOT_IN_KT); 58 | expect(result.error).toEqual('Signed key list undefined'); 59 | }); 60 | 61 | it('should fail signature verification', async () => { 62 | const result = await verifyPublicKeys( 63 | keyList, 64 | testEmail, 65 | { ...skl, Data: `${skl.Data.slice(0, 12)}3${skl.Data.slice(13)}` }, 66 | mockApi(epoch, proof, mockAddress) 67 | ); 68 | expect(result.code).toEqual(KT_STATUS.KT_FAILED); 69 | expect(result.error).toEqual('Signature verification failed (SKL during PK verification)'); 70 | }); 71 | 72 | it('should fail signed key list check', async () => { 73 | const result = await verifyPublicKeys([keyList[0]], testEmail, skl, mockApi(epoch, proof, mockAddress)); 74 | expect(result.code).toEqual(KT_STATUS.KT_FAILED); 75 | expect(result.error).toEqual( 76 | 'Mismatch found between key list and signed key list. Key list and signed key list have different lengths' 77 | ); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /lib/fetchHelper.ts: -------------------------------------------------------------------------------- 1 | import { getKeys, signMessage } from 'pmcrypto'; 2 | import { getSignedKeyLists } from './helpers/api/keys'; 3 | import { 4 | getCertificate, 5 | getEpochs, 6 | getLatestVerifiedEpoch, 7 | getProof, 8 | uploadVerifiedEpoch, 9 | } from './helpers/api/keyTransparency'; 10 | import { Address } from './helpers/interfaces/Address'; 11 | import { Api } from './helpers/interfaces/Api'; 12 | import { SignedKeyListEpochs } from './helpers/interfaces/SignedKeyList'; 13 | import { Epoch, EpochExtended, Proof } from './interfaces'; 14 | 15 | const cachedEpochs: Map = new Map(); 16 | 17 | export async function fetchLastEpoch(api: Api) { 18 | const epoch: { Code: number; Epochs: Epoch[] } = await api(getEpochs({})); 19 | if (epoch.Code === 1000) return epoch.Epochs[0].EpochID; 20 | throw new Error('Fetching last epoch failed'); 21 | } 22 | 23 | export async function fetchEpoch(epochID: number, api: Api) { 24 | const cachedEpoch = cachedEpochs.get(epochID); 25 | if (cachedEpoch) { 26 | return cachedEpoch; 27 | } 28 | 29 | const { Code: code, ...epoch } = await api(getCertificate({ EpochID: epochID })); 30 | if (code === 1000) { 31 | cachedEpochs.set(epochID, epoch as Epoch); 32 | return epoch as Epoch; 33 | } 34 | throw new Error(epoch.Error); 35 | } 36 | 37 | export async function fetchProof(epochID: number, email: string, api: Api) { 38 | const { Code: code, ...proof } = await api(getProof({ EpochID: epochID, Email: email })); 39 | if (code === 1000) return proof as Proof; 40 | throw new Error(proof.Error); 41 | } 42 | 43 | export async function getParsedSignedKeyLists( 44 | api: Api, 45 | epochID: number, 46 | email: string, 47 | includeLastExpired: boolean 48 | ): Promise { 49 | const { Code: code, ...fetchedSKLs } = await api(getSignedKeyLists({ SinceEpochID: epochID, Email: email })); 50 | /* 51 | fetchedSKLs.SignedKeyLists contains: 52 | - the last expired SKL, i.e. the newest SKL such that MinEpochID <= SinceEpochID 53 | - all SKLs such that MinEpochID > SinceEpochID 54 | - the latest SKL, i.e. such that MinEpochID is null 55 | in chronological order. 56 | */ 57 | if (code === 1000) return fetchedSKLs.SignedKeyLists.slice(includeLastExpired ? 0 : 1); 58 | throw new Error(fetchedSKLs.Error); 59 | } 60 | 61 | export async function getVerifiedEpoch( 62 | api: Api, 63 | addressID: string 64 | ): Promise<{ Data: string; Signature: string } | undefined> { 65 | let verifiedEpoch; 66 | let code; 67 | try { 68 | const { Code: c, ...vE } = await api(getLatestVerifiedEpoch({ AddressID: addressID })); 69 | code = c; 70 | verifiedEpoch = vE; 71 | } catch (err) { 72 | return; 73 | } 74 | 75 | if (code === 1000) return verifiedEpoch as { Data: string; Signature: string }; 76 | throw new Error(verifiedEpoch.Error); 77 | } 78 | 79 | export async function uploadEpoch(epoch: EpochExtended, address: Address, api: Api) { 80 | const bodyData = JSON.stringify({ 81 | EpochID: epoch.EpochID, 82 | ChainHash: epoch.ChainHash, 83 | CertificateDate: epoch.CertificateDate, 84 | }); 85 | 86 | const [privateKey] = address.Keys.map((key) => key.PrivateKey); 87 | await api( 88 | uploadVerifiedEpoch({ 89 | AddressID: address.ID, 90 | Data: bodyData, 91 | Signature: ( 92 | await signMessage({ 93 | data: bodyData, 94 | privateKeys: await getKeys(privateKey), 95 | detached: true, 96 | }) 97 | ).signature, 98 | }) 99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /lib/certTransparency.ts: -------------------------------------------------------------------------------- 1 | import * as asn1js from 'asn1js'; 2 | import Certificate from 'pkijs/src/Certificate'; 3 | import { verifySCTsForCertificate } from 'pkijs/src/SignedCertificateTimestampList'; 4 | import GeneralName from 'pkijs/src/GeneralName'; 5 | import { base64StringToUint8Array } from './helpers/encoding'; 6 | import { ctLogs, rootCertificates } from './certificates'; 7 | 8 | function pemToBinary(pem: string) { 9 | const lines = pem.split('\n'); 10 | let encoded = ''; 11 | for (let i = 0; i < lines.length; i++) { 12 | if ( 13 | lines[i].trim().length > 0 && 14 | lines[i].indexOf('-BEGIN CERTIFICATE-') < 0 && 15 | lines[i].indexOf('-END CERTIFICATE-') < 0 16 | ) { 17 | encoded += lines[i].trim(); 18 | } 19 | } 20 | return base64StringToUint8Array(encoded).buffer; 21 | } 22 | 23 | export function parseCertificate(cert: string) { 24 | const asn1Certificate = asn1js.fromBER(pemToBinary(cert)); 25 | return new Certificate({ schema: asn1Certificate.result }); 26 | } 27 | 28 | export function parseCertChain(certChain: string) { 29 | let certArr = certChain.split('-----END CERTIFICATE-----\n\n-----BEGIN CERTIFICATE-----'); 30 | // Temporary change for legacy epochs 31 | if (certArr.length === 1) { 32 | certArr = certChain.split('-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----'); 33 | } 34 | // END 35 | for (let i = 0; i < certArr.length; i++) { 36 | switch (i) { 37 | case 0: 38 | certArr[i] = `${certArr[i]}-----END CERTIFICATE-----`; 39 | break; 40 | case certArr.length - 1: 41 | certArr[i] = `-----BEGIN CERTIFICATE-----${certArr[i]}`; 42 | break; 43 | default: 44 | certArr[i] = `-----BEGIN CERTIFICATE-----${certArr[i]}-----END CERTIFICATE-----`; 45 | break; 46 | } 47 | } 48 | const result = []; 49 | for (let i = 0; i < certArr.length; i++) { 50 | try { 51 | result.push(parseCertificate(certArr[i])); 52 | } catch (err) { 53 | throw new Error(`Certificate[${i}] parsing failed with error: ${err.message}`); 54 | } 55 | } 56 | return result; 57 | } 58 | 59 | export function checkAltName(certificate: Certificate, ChainHash: string, EpochID: number) { 60 | if (!certificate.extensions) { 61 | throw new Error('Epoch certificate does not have extensions'); 62 | } 63 | const altNamesExt = certificate.extensions.find((ext) => ext.extnID === '2.5.29.17'); 64 | if (!altNamesExt) { 65 | throw new Error('Epoch certificate does not have AltName extension'); 66 | } 67 | altNamesExt.parsedValue.altNames.sort( 68 | (firstEl: GeneralName, secondEl: GeneralName) => secondEl.value.length - firstEl.value.length 69 | ); 70 | const altName = altNamesExt.parsedValue.altNames[0].value; 71 | const domain = altNamesExt.parsedValue.altNames[1].value; 72 | if (`${ChainHash.slice(0, 32)}.${ChainHash.slice(32)}.${EpochID}.0.${domain.slice(6, domain.length)}` !== altName) { 73 | throw new Error('Epoch certificate alternative name does not match'); 74 | } 75 | } 76 | 77 | async function verifyTopCert(topCert: Certificate) { 78 | let parentCAcert: Certificate; 79 | try { 80 | const parentName = topCert.issuer.typesAndValues.filter( 81 | (issuerName) => issuerName.type.toString() === '2.5.4.3' 82 | )[0]; 83 | const parentCN = parentName.value.valueBlock.value.split(' ').join(''); 84 | if (!Object.keys(rootCertificates).includes(parentCN)) return false; 85 | parentCAcert = parseCertificate(rootCertificates[parentCN]); 86 | } catch (err) { 87 | return false; 88 | } 89 | return topCert.verify(parentCAcert); 90 | } 91 | 92 | export async function verifyLEcert(certChain: Certificate[]) { 93 | let verificationCert = certChain[certChain.length - 1]; 94 | if (!(await verifyTopCert(verificationCert))) { 95 | throw new Error('Epoch certificate did not pass verification of top certificate'); 96 | } 97 | let verified = true; 98 | for (let i = certChain.length - 2; i >= 0; i--) { 99 | verified = verified && (await certChain[i].verify(verificationCert)); 100 | verificationCert = certChain[i]; 101 | } 102 | if (!verified) { 103 | throw new Error("Epoch certificate did not pass verification against issuer's certificate chain"); 104 | } 105 | } 106 | 107 | export async function verifySCT(certificate: Certificate, issuerCert: Certificate) { 108 | // issuerCert is the certificate that signed the epoch certificate. At this point we 109 | // assume that issuerCert was already verified in the certificate chain. 110 | let verificationResult: boolean[]; 111 | try { 112 | verificationResult = await verifySCTsForCertificate(certificate, issuerCert, ctLogs); 113 | } catch (err) { 114 | throw new Error(`SCT verification halted with error "${err.message}"`); 115 | } 116 | const verified = verificationResult.reduce((previous, current) => { 117 | return previous && current; 118 | }); 119 | if (!verified) { 120 | throw new Error('SCT verification failed'); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /test/certTransparency.spec.js: -------------------------------------------------------------------------------- 1 | import * as asn1js from 'asn1js'; 2 | import { epoch } from './keyTransparency.data'; 3 | import { parseCertChain, parseCertificate, checkAltName, verifyLEcert, verifySCT } from '../lib/certTransparency'; 4 | 5 | describe('certificate transparency', () => { 6 | it('should verify a certificate', async () => { 7 | const { Certificate, ChainHash, EpochID } = epoch; 8 | 9 | const certChain = parseCertChain(Certificate); 10 | const epochCert = certChain[0]; 11 | const issuerCert = certChain[1]; 12 | await verifyLEcert(certChain); 13 | checkAltName(epochCert, ChainHash, EpochID); 14 | await verifySCT(epochCert, issuerCert); 15 | }); 16 | 17 | it('should fail to parse with corrupt certificate', () => { 18 | let errorThrown = true; 19 | try { 20 | parseCertificate('corrupt'); 21 | errorThrown = false; 22 | } catch (err) { 23 | expect(err.message).toEqual("Object's schema was not verified against input data for Certificate"); 24 | } 25 | expect(errorThrown).toEqual(true); 26 | }); 27 | 28 | it('should fail in checkAltName with missing extensions', () => { 29 | const { Certificate, ChainHash, EpochID } = epoch; 30 | const cert = parseCertificate(Certificate); 31 | const { extensions, ...certNoExt } = cert; 32 | 33 | let errorThrown = true; 34 | try { 35 | checkAltName(certNoExt, ChainHash, EpochID); 36 | errorThrown = false; 37 | } catch (err) { 38 | expect(err.message).toEqual('Epoch certificate does not have extensions'); 39 | } 40 | expect(errorThrown).toEqual(true); 41 | }); 42 | 43 | it('should fail in checkAltName with missing AltName extension', () => { 44 | const { Certificate, ChainHash, EpochID } = epoch; 45 | const cert = parseCertificate(Certificate); 46 | const corruptExt = cert.extensions.filter((ext) => ext.extnID !== '2.5.29.17'); 47 | 48 | let errorThrown = true; 49 | try { 50 | checkAltName({ ...cert, extensions: corruptExt }, ChainHash, EpochID); 51 | errorThrown = false; 52 | } catch (err) { 53 | expect(err.message).toEqual('Epoch certificate does not have AltName extension'); 54 | } 55 | expect(errorThrown).toEqual(true); 56 | }); 57 | 58 | it('should fail in checkAltName with corrupt altName', () => { 59 | const { Certificate, ChainHash, EpochID } = epoch; 60 | const cert = parseCertificate(Certificate); 61 | cert.extensions.map((ext) => { 62 | if (ext.extnID === '2.5.29.17') { 63 | ext.parsedValue.altNames[0].value = 'corrupt'; 64 | } 65 | return ext; 66 | }); 67 | 68 | let errorThrown = true; 69 | try { 70 | checkAltName(cert, ChainHash, EpochID); 71 | errorThrown = false; 72 | } catch (err) { 73 | expect(err.message).toEqual('Epoch certificate alternative name does not match'); 74 | } 75 | expect(errorThrown).toEqual(true); 76 | }); 77 | 78 | it('should fail certificate verification', async () => { 79 | const { Certificate } = epoch; 80 | const certChain = parseCertChain(Certificate); 81 | certChain[0].tbs = new Uint8Array(10).buffer; 82 | 83 | let errorThrown = true; 84 | try { 85 | await verifyLEcert(certChain); 86 | errorThrown = false; 87 | } catch (err) { 88 | expect(err.message).toEqual( 89 | "Epoch certificate did not pass verification against issuer's certificate chain" 90 | ); 91 | } 92 | expect(errorThrown).toEqual(true); 93 | }); 94 | 95 | it('should fail in verifySCT with missing extensions', async () => { 96 | const { Certificate } = epoch; 97 | const cert = parseCertificate(Certificate); 98 | const { extensions, ...certNoExt } = cert; 99 | 100 | let errorThrown = true; 101 | try { 102 | await verifySCT(certNoExt); 103 | errorThrown = false; 104 | } catch (err) { 105 | expect(err.message).toEqual( 106 | `SCT verification halted with error "Cannot read property 'length' of undefined"` 107 | ); 108 | } 109 | expect(errorThrown).toEqual(true); 110 | }); 111 | 112 | it('should fail in verifySCT with missing SCTs extension', async () => { 113 | const { Certificate } = epoch; 114 | const cert = parseCertificate(Certificate); 115 | const corruptExt = cert.extensions.filter((ext) => ext.extnID !== '1.3.6.1.4.1.11129.2.4.2'); 116 | 117 | let errorThrown = true; 118 | try { 119 | await verifySCT({ ...cert, extensions: corruptExt }); 120 | errorThrown = false; 121 | } catch (err) { 122 | expect(err.message).toEqual( 123 | `SCT verification halted with error "No SignedCertificateTimestampList extension in the specified certificate"` 124 | ); 125 | } 126 | expect(errorThrown).toEqual(true); 127 | }); 128 | 129 | it('should fail in verifySCT with missing SCTs', async () => { 130 | const { Certificate } = epoch; 131 | const cert = parseCertificate(Certificate); 132 | cert.extensions.map((ext) => { 133 | if (ext.extnID === '1.3.6.1.4.1.11129.2.4.2') { 134 | ext.parsedValue.timestamps = []; 135 | } 136 | return ext; 137 | }); 138 | 139 | let errorThrown = true; 140 | try { 141 | await verifySCT(cert); 142 | errorThrown = false; 143 | } catch (err) { 144 | expect(err.message).toEqual(`SCT verification halted with error "Nothing to verify in the certificate"`); 145 | } 146 | expect(errorThrown).toEqual(true); 147 | }); 148 | 149 | it('should fail in verifySCT with corrupt certificate', async () => { 150 | const { Certificate } = epoch; 151 | const certChain = parseCertChain(Certificate); 152 | const epochCert = certChain[0]; 153 | const issuerCert = certChain[1]; 154 | epochCert.serialNumber = new asn1js.Integer(); 155 | 156 | let errorThrown = true; 157 | try { 158 | await verifySCT(epochCert, issuerCert); 159 | errorThrown = false; 160 | } catch (err) { 161 | expect(err.message).toEqual('SCT verification failed'); 162 | } 163 | expect(errorThrown).toEqual(true); 164 | }); 165 | }); 166 | -------------------------------------------------------------------------------- /test/merkleTree.spec.js: -------------------------------------------------------------------------------- 1 | import { testEmail, skl, epoch, proof } from './keyTransparency.data'; 2 | import { verifyProof, verifyChainHash } from '../lib/merkleTree'; 3 | 4 | describe('merkle tree', () => { 5 | it('should verify a proof', async () => { 6 | const { TreeHash } = epoch; 7 | const { Data } = skl; 8 | 9 | await verifyProof(proof, TreeHash, Data, testEmail); 10 | }); 11 | 12 | it('should fail with corrupt length', async () => { 13 | const { TreeHash } = epoch; 14 | const { Data } = skl; 15 | 16 | let errorThrown = true; 17 | try { 18 | await verifyProof({ ...proof, Proof: proof.Proof.slice(0, 70) }, TreeHash, Data, testEmail); 19 | errorThrown = false; 20 | } catch (err) { 21 | expect(err.message).toEqual('VRF verification failed with error "Length mismatch found"'); 22 | } 23 | expect(errorThrown).toEqual(true); 24 | }); 25 | 26 | it('should fail with corrupt initial byte', async () => { 27 | const { TreeHash } = epoch; 28 | const { Data } = skl; 29 | 30 | let errorThrown = true; 31 | try { 32 | await verifyProof( 33 | { ...proof, Proof: `00${proof.Proof.slice(2, proof.Proof.length)}` }, 34 | TreeHash, 35 | Data, 36 | testEmail 37 | ); 38 | errorThrown = false; 39 | } catch (err) { 40 | expect(err.message).toEqual('VRF verification failed with error "Proof decoding failed"'); 41 | } 42 | expect(errorThrown).toEqual(true); 43 | }); 44 | 45 | it('should fail with corrupt name', async () => { 46 | const { TreeHash } = epoch; 47 | const { Data } = skl; 48 | 49 | let errorThrown = true; 50 | try { 51 | await verifyProof( 52 | { ...proof, Name: `00${proof.Name.slice(2, proof.Name.length)}` }, 53 | TreeHash, 54 | Data, 55 | testEmail 56 | ); 57 | errorThrown = false; 58 | } catch (err) { 59 | expect(err.message).toEqual( 60 | 'VRF verification failed with error "Fetched name is different than name in proof"' 61 | ); 62 | } 63 | expect(errorThrown).toEqual(true); 64 | }); 65 | 66 | it('should fail with corrupt proof', async () => { 67 | const { TreeHash } = epoch; 68 | const { Data } = skl; 69 | 70 | let errorThrown = true; 71 | try { 72 | await verifyProof( 73 | { ...proof, Proof: `${proof.Proof.slice(0, proof.Proof.length - 2)}00` }, 74 | TreeHash, 75 | Data, 76 | testEmail 77 | ); 78 | errorThrown = false; 79 | } catch (err) { 80 | expect(err.message).toEqual('VRF verification failed with error "Verification went through but failed"'); 81 | } 82 | expect(errorThrown).toEqual(true); 83 | }); 84 | 85 | it('should fail with corrupt root hash', async () => { 86 | const { TreeHash } = epoch; 87 | const { Data } = skl; 88 | 89 | let errorThrown = true; 90 | try { 91 | await verifyProof(proof, `00${TreeHash.slice(2, TreeHash.length)}`, Data, testEmail); 92 | errorThrown = false; 93 | } catch (err) { 94 | expect(err.message).toEqual('Hash chain does not result in TreeHash'); 95 | } 96 | expect(errorThrown).toEqual(true); 97 | }); 98 | 99 | it('should fail with corrupt revision', async () => { 100 | const { TreeHash } = epoch; 101 | const { Data } = skl; 102 | 103 | let errorThrown = true; 104 | try { 105 | await verifyProof({ ...proof, Revision: proof.Revision + 1 }, TreeHash, Data, testEmail); 106 | errorThrown = false; 107 | } catch (err) { 108 | expect(err.message).toEqual('Hash chain does not result in TreeHash'); 109 | } 110 | expect(errorThrown).toEqual(true); 111 | }); 112 | 113 | it('should fail with corrupt skl', async () => { 114 | const { TreeHash } = epoch; 115 | const { Data } = skl; 116 | 117 | let errorThrown = true; 118 | try { 119 | await verifyProof(proof, TreeHash, `00${Data.slice(2, Data.length)}`, testEmail); 120 | errorThrown = false; 121 | } catch (err) { 122 | expect(err.message).toEqual('Hash chain does not result in TreeHash'); 123 | } 124 | expect(errorThrown).toEqual(true); 125 | }); 126 | 127 | it('should fail with corrupt but matching names', async () => { 128 | const { TreeHash } = epoch; 129 | const { Data } = skl; 130 | 131 | let errorThrown = true; 132 | try { 133 | await verifyProof( 134 | { 135 | ...proof, 136 | Name: `00${proof.Name.slice(2, proof.Name.length)}`, 137 | Proof: `${proof.Proof.slice(0, 2)}00${proof.Proof.slice(4, proof.Proof.length)}`, 138 | }, 139 | TreeHash, 140 | Data, 141 | testEmail 142 | ); 143 | errorThrown = false; 144 | } catch (err) { 145 | // NOTE: the error message in this case varies depending on the corruption, i.e. depending on the 146 | // corrupt Name being parsable to an EC point by OS2ECP. If it is, then the error message will be 147 | // "Verification went through but failed", if it isn't the errow will be "Proof decoding failed". 148 | expect(err.message.substring(0, 36)).toEqual('VRF verification failed with error "'); 149 | } 150 | expect(errorThrown).toEqual(true); 151 | }); 152 | 153 | it('should fail with corrupt email', async () => { 154 | const { TreeHash } = epoch; 155 | const { Data } = skl; 156 | 157 | let errorThrown = true; 158 | try { 159 | await verifyProof(proof, TreeHash, Data, 'corrupt@protonmail.blue'); 160 | errorThrown = false; 161 | } catch (err) { 162 | // NOTE: the error message in this case varies depending on the corruption, i.e. depending on 163 | // SHA256(email||pk|ctr) being parsable to an EC point by OS2ECP within the LIMIT constant. 164 | // If it is, then the error message will be "Verification went through but failed", if it 165 | // isn't the errow will be "Point generation failed". 166 | expect(err.message.substring(0, 36)).toEqual('VRF verification failed with error "'); 167 | } 168 | expect(errorThrown).toEqual(true); 169 | }); 170 | 171 | it('should fail with corrupt neighbors', async () => { 172 | const { TreeHash } = epoch; 173 | const { Data } = skl; 174 | 175 | let errorThrown = true; 176 | try { 177 | await verifyProof( 178 | { 179 | ...proof, 180 | Neighbors: [ 181 | ...proof.Neighbors.slice(0, proof.Neighbors.length - 1), 182 | '250e8651e520ac6ff1b163c892f1a262006bc546c14d428641ef663f3fc366f3', 183 | ], 184 | }, 185 | TreeHash, 186 | Data, 187 | testEmail 188 | ); 189 | errorThrown = false; 190 | } catch (err) { 191 | expect(err.message).toEqual('Hash chain does not result in TreeHash'); 192 | } 193 | expect(errorThrown).toEqual(true); 194 | }); 195 | 196 | it('should verify chain hash consistency', async () => { 197 | const { TreeHash, ChainHash, PrevChainHash } = epoch; 198 | 199 | await verifyChainHash(TreeHash, PrevChainHash, ChainHash); 200 | }); 201 | 202 | it('should fail chain hash consistency', async () => { 203 | const { TreeHash, ChainHash, PrevChainHash } = epoch; 204 | 205 | let errorThrown = true; 206 | try { 207 | await verifyChainHash(TreeHash, PrevChainHash, `0${ChainHash.slice(1)}`); 208 | errorThrown = false; 209 | } catch (err) { 210 | expect(err.message).toEqual('Chain hash of fetched epoch is not consistent'); 211 | } 212 | expect(errorThrown).toEqual(true); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | verifyMessage, 3 | OpenPGPKey, 4 | getSHA256Fingerprints, 5 | getKeys, 6 | VERIFICATION_STATUS, 7 | getSignature, 8 | createMessage, 9 | OpenPGPSignature, 10 | } from 'pmcrypto'; 11 | import { Api } from './helpers/interfaces/Api'; 12 | import { Epoch, EpochExtended, KeyInfo } from './interfaces'; 13 | import { SignedKeyListEpochs } from './helpers/interfaces/SignedKeyList'; 14 | import { fetchProof, fetchEpoch } from './fetchHelper'; 15 | import { checkAltName, verifyLEcert, verifySCT, parseCertChain } from './certTransparency'; 16 | import { verifyProof, verifyChainHash } from './merkleTree'; 17 | import { MAX_EPOCH_INTERVAL } from './constants'; 18 | import { getItem, hasStorage, removeItem } from './helpers/storage'; 19 | 20 | export function compareKeyInfo(keyInfo: KeyInfo, sklKeyInfo: KeyInfo) { 21 | // Check fingerprints 22 | if (keyInfo.Fingerprint !== sklKeyInfo.Fingerprint) { 23 | throw new Error('Fingerprints'); 24 | } 25 | 26 | // Check SHA256Fingerprints 27 | if (keyInfo.SHA256Fingerprints.length !== sklKeyInfo.SHA256Fingerprints.length) { 28 | throw new Error('SHA256Fingerprints length'); 29 | } 30 | keyInfo.SHA256Fingerprints.forEach((sha256Fingerprint, i) => { 31 | if (sha256Fingerprint !== sklKeyInfo.SHA256Fingerprints[i]) { 32 | throw new Error('SHA256Fingerprints'); 33 | } 34 | }); 35 | 36 | // Check Flags 37 | if (keyInfo.Flags !== sklKeyInfo.Flags) { 38 | throw new Error('Flags'); 39 | } 40 | 41 | // Check primariness 42 | if (keyInfo.Primary !== sklKeyInfo.Primary) { 43 | throw new Error('Primariness'); 44 | } 45 | } 46 | 47 | export async function verifyKeyLists( 48 | keyList: { 49 | Flags: number; 50 | PublicKey: OpenPGPKey; 51 | }[], 52 | signedKeyListData: KeyInfo[] 53 | ) { 54 | // Check arrays validity 55 | if (keyList.length === 0) { 56 | throw new Error('No keys detected'); 57 | } 58 | if (keyList.length !== signedKeyListData.length) { 59 | throw new Error('Key list and signed key list have different lengths'); 60 | } 61 | 62 | // Prepare key lists 63 | const keyListInfo = await Promise.all( 64 | keyList.map(async (key, i) => { 65 | return { 66 | Fingerprint: key.PublicKey.getFingerprint().toLowerCase(), 67 | SHA256Fingerprints: (await getSHA256Fingerprints(key.PublicKey)).map((sha256fingerprint: string) => 68 | sha256fingerprint.toLowerCase() 69 | ), 70 | Primary: i === 0 ? 1 : 0, 71 | Flags: key.Flags, 72 | }; 73 | }) 74 | ); 75 | keyListInfo.sort((key1, key2) => { 76 | return key1.Fingerprint.localeCompare(key2.Fingerprint); 77 | }); 78 | 79 | const signedKeyListInfo = signedKeyListData.map((keyInfo) => { 80 | return { 81 | ...keyInfo, 82 | Fingerprint: keyInfo.Fingerprint.toLowerCase(), 83 | SHA256Fingerprints: keyInfo.SHA256Fingerprints.map((sha256fingerprint: string) => 84 | sha256fingerprint.toLowerCase() 85 | ), 86 | }; 87 | }); 88 | signedKeyListInfo.sort((key1, key2) => { 89 | return key1.Fingerprint.localeCompare(key2.Fingerprint); 90 | }); 91 | 92 | // Check keys 93 | keyListInfo.forEach((key, i) => { 94 | compareKeyInfo(key, signedKeyListInfo[i]); 95 | }); 96 | } 97 | 98 | export async function verifyEpoch( 99 | epoch: Epoch, 100 | email: string, 101 | signedKeyListArmored: string, 102 | api: Api 103 | ): Promise { 104 | // Fetch and verify proof 105 | const proof = await fetchProof(epoch.EpochID, email, api); 106 | await verifyProof(proof, epoch.TreeHash, signedKeyListArmored, email); 107 | 108 | // Verify ChainHash 109 | await verifyChainHash(epoch.TreeHash, epoch.PrevChainHash, epoch.ChainHash); 110 | 111 | // Parse and verify certificates 112 | const certChain = parseCertChain(epoch.Certificate); 113 | const epochCert = certChain[0]; 114 | const issuerCert = certChain[1]; 115 | await verifyLEcert(certChain); 116 | checkAltName(epochCert, epoch.ChainHash, epoch.EpochID); 117 | await verifySCT(epochCert, issuerCert); 118 | 119 | let returnedDate: number; 120 | switch (epochCert.notBefore.toJSON().type) { 121 | case 0: 122 | case 1: 123 | returnedDate = epochCert.notBefore.toJSON().value.getTime(); 124 | break; 125 | default: 126 | throw new Error(`Certificate's notBefore date is invalid (type = ${epochCert.notBefore.toJSON().type})`); 127 | } 128 | 129 | return returnedDate; 130 | } 131 | 132 | export async function parseKeyLists( 133 | keyList: { 134 | Flags: number | undefined; 135 | PublicKey: string; 136 | }[], 137 | signedKeyListData: string 138 | ): Promise<{ 139 | signedKeyListData: KeyInfo[]; 140 | parsedKeyList: { Flags: number; PublicKey: OpenPGPKey }[]; 141 | }> { 142 | return { 143 | signedKeyListData: JSON.parse(signedKeyListData), 144 | parsedKeyList: await Promise.all( 145 | keyList.map(async (key) => { 146 | return { 147 | Flags: key.Flags ? key.Flags : 0, 148 | PublicKey: (await getKeys(key.PublicKey))[0], 149 | }; 150 | }) 151 | ), 152 | }; 153 | } 154 | 155 | export async function checkSignature( 156 | message: string, 157 | publicKeys: OpenPGPKey[], 158 | signature: string, 159 | failMessage: string 160 | ) { 161 | const { verified } = await verifyMessage({ 162 | message: createMessage(message), 163 | publicKeys, 164 | signature: await getSignature(signature), 165 | }); 166 | if (verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) { 167 | throw new Error(`Signature verification failed (${failMessage})`); 168 | } 169 | } 170 | 171 | export function getSignatureTime(signature: OpenPGPSignature): number { 172 | const packet = signature.packets.findPacket(2); 173 | if (!packet) { 174 | throw new Error('Signature contains no signature packet'); 175 | } 176 | return (packet as any).created.getTime(); 177 | } 178 | 179 | export function isTimestampTooOld(time: number, refereceTime?: number) { 180 | if (!refereceTime) { 181 | refereceTime = Date.now(); 182 | } 183 | return Math.abs(refereceTime - time) > MAX_EPOCH_INTERVAL; 184 | } 185 | 186 | export async function verifyCurrentEpoch(signedKeyList: SignedKeyListEpochs, email: string, api: Api) { 187 | const currentEpoch = await fetchEpoch(signedKeyList.MaxEpochID as number, api); 188 | 189 | const returnedDate: number = await verifyEpoch(currentEpoch, email, signedKeyList.Data, api); 190 | 191 | if (isTimestampTooOld(returnedDate)) { 192 | throw new Error('Returned date is older than MAX_EPOCH_INTERVAL'); 193 | } 194 | 195 | const { Revision }: { Revision: number } = await fetchProof(currentEpoch.EpochID, email, api); 196 | 197 | return { 198 | ...currentEpoch, 199 | Revision, 200 | CertificateDate: returnedDate, 201 | } as EpochExtended; 202 | } 203 | 204 | export function getKTBlobs(addressID: string) { 205 | const returnedMap: Map = new Map(); 206 | 207 | if (!hasStorage()) { 208 | return returnedMap; 209 | } 210 | 211 | for (let i = 0; i < localStorage.length; i++) { 212 | const key = localStorage.key(i); 213 | if (!key) { 214 | continue; 215 | } 216 | const splitKey = key.split(':'); 217 | if (splitKey[0] === 'kt' && splitKey[2] === addressID) { 218 | returnedMap.set(key, getItem(key) as string); 219 | } 220 | } 221 | 222 | return returnedMap; 223 | } 224 | 225 | export function getFromLS(addressID: string): string[] { 226 | if (!hasStorage()) { 227 | return []; 228 | } 229 | 230 | const ktBlobs = getKTBlobs(addressID); 231 | 232 | const values: string[] = []; 233 | for (const element of ktBlobs) { 234 | const [key, value] = element; 235 | const splitKey = key.split(':'); 236 | values[parseInt(splitKey[1], 10)] = value; 237 | } 238 | 239 | return values; 240 | } 241 | 242 | export function removeFromLS(index: number, addressID: string) { 243 | if (!hasStorage()) { 244 | throw new Error('localStorage unavailable'); 245 | } 246 | 247 | const ktBlobs = getKTBlobs(addressID); 248 | 249 | let removed = false; 250 | for (const element of ktBlobs) { 251 | const [key] = element; 252 | const splitKey = key.split(':'); 253 | if (splitKey[1] === `${index}`) { 254 | removeItem(key); 255 | removed = true; 256 | break; 257 | } 258 | } 259 | 260 | if (!removed) { 261 | throw new Error('Cannot remove blob from localStorage'); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /test/keyTransparency.data.js: -------------------------------------------------------------------------------- 1 | export const testEmail = 'testkt@protonmail.blue'; 2 | 3 | export const keyList = [ 4 | { 5 | Flags: 3, 6 | PublicKey: 7 | '-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: ProtonMail\n\nxjMEX7fi2xYJKwYBBAHaRw8BAQdAh2w+hH5YnWL1IMxztRENWKHt9Ob6fnzl\nRt5/4IuxMvLNL3Rlc3RrdEBwcm90b25tYWlsLmJsdWUgPHRlc3RrdEBwcm90\nb25tYWlsLmJsdWU+wo8EEBYKACAFAl+34tsGCwkHCAMCBBUICgIEFgIBAAIZ\nAQIbAwIeAQAhCRAtPf2R8K5BDhYhBNE/DzijnomlHBt47i09/ZHwrkEOa+QB\nAJ1fneSwmdUd0PJieCJW29r8cPT2T6njszJlG/ldJE58APsGxLElNFf0ksQn\nNRWIwzHarr9bSQON7uXEz6lJdNdDCM44BF+34tsSCisGAQQBl1UBBQEBB0BM\nip9yyeQtlwK3QmCsCTguMmzTs9QiEFz3X4OyV3DVfwMBCAfCeAQYFggACQUC\nX7fi2wIbDAAhCRAtPf2R8K5BDhYhBNE/DzijnomlHBt47i09/ZHwrkEOnlgA\n/A2z1acf83Vdt+hKze/aSqfkJbC3ud11UV6/JfJb+vzJAP4rPLZPa+N8f0Jt\nk1XUNfs4amk7k/4ZS0Y1Yu0WWL4XBg==\n=QWnX\n-----END PGP PUBLIC KEY BLOCK-----\n', 8 | }, 9 | { 10 | Flags: 3, 11 | PublicKey: 12 | '-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: ProtonMail\n\nxjMEX9JNmRYJKwYBBAHaRw8BAQdAG3Wic5hQVSwtIAy15+9g012aAHy0j+Gd\nttt9UFp4Tc3NL3Rlc3RrdEBwcm90b25tYWlsLmJsdWUgPHRlc3RrdEBwcm90\nb25tYWlsLmJsdWU+wo8EEBYKACAFAl/STZkGCwkHCAMCBBUICgIEFgIBAAIZ\nAQIbAwIeAQAhCRA9lzggpCL88BYhBOxQu+9pNr6WJlDLtT2XOCCkIvzwOmQA\n/iqfTFo0buKu/dQ27zZ9j6fdCIzApvNEygoGcokIog+lAP98jtLP/O1efx5v\nduQzotqgFOK8Cz6C9BPAXAnsHkZVAc44BF/STZkSCisGAQQBl1UBBQEBB0Ct\nniJQ2HO8TNUZeKhVIsWO/lHVVVlDy26uR5MBORO6JgMBCAfCeAQYFggACQUC\nX9JNmQIbDAAhCRA9lzggpCL88BYhBOxQu+9pNr6WJlDLtT2XOCCkIvzwm6oB\nALKp4D0Vfb8xoSrJP8FI0uiJXQ10BCUaTcAI3SAg4rkhAQC5sU2UNjFSvWjr\nhmaj26j8rgZOEkDvwpKOAR05zmcrCQ==\n=RCXQ\n-----END PGP PUBLIC KEY BLOCK-----\n', 13 | }, 14 | { 15 | Flags: 3, 16 | PublicKey: 17 | '-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: ProtonMail\n\nxjMEX65OxhYJKwYBBAHaRw8BAQdA/aaL6OYS+eKAAadqW5VO47pomJ/NPWiw\nVFNPh12tDZfNL3Rlc3RrdEBwcm90b25tYWlsLmJsdWUgPHRlc3RrdEBwcm90\nb25tYWlsLmJsdWU+wo8EEBYKACAFAl+uTsYGCwkHCAMCBBUICgIEFgIBAAIZ\nAQIbAwIeAQAhCRDC+x6+QIJ6NxYhBOWL6RLX0UliQqDi58L7Hr5Agno33PMB\nAOLSPPxKQERwO7NNTAFAWDfvL+hrgy6hzcsaTaemDsxkAP9W51hzI50E52DP\nQC+cf75dl4NJVZZeMjP8SbEKFB6yD844BF+uTsYSCisGAQQBl1UBBQEBB0DE\n6KEsEndfs+kPOrwD9pAPdpuvTiTOYxWxg365MI7QYAMBCAfCeAQYFggACQUC\nX65OxgIbDAAhCRDC+x6+QIJ6NxYhBOWL6RLX0UliQqDi58L7Hr5Agno3DKQB\nAL6o7MAdJ8SuPqEfWTpSn1Hg0q5kWOltvXfXt/KfrYyJAQDy/TjIWFP+Y05b\nbLDIq5flV2EV6EMNuk4XAIoQ6PPyDg==\n=uwh3\n-----END PGP PUBLIC KEY BLOCK-----\n', 18 | }, 19 | ]; 20 | 21 | export const skl = { 22 | MinEpochID: 1, 23 | MaxEpochID: 5, 24 | Data: 25 | '[{"Primary":1,"Flags":3,"Fingerprint":"d13f0f38a39e89a51c1b78ee2d3dfd91f0ae410e","SHA256Fingerprints":["a2bc20d55c951e60ce9d9250bd7cdb011564b29797b575b9a342d211cacac752","3e41fa1a3a06a30866d24fd83aef33e82508a417b7338f3fb9caee12781ff320"]},{"Primary":0,"Flags":3,"Fingerprint":"e58be912d7d1496242a0e2e7c2fb1ebe40827a37","SHA256Fingerprints":["79bede9527be6b05103de4d8217bd8dadf7cc080b9c21462f1ddd4f160929d60","2a8afc978459ba42e3a64806acedc931c4bcc755d2dfd2d0ffaed88f923da2a7"]},{"Primary":0,"Flags":3,"Fingerprint":"ec50bbef6936be962650cbb53d973820a422fcf0","SHA256Fingerprints":["f468211106512bcec301e8eaa4799b038d83739ff5fbb2ab3e1bcd02ab7df012","660f2d02e99c5a6a2518c40c852bebf7f1e22fcb18c3c8f1d7447cf5e635bf18"]}]', 26 | Signature: 27 | '-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.9\r\nComment: https://openpgpjs.org\r\n\r\nwnUEARYKAAYFAl/STZkAIQkQLT39kfCuQQ4WIQTRPw84o56JpRwbeO4tPf2R\r\n8K5BDvjpAP9bJoOKYWArncEWsLcNszz9gDIWHYN7PDeNvPuPjlUdowD9Eq8f\r\nrf/5uGa5tIIOnTA4Wq3dns55vLJQyKiNFVanBQM=\r\n=qxpK\r\n-----END PGP SIGNATURE-----\r\n', 28 | }; 29 | 30 | export const epoch = { 31 | Code: 1000, 32 | EpochID: 5, 33 | TreeHash: 'e1e6915b3a260b7de8c7b13b3c2a56fa1891cb07f7c81bd18cf7d3abd9414112', 34 | ChainHash: 'c37a8c9a0f912269920be5106d176eb4cad03635801720499d4ba46598fb0dcd', 35 | PrevChainHash: '9042f9d84bd9e10cffb6087c71063fcabffdca31425c5f0be846ce212e41b4ea', 36 | Certificate: 37 | '-----BEGIN CERTIFICATE-----\nMIIFjTCCBHWgAwIBAgISBILAD6JTiWePxDOjR1tmVa5lMA0GCSqGSIb3DQEBCwUA\nMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD\nEwJSMzAeFw0yMDEyMTEwNzM2MzRaFw0yMTAzMTEwNzM2MzRaMCIxIDAeBgNVBAMT\nF2Vwb2NoLmt0LnByb3RvbnRlY2gueHl6MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAsLHQu7JuIsuH2dZamTryuYSPxtcpK2b273PTLsRrxmK3EwhEFXCg\nnoi61iIjskAH9RjehWsDxMDS4ZnncJNlxIcoWwZOruk5++acw3g2/4zV88YJhg2z\nM11EtjjZoRkaQWx2JERhSTYRuDkLvDQtJ4BlYMZ//gNpr4nBYafraTtj9Fp23Ame\nINWYDSKon2gGX+pd89ZkiToluaYgmQZL7oYuUHNroTEWi1iGdOmCACBCxLvd84AX\n1CSCOZR3vUSVbOKmnTwX7Xf8JqNfiBnJ9/gDjIMfxjy7HkCBEwBhTnaX9zWaM220\nHX81AfsY0hY82Ys5Y6uZk6P1zp+9KJ2OnQIDAQABo4ICqzCCAqcwDgYDVR0PAQH/\nBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8E\nAjAAMB0GA1UdDgQWBBRYbBNneclBwrxuXdS5UsltvsezxDAfBgNVHSMEGDAWgBQU\nLrMXt1hWy65QCUDmH6+dixTCxjBVBggrBgEFBQcBAQRJMEcwIQYIKwYBBQUHMAGG\nFWh0dHA6Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmku\nbGVuY3Iub3JnLzB7BgNVHREEdDBygldjMzdhOGM5YTBmOTEyMjY5OTIwYmU1MTA2\nZDE3NmViNC5jYWQwMzYzNTgwMTcyMDQ5OWQ0YmE0NjU5OGZiMGRjZC41LjAua3Qu\ncHJvdG9udGVjaC54eXqCF2Vwb2NoLmt0LnByb3RvbnRlY2gueHl6MEwGA1UdIARF\nMEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6\nLy9jcHMubGV0c2VuY3J5cHQub3JnMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDwAHYA\nXNxDkv7mq0VEsV6a1FbmEDf71fpH3KFzlLJe5vbHDsoAAAF2UPHVzgAABAMARzBF\nAiB0cicJhu8Q5KFo8w8Vu4GjCiIGm0/B2acT6XtSNa/xXgIhAIopalMjXsFVdueS\nTzpG8b5yvgxZfKsIlfbdeJqs2C1lAHYA9lyUL9F3MCIUVBgIMJRWjuNNExkzv98M\nLyALzE7xZOMAAAF2UPHXtQAABAMARzBFAiEAlkO8p25Kzp7DincgsS4C+kCVSw6m\nNqpQfAowYvvZLlUCIDFpvanpb0lF86W5++tXG6tfdT4WvxjlpwA1LVwBKZ1YMA0G\nCSqGSIb3DQEBCwUAA4IBAQCFb1MaV7IkFmecJ8nDpWjjoWEUg4P12ggXytXnxTju\nkp+ysEQde6w2i54vGkoF5PMqZ4B7+3bVdKgWwPGljJ80RreYO98jahUodDXLAHoJ\nivvgJTMXmSkw+1e6RF+c5kBXiTtpG4evZ7Nu2kzIzXKVh8nlk0CaG5CU9kbaoScj\npfdR7s6wuTrOS+D71Xrh67lIPgRi16i1qOJsp3k+a5SUv8plymWoy98zXKqLoChV\nQqt1uNq6ZK1Hatwrfoo1tf43A9o4oaF5slVPZ8XMC7aQuuV0TA7AlTF0MeRIvi1F\nYfLm5eGbOibDYHwVmBtrONqYTB+gnKsAnbdfTPKU+8FJ\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEZTCCA02gAwIBAgIQQAF1BIMUpMghjISpDBbN3zANBgkqhkiG9w0BAQsFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTIwMTAwNzE5MjE0MFoXDTIxMDkyOTE5MjE0MFow\nMjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxCzAJBgNVBAMT\nAlIzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuwIVKMz2oJTTDxLs\njVWSw/iC8ZmmekKIp10mqrUrucVMsa+Oa/l1yKPXD0eUFFU1V4yeqKI5GfWCPEKp\nTm71O8Mu243AsFzzWTjn7c9p8FoLG77AlCQlh/o3cbMT5xys4Zvv2+Q7RVJFlqnB\nU840yFLuta7tj95gcOKlVKu2bQ6XpUA0ayvTvGbrZjR8+muLj1cpmfgwF126cm/7\ngcWt0oZYPRfH5wm78Sv3htzB2nFd1EbjzK0lwYi8YGd1ZrPxGPeiXOZT/zqItkel\n/xMY6pgJdz+dU/nPAeX1pnAXFK9jpP+Zs5Od3FOnBv5IhR2haa4ldbsTzFID9e1R\noYvbFQIDAQABo4IBaDCCAWQwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8E\nBAMCAYYwSwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5p\nZGVudHJ1c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTE\np7Gkeyxx+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEE\nAYLfEwEBATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2Vu\nY3J5cHQub3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0\nLmNvbS9EU1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYf\nr52LFMLGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0B\nAQsFAAOCAQEA2UzgyfWEiDcx27sT4rP8i2tiEmxYt0l+PAK3qB8oYevO4C5z70kH\nejWEHx2taPDY/laBL21/WKZuNTYQHHPD5b1tXgHXbnL7KqC401dk5VvCadTQsvd8\nS8MXjohyc9z9/G2948kLjmE6Flh9dDYrVYA9x2O+hEPGOaEOa1eePynBgPayvUfL\nqjBstzLhWVQLGAkXXmNs+5ZnPBxzDJOLxhF2JIbeQAcH5H0tZrUlo5ZYyOqA7s9p\nO5b85o3AM/OJ+CktFBQtfvBhcJVd9wvlwPsk+uyOy2HI7mNxKKgsBTt375teA2Tw\nUdHkhVNcsAKX1H7GNNLOEADksd86wuoXvg==\n-----END CERTIFICATE-----\n', 38 | IssuerKeyHash: '8d02536c887482bc34ff54e41d2ba659bf85b341a0a20afadb5813dcfbcf286d', 39 | }; 40 | 41 | export const proof = { 42 | Code: 1000, 43 | Neighbors: [ 44 | '9f1f8a2f0d4cc05b82736c2f886fc44ec053e2d60dd3f439cf08a76235a42225', 45 | null, 46 | null, 47 | null, 48 | null, 49 | 'e5900436b115289c40a57c135861047b61fe987e1b324971ec7bc73cdc7354cb', 50 | null, 51 | null, 52 | null, 53 | null, 54 | null, 55 | null, 56 | null, 57 | null, 58 | null, 59 | null, 60 | null, 61 | null, 62 | null, 63 | null, 64 | null, 65 | null, 66 | null, 67 | null, 68 | null, 69 | null, 70 | null, 71 | null, 72 | null, 73 | null, 74 | null, 75 | null, 76 | null, 77 | null, 78 | null, 79 | null, 80 | null, 81 | null, 82 | null, 83 | null, 84 | null, 85 | null, 86 | null, 87 | null, 88 | null, 89 | null, 90 | null, 91 | null, 92 | null, 93 | null, 94 | null, 95 | null, 96 | null, 97 | null, 98 | null, 99 | null, 100 | null, 101 | null, 102 | null, 103 | null, 104 | null, 105 | null, 106 | null, 107 | null, 108 | null, 109 | null, 110 | null, 111 | null, 112 | null, 113 | null, 114 | null, 115 | null, 116 | null, 117 | null, 118 | null, 119 | null, 120 | null, 121 | null, 122 | null, 123 | null, 124 | null, 125 | null, 126 | null, 127 | null, 128 | null, 129 | null, 130 | null, 131 | null, 132 | null, 133 | null, 134 | null, 135 | null, 136 | null, 137 | null, 138 | null, 139 | null, 140 | null, 141 | null, 142 | null, 143 | null, 144 | null, 145 | null, 146 | null, 147 | null, 148 | null, 149 | null, 150 | null, 151 | null, 152 | null, 153 | null, 154 | null, 155 | null, 156 | null, 157 | null, 158 | null, 159 | null, 160 | null, 161 | null, 162 | null, 163 | null, 164 | null, 165 | null, 166 | null, 167 | null, 168 | null, 169 | null, 170 | null, 171 | null, 172 | null, 173 | null, 174 | null, 175 | null, 176 | null, 177 | null, 178 | null, 179 | null, 180 | null, 181 | null, 182 | null, 183 | null, 184 | null, 185 | null, 186 | null, 187 | null, 188 | null, 189 | null, 190 | null, 191 | null, 192 | null, 193 | null, 194 | null, 195 | null, 196 | null, 197 | null, 198 | null, 199 | null, 200 | null, 201 | null, 202 | null, 203 | null, 204 | null, 205 | null, 206 | null, 207 | null, 208 | null, 209 | null, 210 | null, 211 | null, 212 | null, 213 | null, 214 | null, 215 | null, 216 | null, 217 | null, 218 | null, 219 | null, 220 | null, 221 | null, 222 | null, 223 | null, 224 | null, 225 | null, 226 | null, 227 | null, 228 | null, 229 | null, 230 | null, 231 | null, 232 | null, 233 | null, 234 | null, 235 | null, 236 | null, 237 | null, 238 | null, 239 | null, 240 | null, 241 | null, 242 | null, 243 | null, 244 | null, 245 | null, 246 | null, 247 | null, 248 | null, 249 | null, 250 | null, 251 | null, 252 | null, 253 | null, 254 | null, 255 | null, 256 | null, 257 | null, 258 | null, 259 | null, 260 | null, 261 | null, 262 | null, 263 | null, 264 | null, 265 | null, 266 | null, 267 | null, 268 | null, 269 | null, 270 | null, 271 | null, 272 | null, 273 | null, 274 | null, 275 | null, 276 | null, 277 | null, 278 | null, 279 | null, 280 | null, 281 | null, 282 | null, 283 | null, 284 | null, 285 | null, 286 | null, 287 | null, 288 | null, 289 | null, 290 | null, 291 | null, 292 | null, 293 | null, 294 | null, 295 | null, 296 | null, 297 | null, 298 | null, 299 | null, 300 | ], 301 | Revision: 0, 302 | Proof: 303 | '03d3641ddc1be31c88f0e4acd46b755534571cb5969c4065b74b833d01fd0de0d5423b3c93e1544a691ed77a8196dfb4a04a1bed6b07e9fe1ec5b94184286619e71204f51a5a14239602c7e3e50fb0fd42', 304 | Name: 'd3641ddc1be31c88f0e4acd46b755534571cb5969c4065b74b833d01fd0de0d5', 305 | }; 306 | -------------------------------------------------------------------------------- /lib/keyTransparency.ts: -------------------------------------------------------------------------------- 1 | import { OpenPGPKey, getSignature, encryptMessage, decryptMessage, getMessage } from 'pmcrypto'; 2 | import { Api } from './helpers/interfaces/Api'; 3 | import { Address } from './helpers/interfaces/Address'; 4 | import { Epoch, EpochExtended, KTInfo, KTInfoSelfAudit, KTInfoToLS } from './interfaces'; 5 | import { SignedKeyList, SignedKeyListEpochs } from './helpers/interfaces/SignedKeyList'; 6 | import { 7 | getParsedSignedKeyLists, 8 | fetchProof, 9 | fetchEpoch, 10 | getVerifiedEpoch, 11 | uploadEpoch, 12 | fetchLastEpoch, 13 | } from './fetchHelper'; 14 | import { setItem, hasStorage, removeItem } from './helpers/storage'; 15 | import { getCanonicalEmailMap } from './helpers/api/canonicalEmailMap'; 16 | import { KT_STATUS, EXP_EPOCH_INTERVAL, KTError } from './constants'; 17 | import { SimpleMap } from './helpers/interfaces/utils'; 18 | import { 19 | checkSignature, 20 | getFromLS, 21 | getKTBlobs, 22 | getSignatureTime, 23 | isTimestampTooOld, 24 | parseKeyLists, 25 | removeFromLS, 26 | verifyCurrentEpoch, 27 | verifyEpoch, 28 | verifyKeyLists, 29 | } from './utils'; 30 | import { parseCertChain } from './certTransparency'; 31 | import { KeyPair } from './helpers/interfaces/Key'; 32 | 33 | export async function verifyPublicKeys( 34 | keyList: { 35 | Flags: number; 36 | PublicKey: string; 37 | }[], 38 | email: string, 39 | signedKeyList: SignedKeyListEpochs | undefined, 40 | api: Api 41 | ): Promise { 42 | if (!signedKeyList) { 43 | return { 44 | code: KT_STATUS.KTERROR_ADDRESS_NOT_IN_KT, 45 | error: 'Signed key list undefined', 46 | }; 47 | } 48 | 49 | let canonicalEmail: string | undefined; 50 | try { 51 | canonicalEmail = (await getCanonicalEmailMap([email], api))[email]; 52 | } catch (err) { 53 | return { code: KT_STATUS.KT_FAILED, error: err.message }; 54 | } 55 | if (!canonicalEmail) { 56 | return { 57 | code: KT_STATUS.KT_FAILED, 58 | error: `Failed to canonize email "${email}"`, 59 | }; 60 | } 61 | // Parse key lists 62 | const { signedKeyListData, parsedKeyList } = await parseKeyLists(keyList, signedKeyList.Data); 63 | 64 | // Check signature 65 | try { 66 | await checkSignature( 67 | signedKeyList.Data, 68 | parsedKeyList.map((key) => key.PublicKey), 69 | signedKeyList.Signature, 70 | 'SKL during PK verification' 71 | ); 72 | } catch (err) { 73 | return { code: KT_STATUS.KT_FAILED, error: err.message }; 74 | } 75 | 76 | // Check key list and signed key list 77 | try { 78 | await verifyKeyLists(parsedKeyList, signedKeyListData); 79 | } catch (error) { 80 | return { 81 | code: KT_STATUS.KT_FAILED, 82 | error: `Mismatch found between key list and signed key list. ${error.message}`, 83 | }; 84 | } 85 | 86 | // If signedKeyList is (allegedly) too young, users is warned and verification cannot continue 87 | if (!signedKeyList.MaxEpochID || signedKeyList.MaxEpochID === null) { 88 | return { 89 | code: KT_STATUS.KTERROR_MINEPOCHID_NULL, 90 | error: 'The keys were generated too recently to be included in key transparency', 91 | }; 92 | } 93 | 94 | // Verify latest epoch 95 | let maxEpoch: Epoch; 96 | try { 97 | maxEpoch = await fetchEpoch(signedKeyList.MaxEpochID, api); 98 | } catch (err) { 99 | let status = KT_STATUS.KT_FAILED; 100 | if (err.message === 'Leaf node does not exist') status = KT_STATUS.KTERROR_ADDRESS_NOT_IN_KT; 101 | return { code: status, error: err.message }; 102 | } 103 | 104 | let returnedDate: number; 105 | try { 106 | returnedDate = await verifyEpoch(maxEpoch, canonicalEmail, signedKeyList.Data, api); 107 | } catch (err) { 108 | return { code: KT_STATUS.KT_FAILED, error: err.message }; 109 | } 110 | 111 | if (isTimestampTooOld(returnedDate)) { 112 | return { 113 | code: KT_STATUS.KT_FAILED, 114 | error: 'Returned date is older than MAX_EPOCH_INTERVAL', 115 | }; 116 | } 117 | 118 | return { code: KT_STATUS.KT_PASSED, error: '' }; 119 | } 120 | 121 | export async function ktSelfAudit( 122 | apis: Api[], 123 | addresses: Address[], 124 | userKeys: { privateKey?: OpenPGPKey }[] | undefined 125 | ): Promise> { 126 | // silentApi is used to prevent red banner when a verified epoch is not found 127 | const [api, silentApi] = apis; 128 | 129 | // Initialise output 130 | const addressesToVerifiedEpochs: Map< 131 | string, 132 | { 133 | code: number; 134 | verifiedEpoch?: EpochExtended; 135 | error: string; 136 | } 137 | > = new Map(); 138 | 139 | // Canonize emails 140 | let canonicalEmailMap: SimpleMap | undefined; 141 | try { 142 | canonicalEmailMap = await getCanonicalEmailMap( 143 | addresses.map((address) => address.Email), 144 | api 145 | ); 146 | } catch (err) { 147 | canonicalEmailMap = undefined; 148 | } 149 | 150 | // Prepare user private key for localStorage decrypt 151 | const userKey = userKeys ? userKeys[0].privateKey : undefined; 152 | 153 | // Main loop through addresses 154 | for (let i = 0; i < addresses.length; i++) { 155 | // Parse info from address 156 | const address = addresses[i]; 157 | if (!canonicalEmailMap) { 158 | addressesToVerifiedEpochs.set(address.ID, { 159 | code: KT_STATUS.KT_FAILED, 160 | error: 'Failed to get canonized emails', 161 | }); 162 | continue; 163 | } 164 | const email = canonicalEmailMap[address.Email]; 165 | if (!email) { 166 | addressesToVerifiedEpochs.set(address.ID, { 167 | code: KT_STATUS.KT_FAILED, 168 | error: `Failed to canonize email ${address.Email}`, 169 | }); 170 | continue; 171 | } 172 | 173 | if (!address.SignedKeyList) { 174 | addressesToVerifiedEpochs.set(address.ID, { 175 | code: KT_STATUS.KT_FAILED, 176 | error: `Signed key list not found for ${address.Email}`, 177 | }); 178 | continue; 179 | } 180 | 181 | // Parse key lists 182 | const { signedKeyListData, parsedKeyList } = await parseKeyLists( 183 | address.Keys.map((key) => ({ 184 | Flags: key.Flags, 185 | PublicKey: key.PublicKey, 186 | })), 187 | address.SignedKeyList.Data 188 | ); 189 | 190 | // Check content of localStorage 191 | const ktBlobs = getFromLS(address.ID); 192 | let errorFlag = false; 193 | for (let i = 0; i < ktBlobs.length; i++) { 194 | const ktBlob = ktBlobs[i]; 195 | if (ktBlob) { 196 | // Decrypt and parse ktBlob 197 | if (!userKey) { 198 | addressesToVerifiedEpochs.set(address.ID, { 199 | code: KT_STATUS.KT_FAILED, 200 | error: `Missing primary user key`, 201 | }); 202 | errorFlag = true; 203 | break; 204 | } 205 | let decryptedBlob; 206 | try { 207 | decryptedBlob = JSON.parse( 208 | ( 209 | await decryptMessage({ 210 | message: await getMessage(ktBlob), 211 | privateKeys: userKey, 212 | }) 213 | ).data 214 | ); 215 | } catch (error) { 216 | addressesToVerifiedEpochs.set(address.ID, { 217 | code: KT_STATUS.KT_FAILED, 218 | error: `Decrytption of ktBlob in localStorage failed with error "${error.message}"`, 219 | }); 220 | errorFlag = true; 221 | break; 222 | } 223 | const { SignedKeyList: localSKL, EpochID: localEpochID } = decryptedBlob; 224 | const localSignature = await getSignature(localSKL.Signature); 225 | 226 | // Retrieve oldest SKL since localEpochID 227 | const fetchedSKLs = await getParsedSignedKeyLists(api, localEpochID, email, false); 228 | const includedSKLarray: SignedKeyListEpochs[] = await Promise.all( 229 | fetchedSKLs.filter(async (skl) => { 230 | const sklSignature = await getSignature(skl.Signature); 231 | return ( 232 | (!skl.MinEpochID || skl.MinEpochID === null || skl.MinEpochID > localEpochID) && 233 | getSignatureTime(sklSignature) >= getSignatureTime(localSignature) 234 | ); 235 | }) 236 | ); 237 | 238 | // If we are checking the first blob, then included SKL should be the oldest, otherwise it 239 | // should be the one immediately after. 240 | const includedSKL = includedSKLarray[i]; 241 | if (!includedSKL) { 242 | addressesToVerifiedEpochs.set(address.ID, { 243 | code: KT_STATUS.KT_FAILED, 244 | error: 'Included signed key list not found', 245 | }); 246 | errorFlag = true; 247 | break; 248 | } 249 | const includedSignature = await getSignature(includedSKL.Signature); 250 | 251 | if (isTimestampTooOld(getSignatureTime(localSignature), getSignatureTime(includedSignature))) { 252 | addressesToVerifiedEpochs.set(address.ID, { 253 | code: KT_STATUS.KT_FAILED, 254 | error: 255 | 'Signed key list in localStorage is older than included signed key list by more than MAX_EPOCH_INTERVAL', 256 | }); 257 | errorFlag = true; 258 | break; 259 | } 260 | 261 | // Check signature 262 | try { 263 | await checkSignature( 264 | includedSKL.Data, 265 | parsedKeyList.map((key) => key.PublicKey), 266 | includedSKL.Signature, 267 | 'Included SKL localStorage self-audit (NOTE: the correct key might have been deleted)' 268 | ); 269 | } catch (err) { 270 | addressesToVerifiedEpochs.set(address.ID, { 271 | code: KT_STATUS.KT_FAILED, 272 | error: err.message, 273 | }); 274 | errorFlag = true; 275 | break; 276 | } 277 | 278 | // If the includedSKL hasn't had time of entering an epoch, self-audit proceeds. 279 | // Otherwise, we check it's there. 280 | if (includedSKL.MinEpochID && includedSKL.MinEpochID !== null) { 281 | const minEpoch = await fetchEpoch(includedSKL.MinEpochID, api); 282 | 283 | const returnedDate = await verifyEpoch(minEpoch, email, includedSKL.Data, api); 284 | 285 | if (isTimestampTooOld(getSignatureTime(localSignature), returnedDate)) { 286 | addressesToVerifiedEpochs.set(address.ID, { 287 | code: KT_STATUS.KT_FAILED, 288 | error: 289 | 'Returned date is older than the signed key list in localStorage by more than MAX_EPOCH_INTERVAL', 290 | }); 291 | errorFlag = true; 292 | break; 293 | } 294 | 295 | try { 296 | removeFromLS(i, address.ID); 297 | } catch (err) { 298 | addressesToVerifiedEpochs.set(address.ID, { 299 | code: KT_STATUS.KT_FAILED, 300 | error: `Removing object from localStorag failed with error "${err.message}"`, 301 | }); 302 | errorFlag = true; 303 | break; 304 | } 305 | } else if (isTimestampTooOld(getSignatureTime(localSignature))) { 306 | addressesToVerifiedEpochs.set(address.ID, { 307 | code: KT_STATUS.KT_FAILED, 308 | error: 'Signed key list in localStorage is older than MAX_EPOCH_INTERVAL', 309 | }); 310 | errorFlag = true; 311 | break; 312 | } 313 | } 314 | } 315 | if (errorFlag) { 316 | continue; 317 | } 318 | 319 | // Check key list and signed key list 320 | try { 321 | await verifyKeyLists(parsedKeyList, signedKeyListData); 322 | } catch (error) { 323 | addressesToVerifiedEpochs.set(address.ID, { 324 | code: KT_STATUS.KT_FAILED, 325 | error: `Mismatch found between key list and signed key list. ${error.message}`, 326 | }); 327 | continue; 328 | } 329 | 330 | // Check signature 331 | try { 332 | await checkSignature( 333 | address.SignedKeyList.Data, 334 | parsedKeyList.map((key) => key.PublicKey), 335 | address.SignedKeyList.Signature, 336 | 'Fetched SKL elf-audit' 337 | ); 338 | } catch (err) { 339 | addressesToVerifiedEpochs.set(address.ID, { 340 | code: KT_STATUS.KT_FAILED, 341 | error: err.message, 342 | }); 343 | continue; 344 | } 345 | 346 | // If its MinEpochID is null, the SKL must be recent 347 | const signatureSKL = await getSignature(address.SignedKeyList.Signature); 348 | if (address.SignedKeyList.MinEpochID === null && isTimestampTooOld(getSignatureTime(signatureSKL))) { 349 | addressesToVerifiedEpochs.set(address.ID, { 350 | code: KT_STATUS.KT_FAILED, 351 | error: 'Signed key list is older than MAX_EPOCH_INTERVAL', 352 | }); 353 | continue; 354 | } 355 | 356 | // Fetch the last verified epoch. If there isn't any, the address is recent 357 | const verifiedEpoch = await getVerifiedEpoch(silentApi, address.ID); 358 | if (!verifiedEpoch) { 359 | // If the MinEpochID is null the address was created after the last epoch generation, therefore 360 | // self-audit is postponed at least until the SKL is included in the next epoch 361 | if (!address.SignedKeyList.MinEpochID || address.SignedKeyList.MinEpochID === null) { 362 | addressesToVerifiedEpochs.set(address.ID, { 363 | code: KT_STATUS.KT_WARNING, 364 | error: 'Signed key list has not been included in any epoch yet, self-audit is postponed', 365 | }); 366 | continue; 367 | } 368 | 369 | // Otherwise, verify the epoch corresponding to its MinEpochID. Note that this can be arbitrarly 370 | // in the past, but that's ok because if we verified the SKL was in MinEpochID, then by construction 371 | // it has been kept in the tree until the current epoch. 372 | const minEpoch = await fetchEpoch(address.SignedKeyList.MinEpochID, api); 373 | 374 | let returnedDate; 375 | try { 376 | returnedDate = await verifyEpoch(minEpoch, email, address.SignedKeyList.Data, api); 377 | } catch (err) { 378 | addressesToVerifiedEpochs.set(address.ID, { 379 | code: KT_STATUS.KT_FAILED, 380 | error: err.message, 381 | }); 382 | continue; 383 | } 384 | 385 | if (isTimestampTooOld(getSignatureTime(signatureSKL), returnedDate)) { 386 | addressesToVerifiedEpochs.set(address.ID, { 387 | code: KT_STATUS.KT_FAILED, 388 | error: 'MinEpochID certificate was issued more than MAX_EPOCH_INTERVAL after SKL generation', 389 | }); 390 | continue; 391 | } 392 | 393 | const { Revision }: { Revision: number } = await fetchProof(minEpoch.EpochID, email, api); 394 | 395 | const verifiedCurrent = { 396 | ...minEpoch, 397 | Revision, 398 | CertificateDate: returnedDate, 399 | } as EpochExtended; 400 | 401 | addressesToVerifiedEpochs.set(address.ID, { 402 | code: KT_STATUS.KT_PASSED, 403 | verifiedEpoch: verifiedCurrent, 404 | error: '', 405 | }); 406 | uploadEpoch(verifiedCurrent, address, api); 407 | continue; 408 | } 409 | const verifiedEpochData: { EpochID: number; ChainHash: string; CertificateDate: number } = JSON.parse( 410 | verifiedEpoch.Data 411 | ); 412 | 413 | // Check signature of verified epoch 414 | try { 415 | await checkSignature( 416 | verifiedEpoch.Data, 417 | parsedKeyList.map((key) => key.PublicKey), 418 | verifiedEpoch.Signature, 419 | 'Verified epoch self-audit' 420 | ); 421 | } catch (err) { 422 | addressesToVerifiedEpochs.set(address.ID, { 423 | code: KT_STATUS.KT_FAILED, 424 | error: err.message, 425 | }); 426 | continue; 427 | } 428 | 429 | // Fetch all new SKLs and corresponding epochs 430 | const newSKLs = await getParsedSignedKeyLists(api, verifiedEpochData.EpochID, email, true); 431 | 432 | // There can be at most three SKLs in newSKLs: 433 | // - the last one before verifiedEpochData.EpochID (i.e. the old one); 434 | // - a new SKL uploaded between verifiedEpochData.EpochID and (verifiedEpochData.EpochID + 1) 435 | // - a new SKL uploaded between (verifiedEpochData.EpochID + 1) and the current self-audit 436 | if (newSKLs.length === 0 || newSKLs.length > 3) { 437 | addressesToVerifiedEpochs.set(address.ID, { 438 | code: KT_STATUS.KT_FAILED, 439 | error: 'There should be between 1 and 3 fetched SKLs', 440 | }); 441 | continue; 442 | } 443 | 444 | // The epochs are fetched according to when SKLs changed. There could be at most one SKL such that 445 | // MinEpochID is null, which is excluded because it does not belong to any epoch. 446 | const newEpochs: EpochExtended[] = await Promise.all( 447 | newSKLs 448 | .filter((skl) => skl.MinEpochID !== null) 449 | .map(async (skl) => { 450 | const epoch = await fetchEpoch(skl.MinEpochID as number, api); 451 | 452 | const { Revision }: { Revision: number } = await fetchProof(epoch.EpochID, email, api); 453 | 454 | return { 455 | ...epoch, 456 | Revision, 457 | CertificateDate: 0, 458 | }; 459 | }) 460 | ); 461 | // NOTE: if the old SKL hadn't been changed in a while, the first element of newSKLs can be arbitrarily old, 462 | // therefore the epoch corresponding to its MinEpochID will be older than verifiedEpoch. 463 | 464 | // Check revision consistency 465 | let checkRevision = true; 466 | for (let j = 1; j < newEpochs.length; j++) { 467 | checkRevision = checkRevision && newEpochs[j].Revision === newEpochs[j - 1].Revision + 1; 468 | } 469 | if (!checkRevision) { 470 | addressesToVerifiedEpochs.set(address.ID, { 471 | code: KT_STATUS.KT_FAILED, 472 | error: 'Revisions for new signed key lists have not been incremented correctly', 473 | }); 474 | continue; 475 | } 476 | 477 | // If there aren't any new SKLs or if the latest SKL has MinEpochID equal to null, 478 | // then newEpochs will only have one element: the last one before verifiedEpochData.EpochID. 479 | if (newEpochs.length === 1) { 480 | // Extract the first SKL which, by construction of the getSignedKeyLists route, is the oldest 481 | const [oldSKL] = newSKLs; 482 | if (!oldSKL) { 483 | addressesToVerifiedEpochs.set(address.ID, { 484 | code: KT_STATUS.KT_FAILED, 485 | error: 'Existing SKL not found', 486 | }); 487 | continue; 488 | } 489 | 490 | // Verify current epoch 491 | let verifiedCurrent; 492 | try { 493 | verifiedCurrent = await verifyCurrentEpoch(oldSKL, email, api); 494 | } catch (err) { 495 | addressesToVerifiedEpochs.set(address.ID, { 496 | code: KT_STATUS.KT_FAILED, 497 | error: err.message, 498 | }); 499 | continue; 500 | } 501 | addressesToVerifiedEpochs.set(address.ID, { 502 | code: KT_STATUS.KT_PASSED, 503 | verifiedEpoch: verifiedCurrent, 504 | error: '', 505 | }); 506 | uploadEpoch(verifiedCurrent, address, api); 507 | continue; 508 | } 509 | 510 | // Extract the second SKL which, by construction of the getSignedKeyLists route, is immediately younger 511 | // or equal to the SKL given as input. 512 | const [, previousSKL] = newSKLs; 513 | if (!previousSKL) { 514 | addressesToVerifiedEpochs.set(address.ID, { 515 | code: KT_STATUS.KT_FAILED, 516 | error: 'Previous SKL not found', 517 | }); 518 | continue; 519 | } 520 | 521 | // Check all new SKL in the corresponding epoch. At this point, newEpochs has 2 or 3 522 | // elements, the first of which has to be ignored because is is the old SKL. 523 | errorFlag = false; 524 | for (let j = 1; j < newEpochs.length; j++) { 525 | const epochToVerify = newEpochs[j]; 526 | const previousEpoch = j === 1 ? verifiedEpochData : newEpochs[j - 1]; 527 | 528 | // Verify the newest epoch 529 | if (epochToVerify.EpochID <= previousEpoch.EpochID) { 530 | addressesToVerifiedEpochs.set(address.ID, { 531 | code: KT_STATUS.KT_FAILED, 532 | error: 'Current epoch is older than or equal to verified epoch', 533 | }); 534 | errorFlag = true; 535 | break; 536 | } 537 | 538 | const includedSKL = 539 | !address.SignedKeyList.MinEpochID || 540 | address.SignedKeyList.MinEpochID === null || 541 | address.SignedKeyList.MinEpochID > epochToVerify.EpochID 542 | ? previousSKL 543 | : address.SignedKeyList; 544 | 545 | if (!includedSKL) { 546 | addressesToVerifiedEpochs.set(address.ID, { 547 | code: KT_STATUS.KT_FAILED, 548 | error: 'Included SKL could not be defined', 549 | }); 550 | errorFlag = true; 551 | break; 552 | } 553 | 554 | epochToVerify.CertificateDate = await verifyEpoch(epochToVerify, email, includedSKL.Data, api); 555 | 556 | if ( 557 | epochToVerify.CertificateDate < previousEpoch.CertificateDate && 558 | isTimestampTooOld(previousEpoch.CertificateDate, epochToVerify.CertificateDate) 559 | ) { 560 | addressesToVerifiedEpochs.set(address.ID, { 561 | code: KT_STATUS.KT_FAILED, 562 | error: 'Certificate date control error', 563 | }); 564 | errorFlag = true; 565 | break; 566 | } 567 | 568 | if ( 569 | (!address.SignedKeyList.MinEpochID || 570 | address.SignedKeyList.MinEpochID === null || 571 | address.SignedKeyList.MinEpochID > epochToVerify.EpochID) && 572 | isTimestampTooOld(getSignatureTime(signatureSKL), epochToVerify.CertificateDate) 573 | ) { 574 | addressesToVerifiedEpochs.set(address.ID, { 575 | code: KT_STATUS.KT_FAILED, 576 | error: 577 | "The certificate date is older than signed key list's signature by more than MAX_EPOCH_INTERVAL", 578 | }); 579 | errorFlag = true; 580 | break; 581 | } 582 | } 583 | if (errorFlag) { 584 | continue; 585 | } 586 | 587 | // Check latest certificate is within acceptable range 588 | if (isTimestampTooOld(newEpochs[newEpochs.length - 1].CertificateDate, getSignatureTime(signatureSKL))) { 589 | addressesToVerifiedEpochs.set(address.ID, { 590 | code: KT_STATUS.KT_FAILED, 591 | error: 'Last certificate date is older than the last signed key list by more than MAX_EPOCH_INTERVAL', 592 | }); 593 | continue; 594 | } 595 | 596 | // Set output for current address 597 | addressesToVerifiedEpochs.set(address.ID, { 598 | code: KT_STATUS.KT_PASSED, 599 | verifiedEpoch: newEpochs[newEpochs.length - 1], 600 | error: '', 601 | }); 602 | uploadEpoch(newEpochs[newEpochs.length - 1], address, api); 603 | } 604 | 605 | return addressesToVerifiedEpochs; 606 | } 607 | 608 | export async function verifySelfAuditResult( 609 | address: Address | undefined, 610 | submittedSKL: SignedKeyList, 611 | ktSelfAuditResult: Map, 612 | lastSelfAudit: number, 613 | isRunning: boolean, 614 | api: Api 615 | ): Promise { 616 | if (!address) { 617 | throw new KTError('Address is undefined'); 618 | } 619 | 620 | if (isRunning) { 621 | throw new KTError('Self-audit is still running'); 622 | } 623 | 624 | if (Date.now() - lastSelfAudit > EXP_EPOCH_INTERVAL) { 625 | throw new KTError('Self-audit should run before proceeding'); 626 | } 627 | 628 | const ktResult = ktSelfAuditResult.get(address.ID); 629 | 630 | if (!ktResult) { 631 | throw new KTError(`${address.Email} was not audited`); 632 | } 633 | 634 | if (ktResult.code === KT_STATUS.KT_FAILED) { 635 | throw new KTError(`Self-audit failed for ${address.Email} with error "${ktResult.error}"`); 636 | } 637 | 638 | let verifiedEpoch; 639 | if (ktResult.code === KT_STATUS.KT_WARNING) { 640 | // Last epoch before address creation 641 | const lastEpochID = await fetchLastEpoch(api); 642 | const lastEpoch = await fetchEpoch(lastEpochID, api); 643 | const lastEpochCert = parseCertChain(lastEpoch.Certificate)[0]; 644 | verifiedEpoch = { 645 | ...lastEpoch, 646 | Revision: 0, 647 | CertificateDate: lastEpochCert.notBefore.toJSON().value.getTime(), 648 | }; 649 | } else { 650 | verifiedEpoch = ktResult.verifiedEpoch; 651 | } 652 | 653 | if (!verifiedEpoch) { 654 | throw new KTError('Verified epoch not found'); 655 | } 656 | 657 | if ( 658 | isTimestampTooOld(verifiedEpoch.CertificateDate, getSignatureTime(await getSignature(submittedSKL.Signature))) 659 | ) { 660 | throw new KTError(`Verified epoch for ${address.Email} has invalid CertificateDate`); 661 | } 662 | 663 | return { 664 | message: JSON.stringify({ 665 | EpochID: verifiedEpoch.EpochID, 666 | SignedKeyList: submittedSKL, 667 | }), 668 | addressID: address.ID, 669 | }; 670 | } 671 | 672 | export async function ktSaveToLS(messageObject: KTInfoToLS | undefined, userKeys: KeyPair[] | undefined, api: Api) { 673 | if (!messageObject) { 674 | throw new KTError('Message object not found'); 675 | } 676 | 677 | const { message, addressID } = messageObject; 678 | 679 | if (hasStorage()) { 680 | const userKey = userKeys ? userKeys[0].publicKey : undefined; 681 | if (!userKey) { 682 | throw new KTError('Missing primary user key'); 683 | } 684 | 685 | // Check if there is something in localStorage with counter either 0 or 1 and the previous or current epoch 686 | // Format is kt:{counter}:{addressID}:{epoch}, therefore splitKey = [kt, {counter}, {addressID}, {epoch}]. 687 | const ktBlobs = getKTBlobs(addressID); 688 | const currentEpoch = await fetchLastEpoch(api); 689 | let counter = 0; 690 | 691 | switch (ktBlobs.size) { 692 | case 0: 693 | break; 694 | case 1: { 695 | const key: string = ktBlobs.keys().next().value; 696 | const [counterBlob, , epochBlob] = key 697 | .split(':') 698 | .slice(1) 699 | .map((n) => +n); 700 | if (epochBlob > currentEpoch) { 701 | throw new KTError('Inconsistent data in localStorage'); 702 | } 703 | if (counterBlob !== 0) { 704 | removeItem(key); 705 | setItem(`kt:0:${addressID}:${epochBlob}`, ktBlobs.get(key) as string); 706 | } 707 | counter = epochBlob !== currentEpoch ? 1 : 0; 708 | break; 709 | } 710 | case 2: { 711 | for (const element of ktBlobs) { 712 | const [key] = element; 713 | const [counterBlob, , epochBlob] = key 714 | .split(':') 715 | .slice(1) 716 | .map((n) => +n); 717 | if (counterBlob === 0) { 718 | if (epochBlob >= currentEpoch) { 719 | throw new KTError('Inconsistent data in localStorage'); 720 | } 721 | } else { 722 | if (epochBlob > currentEpoch) { 723 | throw new KTError('Inconsistent data in localStorage'); 724 | } 725 | counter = 1; 726 | } 727 | } 728 | break; 729 | } 730 | default: 731 | throw new KTError('There are too many blobs in localStorage'); 732 | } 733 | 734 | // Save the new blob 735 | setItem( 736 | `kt:${counter}:${addressID}:${currentEpoch}`, 737 | ( 738 | await encryptMessage({ 739 | data: message, 740 | publicKeys: userKey, 741 | }) 742 | ).data 743 | ); 744 | } 745 | } 746 | -------------------------------------------------------------------------------- /lib/certificates.ts: -------------------------------------------------------------------------------- 1 | export const rootCertificates: { [key: string]: string } = { 2 | ISRGRootX1: 3 | '-----BEGIN CERTIFICATE-----\nMIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\nTzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\ncmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\nWhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\nZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\nMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\nh77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\nA5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\nT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\nB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\nB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\nKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\nOlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\njh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\nqHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\nrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\nHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\nhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\nubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\nNFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\nORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\nTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\njNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\noyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\nmRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\nemyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n-----END CERTIFICATE-----', 4 | ISRGRootX2: 5 | '-----BEGIN CERTIFICATE-----\nMIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw\nCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg\nR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00\nMDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT\nZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw\nEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW\n+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9\nItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T\nAQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI\nzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW\ntL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1\n/q4AaOeMSQ+2b1tbFfLn\n-----END CERTIFICATE-----', 6 | DSTRootCAX3: 7 | '-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----', 8 | }; 9 | 10 | export const ctLogs = [ 11 | { 12 | description: "Google 'Argon2020' log", 13 | log_id: 'sh4FzIuizYogTodm+Su5iiUgZ2va+nDnsklTLe+LkF4=', 14 | key: 15 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6Tx2p1yKY4015NyIYvdrk36es0uAc1zA4PQ+TGRY+3ZjUTIYY9Wyu+3q/147JG4vNVKLtDWarZwVqGkg6lAYzA==', 16 | url: 'https://ct.googleapis.com/logs/argon2020/', 17 | state: { 18 | usable: { 19 | timestamp: '2018-06-15T02:30:13Z', 20 | }, 21 | }, 22 | temporal_interval: { 23 | start_inclusive: '2020-01-01T00:00:00Z', 24 | end_exclusive: '2021-01-01T00:00:00Z', 25 | }, 26 | maximum_merge_delay: 86400, 27 | }, 28 | { 29 | description: "Google 'Argon2021' log", 30 | log_id: '9lyUL9F3MCIUVBgIMJRWjuNNExkzv98MLyALzE7xZOM=', 31 | key: 32 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETeBmZOrzZKo4xYktx9gI2chEce3cw/tbr5xkoQlmhB18aKfsxD+MnILgGNl0FOm0eYGilFVi85wLRIOhK8lxKw==', 33 | url: 'https://ct.googleapis.com/logs/argon2021/', 34 | state: { 35 | usable: { 36 | timestamp: '2018-06-15T02:30:13Z', 37 | }, 38 | }, 39 | temporal_interval: { 40 | start_inclusive: '2021-01-01T00:00:00Z', 41 | end_exclusive: '2022-01-01T00:00:00Z', 42 | }, 43 | maximum_merge_delay: 86400, 44 | }, 45 | { 46 | description: "Google 'Argon2022' log", 47 | log_id: 'KXm+8J45OSHwVnOfY6V35b5XfZxgCvj5TV0mXCVdx4Q=', 48 | key: 49 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeIPc6fGmuBg6AJkv/z7NFckmHvf/OqmjchZJ6wm2qN200keRDg352dWpi7CHnSV51BpQYAj1CQY5JuRAwrrDwg==', 50 | url: 'https://ct.googleapis.com/logs/argon2022/', 51 | state: { 52 | usable: { 53 | timestamp: '2019-12-17T18:38:01Z', 54 | }, 55 | }, 56 | temporal_interval: { 57 | start_inclusive: '2022-01-01T00:00:00Z', 58 | end_exclusive: '2023-01-01T00:00:00Z', 59 | }, 60 | maximum_merge_delay: 86400, 61 | }, 62 | { 63 | description: "Google 'Argon2023' log", 64 | log_id: '6D7Q2j71BjUy51covIlryQPTy9ERa+zraeF3fW0GvW4=', 65 | key: 66 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0JCPZFJOQqyEti5M8j13ALN3CAVHqkVM4yyOcKWCu2yye5yYeqDpEXYoALIgtM3TmHtNlifmt+4iatGwLpF3eA==', 67 | url: 'https://ct.googleapis.com/logs/argon2023/', 68 | state: { 69 | usable: { 70 | timestamp: '2019-12-17T18:38:01Z', 71 | }, 72 | }, 73 | temporal_interval: { 74 | start_inclusive: '2023-01-01T00:00:00Z', 75 | end_exclusive: '2024-01-01T00:00:00Z', 76 | }, 77 | maximum_merge_delay: 86400, 78 | }, 79 | { 80 | description: "Google 'Xenon2020' log", 81 | log_id: 'B7dcG+V9aP/xsMYdIxXHuuZXfFeUt2ruvGE6GmnTohw=', 82 | key: 83 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEZU75VqjyzSTgFZKAnWg1QeYfFFIRZTMK7q3kWWZsmHhQdrBYnHRZ3OA4kUeUx0JN+xX+dSgt1ruqUhhl7jOvmw==', 84 | url: 'https://ct.googleapis.com/logs/xenon2020/', 85 | state: { 86 | usable: { 87 | timestamp: '2019-06-17T21:23:01Z', 88 | }, 89 | }, 90 | temporal_interval: { 91 | start_inclusive: '2020-01-01T00:00:00Z', 92 | end_exclusive: '2021-01-01T00:00:00Z', 93 | }, 94 | maximum_merge_delay: 86400, 95 | }, 96 | { 97 | description: "Google 'Xenon2021' log", 98 | log_id: 'fT7y+I//iFVoJMLAyp5SiXkrxQ54CX8uapdomX4i8Nc=', 99 | key: 100 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAER+1MInu8Q39BwDZ5Rp9TwXhwm3ktvgJzpk/r7dDgGk7ZacMm3ljfcoIvP1E72T8jvyLT1bvdapylajZcTH6W5g==', 101 | url: 'https://ct.googleapis.com/logs/xenon2021/', 102 | state: { 103 | usable: { 104 | timestamp: '2019-06-17T21:23:01Z', 105 | }, 106 | }, 107 | temporal_interval: { 108 | start_inclusive: '2021-01-01T00:00:00Z', 109 | end_exclusive: '2022-01-01T00:00:00Z', 110 | }, 111 | maximum_merge_delay: 86400, 112 | }, 113 | { 114 | description: "Google 'Xenon2022' log", 115 | log_id: 'RqVV63X6kSAwtaKJafTzfREsQXS+/Um4havy/HD+bUc=', 116 | key: 117 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+WS9FSxAYlCVEzg8xyGwOrmPonoV14nWjjETAIdZvLvukPzIWBMKv6tDNlQjpIHNrUcUt1igRPpqoKDXw2MeKw==', 118 | url: 'https://ct.googleapis.com/logs/xenon2022/', 119 | state: { 120 | usable: { 121 | timestamp: '2019-06-17T21:23:01Z', 122 | }, 123 | }, 124 | temporal_interval: { 125 | start_inclusive: '2022-01-01T00:00:00Z', 126 | end_exclusive: '2023-01-01T00:00:00Z', 127 | }, 128 | maximum_merge_delay: 86400, 129 | }, 130 | { 131 | description: "Google 'Xenon2023' log", 132 | log_id: 'rfe++nz/EMiLnT2cHj4YarRnKV3PsQwkyoWGNOvcgoo=', 133 | key: 134 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEchY+C+/vzj5g3ZXLY3q5qY1Kb2zcYYCmRV4vg6yU84WI0KV00HuO/8XuQqLwLZPjwtCymeLhQunSxgAnaXSuzg==', 135 | url: 'https://ct.googleapis.com/logs/xenon2023/', 136 | state: { 137 | usable: { 138 | timestamp: '2019-12-17T18:38:01Z', 139 | }, 140 | }, 141 | temporal_interval: { 142 | start_inclusive: '2023-01-01T00:00:00Z', 143 | end_exclusive: '2024-01-01T00:00:00Z', 144 | }, 145 | maximum_merge_delay: 86400, 146 | }, 147 | { 148 | description: "Google 'Aviator' log", 149 | log_id: 'aPaY+B9kgr46jO65KB1M/HFRXWeT1ETRCmesu09P+8Q=', 150 | key: 151 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1/TMabLkDpCjiupacAlP7xNi0I1JYP8bQFAHDG1xhtolSY1l4QgNRzRrvSe8liE+NPWHdjGxfx3JhTsN9x8/6Q==', 152 | url: 'https://ct.googleapis.com/aviator/', 153 | state: { 154 | readonly: { 155 | timestamp: '2016-11-30T13:24:18Z', 156 | final_tree_head: { 157 | sha256_root_hash: 'LcGcZRsm+LGYmrlyC5LXhV1T6OD8iH5dNlb0sEJl9bA=', 158 | tree_size: 46466472, 159 | }, 160 | }, 161 | }, 162 | maximum_merge_delay: 86400, 163 | }, 164 | { 165 | description: "Google 'Icarus' log", 166 | log_id: 'KTxRllTIOWW6qlD8WAfUt2+/WHopctykwwz05UVH9Hg=', 167 | key: 168 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETtK8v7MICve56qTHHDhhBOuV4IlUaESxZryCfk9QbG9co/CqPvTsgPDbCpp6oFtyAHwlDhnvr7JijXRD9Cb2FA==', 169 | url: 'https://ct.googleapis.com/icarus/', 170 | state: { 171 | usable: { 172 | timestamp: '2017-03-06T19:35:01Z', 173 | }, 174 | }, 175 | maximum_merge_delay: 86400, 176 | }, 177 | { 178 | description: "Google 'Pilot' log", 179 | log_id: 'pLkJkLQYWBSHuxOizGdwCjw1mAT5G9+443fNDsgN3BA=', 180 | key: 181 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA==', 182 | url: 'https://ct.googleapis.com/pilot/', 183 | state: { 184 | usable: { 185 | timestamp: '2014-09-02T20:41:44Z', 186 | }, 187 | }, 188 | maximum_merge_delay: 86400, 189 | }, 190 | { 191 | description: "Google 'Rocketeer' log", 192 | log_id: '7ku9t3XOYLrhQmkfq+GeZqMPfl+wctiDAMR7iXqo/cs=', 193 | key: 194 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg==', 195 | url: 'https://ct.googleapis.com/rocketeer/', 196 | state: { 197 | usable: { 198 | timestamp: '2015-08-04T19:00:05Z', 199 | }, 200 | }, 201 | maximum_merge_delay: 86400, 202 | }, 203 | { 204 | description: "Google 'Skydiver' log", 205 | log_id: 'u9nfvB+KcbWTlCOXqpJ7RzhXlQqrUugakJZkNo4e0YU=', 206 | key: 207 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEmyGDvYXsRJsNyXSrYc9DjHsIa2xzb4UR7ZxVoV6mrc9iZB7xjI6+NrOiwH+P/xxkRmOFG6Jel20q37hTh58rA==', 208 | url: 'https://ct.googleapis.com/skydiver/', 209 | state: { 210 | usable: { 211 | timestamp: '2017-03-06T19:35:01Z', 212 | }, 213 | }, 214 | maximum_merge_delay: 86400, 215 | }, 216 | { 217 | description: "Cloudflare 'Nimbus2020' Log", 218 | log_id: 'Xqdz+d9WwOe1Nkh90EngMnqRmgyEoRIShBh1loFxRVg=', 219 | key: 220 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01EAhx4o0zPQrXTcYjgCt4MVFsT0Pwjzb1RwrM0lhWDlxAYPP6/gyMCXNkOn/7KFsjL7rwk78tHMpY8rXn8AYg==', 221 | url: 'https://ct.cloudflare.com/logs/nimbus2020/', 222 | state: { 223 | usable: { 224 | timestamp: '2018-06-15T02:30:13Z', 225 | }, 226 | }, 227 | temporal_interval: { 228 | start_inclusive: '2020-01-01T00:00:00Z', 229 | end_exclusive: '2021-01-01T00:00:00Z', 230 | }, 231 | maximum_merge_delay: 86400, 232 | }, 233 | { 234 | description: "Cloudflare 'Nimbus2021' Log", 235 | log_id: 'RJRlLrDuzq/EQAfYqP4owNrmgr7YyzG1P9MzlrW2gag=', 236 | key: 237 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAExpon7ipsqehIeU1bmpog9TFo4Pk8+9oN8OYHl1Q2JGVXnkVFnuuvPgSo2Ep+6vLffNLcmEbxOucz03sFiematg==', 238 | url: 'https://ct.cloudflare.com/logs/nimbus2021/', 239 | state: { 240 | usable: { 241 | timestamp: '2018-06-15T02:30:13Z', 242 | }, 243 | }, 244 | temporal_interval: { 245 | start_inclusive: '2021-01-01T00:00:00Z', 246 | end_exclusive: '2022-01-01T00:00:00Z', 247 | }, 248 | maximum_merge_delay: 86400, 249 | }, 250 | { 251 | description: "Cloudflare 'Nimbus2022' Log", 252 | log_id: 'QcjKsd8iRkoQxqE6CUKHXk4xixsD6+tLx2jwkGKWBvY=', 253 | key: 254 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESLJHTlAycmJKDQxIv60pZG8g33lSYxYpCi5gteI6HLevWbFVCdtZx+m9b+0LrwWWl/87mkNN6xE0M4rnrIPA/w==', 255 | url: 'https://ct.cloudflare.com/logs/nimbus2022/', 256 | state: { 257 | usable: { 258 | timestamp: '2019-10-31T19:22:00Z', 259 | }, 260 | }, 261 | temporal_interval: { 262 | start_inclusive: '2022-01-01T00:00:00Z', 263 | end_exclusive: '2023-01-01T00:00:00Z', 264 | }, 265 | maximum_merge_delay: 86400, 266 | }, 267 | { 268 | description: "Cloudflare 'Nimbus2023' Log", 269 | log_id: 'ejKMVNi3LbYg6jjgUh7phBZwMhOFTTvSK8E6V6NS61I=', 270 | key: 271 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/8tkhjLRp0SXrlZdTzNkTd6HqmcmXiDJz3fAdWLgOhjmv4mohvRhwXul9bgW0ODgRwC9UGAgH/vpGHPvIS1qA==', 272 | url: 'https://ct.cloudflare.com/logs/nimbus2023/', 273 | state: { 274 | usable: { 275 | timestamp: '2019-10-31T19:22:00Z', 276 | }, 277 | }, 278 | temporal_interval: { 279 | start_inclusive: '2023-01-01T00:00:00Z', 280 | end_exclusive: '2024-01-01T00:00:00Z', 281 | }, 282 | maximum_merge_delay: 86400, 283 | }, 284 | { 285 | description: 'DigiCert Log Server', 286 | log_id: 'VhQGmi/XwuzT9eG9RLI+x0Z2ubyZEVzA75SYVdaJ0N0=', 287 | key: 288 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAkbFvhu7gkAW6MHSrBlpE1n4+HCFRkC5OLAjgqhkTH+/uzSfSl8ois8ZxAD2NgaTZe1M9akhYlrYkes4JECs6A==', 289 | url: 'https://ct1.digicert-ct.com/log/', 290 | state: { 291 | usable: { 292 | timestamp: '2015-05-20T16:40:09Z', 293 | }, 294 | }, 295 | maximum_merge_delay: 86400, 296 | }, 297 | { 298 | description: 'DigiCert Log Server 2', 299 | log_id: 'h3W/51l8+IxDmV+9827/Vo1HVjb/SrVgwbTq/16ggw8=', 300 | key: 301 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzF05L2a4TH/BLgOhNKPoioYCrkoRxvcmajeb8Dj4XQmNY+gxa4Zmz3mzJTwe33i0qMVp+rfwgnliQ/bM/oFmhA==', 302 | url: 'https://ct2.digicert-ct.com/log/', 303 | state: { 304 | retired: { 305 | timestamp: '2020-05-04T00:00:40Z', 306 | }, 307 | }, 308 | maximum_merge_delay: 86400, 309 | }, 310 | { 311 | description: 'DigiCert Yeti2020 Log', 312 | log_id: '8JWkWfIA0YJAEC0vk4iOrUv+HUfjmeHQNKawqKqOsnM=', 313 | key: 314 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEURAG+Zo0ac3n37ifZKUhBFEV6jfcCzGIRz3tsq8Ca9BP/5XUHy6ZiqsPaAEbVM0uI3Tm9U24RVBHR9JxDElPmg==', 315 | url: 'https://yeti2020.ct.digicert.com/log/', 316 | state: { 317 | usable: { 318 | timestamp: '2018-08-24T00:53:07Z', 319 | }, 320 | }, 321 | temporal_interval: { 322 | start_inclusive: '2020-01-01T00:00:00Z', 323 | end_exclusive: '2021-01-01T00:00:00Z', 324 | }, 325 | maximum_merge_delay: 86400, 326 | }, 327 | { 328 | description: 'DigiCert Yeti2021 Log', 329 | log_id: 'XNxDkv7mq0VEsV6a1FbmEDf71fpH3KFzlLJe5vbHDso=', 330 | key: 331 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6J4EbcpIAl1+AkSRsbhoY5oRTj3VoFfaf1DlQkfi7Rbe/HcjfVtrwN8jaC+tQDGjF+dqvKhWJAQ6Q6ev6q9Mew==', 332 | url: 'https://yeti2021.ct.digicert.com/log/', 333 | state: { 334 | usable: { 335 | timestamp: '2018-08-24T00:53:07Z', 336 | }, 337 | }, 338 | temporal_interval: { 339 | start_inclusive: '2021-01-01T00:00:00Z', 340 | end_exclusive: '2022-01-01T00:00:00Z', 341 | }, 342 | maximum_merge_delay: 86400, 343 | }, 344 | { 345 | description: 'DigiCert Yeti2022 Log', 346 | log_id: 'IkVFB1lVJFaWP6Ev8fdthuAjJmOtwEt/XcaDXG7iDwI=', 347 | key: 348 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEn/jYHd77W1G1+131td5mEbCdX/1v/KiYW5hPLcOROvv+xA8Nw2BDjB7y+RGyutD2vKXStp/5XIeiffzUfdYTJg==', 349 | url: 'https://yeti2022.ct.digicert.com/log/', 350 | state: { 351 | usable: { 352 | timestamp: '2018-08-24T00:53:07Z', 353 | }, 354 | }, 355 | temporal_interval: { 356 | start_inclusive: '2022-01-01T00:00:00Z', 357 | end_exclusive: '2023-01-01T00:00:00Z', 358 | }, 359 | maximum_merge_delay: 86400, 360 | }, 361 | { 362 | description: 'DigiCert Yeti2023 Log', 363 | log_id: 'Nc8ZG7+xbFe/D61MbULLu7YnICZR6j/hKu+oA8M71kw=', 364 | key: 365 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfQ0DsdWYitzwFTvG3F4Nbj8Nv5XIVYzQpkyWsU4nuSYlmcwrAp6m092fsdXEw6w1BAeHlzaqrSgNfyvZaJ9y0Q==', 366 | url: 'https://yeti2023.ct.digicert.com/log/', 367 | state: { 368 | usable: { 369 | timestamp: '2019-10-31T19:22:00Z', 370 | }, 371 | }, 372 | temporal_interval: { 373 | start_inclusive: '2023-01-01T00:00:00Z', 374 | end_exclusive: '2024-01-01T00:00:00Z', 375 | }, 376 | maximum_merge_delay: 86400, 377 | }, 378 | { 379 | description: 'DigiCert Nessie2020 Log', 380 | log_id: 'xlKg7EjOs/yrFwmSxDqHQTMJ6ABlomJSQBujNioXxWU=', 381 | key: 382 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4hHIyMVIrR9oShgbQMYEk8WX1lmkfFKB448Gn93KbsZnnwljDHY6MQqEnWfKGgMOq0gh3QK48c5ZB3UKSIFZ4g==', 383 | url: 'https://nessie2020.ct.digicert.com/log/', 384 | state: { 385 | usable: { 386 | timestamp: '2019-05-09T22:11:02Z', 387 | }, 388 | }, 389 | temporal_interval: { 390 | start_inclusive: '2020-01-01T00:00:00Z', 391 | end_exclusive: '2021-01-01T00:00:00Z', 392 | }, 393 | maximum_merge_delay: 86400, 394 | }, 395 | { 396 | description: 'DigiCert Nessie2021 Log', 397 | log_id: '7sCV7o1yZA+S48O5G8cSo2lqCXtLahoUOOZHssvtxfk=', 398 | key: 399 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9o7AiwrbGBIX6Lnc47I6OfLMdZnRzKoP5u072nBi6vpIOEooktTi1gNwlRPzGC2ySGfuc1xLDeaA/wSFGgpYFg==', 400 | url: 'https://nessie2021.ct.digicert.com/log/', 401 | state: { 402 | usable: { 403 | timestamp: '2019-05-09T22:11:02Z', 404 | }, 405 | }, 406 | temporal_interval: { 407 | start_inclusive: '2021-01-01T00:00:00Z', 408 | end_exclusive: '2022-01-01T00:00:00Z', 409 | }, 410 | maximum_merge_delay: 86400, 411 | }, 412 | { 413 | description: 'DigiCert Nessie2022 Log', 414 | log_id: 'UaOw9f0BeZxWbbg3eI8MpHrMGyfL956IQpoN/tSLBeU=', 415 | key: 416 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJyTdaAMoy/5jvg4RR019F2ihEV1McclBKMe2okuX7MCv/C87v+nxsfz1Af+p+0lADGMkmNd5LqZVqxbGvlHYcQ==', 417 | url: 'https://nessie2022.ct.digicert.com/log/', 418 | state: { 419 | usable: { 420 | timestamp: '2019-05-09T22:11:02Z', 421 | }, 422 | }, 423 | temporal_interval: { 424 | start_inclusive: '2022-01-01T00:00:00Z', 425 | end_exclusive: '2023-01-01T00:00:00Z', 426 | }, 427 | maximum_merge_delay: 86400, 428 | }, 429 | { 430 | description: 'DigiCert Nessie2023 Log', 431 | log_id: 's3N3B+GEUPhjhtYFqdwRCUp5LbFnDAuH3PADDnk2pZo=', 432 | key: 433 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEXu8iQwSCRSf2CbITGpUpBtFVt8+I0IU0d1C36Lfe1+fbwdaI0Z5FktfM2fBoI1bXBd18k2ggKGYGgdZBgLKTg==', 434 | url: 'https://nessie2023.ct.digicert.com/log/', 435 | state: { 436 | usable: { 437 | timestamp: '2019-10-31T19:22:00Z', 438 | }, 439 | }, 440 | temporal_interval: { 441 | start_inclusive: '2023-01-01T00:00:00Z', 442 | end_exclusive: '2024-01-01T00:00:00Z', 443 | }, 444 | maximum_merge_delay: 86400, 445 | }, 446 | { 447 | description: 'Symantec log', 448 | log_id: '3esdK3oNT6Ygi4GtgWhwfi6OnQHVXIiNPRHEzbbsvsw=', 449 | key: 450 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEluqsHEYMG1XcDfy1lCdGV0JwOmkY4r87xNuroPS2bMBTP01CEDPwWJePa75y9CrsHEKqAy8afig1dpkIPSEUhg==', 451 | url: 'https://ct.ws.symantec.com/', 452 | state: { 453 | retired: { 454 | timestamp: '2019-02-16T00:00:00Z', 455 | }, 456 | }, 457 | maximum_merge_delay: 86400, 458 | }, 459 | { 460 | description: "Symantec 'Vega' log", 461 | log_id: 'vHjh38X2PGhGSTNNoQ+hXwl5aSAJwIG08/aRfz7ZuKU=', 462 | key: 463 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6pWeAv/u8TNtS4e8zf0ZF2L/lNPQWQc/Ai0ckP7IRzA78d0NuBEMXR2G3avTK0Zm+25ltzv9WWis36b4ztIYTQ==', 464 | url: 'https://vega.ws.symantec.com/', 465 | state: { 466 | retired: { 467 | timestamp: '2019-02-16T00:00:00Z', 468 | }, 469 | }, 470 | maximum_merge_delay: 86400, 471 | }, 472 | { 473 | description: "Symantec 'Sirius' log", 474 | log_id: 'FZcEiNe5l6Bb61JRKt7o0ui0oxZSZBIan6v71fha2T8=', 475 | key: 476 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEowJkhCK7JewN47zCyYl93UXQ7uYVhY/Z5xcbE4Dq7bKFN61qxdglnfr0tPNuFiglN+qjN2Syxwv9UeXBBfQOtQ==', 477 | url: 'https://sirius.ws.symantec.com/', 478 | state: { 479 | retired: { 480 | timestamp: '2019-02-16T00:00:00Z', 481 | }, 482 | }, 483 | maximum_merge_delay: 86400, 484 | }, 485 | { 486 | description: 'Certly.IO log', 487 | log_id: 'zbUXm3/BwEb+6jETaj+PAC5hgvr4iW/syLL1tatgSQA=', 488 | key: 489 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECyPLhWKYYUgEc+tUXfPQB4wtGS2MNvXrjwFCCnyYJifBtd2Sk7Cu+Js9DNhMTh35FftHaHu6ZrclnNBKwmbbSA==', 490 | url: 'https://log.certly.io/', 491 | state: { 492 | retired: { 493 | timestamp: '2016-04-15T00:00:00Z', 494 | }, 495 | }, 496 | maximum_merge_delay: 86400, 497 | }, 498 | { 499 | description: 'Izenpe log', 500 | log_id: 'dGG0oJz7PUHXUVlXWy52SaRFqNJ3CbDMVkpkgrfrQaM=', 501 | key: 502 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ2Q5DC3cUBj4IQCiDu0s6j51up+TZAkAEcQRF6tczw90rLWXkJMAW7jr9yc92bIKgV8vDXU4lDeZHvYHduDuvg==', 503 | url: 'https://ct.izenpe.com/', 504 | state: { 505 | retired: { 506 | timestamp: '2016-05-30T00:00:00Z', 507 | }, 508 | }, 509 | maximum_merge_delay: 86400, 510 | }, 511 | { 512 | description: 'WoSign log', 513 | log_id: 'QbLcLonmPOSvG6e7Kb9oxt7m+fHMBH4w3/rjs7olkmM=', 514 | key: 515 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBGIey1my66PTTBmJxklIpMhRrQvAdPG+SvVyLpzmwai8IoCnNBrRhgwhbrpJIsO0VtwKAx+8TpFf1rzgkJgMQ==', 516 | url: 'https://ctlog.wosign.com/', 517 | state: { 518 | retired: { 519 | timestamp: '2018-02-12T23:59:59Z', 520 | }, 521 | }, 522 | maximum_merge_delay: 86400, 523 | }, 524 | { 525 | description: 'Venafi log', 526 | log_id: 'rDua7X+pZ0dXFZ5tfVdWcvnZgQCUHpve/+yhMTt1eC0=', 527 | key: 528 | 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAolpIHxdSlTXLo1s6H1OCdpSj/4DyHDc8wLG9wVmLqy1lk9fz4ATVmm+/1iN2Nk8jmctUKK2MFUtlWXZBSpym97M7frGlSaQXUWyA3CqQUEuIJOmlEjKTBEiQAvpfDjCHjlV2Be4qTM6jamkJbiWtgnYPhJL6ONaGTiSPm7Byy57iaz/hbckldSOIoRhYBiMzeNoA0DiRZ9KmfSeXZ1rB8y8X5urSW+iBzf2SaOfzBvDpcoTuAaWx2DPazoOl28fP1hZ+kHUYvxbcMjttjauCFx+JII0dmuZNIwjfeG/GBb9frpSX219k1O4Wi6OEbHEr8at/XQ0y7gTikOxBn/s5wQIDAQAB', 529 | url: 'https://ctlog.api.venafi.com/', 530 | state: { 531 | retired: { 532 | timestamp: '2017-02-28T18:42:26Z', 533 | }, 534 | }, 535 | maximum_merge_delay: 86400, 536 | }, 537 | { 538 | description: 'CNNIC CT log', 539 | log_id: 'pXesnO11SN2PAltnokEInfhuD0duwgPC7L7bGF8oJjg=', 540 | key: 541 | 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7UIYZopMgTTJWPp2IXhhuAf1l6a9zM7gBvntj5fLaFm9pVKhKYhVnno94XuXeN8EsDgiSIJIj66FpUGvai5samyetZhLocRuXhAiXXbDNyQ4KR51tVebtEq2zT0mT9liTtGwiksFQccyUsaVPhsHq9gJ2IKZdWauVA2Fm5x9h8B9xKn/L/2IaMpkIYtd967TNTP/dLPgixN1PLCLaypvurDGSVDsuWabA3FHKWL9z8wr7kBkbdpEhLlg2H+NAC+9nGKx+tQkuhZ/hWR65aX+CNUPy2OB9/u2rNPyDydb988LENXoUcMkQT0dU3aiYGkFAY0uZjD2vH97TM20xYtNQIDAQAB', 542 | url: 'https://ctserver.cnnic.cn/', 543 | state: { 544 | retired: { 545 | timestamp: '2018-09-18T00:00:00Z', 546 | }, 547 | }, 548 | maximum_merge_delay: 86400, 549 | }, 550 | { 551 | description: 'StartCom log', 552 | log_id: 'NLtq1sPfnAPuqKSZ/3iRSGydXlysktAfe/0bzhnbSO8=', 553 | key: 554 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESPNZ8/YFGNPbsu1Gfs/IEbVXsajWTOaft0oaFIZDqUiwy1o/PErK38SCFFWa+PeOQFXc9NKv6nV0+05/YIYuUQ==', 555 | url: 'https://ct.startssl.com/', 556 | state: { 557 | retired: { 558 | timestamp: '2018-02-12T23:59:59Z', 559 | }, 560 | }, 561 | maximum_merge_delay: 86400, 562 | }, 563 | { 564 | description: "Sectigo 'Sabre' CT log", 565 | log_id: 'VYHUwhaQNgFK6gubVzxT8MDkOHhwJQgXL6OqHQcT0ww=', 566 | key: 567 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8m/SiQ8/xfiHHqtls9m7FyOMBg4JVZY9CgiixXGz0akvKD6DEL8S0ERmFe9U4ZiA0M4kbT5nmuk3I85Sk4bagA==', 568 | url: 'https://sabre.ct.comodo.com/', 569 | state: { 570 | usable: { 571 | timestamp: '2017-10-10T00:38:10Z', 572 | }, 573 | }, 574 | maximum_merge_delay: 86400, 575 | }, 576 | { 577 | description: "Sectigo 'Mammoth' CT log", 578 | log_id: 'b1N2rDHwMRnYmQCkURX/dxUcEdkCwQApBo2yCJo32RM=', 579 | key: 580 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7+R9dC4VFbbpuyOL+yy14ceAmEf7QGlo/EmtYU6DRzwat43f/3swtLr/L8ugFOOt1YU/RFmMjGCL17ixv66MZw==', 581 | url: 'https://mammoth.ct.comodo.com/', 582 | state: { 583 | usable: { 584 | timestamp: '2017-10-10T00:38:10Z', 585 | }, 586 | }, 587 | maximum_merge_delay: 86400, 588 | }, 589 | { 590 | description: "Let's Encrypt 'Oak2020' log", 591 | log_id: '5xLysDd+GmL7jskMYYTx6ns3y1YdESZb8+DzS/JBVG4=', 592 | key: 593 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfzb42Zdr/h7hgqgDCo1vrNJqGqbcUvJGJEER9DDqp19W/wFSB0l166hD+U5cAXchpH8ZkBNUuvOHS0OnJ4oJrQ==', 594 | url: 'https://oak.ct.letsencrypt.org/2020/', 595 | state: { 596 | usable: { 597 | timestamp: '2020-01-27T18:18:26Z', 598 | }, 599 | }, 600 | temporal_interval: { 601 | start_inclusive: '2020-01-01T00:00:00Z', 602 | end_exclusive: '2021-01-07T00:00:00Z', 603 | }, 604 | maximum_merge_delay: 86400, 605 | }, 606 | { 607 | description: "Let's Encrypt 'Oak2021' log", 608 | log_id: 'lCC8Ho7VjWyIcx+CiyIsDdHaTV5sT5Q9YdtOL1hNosI=', 609 | key: 610 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAELsYzGMNwo8rBIlaklBIdmD2Ofn6HkfrjK0Ukz1uOIUC6Lm0jTITCXhoIdjs7JkyXnwuwYiJYiH7sE1YeKu8k9w==', 611 | url: 'https://oak.ct.letsencrypt.org/2021/', 612 | state: { 613 | usable: { 614 | timestamp: '2020-01-27T18:18:26Z', 615 | }, 616 | }, 617 | temporal_interval: { 618 | start_inclusive: '2021-01-01T00:00:00Z', 619 | end_exclusive: '2022-01-07T00:00:00Z', 620 | }, 621 | maximum_merge_delay: 86400, 622 | }, 623 | { 624 | description: "Let's Encrypt 'Oak2022' log", 625 | log_id: '36Veq2iCTx9sre64X04+WurNohKkal6OOxLAIERcKnM=', 626 | key: 627 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEhjyxDVIjWt5u9sB/o2S8rcGJ2pdZTGA8+IpXhI/tvKBjElGE5r3de4yAfeOPhqTqqc+o7vPgXnDgu/a9/B+RLg==', 628 | url: 'https://oak.ct.letsencrypt.org/2022/', 629 | state: { 630 | usable: { 631 | timestamp: '2020-01-27T18:18:26Z', 632 | }, 633 | }, 634 | temporal_interval: { 635 | start_inclusive: '2022-01-01T00:00:00Z', 636 | end_exclusive: '2023-01-07T00:00:00Z', 637 | }, 638 | maximum_merge_delay: 86400, 639 | }, 640 | { 641 | description: "Let's Encrypt 'Oak2023' log", 642 | log_id: 'tz77JN+cTbp18jnFulj0bF38Qs96nzXEnh0JgSXttJk=', 643 | key: 644 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsz0OeL7jrVxEXJu+o4QWQYLKyokXHiPOOKVUL3/TNFFquVzDSer7kZ3gijxzBp98ZTgRgMSaWgCmZ8OD74mFUQ==', 645 | url: 'https://oak.ct.letsencrypt.org/2023/', 646 | state: { 647 | qualified: { 648 | timestamp: '2020-08-18T00:00:00Z', 649 | }, 650 | }, 651 | temporal_interval: { 652 | start_inclusive: '2023-01-01T00:00:00Z', 653 | end_exclusive: '2024-01-07T00:00:00Z', 654 | }, 655 | maximum_merge_delay: 86400, 656 | }, 657 | { 658 | description: 'Trust Asia Log2020', 659 | log_id: 'pZWUO1NwvukG4AUNH7W7xqQOZfJlroUsdjY/rbIzNu0=', 660 | key: 661 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbsWC7ukn2WYOMxTAcqL8gMRZEQTZF9+Ho1MB9WLhHIaCHpHsJSx0DjJdVILW9mtM5xZtWywMWMQ9/R3OBgQEXQ==', 662 | url: 'https://ct.trustasia.com/log2020/', 663 | state: { 664 | qualified: { 665 | timestamp: '2020-08-18T00:00:00Z', 666 | }, 667 | }, 668 | temporal_interval: { 669 | start_inclusive: '2020-01-01T00:00:00Z', 670 | end_exclusive: '2021-01-01T00:00:00Z', 671 | }, 672 | maximum_merge_delay: 86400, 673 | }, 674 | { 675 | description: 'Trust Asia Log2021', 676 | log_id: 'Z422Wz50Q7bzo3DV4TqxtDvgoNNR98p0IlDHxvpRqIo=', 677 | key: 678 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEjwlzYzssDEG4DpPoOS73Ewsdohc0MzaohzRmUz9dih7Z8SHyyviKmnQL1KKfY6VGFnt0ulbVupzGXSaYUAoupA==', 679 | url: 'https://ct.trustasia.com/log2021/', 680 | state: { 681 | qualified: { 682 | timestamp: '2020-08-18T00:00:00Z', 683 | }, 684 | }, 685 | temporal_interval: { 686 | start_inclusive: '2021-01-01T00:00:00Z', 687 | end_exclusive: '2022-01-01T00:00:00Z', 688 | }, 689 | maximum_merge_delay: 86400, 690 | }, 691 | { 692 | description: 'Trust Asia Log2022', 693 | log_id: 'w2X5s2VPMoPHnamOk9dBj1ure+MlLJjh0vBLuetCfSM=', 694 | key: 695 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEu1LyFs+SC8555lRtwjdTpPX5OqmzBewdvRbsMKwu+HliNRWOGtgWLuRIa/bGE/GWLlwQ/hkeqBi4Dy3DpIZRlw==', 696 | url: 'https://ct.trustasia.com/log2022/', 697 | state: { 698 | qualified: { 699 | timestamp: '2020-08-18T00:00:00Z', 700 | }, 701 | }, 702 | temporal_interval: { 703 | start_inclusive: '2022-01-01T00:00:00Z', 704 | end_exclusive: '2023-01-01T00:00:00Z', 705 | }, 706 | maximum_merge_delay: 86400, 707 | }, 708 | { 709 | description: 'Trust Asia Log2023', 710 | log_id: '6H6nZgvCbPYALvVyXT/g4zG5OTu5L79Y6zuQSdr1Q1o=', 711 | key: 712 | 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpBFS2xdBTpDUVlESMFL4mwPPTJ/4Lji18Vq6+ji50o8agdqVzDPsIShmxlY+YDYhINnUrF36XBmhBX3+ICP89Q==', 713 | url: 'https://ct.trustasia.com/log2023/', 714 | state: { 715 | qualified: { 716 | timestamp: '2020-08-18T00:00:00Z', 717 | }, 718 | }, 719 | temporal_interval: { 720 | start_inclusive: '2023-01-01T00:00:00Z', 721 | end_exclusive: '2024-01-01T00:00:00Z', 722 | }, 723 | maximum_merge_delay: 86400, 724 | }, 725 | ]; 726 | --------------------------------------------------------------------------------