Skip to main content

hw-attest-verify

Verification library for RFC Hardware-Attestation and Hardware-Trust-Proof email headers.

Receiving mail servers use this library to verify that an email was sent from a hardware-attested identity, as defined in draft-drake-email-hardware-attestation.

Installation

pip install hw-attest-verify

Usage

The simplest correct use is the CLI or verify_email_from_raw, which pass every header instance in order (duplicates and DKIM h= selection) and apply the Combined-mode rules when a message carries both fields:

from hw_attest_verify.__main__ import verify_email_from_raw

results = verify_email_from_raw(raw_email_text)
mode1 = results.get("_mode1_result_object")   # VerificationResult or None
mode2 = results.get("_mode2_result_object")   # Mode2VerificationResult or None

Every result carries authentication_results_result, the draft's result name: pass, fail, policy (cryptographically valid, declined by local policy, e.g. too old or an untrusted hidden-mode issuer), temperror (the AIRS Registry or the issuer could not be reached) or permerror (malformed, unsupported, duplicated fields). is_valid is true only for pass.

Mode 1: Hardware-Attestation (Direct Hardware Attestation)

from hw_attest_verify import verify_hardware_attestation

result = verify_hardware_attestation(
    header_value=hardware_attestation_value,
    email_headers=lowercased_header_dict,
    body=raw_body_bytes,
    ordered_header_pairs=every_header_in_order,      # [(name, unfolded value), ...]
)
print(result.authentication_results_result, result.trust_path_that_succeeded,
      result.cms_signature_and_message_binding_verified, result.trust_tier, result.agent_identity_urn)

The signed input is the 92-octet attestation-input of revision -03: "AIRS-Email-Mode1-v1" || 0x00 || h-hash || bh-raw || ts (the 20-octet purpose label, the header hash, the body hash and the 8-octet timestamp). The h= tag must list the thirteen always-covered fields (From, To, Cc, Subject, Date, Message-ID, Sender, Reply-To, In-Reply-To, References, MIME-Version, Content-Type, Content-Transfer-Encoding), whether or not each is present.

What pass means. A valid CMS signature over the message is not by itself a pass. pass requires at least one trust path that succeeded, and every path the verifier evaluated must have succeeded. The one path the draft defines today is Registrar-bound: a message with aid and bind whose binding JWS is verified against the AIRS Registry's currentIssuer for that aid and that issuer's RFC 8414 JWK Set, with its cnf.jwk equal to the CMS signer key. The result then has trust_path_that_succeeded == "registrar", and trust_tier and agent_identity_urn are reported (and only then). A message whose CMS signature verifies but which has no verified Registrar binding is policy, with the reason in failure_reasons; cms_signature_and_message_binding_verified tells such a message apart from a forged one.

The manufacturer-rooted path of earlier revisions is reserved: revision -03 names no evidence profile for it, so this library never reports one (manufacturer_rooted_path_verified stays None). trusted_root_certificates and the CLI's --trust-store still make the library validate the signer's certificate path to those roots, but the outcome is only informational (signer_certificate_path_to_supplied_roots_is_valid), never a trust path.

Other results the draft assigns to policy rather than fail:

  • the Registrar binding assertion has expired (registrar_assertion_has_expired): the CMS signature was good, the assertion was just no longer current;
  • the identity is in the urn:aid:test: namespace (aid_is_in_the_registry_test_namespace): no tier and no aid are reported;
  • ts outside the freshness window. The window has two separate bounds: max_timestamp_skew_seconds (default 300) is the clock-skew allowance for a ts in the future, and max_timestamp_age_seconds (default: the same value) is how OLD a ts may be. A receiver that verifies after queueing and greylisting (the first trusted MTA) raises only the age to the lifetime of the Registrar's mail assertions (the MailPal hook uses one hour); an MSA keeps the strict default (draft -03, "Freshness at Submission").

A forged or malformed message is still fail or permerror, including a non-numeric or non-finite iat / exp, a missing aid object in the binding and a header field over maximum_header_field_value_octets (65536).

Mode 2: Hardware-Trust-Proof (SD-JWT Trust Proof)

from hw_attest_verify import verify_hardware_trust_proof

result = verify_hardware_trust_proof(
    header_value=hardware_trust_proof_value,
    email_headers=lowercased_header_dict,
    body=raw_body_bytes,
    ordered_header_pairs=every_header_in_order,
    trusted_hidden_mode_issuers=["https://1id.com/realms/agents"],  # local policy
)
print(result.authentication_results_result, result.is_identified_mode,
      result.trust_tier, result.agent_identity_urn, result.issuer)

