Skip to content

Commit 7066235

Browse files
committed
generate canary.py script to check signatures from other server.
1 parent ce9a7e7 commit 7066235

File tree

4 files changed

+91
-6
lines changed

4 files changed

+91
-6
lines changed

README.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ The content is encrypted with AES-256 in Python using PyCryptodome, and decrypte
2929
* ~~Rework password handling or inventory of some sort~~
3030
* ~~Rework crypto (PBKDF2 + AES256)~~
3131
* ~~Save the generated random keys instead of passwords to session( or local) storage~~
32-
* Sign generated generated and javascript files used in encrypted pages to make it more tamper proof
32+
* ~~Sign generated generated and javascript files used in encrypted pages to make it more tamper proof~~
3333
* ~~Add check for latin1 encoding in passwords, as it pycryptodome's implementation of PBKDF2 requires it~~
3434
* ~~find an equivalent way to define multiple passwords in the password inventory as global password~~
3535
* ~~make it possible to define passwords in external yaml file(s)~~
@@ -706,6 +706,37 @@ some_image_1_bb80db433751833b8f8b4ad23767c0fc.jpg
706706

707707
> The file name obfuscation is currently applied to the whole site - not just the encrypted pages...
708708

709+
### Signing of generated files
710+
711+
An attacker would most likely have a hard time brute forcing your encrypted content, given a good
712+
password entropy. It would be much easier for him to fish for passwords by modifying the
713+
generated pages, if he is able to hack your web space.
714+
715+
This feature will sign all encrypted pages and used javascript files with Ed25519. It will also generate
716+
an example [canary script](https://en.wikipedia.org/wiki/Domestic_canary#Miner's_canary), which can be
717+
customized to alert if files were modified.
718+
719+
> **NOTE** If Mkdocs is running with `mkdocs serve`, then signature verification of encrypted pages
720+
> will most likely fail, because the files are modified by Mkdocs to enable live reload.
721+
722+
```yaml
723+
sign_files: 'signatures.json'
724+
sign_key: 'encryptcontent.key' #optional
725+
canary_template_path: '/path/to/canary.tpl.py' #optional
726+
```
727+
728+
First an Ed25519 private key is generated at "encryptcontent.key" (besides `mkdocs.yml`), however you can supply an
729+
existing private key as long as it's in PEM format.
730+
731+
After generation the signatures are saved to "signatures.json" in `site_dir`, so this file also needs to be uploaded
732+
to the web space. The canary script will download this file and compare the URLs to its own list and then download
733+
all files and verify the signatures.
734+
735+
As long as the private key used for signing remains secret, the canary script will be able to determine
736+
if someone tampered with the files on the server. But you should run the canary script from another machine
737+
that is not related to the server, otherwise the attacker could also modify the canary script or sign with his
738+
private key instead.
739+
709740
# Contributing
710741

711742
From reporting a bug to submitting a pull request: every contribution is appreciated and welcome.

encryptcontent/canary.tpl.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import json
2+
import base64
3+
from Crypto.Hash import SHA512
4+
from Crypto.PublicKey import ECC
5+
from Crypto.Signature import eddsa
6+
from urllib.request import urlopen
7+
8+
publickey = """{{ public_key }}"""
9+
10+
signature_url = '{{ signature_url }}'
11+
12+
urls_to_verify = {{ urls_to_verify }}
13+
14+
def __verify_url__(url, signature, verifier):
15+
h = SHA512.new()
16+
with urlopen(url) as response:
17+
h.update(response.read())
18+
try:
19+
verifier.verify(h, signature)
20+
return True
21+
except ValueError:
22+
return False
23+
24+
verifier = eddsa.new(ECC.import_key(publickey), 'rfc8032')
25+
26+
print("NOTE: Checking of generated pages while 'mkdocs serve' will fail, because they are modified with the livereload script")
27+
28+
try:
29+
with urlopen(signature_url) as response:
30+
signatures = json.loads(response.read())
31+
for url in urls_to_verify:
32+
if url not in signatures.keys():
33+
print(url + ": MISSING!") # signature_url modified or just need to recreate canary script?
34+
else:
35+
if __verify_url__(url, base64.b64decode(signatures[url]), verifier):
36+
print(url + ": ok")
37+
else:
38+
print(url + ": FAILED!") # file modified!
39+
except:
40+
print("Couldn't download signatures!")

encryptcontent/plugin.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class encryptContentPlugin(BasePlugin):
8484
('html_extra_vars', config_options.Type(dict, default={})),
8585
('js_template_path', config_options.Type(string_types, default=str(os.path.join(PLUGIN_DIR, 'decrypt-contents.tpl.js')))),
8686
('js_extra_vars', config_options.Type(dict, default={})),
87+
('canary_template_path', config_options.Type(string_types, default=str(os.path.join(PLUGIN_DIR, 'canary.tpl.py')))),
8788
# others features
8889
('encrypted_something', config_options.Type(dict, default={})),
8990
('search_index', config_options.Choice(('clear', 'dynamically', 'encrypted'), default='encrypted')),
@@ -315,6 +316,9 @@ def on_config(self, config, **kwargs):
315316
logger.debug('Load JS template from file: "{file}".'.format(file=str(self.config['js_template_path'])))
316317
with open(self.config['js_template_path'], 'r') as template_js:
317318
self.setup['js_template'] = template_js.read()
319+
logger.debug('Load canary template from file: "{file}".'.format(file=str(self.config['canary_template_path'])))
320+
with open(self.config['canary_template_path'], 'r') as template_html:
321+
self.setup['canary_template'] = template_html.read()
318322

319323
# Check if hljs feature need to be enabled, based on theme configuration
320324
if ('highlightjs' in config['theme']._vars
@@ -361,6 +365,8 @@ def on_config(self, config, **kwargs):
361365
# Get path to site in case of subdir in site_url
362366
self.setup['site_path'] = urlsplit(config.data["site_url"] or '/').path[1::]
363367

368+
self.setup['config_path'] = Path(config['config_file_path']).parents[0]
369+
364370
self.setup['search_plugin_found'] = False
365371
encryptcontent_plugin_found = False
366372
for plugin in config['plugins']:
@@ -421,8 +427,7 @@ def on_config(self, config, **kwargs):
421427
self.setup['level_keystore'][level] = new_entry
422428

423429
if self.config['sign_files']:
424-
configpath = Path(config['config_file_path']).parents[0]
425-
sign_key_path = configpath.joinpath(self.config['sign_key'])
430+
sign_key_path = self.setup['config_path'].joinpath(self.config['sign_key'])
426431
if not exists(sign_key_path):
427432
logger.info('Generating signing key and saving to "{file}".'.format(file=str(self.config['sign_key'])))
428433
key = ECC.generate(curve='Ed25519')
@@ -470,8 +475,7 @@ def on_pre_build(self, config, **kwargs):
470475
if self.config['selfhost'] and self.config['selfhost_download']:
471476
logger.info('Downloading cryptojs for self-hosting (if needed)...')
472477
if self.config['selfhost_dir']:
473-
configpath = Path(config['config_file_path']).parents[0]
474-
dlpath = configpath.joinpath(self.config['selfhost_dir'] + '/assets/javascripts/cryptojs/')
478+
dlpath = self.setup['config_path'].joinpath(self.config['selfhost_dir'] + '/assets/javascripts/cryptojs/')
475479
else:
476480
dlpath = Path(config.data['docs_dir'] + '/assets/javascripts/cryptojs/')
477481
dlpath.mkdir(parents=True, exist_ok=True)
@@ -774,7 +778,7 @@ def on_post_build(self, config, **kwargs):
774778
new_entry = {}
775779
if self.config['selfhost']:
776780
new_entry['file'] = Path(config.data["site_dir"] + '/assets/javascripts/cryptojs/' + jsurl[0].rsplit('/',1)[1])
777-
new_entry['url'] = config.data["site_url"] + '/assets/javascripts/cryptojs/' + jsurl[0].rsplit('/',1)[1]
781+
new_entry['url'] = config.data["site_url"] + 'assets/javascripts/cryptojs/' + jsurl[0].rsplit('/',1)[1]
778782
else:
779783
new_entry['file'] = ""
780784
new_entry['url'] = "https:" + jsurl[0]
@@ -838,3 +842,12 @@ def on_post_build(self, config, **kwargs):
838842
sign_file_path = Path(config.data["site_dir"] + '/' + self.config['sign_files'])
839843
with open(sign_file_path, "w") as file:
840844
file.write(json.dumps(signatures))
845+
846+
canary_file = self.setup['config_path'].joinpath('canary.py')
847+
if not exists(canary_file):
848+
canary_py = Template(self.setup['canary_template']).render({
849+
'public_key': self.setup['sign_key'].public_key().export_key(format='PEM'),
850+
'signature_url' : config.data["site_url"] + self.config['sign_files']
851+
})
852+
with open(canary_file, 'w') as file:
853+
file.write(canary_py)

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def read(fname):
4949
package_data={'encryptcontent': [
5050
'*.tpl.js',
5151
'*.tpl.html',
52+
'*.tpl.py',
5253
'contrib/templates/search/*.js'
5354
]},
5455
include_package_data=True

0 commit comments

Comments
 (0)