feat: implement policy_flags and apply these policies to submission requests
This commit is contained in:
parent
9182582589
commit
41de3b5704
6 changed files with 183 additions and 22 deletions
|
@ -45,7 +45,6 @@ easywksserver_gpgwksclient:
|
||||||
- |
|
- |
|
||||||
cat > /tmp/easywks.yml <<EOF
|
cat > /tmp/easywks.yml <<EOF
|
||||||
directory: /tmp/easywks
|
directory: /tmp/easywks
|
||||||
permit_unsigned_response: true # required for gpg-wks-client compat
|
|
||||||
httpd:
|
httpd:
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 8080
|
port: 8080
|
||||||
|
@ -53,6 +52,8 @@ easywksserver_gpgwksclient:
|
||||||
domains:
|
domains:
|
||||||
example.org:
|
example.org:
|
||||||
submission_address: webkey@example.org
|
submission_address: webkey@example.org
|
||||||
|
policy_flags:
|
||||||
|
me.s3lph.easywks_permit-unsigned-response: true # required for gpg-wks-client compat
|
||||||
EOF
|
EOF
|
||||||
- easywks --config /tmp/easywks.yml init
|
- easywks --config /tmp/easywks.yml init
|
||||||
- easywks --config /tmp/easywks.yml webserver &
|
- easywks --config /tmp/easywks.yml webserver &
|
||||||
|
|
|
@ -3,6 +3,33 @@ import string
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
POLICY_MAILBOX_ONLY = 'mailbox-only'
|
||||||
|
POLICY_AUTH_SUBMIT = 'auth-submit'
|
||||||
|
POLICY_PROTOCOL_VERSION = 'protocol-version'
|
||||||
|
STANDARD_POLICY_FLAGS = {
|
||||||
|
POLICY_MAILBOX_ONLY: bool,
|
||||||
|
# 'dane-only': bool, # deprecated
|
||||||
|
POLICY_AUTH_SUBMIT: bool,
|
||||||
|
POLICY_PROTOCOL_VERSION: int,
|
||||||
|
}
|
||||||
|
|
||||||
|
EASYWKS_POLICY_PREFIX = 'me.s3lph.easywks_'
|
||||||
|
EWP_PERMIT_UNSIGNED_RESPONSE = EASYWKS_POLICY_PREFIX + 'permit-unsigned-response'
|
||||||
|
EWP_STRIP_UNVERIFIED_UIDS = EASYWKS_POLICY_PREFIX + 'strip-unverified-uids'
|
||||||
|
EWP_STRIP_UA_UIDS = EASYWKS_POLICY_PREFIX + 'strip-ua-uids'
|
||||||
|
EWP_STRIP_3RDPARTY_SIGNATURES = EASYWKS_POLICY_PREFIX + 'strip-3rdparty-signatures'
|
||||||
|
EWP_MAX_REVOKED_KEYS = EASYWKS_POLICY_PREFIX + 'max-revoked-keys'
|
||||||
|
EWP_MINIMIZE_REVOKED_KEYS = EASYWKS_POLICY_PREFIX + 'minimize-revoked-keys'
|
||||||
|
EASYWKS_POLICY_FLAGS = {
|
||||||
|
EWP_PERMIT_UNSIGNED_RESPONSE: bool,
|
||||||
|
EWP_STRIP_UNVERIFIED_UIDS: bool,
|
||||||
|
EWP_STRIP_UA_UIDS: bool,
|
||||||
|
EWP_STRIP_3RDPARTY_SIGNATURES: bool,
|
||||||
|
EWP_MAX_REVOKED_KEYS: int,
|
||||||
|
EWP_MINIMIZE_REVOKED_KEYS: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _validate_mailing_method(value):
|
def _validate_mailing_method(value):
|
||||||
methods = ['stdout', 'smtp']
|
methods = ['stdout', 'smtp']
|
||||||
if value not in methods:
|
if value not in methods:
|
||||||
|
@ -77,6 +104,19 @@ def _validate_policy_flags(value):
|
||||||
for c in flag:
|
for c in flag:
|
||||||
if c not in alphabet:
|
if c not in alphabet:
|
||||||
return f'has invalid key {flag}'
|
return f'has invalid key {flag}'
|
||||||
|
if '_' in flag:
|
||||||
|
if flag.startswith(EASYWKS_POLICY_PREFIX):
|
||||||
|
cls = EASYWKS_POLICY_FLAGS.get(flag)
|
||||||
|
if flag not in EASYWKS_POLICY_FLAGS:
|
||||||
|
return f'unknown policy flag {flag}'
|
||||||
|
if not isinstance(v, cls):
|
||||||
|
return f'invalid type {v.__class__.__name__} for flag {flag}'
|
||||||
|
else:
|
||||||
|
cls = STANDARD_POLICY_FLAGS.get(flag)
|
||||||
|
if flag not in STANDARD_POLICY_FLAGS:
|
||||||
|
return f'unknown policy flag {flag}'
|
||||||
|
if not isinstance(v, cls):
|
||||||
|
return f'invalid type {v.__class__.__name__} for flag {flag}'
|
||||||
|
|
||||||
|
|
||||||
def _validate_responses(value):
|
def _validate_responses(value):
|
||||||
|
@ -201,6 +241,7 @@ Config = _GlobalConfig(
|
||||||
working_directory=_ConfigOption('directory', str, '/var/lib/easywks'),
|
working_directory=_ConfigOption('directory', str, '/var/lib/easywks'),
|
||||||
pending_lifetime=_ConfigOption('pending_lifetime', int, 604800),
|
pending_lifetime=_ConfigOption('pending_lifetime', int, 604800),
|
||||||
mailing_method=_ConfigOption('mailing_method', str, 'stdout', validator=_validate_mailing_method),
|
mailing_method=_ConfigOption('mailing_method', str, 'stdout', validator=_validate_mailing_method),
|
||||||
|
# TODO: permit_unsigned_response is deprecated, but for now retained for backwards compatibility
|
||||||
permit_unsigned_response=_ConfigOption('permit_unsigned_response', bool, False),
|
permit_unsigned_response=_ConfigOption('permit_unsigned_response', bool, False),
|
||||||
httpd=_ConfigOption('httpd', dict, {
|
httpd=_ConfigOption('httpd', dict, {
|
||||||
'host': 'localhost',
|
'host': 'localhost',
|
||||||
|
|
|
@ -36,10 +36,15 @@ def make_submission_address_file(domain: str):
|
||||||
def make_policy_file(domain: str):
|
def make_policy_file(domain: str):
|
||||||
content = f'submission-address: {Config[domain].submission_address}\n'
|
content = f'submission-address: {Config[domain].submission_address}\n'
|
||||||
for flag, value in Config[domain].policy_flags.items():
|
for flag, value in Config[domain].policy_flags.items():
|
||||||
if not value or len(value) == 0:
|
if isinstance(value, bool):
|
||||||
content += f'{flag}: {value}\n'
|
if not value:
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
content += flag + '\n'
|
content += flag + '\n'
|
||||||
|
elif value is None:
|
||||||
|
content += flag + '\n'
|
||||||
|
else:
|
||||||
|
content += f'{flag}: {value}\n'
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import sys
|
import sys
|
||||||
from typing import List, Dict, Tuple
|
from typing import Any, List, Dict, Tuple
|
||||||
|
|
||||||
from .crypto import pgp_decrypt
|
from .crypto import pgp_decrypt
|
||||||
from .mailing import get_mailing_method
|
from .mailing import get_mailing_method
|
||||||
from .config import Config
|
from .config import Config, \
|
||||||
|
POLICY_MAILBOX_ONLY, EWP_MAX_REVOKED_KEYS, EWP_STRIP_UNVERIFIED_UIDS, EWP_STRIP_3RDPARTY_SIGNATURES, \
|
||||||
|
EWP_STRIP_UA_UIDS, EWP_MINIMIZE_REVOKED_KEYS, EWP_PERMIT_UNSIGNED_RESPONSE, POLICY_AUTH_SUBMIT
|
||||||
from .files import read_pending_key, write_public_key, remove_pending_key, write_pending_key
|
from .files import read_pending_key, write_public_key, remove_pending_key, write_pending_key
|
||||||
from .types import SubmissionRequest, ConfirmationResponse, PublishResponse, ConfirmationRequest, EasyWksError,\
|
from .types import SubmissionRequest, ConfirmationResponse, PublishResponse, ConfirmationRequest, EasyWksError,\
|
||||||
XLOOP_HEADER
|
XLOOP_HEADER
|
||||||
|
@ -15,7 +17,7 @@ from email.parser import BytesParser
|
||||||
from email.policy import default
|
from email.policy import default
|
||||||
from email.utils import getaddresses
|
from email.utils import getaddresses
|
||||||
|
|
||||||
from pgpy import PGPMessage, PGPKey, PGPUID
|
from pgpy import PGPMessage, PGPKey, PGPUID, PGPSignature
|
||||||
from pgpy.errors import PGPError
|
from pgpy.errors import PGPError
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,6 +136,62 @@ def _parse_confirmation_response(pgp: PGPMessage, submission: str, sender: str):
|
||||||
return ConfirmationResponse(rdict['sender'], submission, rdict['nonce'], pgp)
|
return ConfirmationResponse(rdict['sender'], submission, rdict['nonce'], pgp)
|
||||||
|
|
||||||
|
|
||||||
|
# There is no API for directly removing a PGPUID or PGPSignature object
|
||||||
|
# noinspection PyProtectedMember
|
||||||
|
def _apply_submission_policy(request: SubmissionRequest, policy: Dict[str, Any]):
|
||||||
|
# Policy: Only permit a certain amount of revoked keys
|
||||||
|
maxrevoke = policy.get(EWP_MAX_REVOKED_KEYS, -1)
|
||||||
|
if maxrevoke > -1 and len(request.revoked_keys) > maxrevoke:
|
||||||
|
raise EasyWksError(f'Submission request contains {len(request.revoked_keys)} revoked keys. '
|
||||||
|
f'Policy permits not more than {maxrevoke}')
|
||||||
|
uid: PGPUID
|
||||||
|
revfprs = [fingerprint(request.key)] + [fingerprint(rk) for rk in request.revoked_keys]
|
||||||
|
for key in [request.key] + request.revoked_keys:
|
||||||
|
# Policy: Strip user attribute (image) UIDs
|
||||||
|
if policy.get(POLICY_MAILBOX_ONLY, False) or policy.get(EWP_STRIP_UA_UIDS, False):
|
||||||
|
for uid in list(key.userattributes):
|
||||||
|
uid._parent = None
|
||||||
|
key._uids.remove(uid)
|
||||||
|
# Policy: Reject keys as invalid if they contain UIDs with non-empty name or comment parts
|
||||||
|
if policy.get(POLICY_MAILBOX_ONLY, False):
|
||||||
|
for uid in list(key.userids):
|
||||||
|
if uid.email == '' or uid.name != '' or uid.comment != '':
|
||||||
|
raise EasyWksError('This WKS server only accepts UIDs without name and comment parts')
|
||||||
|
# Policy: Strip all UIDs except the one being verified
|
||||||
|
if policy.get(EWP_STRIP_UNVERIFIED_UIDS, False):
|
||||||
|
for uid in list(key.userids):
|
||||||
|
if uid.email != request.submitter_address:
|
||||||
|
uid._parent = None
|
||||||
|
key._uids.remove(uid)
|
||||||
|
# Policy: Strip all 3rd party signatures from they key
|
||||||
|
if policy.get(EWP_STRIP_3RDPARTY_SIGNATURES, False):
|
||||||
|
for uid in list(key.userids):
|
||||||
|
for sig in list(uid.third_party_certifications):
|
||||||
|
# Keep signatures signed by the revoked keys
|
||||||
|
sig: PGPSignature
|
||||||
|
if sig.signer_fingerprint not in revfprs:
|
||||||
|
uid._signatures.remove(sig)
|
||||||
|
# Policy: Produce minimal transportable keys
|
||||||
|
if policy.get(EWP_MINIMIZE_REVOKED_KEYS, False):
|
||||||
|
for key in request.revoked_keys:
|
||||||
|
for uid in list(key.userids):
|
||||||
|
# Delete all but the submitter UIDs, and all 3rd party signatures
|
||||||
|
if uid.email != request.submitter_address:
|
||||||
|
uid._parent = None
|
||||||
|
key._uids.remove(uid)
|
||||||
|
else:
|
||||||
|
for sig in list(uid.third_party_certifications):
|
||||||
|
uid._signatures.remove(sig)
|
||||||
|
# Delete UAs
|
||||||
|
for uid in list(key.userattributes):
|
||||||
|
uid._parent = None
|
||||||
|
key._uids.remove(uid)
|
||||||
|
# Delete subkeys
|
||||||
|
for subkey in key._children.values():
|
||||||
|
subkey._parent = None
|
||||||
|
key._children.clear()
|
||||||
|
|
||||||
|
|
||||||
def process_mail(mail: bytes):
|
def process_mail(mail: bytes):
|
||||||
try:
|
try:
|
||||||
msg: Message = BytesParser(policy=default).parsebytes(mail)
|
msg: Message = BytesParser(policy=default).parsebytes(mail)
|
||||||
|
@ -170,17 +228,25 @@ def process_mail(mail: bytes):
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise EasyWksError('There is no submission request for this email address, or it has expired. '
|
raise EasyWksError('There is no submission request for this email address, or it has expired. '
|
||||||
'Please resubmit your submission request.')
|
'Please resubmit your submission request.')
|
||||||
|
# TODO: Config.permit_unsigned_response is deprecated, but for now retained for backwards compatibility
|
||||||
|
if not Config[sender_domain].policy_flags.get(EWP_PERMIT_UNSIGNED_RESPONSE, False) and \
|
||||||
|
not Config.permit_unsigned_response:
|
||||||
# this throws an error if signature verification fails
|
# this throws an error if signature verification fails
|
||||||
request.verify_signature(key)
|
request.verify_signature(key)
|
||||||
response: PublishResponse = request.get_publish_response(key)
|
response: PublishResponse = request.get_publish_response(key)
|
||||||
rmsg = response.create_signed_message()
|
|
||||||
write_public_key(sender_domain, sender_mail, key, revoked_keys)
|
write_public_key(sender_domain, sender_mail, key, revoked_keys)
|
||||||
remove_pending_key(sender_domain, request.nonce)
|
remove_pending_key(sender_domain, request.nonce)
|
||||||
else:
|
else:
|
||||||
request: SubmissionRequest = _parse_submission_request(decrypted, submission_address, sender_mail)
|
request: SubmissionRequest = _parse_submission_request(decrypted, submission_address, sender_mail)
|
||||||
|
policy = Config[sender_domain].policy_flags
|
||||||
|
_apply_submission_policy(request, policy)
|
||||||
|
if policy.get(POLICY_AUTH_SUBMIT, False):
|
||||||
|
response = PublishResponse(request.submitter_address, request.submission_address, request.key)
|
||||||
|
write_public_key(sender_domain, sender_mail, request.key, request.revoked_keys)
|
||||||
|
else:
|
||||||
response: ConfirmationRequest = request.confirmation_request()
|
response: ConfirmationRequest = request.confirmation_request()
|
||||||
rmsg = response.create_signed_message()
|
|
||||||
write_pending_key(sender_domain, response.nonce, request.key, request.revoked_keys)
|
write_pending_key(sender_domain, response.nonce, request.key, request.revoked_keys)
|
||||||
|
rmsg = response.create_signed_message()
|
||||||
except EasyWksError as e:
|
except EasyWksError as e:
|
||||||
rmsg = e.create_message(sender_mail, submission_address)
|
rmsg = e.create_message(sender_mail, submission_address)
|
||||||
method = get_mailing_method(Config.mailing_method)
|
method = get_mailing_method(Config.mailing_method)
|
||||||
|
|
|
@ -13,7 +13,7 @@ from pgpy import PGPKey, PGPMessage, PGPUID
|
||||||
from pgpy.errors import PGPError
|
from pgpy.errors import PGPError
|
||||||
|
|
||||||
from .crypto import pgp_sign
|
from .crypto import pgp_sign
|
||||||
from .config import Config, render_message
|
from .config import render_message
|
||||||
from .util import create_nonce, fingerprint
|
from .util import create_nonce, fingerprint
|
||||||
|
|
||||||
|
|
||||||
|
@ -145,13 +145,9 @@ class ConfirmationResponse:
|
||||||
|
|
||||||
def verify_signature(self, key: PGPKey):
|
def verify_signature(self, key: PGPKey):
|
||||||
if not self._msg.is_signed:
|
if not self._msg.is_signed:
|
||||||
if not Config.permit_unsigned_response:
|
|
||||||
raise EasyWksError('The confirmation response is not signed. If you used an automated tool such as '
|
raise EasyWksError('The confirmation response is not signed. If you used an automated tool such as '
|
||||||
'gpg-wks-client for submitting your response, please update said tool or try '
|
'gpg-wks-client for submitting your response, please update said tool or try '
|
||||||
'responding manually.')
|
'responding manually.')
|
||||||
else:
|
|
||||||
# Unsigned, but permitted
|
|
||||||
return
|
|
||||||
uid: PGPUID = key.get_uid(self._submitter_addr)
|
uid: PGPUID = key.get_uid(self._submitter_addr)
|
||||||
if uid is None or uid.email != self._submitter_addr:
|
if uid is None or uid.email != self._submitter_addr:
|
||||||
raise EasyWksError(f'UID {self._submitter_addr} not found in PGP key')
|
raise EasyWksError(f'UID {self._submitter_addr} not found in PGP key')
|
||||||
|
|
|
@ -11,6 +11,10 @@
|
||||||
# older version of the WKS standard where signing the confirmation
|
# older version of the WKS standard where signing the confirmation
|
||||||
# response is only recommended, but not required. Set this option to
|
# response is only recommended, but not required. Set this option to
|
||||||
# true if you want to accept such unsigned responses.
|
# true if you want to accept such unsigned responses.
|
||||||
|
#
|
||||||
|
# This option is deprecated and will be removed in a future release.
|
||||||
|
# It is replaced by the me.s3lph.easywks_permit-unsigned-response
|
||||||
|
# per-domain policy flag.
|
||||||
#permit_unsigned_response: false
|
#permit_unsigned_response: false
|
||||||
|
|
||||||
# Port configuration for the webserver. Put this behind a
|
# Port configuration for the webserver. Put this behind a
|
||||||
|
@ -75,3 +79,51 @@ domains:
|
||||||
# or if you're supplying your own password-protected key, set the
|
# or if you're supplying your own password-protected key, set the
|
||||||
# passphrase here:
|
# passphrase here:
|
||||||
#passphrase: "Correct Horse Battery Staple"
|
#passphrase: "Correct Horse Battery Staple"
|
||||||
|
# Policy flags control behavior of the submission process for this
|
||||||
|
# domain. Supports most of the standard policy flags (see
|
||||||
|
# https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service-15#section-4.5
|
||||||
|
# for details) as well as some EasyWKS-namespaced flags.
|
||||||
|
policy_flags:
|
||||||
|
|
||||||
|
# The mail server provider does only accept keys with only a
|
||||||
|
# mailbox in the User ID. In particular User IDs with a real name
|
||||||
|
# in addition to the mailbox will be rejected as invalid.
|
||||||
|
#mailbox-only: false
|
||||||
|
|
||||||
|
# The submission of the mail to the server is done using an
|
||||||
|
# authenticated connection. Thus the submitted key will be
|
||||||
|
# published immediately without any confirmation request.
|
||||||
|
#auth-submit: false
|
||||||
|
|
||||||
|
# This keyword can be used to explicitly claim the support of a
|
||||||
|
# specific version of the Web Key Directory update protocol. This
|
||||||
|
# is in general not needed but implementations may have
|
||||||
|
# workarounds for providers which only support an old protocol
|
||||||
|
# version. If these providers update to a newer version they
|
||||||
|
# should add this keyword so that the implementation can disable
|
||||||
|
# the workaround. The value is an integer corresponding to the
|
||||||
|
# respective draft revision number.
|
||||||
|
#protocol-version: null
|
||||||
|
|
||||||
|
# Some clients (including recent versions of gpg-wks-client
|
||||||
|
# follow an older version of the WKS standard where signing the
|
||||||
|
# confirmation response is only recommended, but not required.
|
||||||
|
# Set this option to true if you want to accept such unsigned
|
||||||
|
# responses.
|
||||||
|
#me.s3lph.easywks_permit-unsigned-response: false
|
||||||
|
|
||||||
|
# Remove all UIDs except the one being verified.
|
||||||
|
#me.s3lph.easywks_strip-unverified-uids: false
|
||||||
|
|
||||||
|
# Remove user attribute (i.e. photo) UIDs.
|
||||||
|
#me.s3lph.easywks_strip-ua-uids: false
|
||||||
|
|
||||||
|
# Remove all third party certifications fom the submitted keys.
|
||||||
|
# Certifications issued by revoked keys submitted in the same
|
||||||
|
# submission request are exempt from this policy.
|
||||||
|
#me.s3lph.easywks_strip-3rdparty-signatures: false
|
||||||
|
|
||||||
|
# Maximal number of revoked keys that can be submitted alongside
|
||||||
|
# a valid key. If this flag is absent or has value -1, an
|
||||||
|
# unlimited number of revoked keys is permitted.
|
||||||
|
#me.s3lph.easywks_max-revoked-keys: -1
|
Loading…
Reference in a new issue