Library Reference
This section documents the public API for using patatt as a library.
Core Classes
PatattMessage
- class patatt.PatattMessage(msgdata)[source]
Bases:
objectRFC2822 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.
- 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_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:
objectDKIM-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.
- 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()orget_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).
- 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:
BodyValidationError – If body hash doesn’t match.
ValidationError – If signature validation fails.
- 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]
Exceptions
- exception patatt.Error(message, errors=None)[source]
Bases:
ExceptionBase exception for patatt errors.
- Parameters:
message (str) – Error description.
errors (List[str] | None) – Optional list of detailed error messages.
- errors: List[str] | None
- exception patatt.SigningError(message, errors=None)[source]
Bases:
ErrorRaised when message signing fails.
- exception patatt.ConfigurationError(message, errors=None)[source]
Bases:
ErrorRaised when configuration is invalid or missing.
- exception patatt.ValidationError(message, errors=None)[source]
Bases:
ErrorRaised when signature validation fails.
- exception patatt.NoKeyError(message, errors=None)[source]
Bases:
ValidationErrorRaised when the public key for validation cannot be found.
- exception patatt.BodyValidationError(message, errors=None)[source]
Bases:
ValidationErrorRaised 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]]
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")