Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
M
mailinabox
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Administrator
mailinabox
Commits
910b473e
Commit
910b473e
authored
Jul 25, 2014
by
Joshua Tauberer
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add a mandatory-pgp-encryption submission port
parent
86ec0f6d
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
204 additions
and
1 deletion
+204
-1
mail-postfix.sh
setup/mail-postfix.sh
+13
-1
encryption-milter.py
tools/encryption-milter.py
+191
-0
No files found.
setup/mail-postfix.sh
View file @
910b473e
...
@@ -30,8 +30,10 @@ source setup/functions.sh # load our functions
...
@@ -30,8 +30,10 @@ source setup/functions.sh # load our functions
source
/etc/mailinabox.conf
# load global vars
source
/etc/mailinabox.conf
# load global vars
# Install packages.
# Install packages.
# python-libmilter is needed by our encryption milter.
apt_install postfix postgrey postfix-pcre
apt_install postfix postgrey postfix-pcre
hide_output pip3
install
git+https://github.com/mail-in-a-box/python-libmilter
# Basic Settings
# Basic Settings
...
@@ -53,11 +55,20 @@ tools/editconf.py /etc/postfix/main.cf \
...
@@ -53,11 +55,20 @@ tools/editconf.py /etc/postfix/main.cf \
# c) Add a new cleanup service specific to the submission service ('authclean')
# c) Add a new cleanup service specific to the submission service ('authclean')
# that filters out privacy-sensitive headers on mail being sent out by
# that filters out privacy-sensitive headers on mail being sent out by
# authenticated users.
# authenticated users.
# d) Create an alternative one running on port 10587 that requires that all recipients have findable
# OpenPGP keys. Encrypts the message for the recipients using a milter on port 882. The milter
# precedes the DKIM milter on 8891 so that the message isn't touched after DKIM signing. If the
# encryption milter isn't running, reject the message so we dont send anything in the clear.
tools/editconf.py /etc/postfix/master.cf
-s
-w
\
tools/editconf.py /etc/postfix/master.cf
-s
-w
\
"submission=inet n - - - - smtpd
"submission=inet n - - - - smtpd
-o syslog_name=postfix/submission
-o syslog_name=postfix/submission
-o smtpd_tls_ciphers=high -o smtpd_tls_protocols=!SSLv2,!SSLv3
-o smtpd_tls_ciphers=high -o smtpd_tls_protocols=!SSLv2,!SSLv3
-o cleanup_service_name=authclean"
\
-o cleanup_service_name=authclean"
\
"10587=inet n - - - - smtpd
-o syslog_name=postfix/submission-encrypted
-o smtpd_tls_ciphers=high -o smtpd_tls_protocols=!SSLv2,!SSLv3
-o cleanup_service_name=authclean
-o smtpd_milters=inet:127.0.0.1:8892,inet:127.0.0.1:8891 -o milter_default_action=reject"
\
"authclean=unix n - - - 0 cleanup
"authclean=unix n - - - 0 cleanup
-o header_checks=pcre:/etc/postfix/outgoing_mail_header_filters"
-o header_checks=pcre:/etc/postfix/outgoing_mail_header_filters"
...
@@ -134,6 +145,7 @@ tools/editconf.py /etc/postfix/main.cf \
...
@@ -134,6 +145,7 @@ tools/editconf.py /etc/postfix/main.cf \
ufw_allow smtp
ufw_allow smtp
ufw_allow submission
ufw_allow submission
ufw_allow 10587
# Restart services
# Restart services
...
...
tools/encryption-milter.py
0 → 100755
View file @
910b473e
#!/usr/bin/env python3
import
sys
import
io
import
re
import
urllib.request
,
urllib
.
error
import
tempfile
import
shutil
import
libmilter
import
gnupg
import
dns.resolver
from
hashlib
import
sha224
# Start logging to syslog.
from
syslog
import
syslog
,
openlog
,
LOG_MAIL
openlog
(
'encryption-milter'
,
facility
=
LOG_MAIL
)
# Replace process title so it looks nicer in top.
try
:
import
setproctitle
setproctitle
.
setproctitle
(
"encryption-milter"
)
except
:
pass
# Globals for DNS resolving. See:
# http://tools.ietf.org/html/draft-ietf-dane-openpgpkey-00
# http://tools.ietf.org/html/draft-ietf-dane-openpgpkey-usage-00
# https://github.com/letoams/openpgpkey-milter/
# I have not tested that this works at all.
resolver
=
dns
.
resolver
.
get_default_resolver
()
#resolver.nameservers = [server]
openpgp_rtype
=
65280
# draft value - changes when RFC
class
EncryptionError
(
Exception
):
pass
class
EncryptionMilter
(
libmilter
.
ForkMixin
,
libmilter
.
MilterProtocol
):
def
__init__
(
self
,
opts
=
0
,
protos
=
0
):
libmilter
.
MilterProtocol
.
__init__
(
self
,
opts
,
protos
)
libmilter
.
ForkMixin
.
__init__
(
self
)
self
.
R
=
[]
# list of recipient keys
self
.
fp
=
io
.
BytesIO
()
# storage for incoming body
def
log
(
self
,
msg
):
print
(
msg
)
syslog
(
'encryption-milter: '
+
msg
)
def
rcpt
(
self
,
rcpt_to
,
cmdDict
):
# Turn recipients into keys. If we don't have a key available,
# then reject the message.
try
:
self
.
R
.
extend
(
self
.
get_pgp_keys
(
rcpt_to
))
return
libmilter
.
CONTINUE
except
EncryptionError
as
e
:
self
.
log
(
str
(
e
))
self
.
setReply
(
b
'554'
,
b
'5.7.1'
,
str
(
e
)
.
encode
(
"utf8"
))
return
libmilter
.
REJECT
@
libmilter
.
noReply
def
header
(
self
,
header
,
value
,
cmdDict
):
self
.
fp
.
write
(
header
)
self
.
fp
.
write
(
b
': '
)
self
.
fp
.
write
(
value
)
self
.
fp
.
write
(
b
'
\n
'
)
return
libmilter
.
CONTINUE
@
libmilter
.
noReply
def
eoh
(
self
,
cmdDict
):
self
.
fp
.
write
(
b
'
\n
'
)
return
libmilter
.
CONTINUE
@
libmilter
.
noReply
def
body
(
self
,
chunk
,
cmdDict
):
self
.
fp
.
write
(
chunk
)
return
libmilter
.
CONTINUE
def
eob
(
self
,
cmdDict
):
msg
=
self
.
fp
.
getvalue
()
gpgdir
=
tempfile
.
mkdtemp
()
gpg
=
gnupg
.
GPG
(
gnupghome
=
gpgdir
)
gpg
.
decode_errors
=
"ignore"
try
:
# Add keys.
for
key
in
self
.
R
:
gpg
.
import_keys
(
key
)
# Target message encryption to all imported keys.
fingerprints
=
','
.
join
(
ikey
[
'fingerprint'
]
for
ikey
in
gpg
.
list_keys
())
# Encrypt message.
enc_msg
=
gpg
.
encrypt
(
msg
,
fingerprints
,
always_trust
=
True
)
if
enc_msg
.
data
==
''
:
# gpg binary and pythong wrapper is bad at giving us an error message
raise
Exception
(
'Encryption failed for an unknown reason. GPG failed.'
)
# Rewrite the message.
self
.
addHeader
(
b
'X-OpenPGPKey'
,
b
'Encrypted to key(s): '
+
fingerprints
.
encode
(
"ascii"
))
self
.
chgHeader
(
b
'Subject'
,
b
'[pgp encrypted message]'
)
self
.
replBody
(
enc_msg
.
data
)
return
libmilter
.
CONTINUE
except
ValueError
:
#Exception as e:
# Exceptions are thrown on things that would be temporary failures.
# But by now it's too late to tell the user there was a problem?
self
.
log
(
str
(
e
))
self
.
setReply
(
b
'554'
,
b
'5.7.1'
,
str
(
e
)
.
encode
(
"utf8"
))
return
libmilter
.
REJECT
finally
:
shutil
.
rmtree
(
gpgdir
)
def
get_pgp_keys
(
self
,
email_addr
):
keys
=
self
.
import_keys_from_keybase
(
email_addr
)
if
keys
:
return
keys
keys
=
self
.
import_keys_from_dns
(
email_addr
)
if
keys
:
return
keys
raise
EncryptionError
(
email_addr
.
decode
(
"utf8"
,
"replace"
)
+
" does not have a known encryption key."
)
def
import_keys_from_keybase
(
self
,
email_addr
):
# Extract the keybase username from the email address.
m
=
re
.
search
(
rb
"
\
+keybase=(.*)@"
,
email_addr
)
if
not
m
:
return
None
keybase_username
=
m
.
group
(
1
)
# Query keybase.
try
:
req
=
urllib
.
request
.
urlopen
(
"https://keybase.io/
%
s/key.asc"
%
keybase_username
.
decode
(
"ascii"
,
"error"
),
timeout
=
20
,
cadefault
=
True
)
openpgpkey
=
req
.
read
()
except
Exception
as
e
:
if
isinstance
(
e
,
urllib
.
error
.
HTTPError
)
and
e
.
code
==
404
:
e
=
"User not found."
raise
EncryptionError
(
"Error getting public key for
%
s at Keybase.io:
%
s"
%
(
keybase_username
.
decode
(
"utf8"
,
"replace"
),
str
(
e
)))
# Return the key.
self
.
log
(
"got keybase.io key for
%
s"
%
keybase_username
.
decode
(
"utf8"
,
"replace"
))
return
[
openpgpkey
]
def
import_keys_from_dns
(
self
,
email_addr
):
(
username
,
domainname
)
=
email_addr
.
split
(
b
'@'
)
qname
=
'
%
s._openpgpkey.
%
s'
%
(
sha224
(
username
)
.
hexdigest
(),
domainname
)
try
:
response
=
dns
.
resolver
.
query
(
qname
,
openpgp_rtype
)
except
dns
.
resolver
.
NoNameservers
:
# could not connect to nameserver
raise
EncryptionError
(
"Could not connect to nameserver."
)
except
(
dns
.
resolver
.
NXDOMAIN
,
dns
.
resolver
.
NoAnswer
):
# host did not have an answer for this query; not sure what the
# difference is between the two exceptions
return
None
if
len
(
result
)
==
0
:
# empty answer? probably not possible...
return
None
# Return all keys found in DNS.
return
[
str
(
value
)
for
value
in
result
]
def
runMilter
():
# Adapted from the python-libmilter example at
# https://github.com/crustymonkey/python-libmilter/blob/master/examples/testmilter.py
import
signal
,
traceback
# Create the milter. Use the ForkFactor to handle each mail in a separate process.
f
=
libmilter
.
ForkFactory
(
'inet:127.0.0.1:8892'
,
EncryptionMilter
,
libmilter
.
SMFIF_ADDHDRS
|
libmilter
.
SMFIF_CHGHDRS
|
libmilter
.
SMFIF_CHGBODY
)
# Add a signal handler to cleanly exit.
def
sigHandler
(
num
,
frame
):
f
.
close
()
sys
.
exit
(
0
)
signal
.
signal
(
signal
.
SIGINT
,
sigHandler
)
# Start the milter.
try
:
f
.
run
()
except
Exception
as
e
:
f
.
close
()
raise
if
__name__
==
'__main__'
:
runMilter
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment