Add pgp keyring management (#5)
This PR adds into the admin panel a front-end to manage PGP keys. Possibilities are many.
This commit is contained in:
parent
03a1e57de6
commit
8519b7fc0e
13 changed files with 634 additions and 4 deletions
|
@ -580,6 +580,63 @@ def smtp_relay_set():
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return (str(e), 500)
|
return (str(e), 500)
|
||||||
|
|
||||||
|
# PGP
|
||||||
|
|
||||||
|
@app.route('/system/pgp/', methods=["GET"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def get_keys():
|
||||||
|
from pgp import get_daemon_key, get_imported_keys, key_representation
|
||||||
|
return {
|
||||||
|
"daemon": key_representation(get_daemon_key()),
|
||||||
|
"imported": list(map(key_representation, get_imported_keys()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.route('/system/pgp/<fpr>', methods=["GET"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def get_key(fpr):
|
||||||
|
from pgp import get_key, key_representation
|
||||||
|
k = get_key(fpr)
|
||||||
|
if k is None:
|
||||||
|
abort(404)
|
||||||
|
return key_representation(k)
|
||||||
|
|
||||||
|
@app.route('/system/pgp/<fpr>', methods=["DELETE"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def delete_key(fpr):
|
||||||
|
from pgp import delete_key
|
||||||
|
try:
|
||||||
|
if delete_key(fpr) is None:
|
||||||
|
abort(404)
|
||||||
|
return "OK"
|
||||||
|
except ValueError as e:
|
||||||
|
return (str(e), 400)
|
||||||
|
|
||||||
|
@app.route('/system/pgp/<fpr>/export', methods=["GET"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def export_key(fpr):
|
||||||
|
from pgp import export_key
|
||||||
|
exp = export_key(fpr)
|
||||||
|
if exp is None:
|
||||||
|
abort(404)
|
||||||
|
return exp
|
||||||
|
|
||||||
|
@app.route('/system/pgp/import', methods=["POST"])
|
||||||
|
@authorized_personnel_only
|
||||||
|
def import_key():
|
||||||
|
from pgp import import_key
|
||||||
|
k = request.form.get('key')
|
||||||
|
try:
|
||||||
|
result = import_key(k)
|
||||||
|
return {
|
||||||
|
"keys_read": result.considered,
|
||||||
|
"keys_added": result.imported,
|
||||||
|
"keys_unchanged": result.unchanged,
|
||||||
|
"uids_added": result.new_user_ids,
|
||||||
|
"sigs_added": result.new_signatures,
|
||||||
|
"revs_added": result.new_revocations
|
||||||
|
}
|
||||||
|
except ValueError as e:
|
||||||
|
return (str(e), 400)
|
||||||
|
|
||||||
# MUNIN
|
# MUNIN
|
||||||
|
|
||||||
|
|
|
@ -21,5 +21,8 @@ management/backup.py 2>&1 | management/email_administrator.py "Backup Status"
|
||||||
# Provision any new certificates for new domains or domains with expiring certificates.
|
# Provision any new certificates for new domains or domains with expiring certificates.
|
||||||
management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result"
|
management/ssl_certificates.py -q 2>&1 | management/email_administrator.py "TLS Certificate Provisioning Result"
|
||||||
|
|
||||||
|
# Renew the daemon's PGP key if about to expire
|
||||||
|
management/pgp.py 2>&1 | management/email_administrator.py "PGP Key Renewal Result"
|
||||||
|
|
||||||
# Run status checks and email the administrator if anything changed.
|
# Run status checks and email the administrator if anything changed.
|
||||||
management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice"
|
management/status_checks.py --show-changes 2>&1 | management/email_administrator.py "Status Checks Change Notice"
|
||||||
|
|
|
@ -7,9 +7,12 @@ import sys
|
||||||
import html
|
import html
|
||||||
import smtplib
|
import smtplib
|
||||||
|
|
||||||
|
from email.mime.application import MIMEApplication
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
|
from pgp import create_signature
|
||||||
|
|
||||||
# In Python 3.6:
|
# In Python 3.6:
|
||||||
#from email.message import Message
|
#from email.message import Message
|
||||||
|
|
||||||
|
@ -45,6 +48,7 @@ content_html = "<html><body><pre>{}</pre></body></html>".format(html.escape(cont
|
||||||
|
|
||||||
msg.attach(MIMEText(content, 'plain'))
|
msg.attach(MIMEText(content, 'plain'))
|
||||||
msg.attach(MIMEText(content_html, 'html'))
|
msg.attach(MIMEText(content_html, 'html'))
|
||||||
|
msg.attach(MIMEApplication(create_signature(content.encode()), Name="signed.asc"))
|
||||||
|
|
||||||
# In Python 3.6:
|
# In Python 3.6:
|
||||||
#msg.set_content(content)
|
#msg.set_content(content)
|
||||||
|
|
125
management/pgp.py
Executable file
125
management/pgp.py
Executable file
|
@ -0,0 +1,125 @@
|
||||||
|
#!/usr/local/lib/mailinabox/env/bin/python
|
||||||
|
# Tools to manipulate PGP keys
|
||||||
|
|
||||||
|
import gpg, utils, datetime
|
||||||
|
|
||||||
|
env = utils.load_environment()
|
||||||
|
|
||||||
|
# Import daemon's keyring - usually in /home/user-data/.gnupg/
|
||||||
|
gpghome = env['GNUPGHOME']
|
||||||
|
daemon_key_fpr = env['PGPKEY']
|
||||||
|
context = gpg.Context(armor=True, home_dir=gpghome)
|
||||||
|
|
||||||
|
# Global auxiliary lookup tables
|
||||||
|
crpyt_algos = {
|
||||||
|
0: "Unknown",
|
||||||
|
gpg.constants.PK_RSA: "RSA",
|
||||||
|
gpg.constants.PK_RSA_E: "RSA-E",
|
||||||
|
gpg.constants.PK_RSA_S: "RSA-S",
|
||||||
|
gpg.constants.PK_ELG_E: "ELG-E",
|
||||||
|
gpg.constants.PK_DSA: "DSA",
|
||||||
|
gpg.constants.PK_ECC: "ECC",
|
||||||
|
gpg.constants.PK_ELG: "ELG",
|
||||||
|
gpg.constants.PK_ECDSA: "ECDSA",
|
||||||
|
gpg.constants.PK_ECDH: "ECDH",
|
||||||
|
gpg.constants.PK_EDDSA: "EDDSA"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auxiliary function to process the key in order to be read more conveniently
|
||||||
|
def key_representation(key):
|
||||||
|
if key is None:
|
||||||
|
return None
|
||||||
|
key_rep = {
|
||||||
|
"master_fpr": key.fpr,
|
||||||
|
"revoked": key.revoked != 0,
|
||||||
|
"ids": [],
|
||||||
|
"subkeys": []
|
||||||
|
}
|
||||||
|
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
key_rep["ids"] = [ id.uid for id in key.uids ]
|
||||||
|
key_rep["subkeys"] = [{
|
||||||
|
"master": skey.fpr == key.fpr,
|
||||||
|
"sign": skey.can_sign == 1,
|
||||||
|
"cert": skey.can_certify == 1,
|
||||||
|
"encr": skey.can_encrypt == 1,
|
||||||
|
"auth": skey.can_authenticate == 1,
|
||||||
|
"fpr": skey.fpr,
|
||||||
|
"expires": skey.expires if skey.expires != 0 else None,
|
||||||
|
"expires_date": datetime.datetime.utcfromtimestamp(skey.expires).strftime("%x") if skey.expires != 0 else None,
|
||||||
|
"expires_days": (datetime.datetime.utcfromtimestamp(skey.expires) - now).days if skey.expires != 0 else None,
|
||||||
|
"expired": skey.expired == 1,
|
||||||
|
"algorithm": crpyt_algos[skey.pubkey_algo] if skey.pubkey_algo in crpyt_algos.keys() else crpyt_algos[0],
|
||||||
|
"bits": skey.length
|
||||||
|
} for skey in key.subkeys ]
|
||||||
|
|
||||||
|
return key_rep
|
||||||
|
|
||||||
|
# Tests an import as for whether we have any sort of private key material in our import
|
||||||
|
def contains_private_keys(imports):
|
||||||
|
import tempfile
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
with gpg.Context(home_dir=tmpdir, armor=True) as tmp:
|
||||||
|
result = tmp.key_import(imports)
|
||||||
|
return result.secret_read != 0
|
||||||
|
|
||||||
|
def get_key(fingerprint):
|
||||||
|
try:
|
||||||
|
return context.get_key(fingerprint, secret=False)
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_daemon_key():
|
||||||
|
if daemon_key_fpr is None or daemon_key_fpr == "":
|
||||||
|
return None
|
||||||
|
return context.get_key(daemon_key_fpr, secret=True)
|
||||||
|
|
||||||
|
def get_imported_keys():
|
||||||
|
# All the keys in the keyring, except for the daemon's key
|
||||||
|
return list(
|
||||||
|
filter(
|
||||||
|
lambda k: k.fpr != daemon_key_fpr,
|
||||||
|
context.keylist(secret=False)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def import_key(key):
|
||||||
|
data = str.encode(key)
|
||||||
|
if contains_private_keys(data):
|
||||||
|
raise ValueError("Import cannot contain private keys!")
|
||||||
|
return context.key_import(data)
|
||||||
|
|
||||||
|
def export_key(fingerprint):
|
||||||
|
if get_key(fingerprint) is None:
|
||||||
|
return None
|
||||||
|
return context.key_export(pattern=fingerprint) # Key does exist, export it!
|
||||||
|
|
||||||
|
def delete_key(fingerprint):
|
||||||
|
key = get_key(fingerprint)
|
||||||
|
if fingerprint == daemon_key_fpr:
|
||||||
|
raise ValueError("You cannot delete the daemon's key!")
|
||||||
|
elif key is None:
|
||||||
|
return None
|
||||||
|
context.op_delete_ext(key, gpg.constants.DELETE_ALLOW_SECRET | gpg.constants.DELETE_FORCE)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Key usage
|
||||||
|
|
||||||
|
# Uses the daemon key to sign the provided message. If 'detached' is True, only the signature will be returned
|
||||||
|
def create_signature(data, detached=False):
|
||||||
|
signed_data, _ = context.sign(data, mode=gpg.constants.SIG_MODE_DETACH if detached else gpg.constants.SIG_MODE_CLEAR)
|
||||||
|
return signed_data
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys, utils
|
||||||
|
# Check if we should renew the key
|
||||||
|
|
||||||
|
daemon_key = get_daemon_key()
|
||||||
|
|
||||||
|
exp = daemon_key.subkeys[0].expires
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
days_left = (datetime.datetime.utcfromtimestamp(exp) - now).days
|
||||||
|
if days_left > 14:
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
utils.shell("check_output", ["management/pgp_renew.sh"])
|
12
management/pgp_renew.sh
Executable file
12
management/pgp_renew.sh
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Renews the daemon's PGP key, if needed.
|
||||||
|
|
||||||
|
source /etc/mailinabox.conf # load global vars
|
||||||
|
export GNUPGHOME # Dump into the environment so that gpg uses it as homedir
|
||||||
|
|
||||||
|
gpg --batch --command-fd=0 --edit-key "${PGPKEY-}" << EOF;
|
||||||
|
key 0
|
||||||
|
expire
|
||||||
|
180d
|
||||||
|
save
|
||||||
|
EOF
|
|
@ -17,6 +17,7 @@ from dns_update import get_dns_zones, build_tlsa_record, get_custom_dns_config,
|
||||||
from web_update import get_web_domains, get_domains_with_a_records
|
from web_update import get_web_domains, get_domains_with_a_records
|
||||||
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
|
from ssl_certificates import get_ssl_certificates, get_domain_ssl_files, check_certificate
|
||||||
from mailconfig import get_mail_domains, get_mail_aliases
|
from mailconfig import get_mail_domains, get_mail_aliases
|
||||||
|
from pgp import get_daemon_key, get_imported_keys
|
||||||
|
|
||||||
from utils import shell, sort_domains, load_env_vars_from_file, load_settings
|
from utils import shell, sort_domains, load_env_vars_from_file, load_settings
|
||||||
|
|
||||||
|
@ -59,6 +60,7 @@ def run_checks(rounded_values, env, output, pool):
|
||||||
shell('check_call', ["/usr/sbin/rndc", "flush"], trap=True)
|
shell('check_call', ["/usr/sbin/rndc", "flush"], trap=True)
|
||||||
|
|
||||||
run_system_checks(rounded_values, env, output)
|
run_system_checks(rounded_values, env, output)
|
||||||
|
run_pgp_checks(env, output)
|
||||||
|
|
||||||
# perform other checks asynchronously
|
# perform other checks asynchronously
|
||||||
|
|
||||||
|
@ -266,6 +268,75 @@ def check_free_memory(rounded_values, env, output):
|
||||||
if rounded_values: memory_msg = "System free memory is below 10%."
|
if rounded_values: memory_msg = "System free memory is below 10%."
|
||||||
output.print_error(memory_msg)
|
output.print_error(memory_msg)
|
||||||
|
|
||||||
|
def run_pgp_checks(env, output):
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
output.add_heading("PGP Keyring")
|
||||||
|
|
||||||
|
# Check daemon key
|
||||||
|
k = None
|
||||||
|
sk = None
|
||||||
|
try:
|
||||||
|
k = get_daemon_key()
|
||||||
|
sk = k.subkeys[0]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if k is None:
|
||||||
|
output.print_error("The daemon's key does not exist!")
|
||||||
|
elif sk.expired == 1:
|
||||||
|
output.print_error(f"The daemon's key ({k.fpr}) expired.")
|
||||||
|
elif k.revoked == 1:
|
||||||
|
output.print_error(f"The daemon's key ({k.fpr}) has been revoked.")
|
||||||
|
else:
|
||||||
|
exp = datetime.datetime.utcfromtimestamp(sk.expires) # Our daemon key only has one subkey
|
||||||
|
if (exp - now).days < 10 and sk.expires != 0:
|
||||||
|
output.print_warning(f"The daemon's key ({k.fpr}) will expire soon, in {(exp - now).days} days on {exp.strftime('%x')}.")
|
||||||
|
else:
|
||||||
|
output.print_ok(f"The daemon's key ({k.fpr}) is good. It expires in {(exp - now).days} days on {exp.strftime('%x')}.")
|
||||||
|
|
||||||
|
# Check imported keys
|
||||||
|
keys = get_imported_keys()
|
||||||
|
if len(keys) == 0:
|
||||||
|
output.print_warning("There are no imported keys here.")
|
||||||
|
else:
|
||||||
|
about_to_expire = []
|
||||||
|
expired = []
|
||||||
|
revoked = []
|
||||||
|
for key in keys:
|
||||||
|
if key.revoked == 1:
|
||||||
|
revoked.append(key)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
for skey in key.subkeys:
|
||||||
|
exp = datetime.datetime.utcfromtimestamp(skey.expires)
|
||||||
|
if skey.expired == 1:
|
||||||
|
expired.append((key, skey))
|
||||||
|
elif (exp - now).days < 10 and skey.expires != 0:
|
||||||
|
about_to_expire.append((key, skey))
|
||||||
|
|
||||||
|
all_good = True
|
||||||
|
def printpair(keytuple):
|
||||||
|
key, skey = keytuple
|
||||||
|
output.print_line(f"Key {key.fpr}, subkey {skey.keyid}")
|
||||||
|
|
||||||
|
if len(about_to_expire) != 0:
|
||||||
|
all_good = False
|
||||||
|
output.print_warning(f"There {'is 1 subkey' if len(about_to_expire) == 1 else f'are {len(about_to_expire)} subkeys'} about to expire.")
|
||||||
|
list(map(printpair, about_to_expire))
|
||||||
|
|
||||||
|
if len(expired) != 0:
|
||||||
|
all_good = False
|
||||||
|
output.print_error(f"There {'is 1 expired subkey' if len(expired) == 1 else f'are {len(expired)} expired subkeys'}.")
|
||||||
|
list(map(printpair, expired))
|
||||||
|
|
||||||
|
if len(revoked) != 0:
|
||||||
|
all_good = False
|
||||||
|
output.print_error(f"There {'is 1 revoked key' if len(revoked) == 1 else f'are {len(revoked)} revoked keys'}.")
|
||||||
|
list(map(lambda k: output.print_line(k.fpr), revoked))
|
||||||
|
|
||||||
|
if all_good:
|
||||||
|
output.print_ok("All imported keys are good.")
|
||||||
|
|
||||||
def run_network_checks(env, output):
|
def run_network_checks(env, output):
|
||||||
# Also see setup/network-checks.sh.
|
# Also see setup/network-checks.sh.
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,8 @@
|
||||||
DNS</a></li>
|
DNS</a></li>
|
||||||
<li class="dropdown-item"><a href="#external_dns"
|
<li class="dropdown-item"><a href="#external_dns"
|
||||||
onclick="return show_panel(this);">External DNS</a></li>
|
onclick="return show_panel(this);">External DNS</a></li>
|
||||||
|
<li class="dropdown-item"><a href="#pgp_keyring"
|
||||||
|
onclick="return show_panel(this);">PGP Keyring Management</a></li>
|
||||||
<li class="dropdown-item"><a href="/admin/munin" target="_blank">Munin Monitoring</a></li>
|
<li class="dropdown-item"><a href="/admin/munin" target="_blank">Munin Monitoring</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
@ -153,6 +155,10 @@
|
||||||
{% include "custom-dns.html" %}
|
{% include "custom-dns.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="panel_pgp_keyring" class="admin_panel">
|
||||||
|
{% include "pgp-keyring.html" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="panel_login" class="admin_panel">
|
<div id="panel_login" class="admin_panel">
|
||||||
{% include "login.html" %}
|
{% include "login.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -199,7 +205,7 @@
|
||||||
|
|
||||||
<div id="global_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle"
|
<div id="global_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="errorModalTitle"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<div class="modal-dialog modal-sm">
|
<div class="modal-dialog modal-sm" style="max-width: 600px;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="errorModalTitle"> </h4>
|
<h4 class="modal-title" id="errorModalTitle"> </h4>
|
||||||
|
|
279
management/templates/pgp-keyring.html
Normal file
279
management/templates/pgp-keyring.html
Normal file
|
@ -0,0 +1,279 @@
|
||||||
|
<style>
|
||||||
|
#pgp_keyring_config .status-error {
|
||||||
|
color: rgb(140, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgp_keyring_config .status-warning {
|
||||||
|
color: rgb(170, 120, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgp_keyring_config .status-ok {
|
||||||
|
color: rgb(0, 140, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgp_keyring_config .status-none {
|
||||||
|
color: rgb(190, 190, 190);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pgp_keyring_config #uids {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<h2>PGP Keyring Management</h2>
|
||||||
|
|
||||||
|
<template id="pgpkey-template">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div id="trustlevel" style="font-size: 14pt;"><b>Trust Level:</b> Ultimate</div>
|
||||||
|
<code id="uids">
|
||||||
|
🤖 Power Mail-in-a-Box Management Daemon <administrator@mailinabox.lan>
|
||||||
|
</code>
|
||||||
|
<h3 style="font-size: 12pt;">Subkeys</h3>
|
||||||
|
<table id="subkeys">
|
||||||
|
<tr id="subkey-template">
|
||||||
|
<td id="ismaster">🔑</td>
|
||||||
|
<td>
|
||||||
|
<b>
|
||||||
|
<a id="sign">S</a>
|
||||||
|
<a id="cert">C</a>
|
||||||
|
<a id="encr">E</a>
|
||||||
|
<a id="auth">A</a>
|
||||||
|
</b>
|
||||||
|
</td>
|
||||||
|
<td style="width: 120pt;">
|
||||||
|
<b id="algo">RSA, 3072 bit</b>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<pre id="fpr">1756 6B81 D8A4 24C7 0098 659E 6872 2633 F692 52C6</pre>
|
||||||
|
</td>
|
||||||
|
<td id="expiration">
|
||||||
|
12/12/20 (119 days)
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td id="options" style="width: 140pt;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div id="pgp_keyring_config">
|
||||||
|
<h3>Daemon's Private Key</h3>
|
||||||
|
<table id="privatekey" class="table container">
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Imported Public Keys</h3>
|
||||||
|
<table id="pubkeys" class="table container">
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Import Key</h3>
|
||||||
|
<p>
|
||||||
|
You can upload your <b>public</b> key/keychain here. Keys <b>must</b> be submitted in ASCII-armored format.
|
||||||
|
<br>
|
||||||
|
If you're using <code>gpg</code>, you can export your public key by following this example:
|
||||||
|
<pre>
|
||||||
|
# Get all the keys in the ring
|
||||||
|
<b>$ gpg --list-keys</b>
|
||||||
|
/home/you/.gnupg/pubring.kbx
|
||||||
|
----------------------------
|
||||||
|
pub rsa4096 1970-01-01 [SC]
|
||||||
|
247C3553B4B36107BA0490C3CAFCCF3B4965761A
|
||||||
|
uid [ full ] Someone That I Used to Know <someone@example.com>
|
||||||
|
sub rsa2048 2020-01-01 [E] [expires: 2021-01-01]
|
||||||
|
|
||||||
|
pub rsa4096 2020-05-24 [SC] [expires: 2021-02-12]
|
||||||
|
52661092E5CD9EEFD7796B19E85F540C9318B69F
|
||||||
|
uid [ultimate] Me, Myself and I <me@mydomain.com>
|
||||||
|
sub rsa2048 2020-05-24 [E] [expires: 2021-02-12]
|
||||||
|
|
||||||
|
# Say that we want to export our own key, this is - "Me, Myself and I". That's the fingerprint "52661..."
|
||||||
|
<b>$ gpg --export --armor 52661092E5CD9EEFD7796B19E85F540C9318B69F</b> # Replace with your key's fingerprint
|
||||||
|
<b>-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
copy and paste this block in the area below
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----</b>
|
||||||
|
</pre>
|
||||||
|
</p>
|
||||||
|
<p><textarea id="pgp_paste_key" class="form-control" style="max-width: 40em; height: 8em" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----
stuff here
-----END PGP PUBLIC KEY BLOCK-----"></textarea></p>
|
||||||
|
<button class="btn btn-primary" onclick="importkey()">Import Key</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function pretty_fpr(fpr) {
|
||||||
|
let pfpr = ""
|
||||||
|
for (let n = 0; n < 2; ++n) {
|
||||||
|
for (let i = 0; i < 5; ++i) {
|
||||||
|
pfpr += `${fpr.substring(n * 20 + i * 4, n * 20 + (i + 1) * 4)} `
|
||||||
|
}
|
||||||
|
pfpr += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
return pfpr.substring(0, pfpr.length - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
function key_html(key, darken_bg, daemon) {
|
||||||
|
let keyrep = $("#pgpkey-template").html()
|
||||||
|
keyrep = $(keyrep)
|
||||||
|
keyrep.attr("id", key.master_fpr)
|
||||||
|
|
||||||
|
// Main key config
|
||||||
|
if (darken_bg) {
|
||||||
|
keyrep.addClass("bg-light")
|
||||||
|
}
|
||||||
|
|
||||||
|
const tlevel = keyrep.find("#trustlevel")
|
||||||
|
if (key.revoked) {
|
||||||
|
tlevel.html("<b class='status-error'>This key was revoked by it's owner.</b>")
|
||||||
|
} else {
|
||||||
|
tlevel.html("<b>Key is not revoked.</b>")
|
||||||
|
}
|
||||||
|
|
||||||
|
let uidtxt = ""
|
||||||
|
if (daemon) {
|
||||||
|
key.ids.forEach(id => {
|
||||||
|
uidtxt += "🤖 " + id + "\n"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
key.ids.forEach(id => {
|
||||||
|
uidtxt += "🕵 " + id + "\n"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
keyrep.find("#uids").text(uidtxt.substring(0, uidtxt.length - 1))
|
||||||
|
|
||||||
|
// Subkeys
|
||||||
|
const keyflags = ["sign", "cert", "encr", "auth"]
|
||||||
|
let subkeys = keyrep.find("#subkeys")
|
||||||
|
let subkeytemplate = subkeys.html()
|
||||||
|
keyrep.find("#subkey-template").remove()
|
||||||
|
key.subkeys.forEach(skey => {
|
||||||
|
let skeyrep = $(subkeytemplate)
|
||||||
|
skeyrep.attr("id", `sub${skey.fpr}`)
|
||||||
|
|
||||||
|
// Master key?
|
||||||
|
if (skey.master) {
|
||||||
|
skeyrep.find("#ismaster").html("🔑")
|
||||||
|
} else {
|
||||||
|
skeyrep.find("#ismaster").html("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage flags
|
||||||
|
keyflags.forEach(flag => {
|
||||||
|
if (!skey[flag]) {
|
||||||
|
skeyrep.find(`#${flag}`).addClass("status-none")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Algorithm and fingerprint
|
||||||
|
skeyrep.find("#algo").html(`${skey.algorithm}, ${skey.bits} bits`)
|
||||||
|
skeyrep.find("#fpr").html(pretty_fpr(skey.fpr))
|
||||||
|
let expiration = skeyrep.find("#expiration")
|
||||||
|
|
||||||
|
// Expiration
|
||||||
|
if (key.revoked) {
|
||||||
|
skeyrep.addClass("status-error")
|
||||||
|
expiration.html(`Revoked`)
|
||||||
|
} else if (skey.expired) {
|
||||||
|
skeyrep.addClass("status-error")
|
||||||
|
expiration.html(`${skey.expires_date} (expired)`)
|
||||||
|
} else if (skey.expires && skey.expires_days <= 14) {
|
||||||
|
skeyrep.addClass("status-warning")
|
||||||
|
expiration.html(`${skey.expires_date} (${skey.expires_days} days)`)
|
||||||
|
} else if (skey.expires) {
|
||||||
|
skeyrep.addClass("status-ok")
|
||||||
|
expiration.html(`${skey.expires_date} (${skey.expires_days} days)`)
|
||||||
|
} else {
|
||||||
|
skeyrep.addClass("status-ok")
|
||||||
|
expiration.html("Does not expire")
|
||||||
|
}
|
||||||
|
skeyrep.appendTo(subkeys)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Options
|
||||||
|
if (daemon) {
|
||||||
|
keyrep.find("#options").html(`<button class="btn btn-primary btn-block" onclick="exportkey('${key.master_fpr}')">Export Public Key</button>`)
|
||||||
|
} else {
|
||||||
|
keyrep.find("#options").html(`<button class="btn btn-secondary btn-block" onclick="exportkey('${key.master_fpr}')">Export Public Key</button><button class="btn btn-danger btn-block" onclick="rmkey('${key.master_fpr}')">Remove Key</button>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyrep
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_pgp_keyring() {
|
||||||
|
$('#privatekey').html("<tr><td class='text-muted'>Loading...</td></tr>")
|
||||||
|
$('#pubkeys').html("<tr><td class='text-muted'>Loading...</td></tr>")
|
||||||
|
api(
|
||||||
|
"/system/pgp/",
|
||||||
|
"GET",
|
||||||
|
{},
|
||||||
|
function(r) {
|
||||||
|
$('#privatekey').html("")
|
||||||
|
$('#pubkeys').html("")
|
||||||
|
key_html(r.daemon, true, true).appendTo("#privatekey")
|
||||||
|
let pendulum = 1
|
||||||
|
r.imported.forEach(k => {
|
||||||
|
key_html(k, pendulum > 0, false).appendTo("#pubkeys")
|
||||||
|
pendulum *= -1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportkey(fpr) {
|
||||||
|
api(
|
||||||
|
`/system/pgp/${fpr}/export`,
|
||||||
|
"GET",
|
||||||
|
{},
|
||||||
|
function(r) {
|
||||||
|
show_modal_error("PGP Key", `Key export for <b>${fpr}</b>:<br><br><pre>${r}</pre>`)
|
||||||
|
},
|
||||||
|
function(_ ,xhr) {
|
||||||
|
if (xhr.status == 404) {
|
||||||
|
show_modal_error("Error", `The key you asked for (<b>${fpr}</b>) does not exist!`)
|
||||||
|
} else {
|
||||||
|
// Fallback to the default error modal
|
||||||
|
show_modal_error("Error", "Something went wrong, sorry.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function rmkey(fpr) {
|
||||||
|
show_modal_confirm("Delete key", `Are you sure you wish to remove the key with the fingerprint ${pretty_fpr(fpr)}?`, "Yes, remove it", () => {
|
||||||
|
api(
|
||||||
|
`/system/pgp/${fpr}`,
|
||||||
|
"DELETE",
|
||||||
|
{},
|
||||||
|
function(r) {
|
||||||
|
show_modal_error("Delete key", r, show_pgp_keyring)
|
||||||
|
},
|
||||||
|
function(r) {
|
||||||
|
show_modal_error("Key deletion error", r)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}, ()=>{})
|
||||||
|
}
|
||||||
|
|
||||||
|
function importkey() {
|
||||||
|
api(
|
||||||
|
"/system/pgp/import",
|
||||||
|
"POST",
|
||||||
|
{
|
||||||
|
key: $("#pgp_paste_key").val()
|
||||||
|
},
|
||||||
|
function(r) {
|
||||||
|
show_modal_error("Import Results", `<ul>
|
||||||
|
<li><b>Keys read:</b> ${r.keys_read}</li>
|
||||||
|
<li><b>Keys added:</b> ${r.keys_added}</li>
|
||||||
|
<li><b>Keys not changed:</b> ${r.keys_unchanged}</li>
|
||||||
|
<li><b>User id's added:</b> ${r.uids_added}</li>
|
||||||
|
<li><b>Signatures added:</b> ${r.sigs_added}</li>
|
||||||
|
<li><b>Revocations added:</b> ${r.revs_added}</li>
|
||||||
|
</ul>`, show_pgp_keyring)
|
||||||
|
},
|
||||||
|
function(r) {
|
||||||
|
show_modal_error("Import Error", r)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -225,3 +225,7 @@ function git_clone {
|
||||||
function php_version {
|
function php_version {
|
||||||
php --version | head -n 1 | cut -d " " -f 2 | cut -c 1-3
|
php --version | head -n 1 | cut -d " " -f 2 | cut -c 1-3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function python_version {
|
||||||
|
python3 --version | cut -d " " -f 2 | cut -c 1-3
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ done
|
||||||
#
|
#
|
||||||
# certbot installs EFF's certbot which we use to
|
# certbot installs EFF's certbot which we use to
|
||||||
# provision free TLS certificates.
|
# provision free TLS certificates.
|
||||||
apt_install duplicity python3-pip virtualenv certbot
|
apt_install duplicity python3-pip python3-gpg virtualenv certbot
|
||||||
hide_output pip3 install --upgrade boto
|
hide_output pip3 install --upgrade boto
|
||||||
|
|
||||||
# Create a virtualenv for the installation of Python 3 packages
|
# Create a virtualenv for the installation of Python 3 packages
|
||||||
|
@ -52,6 +52,11 @@ hide_output $venv/bin/pip install --upgrade \
|
||||||
flask dnspython python-dateutil \
|
flask dnspython python-dateutil \
|
||||||
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver
|
"idna>=2.0.0" "cryptography==2.2.2" boto psutil postfix-mta-sts-resolver
|
||||||
|
|
||||||
|
# Make the venv use the packaged gpgme bindings (the ones pip provides are severely out-of-date)
|
||||||
|
if [ ! -d $venv/lib/python$(python_version)/site-packages/gpg/ ]; then
|
||||||
|
ln -s /usr/lib/python3/dist-packages/gpg/ $venv/lib/python$(python_version)/site-packages/
|
||||||
|
fi
|
||||||
|
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
|
|
||||||
# Create a backup directory and a random key for encrypting backups.
|
# Create a backup directory and a random key for encrypting backups.
|
||||||
|
|
32
setup/pgp.sh
Executable file
32
setup/pgp.sh
Executable file
|
@ -0,0 +1,32 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Daemon PGP Keyring
|
||||||
|
# ------------------
|
||||||
|
#
|
||||||
|
# Initializes the PGP keyring at /home/user-data/.gnupg
|
||||||
|
# For this, we will generate a new PGP keypair (if one isn't already present)
|
||||||
|
|
||||||
|
source setup/functions.sh # load our functions
|
||||||
|
source /etc/mailinabox.conf # load global vars
|
||||||
|
export GNUPGHOME # Dump into the environment so that gpg uses it as homedir
|
||||||
|
|
||||||
|
# Install gnupg
|
||||||
|
apt_install gnupg
|
||||||
|
|
||||||
|
if [ "$(gpg --list-secret-keys 2> /dev/null)" = "" -o "${PGPKEY-}" = "" ]; then
|
||||||
|
echo "No keypair found. Generating daemon's PGP keypair..."
|
||||||
|
gpg --generate-key --batch << EOF;
|
||||||
|
%no-protection
|
||||||
|
Key-Type: RSA
|
||||||
|
Key-Length: 4096
|
||||||
|
Key-Usage: sign,encrypt,auth
|
||||||
|
Name-Real: Power Mail-in-a-Box Management Daemon
|
||||||
|
Name-Email: administrator@${PRIMARY_HOSTNAME}
|
||||||
|
Expire-Date: 180d
|
||||||
|
%commit
|
||||||
|
EOF
|
||||||
|
chown -R root:root $GNUPGHOME
|
||||||
|
# Remove the old key fingerprint if it exists, and add the new one
|
||||||
|
echo "$(cat /etc/mailinabox.conf | grep -v "PGPKEY")" > /etc/mailinabox.conf
|
||||||
|
echo "PGPKEY=$(gpg --list-secret-keys --with-colons | grep fpr | head -n 1 | sed 's/fpr//g' | sed 's/://g')" >> /etc/mailinabox.conf
|
||||||
|
fi
|
|
@ -100,12 +100,15 @@ PUBLIC_IPV6=$PUBLIC_IPV6
|
||||||
PRIVATE_IP=$PRIVATE_IP
|
PRIVATE_IP=$PRIVATE_IP
|
||||||
PRIVATE_IPV6=$PRIVATE_IPV6
|
PRIVATE_IPV6=$PRIVATE_IPV6
|
||||||
MTA_STS_MODE=${MTA_STS_MODE-}
|
MTA_STS_MODE=${MTA_STS_MODE-}
|
||||||
|
GNUPGHOME=${STORAGE_ROOT}/.gnupg/
|
||||||
|
PGPKEY=${DEFAULT_PGPKEY-}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Start service configuration.
|
# Start service configuration.
|
||||||
source setup/system.sh
|
source setup/system.sh
|
||||||
source setup/ssl.sh
|
source setup/ssl.sh
|
||||||
source setup/dns.sh
|
source setup/dns.sh
|
||||||
|
source setup/pgp.sh
|
||||||
source setup/mail-postfix.sh
|
source setup/mail-postfix.sh
|
||||||
source setup/mail-dovecot.sh
|
source setup/mail-dovecot.sh
|
||||||
source setup/mail-users.sh
|
source setup/mail-users.sh
|
||||||
|
|
|
@ -23,7 +23,7 @@ echo "Installing Roundcube (webmail)..."
|
||||||
apt_install \
|
apt_install \
|
||||||
dbconfig-common \
|
dbconfig-common \
|
||||||
php-cli php-sqlite3 php-intl php-json php-common php-curl php-ldap \
|
php-cli php-sqlite3 php-intl php-json php-common php-curl php-ldap \
|
||||||
php-gd php-pspell tinymce libjs-jquery libjs-jquery-mousewheel libmagic1 php-mbstring
|
php-gd php-pspell tinymce libjs-jquery libjs-jquery-mousewheel libmagic1 php-mbstring php-gnupg
|
||||||
|
|
||||||
# Install Roundcube from source if it is not already present or if it is out of date.
|
# Install Roundcube from source if it is not already present or if it is out of date.
|
||||||
# Combine the Roundcube version number with the commit hash of plugins to track
|
# Combine the Roundcube version number with the commit hash of plugins to track
|
||||||
|
@ -126,7 +126,7 @@ cat > $RCM_CONFIG <<EOF;
|
||||||
\$config['support_url'] = 'https://mailinabox.email/';
|
\$config['support_url'] = 'https://mailinabox.email/';
|
||||||
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail';
|
\$config['product_name'] = '$PRIMARY_HOSTNAME Webmail';
|
||||||
\$config['des_key'] = '$SECRET_KEY';
|
\$config['des_key'] = '$SECRET_KEY';
|
||||||
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav');
|
\$config['plugins'] = array('html5_notifier', 'archive', 'zipdownload', 'password', 'managesieve', 'jqueryui', 'persistent_login', 'carddav', 'enigma');
|
||||||
\$config['skin'] = 'elastic';
|
\$config['skin'] = 'elastic';
|
||||||
\$config['login_autocomplete'] = 2;
|
\$config['login_autocomplete'] = 2;
|
||||||
\$config['password_charset'] = 'UTF-8';
|
\$config['password_charset'] = 'UTF-8';
|
||||||
|
@ -134,6 +134,35 @@ cat > $RCM_CONFIG <<EOF;
|
||||||
?>
|
?>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
mkdir -p ${STORAGE_ROOT}/userkeys/
|
||||||
|
chmod 700 ${STORAGE_ROOT}/userkeys/
|
||||||
|
chown www-data:www-data ${STORAGE_ROOT}/userkeys/
|
||||||
|
|
||||||
|
# Configure Enigma
|
||||||
|
cat > ${RCM_PLUGIN_DIR}/enigma/config.inc.php <<EOF;
|
||||||
|
<?php
|
||||||
|
/* Do not edit. Written by Mail-in-a-Box. Regenerated on updates. */
|
||||||
|
\$config['enigma_pgp_driver'] = 'gnupg';
|
||||||
|
\$config['enigma_smime_driver'] = 'phpssl';
|
||||||
|
\$config['enigma_debug'] = false;
|
||||||
|
\$config['enigma_pgp_homedir'] = '${STORAGE_ROOT}/.enigma/';
|
||||||
|
\$config['enigma_pgp_binary'] = '';
|
||||||
|
\$config['enigma_pgp_agent'] = '';
|
||||||
|
\$config['enigma_pgp_gpgconf'] = '';
|
||||||
|
\$config['enigma_pgp_cipher_algo'] = null;
|
||||||
|
\$config['enigma_pgp_digest_algo'] = null;
|
||||||
|
\$config['enigma_multihost'] = false;
|
||||||
|
\$config['enigma_signatures'] = true;
|
||||||
|
\$config['enigma_decryption'] = true;
|
||||||
|
\$config['enigma_encryption'] = true;
|
||||||
|
\$config['enigma_sign_all'] = false;
|
||||||
|
\$config['enigma_encrypt_all'] = false;
|
||||||
|
\$config['enigma_attach_pubkey'] = false;
|
||||||
|
\$config['enigma_password_time'] = 5;
|
||||||
|
\$config['enigma_options_lock'] = array();
|
||||||
|
?>
|
||||||
|
EOF
|
||||||
|
|
||||||
# Configure CardDav
|
# Configure CardDav
|
||||||
cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF;
|
cat > ${RCM_PLUGIN_DIR}/carddav/config.inc.php <<EOF;
|
||||||
<?php
|
<?php
|
||||||
|
|
Loading…
Reference in a new issue