Library Reference

This section documents the public API for using patatt as a library.

Core Classes

PatattMessage

class patatt.PatattMessage(msgdata)[source]

Bases: object

RFC2822 email message with patch attestation support.

Represents an email message that can be signed and validated using DKIM-like developer signatures. Uses git-mailinfo for canonicalization to ensure consistent signatures regardless of mail client formatting.

Parameters:

msgdata (bytes) – Raw message bytes in RFC2822 format.

__init__(msgdata)[source]
headers: List[bytes]
body: bytes
lf: bytes
signed: bool
canon_headers: List[bytes] | None
canon_body: bytes | None
canon_identity: str | None
sigs: List[DevsigHeader] | None
git_canonicalize()[source]

Canonicalize the message using git-mailinfo.

Normalizes headers and body for consistent signing/validation. Results are cached in canon_headers, canon_body, and canon_identity.

sign(algo, keyinfo, identity, selector)[source]

Sign the message and add signature headers.

Parameters:
  • algo (str) – Signing algorithm (‘ed25519’, ‘openpgp’, or ‘openssh’).

  • keyinfo (str | bytes) – Private key data or identifier.

  • identity (str | None) – Signer identity (email). If None, uses canon_identity.

  • selector (str | None) – Key selector for keyring lookup.

Raises:

SigningError – If signing fails or message cannot be canonicalized.

validate(identity, pkey, trim_body=False)[source]

Validate the signature for a specific identity.

Parameters:
  • identity (str) – The signer identity (email) to validate.

  • pkey (bytes | str | None) – Public key data for validation. If None, signature is checked against embedded key data (if available).

  • trim_body (bool) – If True, trim body to length specified in signature.

Returns:

Tuple of (signature_algorithm, public_key_info).

Raises:

ValidationError – If no matching signature or validation fails.

Return type:

Tuple[str, str]

as_bytes()[source]

Return the message as bytes, including any signature headers.

as_string(encoding='utf-8')[source]

Return the message as a string.

Parameters:

encoding (str) – Character encoding to use. Defaults to ‘utf-8’.

load_from_bytes(msgdata)[source]

Parse message data and populate headers and body.

Parameters:

msgdata (bytes) – Raw RFC2822 message bytes.

Raises:

RuntimeError – If the data is not a valid RFC2822 message.

get_sigs()[source]

Extract and return all signature headers from the message.

Returns:

List of DevsigHeader objects parsed from X-Developer-Signature headers. Results are cached after first call.

Raises:

RuntimeError – If headers cannot be parsed.

Return type:

List[DevsigHeader]

DevsigHeader

class patatt.DevsigHeader(hval=None)[source]

Bases: object

DKIM-like signature header for patch attestation.

Manages X-Developer-Signature headers, handling creation and validation of cryptographic signatures using ed25519, OpenPGP, or OpenSSH algorithms.

Parameters:

hval (bytes | None) – Optional raw header value to parse.

__init__(hval=None)[source]
hval: bytes | None
hdata: Dict[str, bytes]
from_bytes(hval)[source]

Parse a raw header value into fields.

Parameters:

hval (bytes) – Raw header bytes to parse.

get_field_as_bytes(field)[source]

Get a header field value as bytes.

Parameters:

field (str) – Field name (e.g., ‘a’, ‘i’, ‘bh’).

Returns:

Field value as bytes, or None if not set.

Return type:

bytes | None

get_field_as_str(field)[source]

Get a header field value as a string.

Parameters:

field (str) – Field name (e.g., ‘a’, ‘i’, ‘bh’).

Returns:

Field value decoded as string, or None if not set.

Return type:

str | None

get_field(field, decode=False)[source]

Get a header field value (deprecated).

Deprecated since version Use: get_field_as_bytes() or get_field_as_str() instead.

set_field(field, value)[source]

Set a header field value.

Parameters:
  • field (str) – Field name (e.g., ‘a’, ‘i’, ‘s’).

  • value (None | str | bytes) – Field value. If None, the field is deleted.

set_body(body, maxlen=None)[source]

Set the message body and compute its hash.

Call this after git-mailinfo normalization.

Parameters:
  • body (bytes) – Message body bytes.

  • maxlen (int | None) – Optional maximum length to hash (for partial body signing).

