Bundle verification¶
Added in version 9.0.0.
Signing a bundle lets someone with your public key check that its contents have not changed since you signed it. Keep the private key for signing. Give the public key to people or applications that need to verify your bundles. They must obtain that public key from a source they trust.
Verification checks the bundle’s XML and every attachment it references. Files that the XML does not reference are not covered by the signature. Recipe, configuration, product, results, and self-test report bundles support signing as XML files, ZIP archives, or directories. The signature is stored inside the bundle’s XML, so there is no separate signature file to distribute.
The existing sign_files option adds attachment hashes, and
validate_signature checks those hashes. A hash is a fingerprint calculated
from a file’s contents. It can detect changes, but it does not identify who
supplied the file. Use the signing and
verification options below when you also need to check the signer.
Install the optional signing dependencies in each environment that signs or verifies bundles:
python -m pip install 'momotor-bundles[signing]'
Create a keypair¶
Use OpenSSL to create a private key protected with a password. Ed25519 is the signing algorithm supported by this library. You can use the commands below without choosing any other cryptography settings:
umask 077
openssl genpkey -algorithm ED25519 -aes-256-cbc -out author.pem
Enter a password when OpenSSL prompts. Create the matching public-key file:
openssl pkey -in author.pem -pubout -out author.pub.pem
Enter the same password to read the private key. Keep author.pem private
and distribute author.pub.pem to people or applications that verify your
bundles. umask 077 restricts access to new key files to your user account.
Sign a bundle with the private key and verify it with the public key:
momotor-bundles sign recipe.zip --private-key author.pem --output signed.zip
momotor-bundles verify signed.zip --trusted-key author.pub.pem
The signing command prompts for the private-key password. Verification reports
VERIFIED and exits with code 0 when the signature and referenced attachments
are valid.
For unattended signing with an unencrypted private key, omit -aes-256-cbc
from the key generation command.
Sign and load a bundle¶
Use the keys generated above for signing and verification. Only Ed25519 keys are supported.
In Python, load_private_key reads the private-key file. For a key protected
with a password, pass password_callback to request the password when needed.
The example uses getpass to hide typed characters and encode() to convert
the password to the bytes expected by the key loader.
load_public_key reads the public-key file you trust. The library uses only
the keys you supply. It does not automatically trust keys found in a bundle,
download keys, or use keys from your operating system.
from contextlib import closing
from getpass import getpass
from momotor.bundles.signing import (
load_private_key, load_public_key, sign_bundle_file, load_verified,
)
private = load_private_key(
"author.pem", password_callback=lambda: getpass("Password: ").encode(),
)
public = load_public_key("author.pub.pem")
sign_bundle_file("recipe.zip", [private], output="signed.zip")
with closing(load_verified("signed.zip", [public])) as bundle:
bundle.verification_report.require_verified()
payload = bundle.files[0].read()
load_verified verifies the bundle and returns a bundle object that reads
from the same copy of the contents that passed verification. Close the bundle
when you are finished. closing in the example does this automatically.
To check a bundle without loading it for use, call verify_bundle_file or
verify_bundle_bytes. They return a report. If you later load the original
file separately, it may have changed since the check. Use load_verified
when your application needs to use the verified contents.
sign_bundle_bytes and verify_bundle_bytes accept XML or ZIP data already
held in memory. If XML refers to external files, provide attachment_source
as their base directory or a dictionary mapping attachment paths to file bytes.
The library copies mutable input buffers before processing them.
For a directory or ZIP, xml_name selects an exact entry name. Without it,
only the defined default bundle XML names at the root are candidates. There
must be exactly one such entry, even if another candidate is malformed or has
an invalid signature. Selection happens before reading, parsing, or authenticating
the XML. A failure in the selected document never selects another document.
Other XML files are not candidates. Ordinary ZIP loading and the CLI use the
same selection rule; use --input-xml-name for an explicit CLI selection.
Verify when loading and sign when exporting¶
You can also verify through Bundle.from_file_factory and sign through
bundle.to_file. Pass VerificationOptions with your trusted public keys
when loading, and SigningOptions with your private keys when exporting.
The example uses the public and private keys loaded above.
Without these options, the existing loading and export behavior is unchanged.
from contextlib import closing
from momotor.bundles import Bundle
from momotor.bundles.signing import VerificationOptions, SigningOptions
with closing(Bundle.from_file_factory(
"signed.zip", verification=VerificationOptions((public,)),
)) as bundle:
bundle.to_file("release.zip", signing=SigningOptions((private,)))
Verification runs before the factory chooses the bundle type. It preserves XML
text, including text beside comments. validate_xml=False and
validate_signature=False do not turn off the verification requested through
VerificationOptions. A factory for a specific bundle type still rejects
other types, such as a recipe passed to a configuration factory.
Signing happens after the bundle has been converted to its output XML. It adds
SHA-256 hashes for every referenced attachment, even with sign_files=False.
Any existing supported hashes must also match the attachment contents.
Missing attachments, missing required hashes, or mismatched hashes cause the
operation to fail. Replace mode does not bypass these checks. After changing an
attachment, update its declared hashes before signing again.
The bundle XML cannot reference itself as an attachment. The output XML also cannot overwrite a referenced file, including a file inside a referenced directory.
Write signed exports to the appropriate destination:
A signed file export prepares the complete output before replacing the destination file. If preparation fails, the existing destination is unchanged.
A signed directory export needs a new directory outside the source directory. It copies the captured attachment contents and leaves the source unchanged.
Signing a directory in place replaces only the selected XML file.
Signing standalone XML with external attachments writes the output XML within the source directory. Export the attachments separately if you need to move the bundle elsewhere.
If you provide an output buffer or file descriptor, a write failure can leave partial output. The library cannot undo bytes already written to that output. Prevent other processes from writing to the same directory destination during export.
When signing standalone XML or a directory in place, prevent attachment changes for the entire signing operation. The library reads the attachments before writing the signature. A change after that read can make the signature’s attachment hashes out of date. Sign again after editing an attachment. Separate directory exports and ZIP outputs use copies of the attachments read during signing, so later changes to the originals do not affect those copies.
Choose which signatures to require¶
The policy argument controls how verification handles multiple signatures:
Policy.ANYis the default. At least one signature must be valid and match a public key you trust. For example, you can trust an old and a new key during a key change without requiring both keys to sign every bundle.Policy.ALL_PRESENTrequires at least one signature. Every signature in the bundle must be valid and match a key you trust. It does not require every trusted key to have signed the bundle.
To require a particular signer, pass its key ID in required_signers as well
as its public key in trusted_keys. A key ID is a fingerprint of the public
key. Obtain it with key_id(public) from momotor.bundles.signing.
Every listed signer must have a valid, trusted signature, whichever policy you
choose. Repeated copies of the same trusted key are treated as one key.
With Policy.ANY, an unknown key or an invalid signature does not cancel out
another valid, trusted signature. The report still includes each signature’s
result. Both policies reject a malformed signature section, duplicate signers,
unsupported signature-section versions, and attachment failures. An unsupported
signing algorithm cannot count as a valid signature.
Read verification results¶
Reports distinguish these results:
UNSIGNEDmeans the library could inspect the bundle, but it found no signature section.load_verifiedrejects unsigned bundles by default.allow_unsigned=Truepermits them while still applying safe parsing and checking any declared attachment hashes. Required signer IDs still apply, so an unsigned bundle cannot pass when you require a signer.VERIFIEDmeans the required signatures and attachment checks passed for the captured contents.FAILEDmeans the bundle format, signature requirements, or attachment checks failed. An empty or malformed signature section produces a failure, not an unsigned result.
VerificationReport gives the result for each signature and attachment. It
also includes the policy, the XML hash used for verification, files within
referenced directories, missing required signers, and problems with the bundle.
Call report.require_verified() to raise an exception if the status is not
VERIFIED. The exception contains the report so your application can explain
the failure. File access errors, invalid keys supplied by your application, and
missing signing dependencies raise exceptions instead of returning a failure
report.
inspect_signatures lists signature information without verifying it. Do not
use inspection results to decide whether to trust a bundle. Its
present field is False for known unsigned input and True for a
signature section found in the input, even if the records are malformed.
None means the source could not be inspected, for example because its XML
could not be parsed or selected. CLI JSON represents this unknown state as
null. Text output says that no signature section exists only when its absence is known.
If someone removes all signatures, the bundle looks the same as one that was
never signed. Removing one signature can also leave a bundle that still passes
Policy.ALL_PRESENT. If your application requires particular signers, reject
unsigned input and list those signers in required_signers.
A valid signature does not tell you whether a bundle is the latest version, whether a key has been withdrawn from use, or whether its code is safe to run. Signing also does not encrypt the bundle. Your application must handle these requirements separately.
Use and close verified bundles¶
Verified loading keeps a copy of the selected XML and every referenced attachment in memory. This copy is called a snapshot. Reads, hashes, copies, exports, and directory listings use that snapshot. Replacing or deleting the original XML, ZIP, or attachment files does not change the loaded contents.
Attachment lookup supports the usual name fallback, file: sources,
inherited basesrc, and scoped references. The snapshot includes referenced
repositories, directory trees, fixtures, expected files, mocks, and report files.
Other XML values, source URLs in metadata, links, and indexes do not cause the
library to fetch files.
Nested fixtures share the parent bundle’s snapshot. Closing the parent also
ends attachment access for those fixtures. Recreating an element does not give
it an independent copy of the attachments. Use snapshot_bundle to make an
independent copy before closing the verified source.
Verified attachments do not expose a writable path to the original file.
Their absolute_path property raises ZippedAttachment, even when the
input came from a directory. The snapshot does not preserve filesystem
permissions or timestamps, because the signature does not cover them.
Executable attributes declared in the XML remain available through the file API.
Rebuilding bundle XML, converting formats, or deduplicating files produces unsigned output unless you request signing again. Copying the bundle without changing any bytes preserves its signatures.
Signature format reference¶
This section describes how signatures are stored and calculated. You need these details if you write another tool that signs or verifies Momotor bundles. The examples above handle this format for you.
The key files use PEM, a text format for storing keys. Private keys use the PKCS#8 structure, and public keys use SubjectPublicKeyInfo. The OpenSSL commands above produce these formats.
The signature section is the last child element of the bundle’s XML root.
Its full name is {urn:momotor:bundles:signing:1}signatures. The element
declares its own signing namespace. Its text contains UTF-8 JSON encoded with
base64url, without padding characters. Decoding it gives a structure like this:
{"version":1,"signatures":[{"key_id":"...","algorithm":"Ed25519","signature":"..."}]}
To calculate a key ID, encode the public key as DER SubjectPublicKeyInfo, hash those bytes with SHA-256, and write the hash as lowercase hexadecimal. Each signature contains exactly 64 bytes encoded with base64url, without padding. Both base64url values must use the canonical encoding.
JSON field order does not matter. The reader rejects duplicate JSON keys, extra fields, wrong value types, duplicate signers, and noncanonical encodings. Each record signs the bundle contents, not the other signature records. The JSON itself does not need canonicalization or a JSON Canonicalization Scheme implementation.
Each signature covers the exact concatenation:
b"bundle-signature-v1\0" + canonical_xml
canonical_xml is a normalized representation of the XML. The normalization
uses inclusive Canonical XML 1.0 without comments on the whole document after
removing the signature element. Removing that element preserves all surrounding
text. XML processing instructions are included in the signed contents.
Comment-only edits do not invalidate signatures. Whitespace text is included, so changing indentation can invalidate a signature. Namespace declarations also participate, including namespaces used only by qualified names in element or attribute values. Renaming a namespace prefix can invalidate a signature. Signing does not reformat the XML.
Each external file reference has a sha256 attribute with no namespace.
A hash value is also called a digest. A reference to a shared file definition
uses the hash on that definition. If inherited settings make one shared
definition refer to different file contents, signing fails because the hashes
would conflict. Replace those references with separate definitions before
signing.
Directory references additionally have a signing-namespace kind attribute
with value directory-v1. Their sha256 is the digest of:
b"bundle-directory-v1\0" + leaf_record_1 + ... + leaf_record_n
Each leaf record describes one file below the referenced directory. Sort the records by the UTF-8 bytes of their relative POSIX paths. Each record contains, in order:
The path’s byte length as an unsigned 64-bit big-endian integer.
The UTF-8 path bytes.
The file size as an unsigned 64-bit big-endian integer.
The file’s raw 32-byte SHA-256 hash.
Every regular file below the directory is included. Empty subdirectories and
filesystem attributes are not included. The snapshot exposes the referenced
directory itself and the parent directories of its verified files. For an
empty referenced directory, hash only the bundle-directory-v1 prefix shown
above, including the final zero byte. The kind attribute distinguishes a
directory hash from a file hash.
In add mode, signing keeps other signers’ records and replaces the records for the keys you supply. It refuses to add a signature if filling attachment hashes would change XML that is already signed. Replace mode discards all existing signature records. Both modes reject malformed existing signature information and check new signatures before writing the output. Keeping another signer’s record does not mean that signature is valid or trusted.
Safety and resource limits¶
Verification reads untrusted input with stricter rules than ordinary loading. Version 1 parsing rejects DOCTYPE declarations, external entity references, XInclude, and stylesheet transformations. Transformed sources use the separate source-and-manifest protocol described below. It does not repair malformed XML or allow network access.
Attachment paths must stay inside the bundle’s source directory. Absolute
paths, .. traversal, backslashes, and network references are rejected.
The library checks every part of a filesystem path without following symlinks.
It also rejects special files, such as devices and named pipes. Reading files
this way requires operating-system support. The operation fails if that support
is unavailable.
ZIP checks reject duplicate names, unsafe paths, symlinks, special files, encrypted entries, and paths used as both a file and a directory. The library also rejects overlapping file data and conflicting ZIP header information. ZIP entries can be stored without compression or compressed with deflate. ZIP64 central directories are not supported. Local ZIP64 size fields are accepted when the ordinary central directory can represent their sizes. Paths retain their exact letter case and Unicode spelling. Verification does not extract ZIP entries onto your filesystem.
Limits controls how much input the library accepts. Every setting must be
a positive integer. The defaults are:
Setting |
Default |
What it limits |
|---|---|---|
|
16 MiB |
XML bytes, including the search for the bundle XML |
|
128 |
XML nesting levels |
|
200,000 |
XML nodes |
|
64 |
Signature records |
|
64 KiB |
Decoded signature-section data |
|
4,096 |
File and directory entries |
|
4,096 |
Attachment reference occurrences |
|
1 GiB |
Bytes per attachment |
|
4 GiB |
Total source and attachment bytes captured |
|
4 GiB |
ZIP archive bytes |
|
64 MiB |
ZIP central-directory metadata |
|
4,096 |
UTF-8 bytes per path |
|
5,000 milliseconds |
Elapsed transformation time, shared across discovery attempts |
|
3 seconds |
CPU time per transformation |
|
512 MiB |
Address space per transformation |
|
8 |
Transformation attempts during discovery |
The library checks both the sizes recorded in a ZIP and the bytes it actually
reads. Verified contents are held in memory, so lower these limits if your
application has less memory available. The total byte limit includes the input
and captured attachments, whether the input is a file, bytes, or a
memoryview. In-memory input counts toward the limit before it is copied or
parsed. Directory scans stop when they exceed the entry limit.
Signatures do not cover ZIP entry order, compression, timestamps, archive comments, empty directory entries, or filesystem attributes absent from XML. Recompressing or reordering a ZIP preserves signatures if the normalized XML and every referenced file remain identical. Unreferenced ZIP entries must still pass the structural safety checks, but their contents are not verified. When signing a ZIP, the library copies unchanged entries’ compressed data and metadata without decompressing them. It rewrites only the selected XML entry.
Compatibility¶
You can read, create, and export unsigned bundles without the signing
dependencies. Signing or verification without those dependencies raises
SigningUnavailable. It does not silently treat the bundle as unsigned.
Inspecting signature information does not require the signing dependencies.
Signing and verification require version 9.0.0 or newer of momotor-bundles.
Older readers can reject signed bundles because their XML schema does not
recognize the signature element. Upgrade applications that read these bundles
before distributing signed output to them. Disabling schema validation in an
older reader does not verify signatures.
API reference¶
Public-key authentication of serialized Momotor bundles.
Added in version 9.0.0.
- exception momotor.bundles.signing.BundleLimitError¶
An untrusted source exceeds a configured resource bound.
- exception momotor.bundles.signing.BundleSigningError¶
The artifact cannot be signed without violating the protocol.
- exception momotor.bundles.signing.BundleVerificationError(report)¶
Authentication failed. The complete diagnostics remain in
report.- Parameters:
report (
VerificationReport) – Failed or unsigned verification report.
The optional signing dependency is unavailable.
- exception momotor.bundles.signing.UnsafeBundleError¶
The source is structurally ambiguous or unsafe to consume.
- class momotor.bundles.signing.AttachmentResult(location, path, hashes=(), leaves=(), problems=())¶
Validation of one reference, including all leaves of directory references.
- hashes: tuple[HashResult, ...] = ()¶
- leaves: tuple[AttachmentResult, ...] = ()¶
- class momotor.bundles.signing.HashResult(algorithm, expected, actual, valid)¶
Expected and measured digest for an XML reference.
- class momotor.bundles.signing.Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8)¶
Finite positive resource bounds for untrusted bundle processing.
Sizes are bytes. Overrides apply to one operation and cannot disable bounds.
- class momotor.bundles.signing.Policy(value)¶
Required trust among the signatures actually present.
- ALL_PRESENT = 'all-present'¶
- ANY = 'any'¶
- class momotor.bundles.signing.SignMode(value)¶
Whether to retain records from other signers.
- ADD = 'add'¶
- REPLACE = 'replace'¶
- class momotor.bundles.signing.SignatureInspection(present, signatures=(), problems=())¶
Unauthenticated signature metadata. This result never establishes trust.
- Parameters:
- signatures: tuple[SignatureResult, ...] = ()¶
- class momotor.bundles.signing.SignatureResult(key_id, algorithm, trusted, valid, reason=None)¶
Trust and cryptographic validity of one unauthenticated wire record.
- class momotor.bundles.signing.SigningOptions(private_keys, mode=SignMode.ADD, limits=Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8))¶
Opt-in signing settings for existing bundle export methods.
- limits: Limits = Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8)¶
- private_keys: tuple[Ed25519PrivateKey, ...]¶
- class momotor.bundles.signing.Status(value)¶
Authentication result, independent of ordinary attachment hashes.
- FAILED = 'FAILED'¶
- UNSIGNED = 'UNSIGNED'¶
- VERIFIED = 'VERIFIED'¶
- class momotor.bundles.signing.VerificationOptions(trusted_keys, policy=Policy.ANY, required_signers=(), allow_unsigned=False, limits=Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8))¶
Opt-in verification settings for existing bundle factories.
- limits: Limits = Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8)¶
- trusted_keys: tuple[Ed25519PublicKey, ...]¶
- class momotor.bundles.signing.VerificationReport(status, policy, xml_digest=None, signatures=(), attachments=(), missing_required_signers=(), problems=())¶
Authentication of XML and its references only, never an archive inventory.
- require_verified()¶
Raise
BundleVerificationErrorunless authentication passed.- Return type:
- attachments: tuple[AttachmentResult, ...] = ()¶
- signatures: tuple[SignatureResult, ...] = ()¶
- momotor.bundles.signing.inspect_signatures(source, *, xml_name=None, limits=Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8))¶
Inspect unauthenticated signature metadata without claiming validity or trust.
- Parameters:
- Return type:
- Returns:
Untrusted records and structural problems; presence is unknown if capture fails.
- momotor.bundles.signing.key_id(key)¶
Return lowercase hexadecimal SHA-256 of DER SubjectPublicKeyInfo.
- Parameters:
key (
Ed25519PublicKey|Ed25519PrivateKey) – Ed25519 public or private key.- Return type:
- Returns:
Canonical 64-character signer identifier.
- Raises:
ValueError – For a key of another type.
- momotor.bundles.signing.load_private_key(source, *, password_callback=None)¶
Load an Ed25519 PKCS#8 PEM private key.
- Parameters:
- Return type:
- Returns:
An Ed25519 private key.
- Raises:
ValueError – For invalid keys or passwords.
SigningUnavailable – When the signing extra is absent.
- momotor.bundles.signing.load_public_key(source)¶
Load an Ed25519 SubjectPublicKeyInfo PEM public key.
- Parameters:
source (
bytes|str|Path) – PEM bytes or a local filename.- Return type:
- Returns:
An Ed25519 public key.
- Raises:
ValueError – For invalid keys or another algorithm.
SigningUnavailable – When the signing extra is absent.
- momotor.bundles.signing.load_verified(source, trusted_keys, *, policy=Policy.ANY, required_signers=(), allow_unsigned=False, xml_name=None, attachment_source=None, limits=Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8))¶
Verify and load from one immutable, confined attachment snapshot.
- Parameters:
source (
str|PathLike[str] |bytes|memoryview) – XML or ZIP bytes, local file, or directory.trusted_keys (
Iterable[Ed25519PublicKey]) – Caller-trusted Ed25519 public keys.policy (
Policy) – ANY or ALL_PRESENT.allow_unsigned (
bool) – Accept UNSIGNED only when no required signer is missing, never FAILED.attachment_source (
str|PathLike[str] |Mapping[str,bytes] |None) – External attachment root or mapping for XML bytes.limits (
Limits) – Finite processing budgets.
- Return type:
- Returns:
Bundle with
verification_report. Close it to release its snapshot.- Raises:
BundleVerificationError – When the requested authentication policy fails.
- momotor.bundles.signing.sign_bundle_bytes(data, private_keys, *, mode=SignMode.ADD, xml_name=None, attachment_source=None, limits=Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8))¶
Sign serialized XML or ZIP bytes after validating and filling attachment hashes.
- Parameters:
data (
bytes|memoryview) – XML or ZIP input. Mutable buffers are copied.private_keys (
Iterable[Ed25519PrivateKey]) – One or more Ed25519 private keys; duplicates are collapsed.mode (
SignMode) – Preserve other signers with ADD, or discard them with REPLACE.attachment_source (
str|PathLike[str] |Mapping[str,bytes] |None) – Confined local root or path-to-bytes mapping for external XML files.limits (
Limits) – Finite processing budgets.
- Return type:
- Returns:
Signed bytes in the original format.
- Raises:
BundleSigningError – For unsafe content, invalid hashes or malformed signature metadata.
- momotor.bundles.signing.sign_bundle_file(source, private_keys, *, output=None, mode=SignMode.ADD, xml_name=None, limits=Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8))¶
Sign a local XML file, ZIP file or directory and publish atomically.
- Parameters:
private_keys (
Iterable[Ed25519PrivateKey]) – One or more Ed25519 private keys.output (
str|PathLike[str] |None) – Destination. Omission updates the source; directory updates replace only its selected XML.mode (
SignMode) – ADD preserves other records; REPLACE discards them.limits (
Limits) – Finite processing budgets.
- Return type:
- Returns:
Written path. Existing output directories are refused, except in-place signing.
- Raises:
BundleSigningError – For invalid input, hashes or signature metadata.
OSError – For operational failures. Existing destinations survive pre-publication failures.
- momotor.bundles.signing.verify_bundle_bytes(data, trusted_keys, *, policy=Policy.ANY, required_signers=(), xml_name=None, attachment_source=None, limits=Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8))¶
Authenticate XML or ZIP bytes and every referenced attachment.
- Parameters:
data (
bytes|memoryview) – Serialized bundle bytes.trusted_keys (
Iterable[Ed25519PublicKey]) – Caller-trusted Ed25519 public keys.policy (
Policy) – ANY or ALL_PRESENT.required_signers (
Iterable[str]) – IDs that must each have a valid trusted signature.attachment_source (
str|PathLike[str] |Mapping[str,bytes] |None) – External attachment root or mapping for XML bytes.limits (
Limits) – Finite processing budgets.
- Return type:
- Returns:
Complete authentication diagnostics. Unreferenced files are outside its guarantee.
- Raises:
SigningUnavailable – When the optional signing extra is absent.
OSError – For operational source access failures.
- momotor.bundles.signing.verify_bundle_file(source, trusted_keys, *, policy=Policy.ANY, required_signers=(), xml_name=None, limits=Limits(xml_bytes=16777216, xml_depth=128, xml_nodes=200000, signatures=64, container_bytes=65536, entries=4096, references=4096, attachment_bytes=1073741824, total_bytes=4294967296, archive_bytes=4294967296, central_directory_bytes=67108864, path_bytes=4096, transform_wall_ms=5000, transform_cpu_seconds=3, transform_memory_bytes=536870912, transforms=8))¶
Authenticate a local XML file, ZIP file or directory.
- Parameters:
source (
str|PathLike[str] |bytes|memoryview) – Local bundle source.trusted_keys (
Iterable[Ed25519PublicKey]) – Caller-trusted Ed25519 public keys.policy (
Policy) – ANY or ALL_PRESENT.limits (
Limits) – Finite processing budgets.
- Return type:
- Returns:
Report over one captured source, without protection for a later ordinary load.
XSLT sources¶
Added in version 9.1.0.
Bundle factories accept source XML whose stylesheet generates the bundle document
and its attachment references. Authors do not need to pre-transform these sources.
Unsigned verification accepts these bundles when allow_unsigned=True.
Ordinary loading retains the existing lxml transformation behavior.
Both loaders exclude signature metadata from the transformation input.
Signing preserves the source XML, including its DTD and stylesheet instruction. Serialization can change lexical formatting. The signer captures external inputs, executes the stylesheet, and hashes the attachments referenced by the result. It stores these expected hashes in a manifest inside the signature container. It does not insert generated attachment hashes into the source or stylesheet.
Verification authenticates the source, captured dependencies, and manifest before executing any transformation. The result must have exactly the reference occurrences and paths recorded in the signed manifest. Each attachment is then hashed independently and compared with its signed expected hash. Loading consumes the same dependency and attachment bytes that were checked.
Embedded stylesheets, relative external stylesheets, includes, imports, DTD
resources, XInclude, and document() reads can use captured bundle resources.
Signing and verification confine these resources to the bundle, like attachments.
Absolute paths, traversal, symlinks, network access, and transformation writes
remain forbidden. In-memory XML needs attachment_source for external inputs.
ZIP inputs resolve dependencies inside the archive. A missing or changed input
fails verification. A stylesheet that requests an input absent from the signed
dependency manifest also fails.
The verification evaluator uses a separate process with a wall-time deadline and bounded XML input and output. POSIX platforms also enforce CPU, address-space, and output-file limits. These controls do not apply to ordinary loading. They are resource controls, not an operating-system security sandbox.
Signature inspection reads metadata without executing the stylesheet or requiring attachments. Malformed or invalid signature metadata cannot become unsigned input through a transformation. As with plain XML, removing all signature metadata is indistinguishable from unsigned input when the caller permits unsigned bundles.
Protocol 2 source and attachment manifest¶
Transformed sources use version 2 with profile source-manifest-1. Plain XML
continues to use the unchanged version 1 protocol. Version 1 signatures cannot
authorize transformations or newly added DTD semantics.
The version 2 container contains exactly version, profile,
source_digest, dependencies, manifest, and signatures.
Signature records use the version 1 encoding. Digests are lowercase SHA-256 hex.
source_digest hashes the UTF-8 lxml serialization of the source document after
removing the signature container and preserving its tail. This serialization
includes comments, processing instructions, and the DTD. External DTD contents
are covered separately by dependencies. This protocol deliberately does not
use XML canonicalization, which omits the DOCTYPE.
dependencies maps each confined resource path to the hash of its exact bytes.
manifest is an ordered list of reference occurrences. Each entry contains
location (the effective tree XPath), path (the resolved attachment path),
sha256, and kind (null for files or directory-v1 for directories).
Directory hashing follows the version 1 directory algorithm documented above.
Missing hashes, duplicate JSON keys, and unknown fields are rejected.
The signed message is b"bundle-signature-v2\0" followed by the JSON encoding
of the four commitment fields profile, source_digest, dependencies,
and manifest. JSON uses sorted keys, compact separators, ASCII escaping,
and UTF-8 encoding. The version and signature records are outside this payload.
Add mode requires the existing commitments to match. Replace mode permits changed
commitments but still rejects malformed metadata.
Metadata updates¶
For a transformed source whose root is not already a bundle, update-meta
updates the effective bundle and writes plain bundle XML. Signed updates verify
the source and attachments before transforming or editing. They then sign the
edited effective bundle with version 1. Updates without keys refuse signed
transformed sources, including directory inputs.