Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an authenticating serializer #88

Merged
merged 1 commit into from
Aug 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Build-Depends:
python3-netaddr,
python3-psycopg2,
python3-pydbus,
python3-pynacl,
python3-pyrad,
python3-pyroute2,
python3-pysnmp4,
Expand Down Expand Up @@ -69,6 +70,7 @@ Depends:
python3-netaddr,
python3-psycopg2,
python3-pydbus,
python3-pynacl,
python3-pyrad,
python3-pyroute2,
python3-pysnmp4,
Expand Down
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ RUN export DEBIAN_FRONTEND="noninteractive" && \
python3-pip \
python3-psycopg2 \
python3-pydbus \
python3-pynacl \
python3-pyrad \
python3-pyroute2 \
python3-pysnmp4 \
Expand Down
1 change: 1 addition & 0 deletions setup.py.in
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ setup(name='@PACKAGE_NAME@',
"psycopg2",
"pydbus",
'pygobject',
'pynacl',
"pyrad",
"pyroute2",
"pysnmp",
Expand Down
132 changes: 132 additions & 0 deletions src/hades/agent/signature.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# -*- coding: utf-8 -*-
"""
A serializer that prepends a JSON header with an ed25519 signature before the
data.

The header is a completely separate JSON document, that is separated from the
body by optional whitespace. This allows parsing the header without parsing the
payload. Also it is not required to encode the payload. Raw data can also be
inspected by humans easily. The only problem with this scheme is, that the
payload may not start with space, because this white space would be striped
and signature verification would fail.

The JSON payload ``{"Foobar": 1}`` would be encoded as follows:

.. code-block :: json

{
"signature": "u9rPWbQh3TNpW8wrimL5SHtelkgm32cTPfzaUgp+djMDGz/Vjf/mb6BtQcXpJ1noJl2xILTWxhrpqtv9ykf2Bw==",
"signer": "w7ADgLSZTlXIDY/qbcfxUCeXht8VcpGoJYOj0lQu1Qw=",
"content_type": "application/json",
"content_encoding": "utf-8"
}
{
"Foobar": 1
}

"""
import io
import json
from typing import Iterable, Union

import nacl.encoding
import nacl.signing
from kombu.serialization import dumps, loads, register as kombu_register
from kombu.utils.encoding import bytes_to_str

__all__ = ['ED25519Serializer', 'register']


class ED25519Serializer(object):
key_codec = nacl.encoding.Base64Encoder()
json_decoder = json.JSONDecoder()
whitespace = b' \t\n\r'

def __init__(self, signing_key: nacl.signing.SigningKey,
verify_keys: Iterable[nacl.signing.VerifyKey],
serializer='json', content_encoding='utf-8'):
self._signing_key = signing_key
self._verify_keys = {
self.key_codec.encode(bytes(key)).decode('ascii'): key
for key in verify_keys
}
self._serializer = serializer
self._signer = self.key_codec.encode(
bytes(self._signing_key.verify_key)).decode('ascii')
self._content_encoding = content_encoding

def _ensure_bytes(self, data: Union[bytes, str]):
if isinstance(data, bytes):
return data
return data.encode(self._content_encoding)

def serialize(self, data):
content_type, content_encoding, body = dumps(
data, serializer=self._serializer)
if content_encoding != self._content_encoding:
raise ValueError("Content encoding of inner serializer {!r} must "
"match ({!r} != {!r})"
.format(self._serializer, content_encoding,
self._content_encoding))
body = self._ensure_bytes(body)
if len(body) > 0 and body[0] in self.whitespace:
raise ValueError("Inner data may not begin with the following "
"characters {!r}"
.format(str(self.whitespace)))
message = self._signing_key.sign(body)
signature = self.key_codec.encode(message.signature).decode('ascii')
header = {
'signature': signature,
'signer': self._signer,
'content_type': content_type,
'content_encoding': content_encoding,
}
buffer = io.BytesIO()
wrapper = io.TextIOWrapper(buffer, self._content_encoding,
write_through=True)
with wrapper:
json.dump(header, wrapper)
buffer.write(b"\n")
buffer.write(message.message)
return buffer.getvalue()

def parse_header(self, data):
return self.json_decoder.raw_decode(data.decode(self._content_encoding))

def deserialize(self, data):
data = self._ensure_bytes(data)
header, end = self.parse_header(data)
# Skip whitespace
length = len(data)
while end < length and data[end] in self.whitespace:
end += 1
header, body = header, data[end:]

signer, signature, content_type, content_encoding = (
header['signer'], header['signature'],
header['content_type'], header['content_encoding']
)
signature = self.key_codec.decode(signature)
if content_encoding != self._content_encoding:
raise ValueError("Invalid inner content encoding ({!r} != {!r})"
.format(content_encoding, self._content_encoding))

try:
verify_key = self._verify_keys[signer]
except KeyError:
raise ValueError("Unknown signer {!r}".format(signer)) from None
verify_key.verify(body, signature)
return loads(bytes_to_str(body), content_type, content_encoding,
force=True)


def register(signing_key: nacl.signing.SigningKey,
verify_keys: Iterable[nacl.signing.VerifyKey],
name: str = 'ed25519', serializer='json',
content_type: str = 'application/x-data-ed25519',
content_encoding: str = 'utf-8'):
"""Register serializer with :mod:`kombu`"""
s = ED25519Serializer(signing_key, verify_keys, serializer,
content_encoding)
kombu_register(name, s.serialize, s.deserialize, content_type,
content_encoding)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How precisely would this serializer be enabled? I don't see any config changes in this PR.