Raises:

ValidationError – If maxlen is larger than the body.

set_headers(headers, mode)[source]

Set message headers for signing or validation.

Call this after git-mailinfo normalization.

Parameters:
  • headers (List[bytes]) – List of raw header lines.

  • mode (str) – Either ‘sign’ or ‘validate’.

Raises:
  • SigningError – If required headers are missing (sign mode).

  • ValidationError – If required headers are not signed (validate mode).

sanity_check()[source]

Verify that required fields are set before signing/validation.

validate(keyinfo)[source]

Validate the signature against the message content.

Parameters:

keyinfo (str | bytes | None) – Public key data. For ed25519/openssh, base64-encoded key. For openpgp, raw key bytes or None to use default keyring.

Returns:

Tuple of (signing_key_id, sign_timestamp).

Raises:
Return type:

Tuple[str, str]

sign(keyinfo, split=True)[source]

Sign the message and generate signature header value.

Parameters:
  • keyinfo (str | bytes) – Private key data. For ed25519, base64-encoded private key. For openpgp/openssh, key identifier string.

  • split (bool) – If True, split long signature across multiple lines.

Returns:

Tuple of (signature_header_value, public_key_info).

Raises:

ValidationError – If algorithm field is missing.

Return type:

Tuple[bytes, bytes]

static splitter(longstr, limit=75)[source]

Exceptions

exception patatt.Error(message, errors=None)[source]

Bases: Exception

Base exception for patatt errors.

Parameters:
  • message (str) – Error description.

  • errors (List[str] | None) – Optional list of detailed error messages.

__init__(message, errors=None)[source]
errors: List[str] | None
exception patatt.SigningError(message, errors=None)[source]

Bases: Error

Raised when message signing fails.

exception patatt.ConfigurationError(message, errors=None)[source]

Bases: Error

Raised when configuration is invalid or missing.

exception patatt.ValidationError(message, errors=None)[source]

Bases: Error

Raised when signature validation fails.

exception patatt.NoKeyError(message, errors=None)[source]

Bases: ValidationError

Raised when the public key for validation cannot be found.

exception patatt.BodyValidationError(message, errors=None)[source]

Bases: ValidationError

Raised when the message body hash does not match the signature.

Public Functions

Signing and Validation

patatt.sign_message(msgdata, algo, keyinfo, identity, selector)[source]

Sign an RFC2822 message and return the signed message bytes.

Parameters:
  • msgdata (bytes) – Raw RFC2822 message bytes.

  • algo (str) – Signing algorithm (‘ed25519’, ‘openpgp’, ‘openssh’).

  • keyinfo (str | bytes) – Private key data or identifier.

  • identity (str | None) – Signer identity (email). If None, extracted from message.

  • selector (str | None) – Key selector for keyring lookup.

Returns:

Signed message bytes with X-Developer-Signature header added.

Return type:

bytes

patatt.validate_message(msgdata, sources, trim_body=False)[source]

Validate all signatures in an RFC2822 message.

Parameters:
  • msgdata (bytes) – Raw RFC2822 message bytes.

  • sources (List[str]) – List of keyring sources to search for public keys.

  • trim_body (bool) – If True, trim body to length specified in signatures.

Returns:

(result_code, identity, timestamp, key_source, algorithm, errors)

Result codes: RES_VALID, RES_BADSIG, RES_NOKEY, RES_NOSIG, RES_ERROR

Return type:

List of attestation tuples, one per signature found

Key Management

patatt.make_pkey_path(keytype, identity, selector)[source]

Construct the standard keyring path for a public key.

Parameters:
  • keytype (str) – Key algorithm type (‘ed25519’, ‘openpgp’, ‘openssh’).

  • identity (str) – Signer identity in email format (local@domain).

  • selector (str) – Key selector for distinguishing multiple keys.

Returns:

keytype/domain/local/selector

Return type:

Path in format

Raises:

ValidationError – If identity is not in valid email format.

patatt.make_byhash_path(keytype, identity, selector)[source]

Construct a privacy-preserving by-hash keyring path.

Computes SHA256 of the standard keypath to avoid exposing identity information in directory structure.

Parameters:
  • keytype (str) – Key algorithm type (‘ed25519’, ‘openpgp’, ‘openssh’).

  • identity (str) – Signer identity in email format (local@domain).

  • selector (str) – Key selector for distinguishing multiple keys.

