Skip to main content

Python bindings for macOS Secure Enclave

Project description

py-secure-enclave

Python bindings to macOS Secure Enclave, built on secure-enclave-rs with PyO3.

I built this library because previously there was no way to interact with Apple's Secure Enclave from Python. This is likely due to the strict signing requirements on MacOS to use the Secure Enclave, however there is a limited subset of use cases in the Python world that may benefit form this. (Such as creating Python bundles)

from py_secure_enclave import SecureEnclaveKey, AccessControlFlags

key = SecureEnclaveKey.generate(
    tag=b"com.example.signing-key",
    access_flags=AccessControlFlags.BIOMETRY_ANY,
)
sig = key.sign(b"hello world")              # prompts Face ID / Touch ID
assert key.verify(b"hello world", sig)

Private key never leaves Apple's Secure Enclave.

⚠️ You can't just pip install and python main.py

Apple's Secure Enclave is reachable only to code-signed processes. A plain python REPL or script is unsigned by default, so:

  • SecureEnclaveKey.generate(..., permanent=True) fails with errSecMissingEntitlement.
  • SecureEnclaveKey.get(...) / remove_by_tag(...) fail the same way.
  • Even the ephemeral-key paths only work when the host python binary itself is code-signed, which the system-shipped python3 is but a Homebrew or pyenv-built one generally is not.

To use this package for anything real, you must bundle the Python interpreter that runs your code into a code-signed .app with a provisioning profile. Tools like py2app or PyInstaller work. You can refer to tests/bundle_app/ for a minimal py2app reference implementation.

This is EXTREMELY NOT recommended but, you could also sign your entire Python Binary. This has horrendous security implications and you would basically be building a back-door into your device's Secure Enclave. I mention this in case you might be thinking about doing this. Don't do this unless you plan to throw out your Mac and all its data by tomorrow. You've been warned.

Requirements

  • macOS with a Secure Enclave:
    • Apple Silicon or
    • Intel with a T2 chip or later
  • Python ≥ 3.9
  • For building from source: a Rust toolchain + maturin

Install

pip install py-secure-enclave

Or from source:

git clone https://github.com/carlvoller/secure-enclave-rs
cd secure-enclave-rs/py_secure_enclave
python -m venv .venv && source .venv/bin/activate
pip install maturin
maturin develop --release

Documentation

from py_secure_enclave import (
    AccessControlFlags,
    SecureEnclaveKey,
    SecureEnclaveJWT,
    SecureEnclaveError,
    KeyNotFoundError,
    AuthFailedError,
    UserCancelledError,
)

SecureEnclaveKey:

  • generate(tag, access_flags=None, permanent=True) → P-256 key in SE
  • get(tag) → retrieve an existing persistent key
  • from_public_key_bytes(bytes) → reconstruct a public-only handle
  • remove() / remove_by_tag(tag)
  • public_key() / public_key_bytes() (65-byte X9.62 uncompressed)
  • sign(data) → DER ECDSA-SHA256 signature
  • verify(data, signature) → bool
  • encrypt(plaintext) / decrypt(ciphertext) → ECIES (X9.63 KDF, AES-GCM)
  • derive_shared_secret(peer_public_key_bytes, output_len=32, shared_info=None)
  • authenticate() → force the biometric / passcode prompt

AccessControlFlags — bitflag composable with |: EMPTY, USER_PRESENCE, BIOMETRY_ANY, BIOMETRY_CURRENT_SET, DEVICE_PASSCODE, APPLICATION_PASSWORD, OR, AND, PRIVATE_KEY_USAGE (auto-added).

SecureEnclaveJWT — builder for ES256 tokens:

jwt = SecureEnclaveJWT()
jwt.with_headers({"kid": "service-key"})
jwt.with_claims({"iss": "me", "aud": ["https://example.com"], "iat": now, "exp": now + 300})
token = jwt.sign(key)

headers, claims = SecureEnclaveJWT().verify_and_decode(key.public_key(), token)

Full API with type stubs: py_secure_enclave/__init__.pyi.

Bundling your app