A presentation that discloses sub is identified: the sub is resolved at the AIRS Registry and iss must equal its currentIssuer. Without sub it is hidden, and only issuers in trusted_hidden_mode_issuers are used (otherwise the result is policy). Disclosures are processed per RFC 9901 (nested and array digests); aid.trust_tier must be disclosed. ES256, RS256 and PS256 issuer signatures are accepted; kid is required.

Combined mode

from hw_attest_verify import apply_combined_mode_requirements_to_mode2_result

apply_combined_mode_requirements_to_mode2_result(mode1_result, mode2_result)

When both fields are present, Mode 2 passes only if Mode 1 passed, Mode 1 h= covers Hardware-Trust-Proof, the SD-JWT cnf.jwk is the CMS signer key, a Mode 1 aid equals the disclosed sub, and the two proofs state the same trust tier. A cnf-bearing presentation in a message without Hardware-Attestation fails. The Hardware-Trust-Proof field named in h= is canonicalized by deleting every whitespace character from its value, so a legal refolding of the long SD-JWT in transit does not break the Mode 1 signature.

Parsing without verification

from hw_attest_verify import parse_hardware_attestation_header

parsed = parse_hardware_attestation_header(header_value)
print(f"typ={parsed.typ}, alg={parsed.alg}, trust_tier={parsed.trust_tier}")
print(f"Signed headers: {parsed.signed_header_names}")
print(f"Timestamp: {parsed.ts}")

CLI: Verify a raw email

# Authentication-Results lines, as in the draft's appendix
python -m hw_attest_verify --auth-results --no-time-check --hostname mailpal.com < email.eml

# JSON (default output)
hw-attest-verify email.eml

Options: --auth-results, --hostname NAME, --no-time-check (archived mail: skip ts/iat/exp and age checks), --trust-store PATH (PEM roots; the signer certificate path to them is validated for information only, it is never a trust path), --trust-hidden-issuer URI (repeatable; local policy for hidden-mode Mode 2), --allow-eddsa (testing only). The exit status is 0 when any method passed. --allow-self-signed was removed in 3.0.0: a self-signed signer certificate has no trust path of its own, so there is nothing for it to allow; the option now prints an explanation and exits with status 2.

Authoritative Registrar Key Discovery

