diff --git a/.eslintignore b/.eslintignore index 5927bc153d..1ee6467b4f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1940,6 +1940,7 @@ packages/tools/update-readme-contributors.js packages/tools/update-readme-download.test.js packages/tools/update-readme-download.js packages/tools/update-readme-sponsors.js +packages/tools/updateCanary.js packages/tools/updateMarkdownDoc.js packages/tools/utils/discourse.test.js packages/tools/utils/discourse.js diff --git a/.gitignore b/.gitignore index 796b3ba3c0..7f9cfe57d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1913,6 +1913,7 @@ packages/tools/update-readme-contributors.js packages/tools/update-readme-download.test.js packages/tools/update-readme-download.js packages/tools/update-readme-sponsors.js +packages/tools/updateCanary.js packages/tools/updateMarkdownDoc.js packages/tools/utils/discourse.test.js packages/tools/utils/discourse.js diff --git a/Assets/keys/joplin-canary-signing-key.asc b/Assets/keys/joplin-canary-signing-key.asc new file mode 100644 index 0000000000..8d4c41c8c5 --- /dev/null +++ b/Assets/keys/joplin-canary-signing-key.asc @@ -0,0 +1,14 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEaZWFlBYJKwYBBAHaRw8BAQdAIh3xQbjaS0EC+8WuKXNPjVF/ayq0/2GZlheR +qj1G3Qe0RUpvcGxpbiBDYW5hcnkgU2lnbmluZyBLZXkgKFdhcnJhbnQgQ2FuYXJ5 +IEtleSkgPGNhbmFyeUBqb3BsaW5hcHAub3JnPoiZBBMWCgBBFiEE+CD4MG3QBaEC +0YzVlGrp+lkV71MFAmmVhZQCGwMFCQPCZwAFCwkIBwICIgIGFQoJCAsCBBYCAwEC +HgcCF4AACgkQlGrp+lkV71MZtwD/Ufd4OAcgkl5T6MSB+WDFg8BXvpaBZfNnZkoo +LrOoqNAA/iqGiiBRoarlus2ATOiWhyXaEpRUQcEeaRhhqHW0BGcCuDgEaZWFlBIK +KwYBBAGXVQEFAQEHQFORKWRLp4hDYzR8Q5IRyF9AIjoziR+sj4icUdvZx4Z6AwEI +B4h+BBgWCgAmFiEE+CD4MG3QBaEC0YzVlGrp+lkV71MFAmmVhZQCGwwFCQPCZwAA +CgkQlGrp+lkV71Nu+AD9Gw4qEmL8WNCNs7idc8CRpGpS2DhasNTV398lbKYzco0B +ANlMrGlMc0w1KhuFxdU4fF3s/ktUUnjJwosxK94l5/MJ +=C9VN +-----END PGP PUBLIC KEY BLOCK----- diff --git a/README.md b/README.md index 7cfcc7104e..06a9a89ac9 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,14 @@ Name | Description Please see the guide for information on how to contribute to the development of Joplin: https://github.com/laurent22/joplin/blob/dev/readme/dev/index.md +## Warrant Canary Signing Key + +Fingerprint: + +F820 F830 6DD0 05A1 02D1 8CD5 946A E9FA 5915 EF53 + +Public key: https://github.com/laurent22/joplin/raw/dev/Assets/keys/joplin-canary-signing-key.asc + # Contributors Thank you to everyone who've contributed to Joplin's source code! diff --git a/cspell.json b/cspell.json index c95390684c..9097ceaf10 100644 --- a/cspell.json +++ b/cspell.json @@ -63,6 +63,7 @@ "/readme/_i18n", "/readme/about/changelog/desktop.md", "/readme/licenses.md", + "/readme/canary.txt", "/readme/i18n", "cspell.json", "node_modules" diff --git a/package.json b/package.json index ef661f6b4b..0199c4b3e0 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "test": "yarn workspaces foreach --worktree --parallel --verbose --interlaced --jobs 2 run test", "tsc": "yarn workspaces foreach --worktree --parallel --verbose --interlaced run tsc", "updateIgnored": "node packages/tools/gulp/tasks/updateIgnoredTypeScriptBuildRun.js", + "updateCanary": "node ./packages/tools/updateCanary", "updateMarkdownDoc": "node ./packages/tools/updateMarkdownDoc", "updateNews": "node ./packages/tools/website/updateNews", "updatePluginTypes": "./packages/generator-joplin/updateTypes.sh", diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index 0d58152ccc..3fac75003f 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -250,4 +250,6 @@ mrjo codegen analyzed Perfetto -appmodules \ No newline at end of file +appmodules +armor +clearsign diff --git a/packages/tools/updateCanary.ts b/packages/tools/updateCanary.ts new file mode 100644 index 0000000000..a8790e2a5a --- /dev/null +++ b/packages/tools/updateCanary.ts @@ -0,0 +1,115 @@ +import { execFileSync } from 'child_process'; +import { writeFile, remove } from 'fs-extra'; +import { dirname, join } from 'path'; +import { createInterface, Interface as ReadlineInterface } from 'readline'; + +const rootDir = dirname(dirname(__dirname)); +const canaryFile = join(rootDir, 'readme', 'canary.txt'); + +function formatDate(date: Date): string { + const day = date.getUTCDate(); + const month = date.toLocaleString('en-US', { month: 'long', timeZone: 'UTC' }); + const year = date.getUTCFullYear(); + return `${day} ${month} ${year}`; +} + +function addDays(date: Date, days: number): Date { + const result = new Date(date); + result.setUTCDate(result.getUTCDate() + days); + return result; +} + +function prompt(rl: ReadlineInterface, question: string): Promise { + return new Promise(resolve => { + rl.question(question, answer => resolve(answer)); + }); +} + +async function promptNonEmpty(rl: ReadlineInterface, question: string): Promise { + while (true) { + const answer = (await prompt(rl, question)).trim(); + if (answer) return answer; + console.log('This field cannot be empty. Please try again.'); + } +} + +function buildCanaryContent(statementDate: Date, headline1: string, headline2: string): string { + const validUntil = addDays(statementDate, 60); + return `Joplin Warrant Canary + +Statement date: ${formatDate(statementDate)} +Valid until: ${formatDate(validUntil)} + +This warrant canary is updated every 60 days. +If this document has not been updated within 75 days of the +Statement date above, it should be considered expired. + +As of the Statement date: + +* No National Security Letters have been received by the project or its maintainer. +* No orders under the USA PATRIOT Act or the Foreign Intelligence Surveillance Act have been received by the project or its maintainer. +* No government request accompanied by a gag order has been received by the project or its maintainer. +* No government agency or law enforcement body has required the introduction of backdoors into Joplin software, infrastructure, or services. +* No government agency or law enforcement body has compelled the project or its maintainer to provide access to user data, servers, or infrastructure associated with Joplin. + +If any such order is received, and we are legally permitted to do so, +this statement will be updated accordingly. If we are not legally +permitted to disclose the existence of such an order, this statement +will not be updated. + +The public key is available at: + +https://github.com/laurent22/joplin/raw/dev/Assets/keys/joplin-canary-signing-key.asc + +To prevent backdating, this statement includes current public events: + +Current international events: + +1. ${headline1} +2. ${headline2} + +Canary signing key fingerprint: + +F820 F830 6DD0 05A1 02D1 8CD5 946A E9FA 5915 EF53 +`; +} + +async function main() { + const rl = createInterface({ input: process.stdin, output: process.stdout }); + + try { + console.log('Enter the current international event headlines to include in the canary.\n'); + const headline1 = await promptNonEmpty(rl, 'Headline 1: '); + const headline2 = await promptNonEmpty(rl, 'Headline 2: '); + + const statementDate = new Date(); + const content = buildCanaryContent(statementDate, headline1, headline2); + + const tmpFile = `${canaryFile}.tmp`; + await writeFile(tmpFile, content, 'utf8'); + execFileSync( + 'gpg', + [ + '--yes', + '--armor', + '--clearsign', + '--local-user', + 'canary@joplinapp.org', + '--output', + canaryFile, + tmpFile, + ], + { stdio: 'inherit' }, + ); + await remove(tmpFile); + + console.log(`Updated: ${canaryFile}`); + } finally { + rl.close(); + } +} + +main().catch(error => { + console.error(error.message || error); + process.exit(1); +}); diff --git a/readme/canary.md b/readme/canary.md new file mode 100644 index 0000000000..cf754abb1a --- /dev/null +++ b/readme/canary.md @@ -0,0 +1,83 @@ +# Warrant Canary + +This repository contains the official warrant canary for Joplin. + +The purpose of the warrant canary is to provide a regularly updated, cryptographically signed statement indicating that no secret legal orders, gag orders, or similar directives have been received as of the stated date. + +If such an order were ever received and disclosure were legally prohibited, the canary would cease to be updated. + +## Location of the Canary + +The current signed canary is published at: + +https://github.com/laurent22/joplin/raw/dev/readme/canary.txt + +## Canary Signing Key + +The canary is signed using a dedicated OpenPGP key. It is linked from the canary.txt file. + +Its fingerprint is present in the canary.txt file itself and duplicated at: + +https://github.com/laurent22/joplin/blob/dev/README.md + +## Updating the canary file + +Run `yarn updateCanary` from the root of the repository and follow the prompt. + +## Key Rotation Policy + +The canary signing key may be rotated for the following reasons: + +* Key expiry +* Suspected compromise +* Maintainer transition +* Operational upgrades (e.g. hardware-backed signing) + +Key rotation will never be performed silently. + +## Key Rotation Procedure + +### 1. Generate a New Key + +Create a new dedicated OpenPGP signing key. + +Export the new public key in ASCII-armoured format. + +### 2. Publish the New Key + +Add the new public key to: + +https://github.com/laurent22/joplin/raw/dev/Assets/keys/joplin-canary-signing-key.asc + +### 3. Update Documentation + +#### Update the README + +* Mark the new fingerprint as **Active** +* Mark the previous fingerprint as **Retired** +* Document the rotation date + +Example: + +``` +Active Canary Signing Key: +NEW FINGERPRINT + +Previous Key (retired 2028-02-18): +OLD FINGERPRINT +``` + +#### Update updateCanary.ts + +Add the new fingerprint to the canary template. + +### 4. Transitional Signing + +For the first canary issued after rotation: + +* Sign with the new key +* Optionally also sign with the old key + +This creates a cryptographic bridge between the two identities. + +If the old key is compromised, do not dual-sign. Instead, publish a revocation statement. diff --git a/readme/canary.txt b/readme/canary.txt new file mode 100644 index 0000000000..08dcfa2600 --- /dev/null +++ b/readme/canary.txt @@ -0,0 +1,46 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +Joplin Warrant Canary + +Statement date: 18 February 2026 +Valid until: 19 April 2026 + +This warrant canary is updated every 60 days. +If this document has not been updated within 75 days of the +Statement date above, it should be considered expired. + +As of the Statement date: + +* No National Security Letters have been received. +* No orders under the USA PATRIOT Act have been received. +* No orders under the Foreign Intelligence Surveillance Act have been received. +* No government requests have been received that are accompanied by a gag order. +* No law enforcement or government agency has required the introduction of backdoors into Joplin. + +If any such order is received, and we are legally permitted to do so, +this statement will be updated accordingly. If we are not legally +permitted to disclose the existence of such an order, this statement +will not be updated. + +The public key is available at: + +https://github.com/laurent22/joplin/blob/dev/Assets/keys/joplin-canary-signing-key.asc + +To prevent backdating, this statement includes current public events: + +Current international events: + +1. Philippine vice-president Sara Duterte announces 2028 presidential bid +2. African football chief ‘occupying seat illegally’ and must go, says leading executive + +Canary signing key fingerprint: + +F820 F830 6DD0 05A1 02D1 8CD5 946A E9FA 5915 EF53 +-----BEGIN PGP SIGNATURE----- + +iIsEARYKADMWIQT4IPgwbdAFoQLRjNWUaun6WRXvUwUCaZWUBBUcY2FuYXJ5QGpv +cGxpbmFwcC5vcmcACgkQlGrp+lkV71N8lwEAuRNboHOsr7/GbDjpEhkLrYOtzf2Y +3uUBB3YBbPabWvEA/RNCb0sUqFaKUT8Zeq/IsoLydsRsLMYUaUP19WIS/rQM +=UEz2 +-----END PGP SIGNATURE-----