Because of the code-signing requirement above, the deployment model is your app ships a Python interpreter, not the user's Python imports your library.

The shape of the bundle must be:

YourApp.app/
| - Contents/
|   | - Info.plist
|   | - MacOS/                        ← entrypoint binary + embedded python
|   | - Frameworks/                   ← libpython + native .so files
|   | - Resources/                    ← your Python code + py_secure_enclave
|   | - embedded.provisionprofile     ← required for persistent keys

and it must be signed recursively with entitlements:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>com.apple.application-identifier</key>
  <string>TEAMID.com.example.myapp</string>
  <key>com.apple.developer.team-identifier</key>
  <string>TEAMID</string>
  <key>keychain-access-groups</key>
  <array><string>TEAMID.com.example.myapp</string></array>
</dict></plist>

See tests/bundle_app/run_tests.sh for a complete worked example: py2app build → embed profile → recursive codesign → launch + capture exit code.

One-time Xcode setup (provisioning profile)

You can issue a free Development provisioning profile from any Apple ID — including a free "Personal Team". A paid Apple Developer Program account is not required.

  1. Xcode → Settings → Accounts → add your Apple ID.
  2. File → New → Project → macOS → App.
  3. Set the Bundle Identifier to match what you'll put in Info.plist and keychain-access-groups.
  4. Pick your Team under Signing & Capabilities.
  5. + CapabilityKeychain Sharing.
  6. Product → Build (⌘B) once.

Xcode writes the profile to ~/Library/Developer/Xcode/UserData/Provisioning Profiles/<uuid>.provisionprofile. You can delete the scratch Xcode project after this — the profile persists.

Profiles issued by a free Personal Team are good for 7 days on iOS and considerably longer on macOS (1 year last time I check).

Running the test suite

cd tests/bundle_app
BUNDLE_ID=com.example.myapp-tests ./run_tests.sh

BUNDLE_ID must match the id on the provisioning profile you created. The script auto-detects the profile, extracts the team id, picks the matching codesigning identity, builds with py2app, embeds the profile, recursively signs, and runs the bundled binary.

To exercise the biometric / passcode paths interactively:

SECURE_ENCLAVE_INTERACTIVE=1 BUNDLE_ID=... ./run_tests.sh

Troubleshooting

SecureEnclaveError: Security framework error (OSStatus -34018)errSecMissingEntitlement. The host process doesn't have the keychain-access-groups entitlement. Your bundle needs to be signed with the entitlements plist above and the matching provisioning profile must be present at Contents/embedded.provisionprofile.