Registrar keys come only from the issuer the AIRS Registry names:

  1. Resolve the aid / sub via AIRS RDAP (https://airs.1id.biz) and read aid_data.currentIssuer; no current issuer = fail, unreachable = temperror.
  2. Fetch RFC 8414 metadata for exactly that issuer (for https://1id.com/realms/agents: https://1id.com/.well-known/oauth-authorization-server/realms/agents); its issuer must equal currentIssuer and it must publish jwks_uri.
  3. Take the key named by the JWS kid from that JWK Set.

A token, certificate or JWS never supplies its own issuer key. Responses are cached for 300 seconds. (The older _hwattest DNS / {domain}/.well-known/jwks.json helper discover_issuer_public_key remains importable but no verification path uses it; the [dns] extra is no longer needed.)

Trust Tiers

typ Trust Tier Hardware
TPM sovereign TPM 2.0, discrete or firmware (Windows, Linux)
PIV portable YubiKey / PIV smartcard
ENC enclave Apple Secure Enclave
VRT virtual Hypervisor virtual TPM (vTPM)
SFT declared Software key

RFC Reference

draft-drake-email-hardware-attestation -- Hardware Attestation for Email Sender Verification

License

MIT

Spec -> code traceability (G6.3)

This is C3, the offline verifier for draft-drake-email-hardware-attestation. Paths relative to this package.

Draft section Implementing file(s)
5 Mode 1 Direct Hardware Attestation (verify) hw_attest_verify/mode1.py (verification flow, digest reconstruction, signature check with the SignerInfo-named certificate; PS256 salt 32), hw_attest_verify/cms_signed_data_profile.py (strict CMS SignedData profile + CMS Algorithm Mapping), hw_attest_verify/signer_certificate_path_building_and_validation.py (path built from the signer to a trusted root, RFC 5280 CA rules)
5.2 attestation-input (92 octets: label, h-hash, bh-raw, ts) + DKIM canon hw_attest_verify/mode1.py _compute_attestation_input, _canonicalise_headers_for_direct_attestation, _canonicalise_selected_header_field_using_dkim2_header_hash_rules (the Hardware-Trust-Proof exception, EML-10), _select_headers_bottom_up_per_dkim; the label is MODE1_ATTESTATION_INPUT_PURPOSE_LABEL_INCLUDING_THE_TERMINATING_ZERO_OCTET in parse.py
5 header grammar (v=/typ=/alg=/h=/bh=/ts=/chain=/aid=) hw_attest_verify/parse.py
Mode 1 Registrar Binding JWS (currentIssuer, RFC 8414 key, cnf.jwk = signer key; ES256/RS256/PS256) hw_attest_verify/mode1.py _verify_registrar_binding_jws, hw_attest_verify/issuer_key_discovery.py
6 Mode 2 SD-JWT Trust Proof (verify) hw_attest_verify/mode2.py verify_hardware_trust_proof (identified/hidden issuer rules, RFC 9901 process_sd_jwt_disclosures_per_rfc9901, ES256/RS256/PS256)
Combined Mode hw_attest_verify/combined.py apply_combined_mode_requirements_to_mode2_result
6.3 Message-binding nonce (fixed thirteen-field set) hw_attest_verify/mode2.py _compute_message_binding_nonce (From,To,Cc,Subject,Date,Message-ID,Sender,Reply-To,In-Reply-To,References,MIME-Version,Content-Type,Content-Transfer-Encoding, in order, then hardware-trust-proof:; absent fields contribute nothing; no oversigning; a duplicated field fails); the list is ALWAYS_COVERED_HEADER_FIELD_NAMES_IN_ORDER in parse.py
7 Acceptable algorithms cms_signed_data_profile.py + mode1.py (ES256/RS256/PS256), issuer_key_discovery.py verify_compact_jws_signature (JWS: ES256/RS256/PS256)
Authoritative Registrar Key Discovery (RDAP currentIssuer, RFC 8414, jwks_uri) hw_attest_verify/mode2.py _resolve_issuer_via_rdap, hw_attest_verify/issuer_key_discovery.py discover_registrar_signing_key
8 IANA A-R: hw-attest (header.typ/alg/tier/aid), hw-trust (header.mode/tier/issuer/aid); results pass/fail/policy/temperror/permerror authentication_results_result on both result classes; hw_attest_verify/__main__.py _format_mode1/2_auth_results_line
CLI (--auth-results --no-time-check --hostname) hw_attest_verify/__main__.py verify_email_from_raw

Verified end-to-end by gates G4.2 (tamper suite), G4.3 (live 5-tier round trips), and G4.6 (the draft's own appendix examples re-verify through this CLI; tests/test_combined_mode_and_real_example_emails.py re-verifies the real appendix emails offline; the revision -02 captures there are recorded as expected failures until they are replaced by captures made through the live system under revision -03). The manufacturer-rooted path is reserved by the draft (EML-07): no evidence profile ties a hardware key to its manufacturer, so none is invented here and none is ever reported (AUD-F21). tests/test_revision_03_combined_mode_end_to_end.py builds complete Combined-mode messages from the draft's text alone and carries a known-answer vector for the 92-octet input that was computed with coreutils only.

Metadata

Release files for hw-attest-verify 3.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hw-attest-verify 3.0.0
File Size Uploaded Provenance Actions
hw_attest_verify-3.0.0.tar.gz 92.8 kB Without provenance Details for hw_attest_verify-3.0.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for hw-attest-verify 3.0.0
File Interpreter ABI Platform Actions
hw_attest_verify-3.0.0-py3-none-any.whl Python 3 none any Details for hw_attest_verify-3.0.0-py3-none-any.whl

Total release size: 148.2 kB

Release files / hw_attest_verify-3.0.0.tar.gz

Download URL hw_attest_verify-3.0.0.tar.gz
Size 92.8 kB
Tags Source
SHA-256 checksum
How to use checksums
fd85905d6bb5efe0f8be7a539a0bfb16fd0e71356dc62d3493054753eed64e93
BLAKE2b-256 checksum
How to use checksums
1e5a332b3d34f4db4408f71f0f17a2e0fa8eb365d5dfaedb69d2002bd85e2e94
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.14

Release files / hw_attest_verify-3.0.0-py3-none-any.whl

Download URL hw_attest_verify-3.0.0-py3-none-any.whl
Size 55.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
56c22c68231f2b1dcbe11f9569a621332e8c8487178cc27c444a1e0e9f4e2b3b
BLAKE2b-256 checksum
How to use checksums
69e07dbd9d562b03b5ea602ede8d0b04fee02c5dff81198147da2273a1b3aed1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.10.14

Security for hw-attest-verify 3.0.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page