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
|
||||
directory: /tmp/easywks
|
||||
permit_unsigned_response: true # required for gpg-wks-client compat
|
||||
httpd:
|
||||
host: 127.0.0.1
|
||||
port: 8080
|
||||
|
@ -53,6 +52,8 @@ easywksserver_gpgwksclient:
|
|||
domains:
|
||||
example.org:
|
||||
submission_address: webkey@example.org
|
||||
policy_flags:
|
||||
me.s3lph.easywks_permit-unsigned-response: true # required for gpg-wks-client compat
|
||||
EOF
|
||||
- easywks --config /tmp/easywks.yml init
|
||||
- easywks --config /tmp/easywks.yml webserver &
|
||||
|
|
|
@ -3,6 +3,33 @@ import string
|
|||
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):
|
||||
methods = ['stdout', 'smtp']
|
||||
if value not in methods:
|
||||
|
@ -77,6 +104,19 @@ def _validate_policy_flags(value):
|
|||
for c in flag:
|
||||
if c not in alphabet:
|
||||
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):
|
||||
|
@ -201,6 +241,7 @@ Config = _GlobalConfig(
|
|||
working_directory=_ConfigOption('directory', str, '/var/lib/easywks'),
|
||||
pending_lifetime=_ConfigOption('pending_lifetime', int, 604800),
|
||||
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),
|
||||
httpd=_ConfigOption('httpd', dict, {
|
||||
'host': 'localhost',
|
||||
|
|
|
@ -36,10 +36,15 @@ def make_submission_address_file(domain: str):
|
|||
def make_policy_file(domain: str):
|
||||
content = f'submission-address: {Config[domain].submission_address}\n'
|
||||
for flag, value in Config[domain].policy_flags.items():
|
||||
if not value or len(value) == 0:
|
||||
content += f'{flag}: {value}\n'
|
||||
if isinstance(value, bool):
|
||||
if not value:
|
||||
continue
|
||||
else:
|
||||
content += flag + '\n'
|
||||
elif value is None:
|
||||
content += flag + '\n'
|
||||
else:
|
||||
content += f'{flag}: {value}\n'
|
||||
return content
|
||||
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import sys
|
||||
from typing import List, Dict, Tuple
|
||||
from typing import Any, List, Dict, Tuple
|
||||
|
||||
from .crypto import pgp_decrypt
|
||||
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 .types import SubmissionRequest, ConfirmationResponse, PublishResponse, ConfirmationRequest, EasyWksError,\
|
||||
XLOOP_HEADER
|
||||
|
@ -15,7 +17,7 @@ from email.parser import BytesParser
|
|||
from email.policy import default
|
||||
from email.utils import getaddresses
|
||||
|
||||
from pgpy import PGPMessage, PGPKey, PGPUID
|
||||
from pgpy import PGPMessage, PGPKey, PGPUID, PGPSignature
|
||||
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)
|
||||
|
||||
|
||||
# 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):
|
||||
try:
|
||||
msg: Message = BytesParser(policy=default).parsebytes(mail)
|
||||
|
@ -170,17 +228,25 @@ def process_mail(mail: bytes):
|
|||
except FileNotFoundError:
|
||||
raise EasyWksError('There is no submission request for this email address, or it has expired. '
|
||||
'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
|
||||
request.verify_signature(key)
|
||||
response: PublishResponse = request.get_publish_response(key)
|
||||
rmsg = response.create_signed_message()
|
||||
write_public_key(sender_domain, sender_mail, key, revoked_keys)
|
||||
remove_pending_key(sender_domain, request.nonce)
|
||||
else:
|
||||
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()
|
||||
rmsg = response.create_signed_message()
|
||||
write_pending_key(sender_domain, response.nonce, request.key, request.revoked_keys)
|
||||
rmsg = response.create_signed_message()
|
||||
except EasyWksError as e:
|
||||
rmsg = e.create_message(sender_mail, submission_address)
|
||||
method = get_mailing_method(Config.mailing_method)
|
||||
|
|
|
@ -13,7 +13,7 @@ from pgpy import PGPKey, PGPMessage, PGPUID
|
|||
from pgpy.errors import PGPError
|
||||
|
||||
from .crypto import pgp_sign
|
||||
from .config import Config, render_message
|
||||
from .config import render_message
|
||||
from .util import create_nonce, fingerprint
|
||||
|
||||
|
||||
|
@ -145,13 +145,9 @@ class ConfirmationResponse:
|
|||
|
||||
def verify_signature(self, key: PGPKey):
|
||||
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 '
|
||||
'gpg-wks-client for submitting your response, please update said tool or try '
|
||||
'responding manually.')
|
||||
else:
|
||||
# Unsigned, but permitted
|
||||
return
|
||||
uid: PGPUID = key.get_uid(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')
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
# 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.
|
||||
#
|
||||
# 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
|
||||
|
||||
# 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
|
||||
# passphrase here:
|
||||
#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