Code Signing Error: No matching profile found (SIGKILL at launch) — AMFI refused the binary. Either the profile isn't embedded, the profile's application-identifier doesn't match the entitlements you signed with, or you claimed entitlements the profile doesn't grant (e.g. get-task-allow on a profile that doesn't include it). Inspect with:

codesign --display --entitlements :- /path/to/YourApp.app
security cms -D -i /path/to/YourApp.app/Contents/embedded.provisionprofile

ModuleNotFoundError: No module named 'json' (inside bundle) — py2app's static module graph sometimes misses modules the PyO3 layer imports dynamically. Add them to packages in your setup.py:

OPTIONS = {"packages": ["py_secure_enclave", "json"]}

License

Copyright © 2026, Carl Ian Voller. Released under the BSD-3-Clause License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

py_secure_enclave-1.0.0-cp314-cp314-macosx_11_0_arm64.whl (326.4 kB view details)

Uploaded CPython 3.14macOS 11.0+ ARM64

py_secure_enclave-1.0.0-cp314-cp314-macosx_10_12_x86_64.whl (336.3 kB view details)

Uploaded CPython 3.14macOS 10.12+ x86-64

py_secure_enclave-1.0.0-cp313-cp313-macosx_11_0_arm64.whl (326.4 kB view details)

Uploaded CPython 3.13macOS 11.0+ ARM64

py_secure_enclave-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl (336.1 kB view details)

Uploaded CPython 3.13macOS 10.12+ x86-64

py_secure_enclave-1.0.0-cp312-cp312-macosx_11_0_arm64.whl (327.0 kB view details)

Uploaded CPython 3.12macOS 11.0+ ARM64

py_secure_enclave-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl (336.8 kB view details)

Uploaded CPython 3.12macOS 10.12+ x86-64

py_secure_enclave-1.0.0-cp311-cp311-macosx_11_0_arm64.whl (328.3 kB view details)

Uploaded CPython 3.11macOS 11.0+ ARM64

py_secure_enclave-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl (342.0 kB view details)

Uploaded CPython 3.11macOS 10.12+ x86-64

File details

Details for the file py_secure_enclave-1.0.0-cp314-cp314-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for py_secure_enclave-1.0.0-cp314-cp314-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 06b48888acbef20b9d6acc868224679fb67939e34ec1d309a0edca1ddd241013
MD5 cf78b1ed0112babac1f4c98a84975e6d
BLAKE2b-256 7dcc85be11c9d1303164029d7f45ec661c0dc68cabbf8fa2539853afac870e47

See more details on using hashes here.

File details

Details for the file py_secure_enclave-1.0.0-cp314-cp314-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for py_secure_enclave-1.0.0-cp314-cp314-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 cc49ce0177e1902732b4c17f0c4e6532175964748b8a608ce9202c0149602611
MD5 03846263eb02b0fc1075cec7710a698d
BLAKE2b-256 71f2cc30fce228a091152b239e36d44b0b2a5a7433f8c85b877e26d15e79caee

See more details on using hashes here.

File details

Details for the file py_secure_enclave-1.0.0-cp313-cp313-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for py_secure_enclave-1.0.0-cp313-cp313-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 a6689f5e35715dafbf8bf7dcada69ada414d9bfc077ca4e3381e021d4733cf68
MD5 530ef46120dfceea5804b4e6e8424a68
BLAKE2b-256 1aecc6fca3d0ac1dd97793bd2cb49ea8ce94c6bad8df5d7bc15b2b3c39d77759

See more details on using hashes here.

File details

Details for the file py_secure_enclave-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for py_secure_enclave-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 8992a92ad8a74df2b79d176d99e36dee1444a92cd5bcccccce56796e8bf318ac
MD5 3b0de1f5324d4c39f4667a2cf2394c93
BLAKE2b-256 9ce7ceecbda16a604909fc00f537fdfea47ab907657d2bf85860712b145a0902

See more details on using hashes here.

File details

Details for the file py_secure_enclave-1.0.0-cp312-cp312-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for py_secure_enclave-1.0.0-cp312-cp312-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 bece4e53318c9f37aa4491475aac7471576a98af41d11c3e3dcb06eeb712fdd8
MD5 95d1b485d2e5dcc35b83a46a4264ce39
BLAKE2b-256 19be2dd533c6040b04a849c7a1eb63e82694c0268099605591d4cde4c3f16d88

See more details on using hashes here.

File details

Details for the file py_secure_enclave-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for py_secure_enclave-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 c9c7acfbd51ee13df2f9e8a6d424586390a1825f9015d6bfc82daa2aa38ac37c
MD5 6b0ac7dce1928fe49d736f4ee1824b15
BLAKE2b-256 394640f2437fede0d61b418dad96e20b1d2b8f0427ca155b2cb37f8f4f468150

See more details on using hashes here.

File details

Details for the file py_secure_enclave-1.0.0-cp311-cp311-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for py_secure_enclave-1.0.0-cp311-cp311-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 29ffc9fef7a49e3bffb33fd950cf4aa6c79dbcdf3db5d2158ffc7c51af20e6b2
MD5 b27a00517d82c7b8ab1f8c4835be817b
BLAKE2b-256 0851784a8cd1fe04449050b48cc476c025abfdb5f5882b33ccceb32cda4bd893

See more details on using hashes here.

File details

Details for the file py_secure_enclave-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for py_secure_enclave-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 19d5dc21831e1f92319c00bfa8930b7745551332cdd76da84ec236133b42a009
MD5 9b81d76d7da9efd599dc78cdb6d68ac3
BLAKE2b-256 664136e5c08a456520f6f6ba977a9ec6921eace32c26e614b8faf299cf963dc2

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page