Returns:

by-hash/XX/YYY… where XX is first 2 hex chars and YYY… is remaining 62 hex chars of SHA256 hash.

Return type:

Path in format

patatt.get_public_key(source, keytype, identity, selector)[source]

Look up a public key from a keyring source.

Searches for the key at the standard path first, then falls back to by-hash lookup if not found.

Parameters:
  • source (str) – Keyring source. Either a filesystem path or a git ref in format ‘ref:repo:refspec:subpath’.

  • keytype (str) – Key algorithm type (‘ed25519’, ‘openpgp’, ‘openssh’).

  • identity (str) – Signer identity in email format (local@domain).

  • selector (str) – Key selector for distinguishing multiple keys.

Returns:

Tuple of (key_data, key_source_description).

Raises:
  • KeyError – If key cannot be found in any location.

  • ConfigurationError – If ref source format is invalid.

Return type:

Tuple[bytes, str]

Configuration

patatt.get_main_config(section=None)[source]

Load patatt configuration from git config.

Parameters:

section (str | None) – Optional subsection name for patatt config. If None, loads base patatt.* settings.

Returns:

Configuration dictionary with keyring sources and settings. Results are cached per section.

Return type:

Dict[str, str | List[str]]

patatt.get_data_dir()[source]

Get the patatt data directory, creating it if necessary.

Returns:

Path to $XDG_DATA_HOME/patatt or ~/.local/share/patatt.

Return type:

Path

Constants

Result Codes

The following constants are returned by validate_message() to indicate the validation result:

patatt.RES_VALID = 0

Signature is valid.

patatt.RES_BADSIG = 1

Signature verification failed.

patatt.RES_NOKEY = 2

Public key not found in any keyring.

patatt.RES_NOSIG = 3

Message has no signatures.

patatt.RES_ERROR = 4

Error during validation (e.g., malformed signature).

Usage Examples

Signing a Message

import patatt

# Read a patch file
with open('patch.eml', 'rb') as f:
    msgdata = f.read()

# Sign with ed25519 key
signed = patatt.sign_message(
    msgdata,
    algo='ed25519',
    keyinfo='path/to/private.key',
    identity='user@example.com',
    selector='default'
)

# Write signed message
with open('signed-patch.eml', 'wb') as f:
    f.write(signed)

Validating a Message

import patatt

# Read a signed patch
with open('signed-patch.eml', 'rb') as f:
    msgdata = f.read()

# Get keyring sources from config
config = patatt.get_main_config()
sources = config.get('keyringsrc', [])

# Validate all signatures
results = patatt.validate_message(msgdata, sources)

for result in results:
    code, identity, timestamp, key_source, algo, errors = result
    if code == patatt.RES_VALID:
        print(f"Valid signature from {identity}")
    elif code == patatt.RES_NOKEY:
        print(f"No public key for {identity}")
    elif code == patatt.RES_BADSIG:
        print(f"Bad signature from {identity}: {errors}")

Working with PatattMessage Directly

import patatt

# Parse a message
with open('patch.eml', 'rb') as f:
    pm = patatt.PatattMessage(f.read())

# Check if signed
if pm.signed:
    # Get all signatures
    for sig in pm.get_sigs():
        print(f"Signed by: {sig.get_field_as_str('i')}")
        print(f"Algorithm: {sig.get_field_as_str('a')}")

# Sign the message
pm.sign(
    algo='ed25519',
    keyinfo='path/to/key',
    identity='user@example.com',
    selector='default'
)

# Get signed message bytes
signed_bytes = pm.as_bytes()

Key Path Utilities

import patatt

# Get standard keyring path
path = patatt.make_pkey_path('ed25519', 'user@example.com', 'default')
# Returns: Path('ed25519/example.com/user/default')

# Get privacy-preserving by-hash path
byhash = patatt.make_byhash_path('ed25519', 'user@example.com', 'default')
# Returns: Path('by-hash/XX/YYY...')

# Look up a public key
try:
    key_data, key_source = patatt.get_public_key(
        '/path/to/keyring',
        'ed25519',
        'user@example.com',
        'default'
    )
except KeyError:
    print("Key not found")