Skip to content
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
256 changes: 224 additions & 32 deletions README.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions owid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
from .error import OwidError
from .io import SIGNATURE_LENGTH
from .owid import Owid
from .parse import ParseResult
from .status import ParseStatus, SignatureStatus
from .version import DEFAULT_VERSION, Version

__all__ = [
Expand All @@ -41,6 +43,12 @@
"Crypto",
"OwidError",
"Owid",
# A caller cannot act on a read without naming the reason it carries, so
# the result and both status vocabularies sit beside the type they
# describe rather than in a module a reader has to go looking for.
"ParseResult",
"ParseStatus",
"SignatureStatus",
"Version",
"DEFAULT_VERSION",
"SIGNATURE_LENGTH",
Expand Down
49 changes: 31 additions & 18 deletions owid/creator.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,40 +105,53 @@ def crypto(self) -> Crypto:
"""Used to sign OWIDs from this creator."""
return self._crypto

def sign(self, owid: Owid) -> None:
def _sign(self, owid: Owid) -> None:
"""Signs the OWID provided, setting the domain to the creator domain,
the date to the current time, and the version to the current version,
then signing it."""
self.sign_with_others(owid, [])
self._sign_with_others(owid, [])

def sign_with_others(self, owid: Owid, others: Sequence[Owid]) -> None:
def _sign_with_others(self, owid: Owid, others: Sequence[Owid]) -> None:
"""Signs the OWID provided together with the other OWIDs provided.

The same others, in the same order, must be passed when verifying.
"""
owid.version = DEFAULT_VERSION
owid.domain = self._domain
owid._version = DEFAULT_VERSION
owid._domain = self._domain
# Truncate to whole minutes so the in-memory date matches the value
# written to the wire format. This keeps a signed OWID equal to a copy
# decoded from its bytes, and ensures signing and verification operate
# on the same minute precise value.
owid.date = datetime.now(timezone.utc).replace(second=0, microsecond=0)
owid._date = datetime.now(timezone.utc).replace(second=0, microsecond=0)
data = owid.data_for_crypto(others)
owid.signature = self._crypto.sign_byte_array(data)
if len(owid.signature) != SIGNATURE_LENGTH:
owid._signature = self._crypto.sign_byte_array(data)
if len(owid._signature) != SIGNATURE_LENGTH:
raise OwidError(
"signature length '{0}' not compatible with '{1}' OWID "
"signature length".format(len(owid.signature), SIGNATURE_LENGTH)
"signature length".format(len(owid._signature), SIGNATURE_LENGTH)
)

def sign_string(self, value: str) -> Owid:
"""Creates a new signed OWID for the creator containing the string as
the payload."""
return self.sign_bytes(value.encode("utf-8"))
def create_string(self, value: str) -> Owid:
"""Creates and signs a new OWID carrying the string as its payload."""
if value is None:
raise OwidError("a payload is required")
return self.create(value.encode("utf-8"))

def sign_bytes(self, value: bytes) -> Owid:
"""Creates a new signed OWID for the creator containing the bytes as
the payload."""
owid = Owid(payload=value)
self.sign(owid)
def create(self, value: bytes, others: Sequence[Owid] = ()) -> Owid:
"""Creates and signs a new OWID carrying the bytes as its payload.

This is one of only two ways an OWID reaches calling code, the other
being a successful parse. The creator owns the version, the domain,
the date and the signature; a caller supplies the payload and nothing
else, so there is no moment at which a partly built OWID exists for
anyone to hold or pass on.

Any others are covered by the signature so that a tree can be verified
as a whole, and the same others in the same order must be passed when
verifying.
"""
if value is None:
raise OwidError("a payload is required")
owid = Owid._create(payload=bytes(value))
self._sign_with_others(owid, list(others))
return owid
16 changes: 12 additions & 4 deletions owid/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,23 @@
# ****************************************************************************
"""The error type raised across the package.

A single exception type carries a human readable message. It is raised for
unsupported versions, malformed buffers, key problems, and the other failures
that can occur when creating, reading, signing, or verifying OWIDs.
A single exception type carries a human readable message. It is raised where
the fault lies in the calling code or in the local key material, being a
domain or payload that cannot be written, a key that cannot be imported or
exported, an attempt to construct an OWID directly, and a version the writer
does not know.

Reading data that came from outside does not raise. Bytes that are not an OWID
are an ordinary outcome, so the parse surfaces answer with a ParseResult
carrying a ParseStatus, and a signature that cannot be judged is reported as a
SignatureStatus rather than as an exception.
"""

from __future__ import annotations


class OwidError(Exception):
"""Raised when an OWID can not be created, read, signed, or verified."""
"""Raised when an OWID can not be created, written, signed, or verified,
and never for external data that turns out not to be an OWID."""

pass
11 changes: 10 additions & 1 deletion owid/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
The format uses little endian unsigned 32 bit integers, null terminated
strings, and a fixed 64 byte signature. The date is stored as the count of
hours (version 1) or minutes (versions 2 and 3) since the base date.

The write helpers here are the ones an OWID is serialized with. The Reader is
not the public read, because external data is read by owid.parse, which walks
the bytes by index and reports a ParseStatus instead of raising.
"""

from __future__ import annotations
Expand Down Expand Up @@ -47,7 +51,12 @@


class Reader:
"""Sequential reader over a byte buffer."""
"""Sequential reader over a byte buffer, raising on anything malformed.

Kept for the tests that assert those messages, and reached only through
the private raising route on Owid. Callers read external data with
Owid.parse, Owid.parse_bytes or Owid.parse_prefix instead.
"""

def __init__(self, buffer: bytes) -> None:
self._buffer = buffer
Expand Down
Loading
Loading