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 witherrSecMissingEntitlement.SecureEnclaveKey.get(...)/remove_by_tag(...)fail the same way.- Even the ephemeral-key paths only work when the host
pythonbinary itself is code-signed, which the system-shippedpython3is but a Homebrew orpyenv-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 SEget(tag)→ retrieve an existing persistent keyfrom_public_key_bytes(bytes)→ reconstruct a public-only handleremove()/remove_by_tag(tag)public_key()/public_key_bytes()(65-byte X9.62 uncompressed)sign(data)→ DER ECDSA-SHA256 signatureverify(data, signature)→ boolencrypt(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.
- Xcode → Settings → Accounts → add your Apple ID.
- File → New → Project → macOS → App.
- Set the Bundle Identifier to match what you'll put in
Info.plistandkeychain-access-groups. - Pick your Team under Signing & Capabilities.
+ Capability→ Keychain Sharing.- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file py_secure_enclave-1.0.0-cp314-cp314-macosx_11_0_arm64.whl.
File metadata
- Download URL: py_secure_enclave-1.0.0-cp314-cp314-macosx_11_0_arm64.whl
- Upload date:
- Size: 326.4 kB
- Tags: CPython 3.14, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
06b48888acbef20b9d6acc868224679fb67939e34ec1d309a0edca1ddd241013
|
|
| MD5 |
cf78b1ed0112babac1f4c98a84975e6d
|
|
| BLAKE2b-256 |
7dcc85be11c9d1303164029d7f45ec661c0dc68cabbf8fa2539853afac870e47
|
File details
Details for the file py_secure_enclave-1.0.0-cp314-cp314-macosx_10_12_x86_64.whl.
File metadata
- Download URL: py_secure_enclave-1.0.0-cp314-cp314-macosx_10_12_x86_64.whl
- Upload date:
- Size: 336.3 kB
- Tags: CPython 3.14, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc49ce0177e1902732b4c17f0c4e6532175964748b8a608ce9202c0149602611
|
|
| MD5 |
03846263eb02b0fc1075cec7710a698d
|
|
| BLAKE2b-256 |
71f2cc30fce228a091152b239e36d44b0b2a5a7433f8c85b877e26d15e79caee
|
File details
Details for the file py_secure_enclave-1.0.0-cp313-cp313-macosx_11_0_arm64.whl.
File metadata
- Download URL: py_secure_enclave-1.0.0-cp313-cp313-macosx_11_0_arm64.whl
- Upload date:
- Size: 326.4 kB
- Tags: CPython 3.13, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a6689f5e35715dafbf8bf7dcada69ada414d9bfc077ca4e3381e021d4733cf68
|
|
| MD5 |
530ef46120dfceea5804b4e6e8424a68
|
|
| BLAKE2b-256 |
1aecc6fca3d0ac1dd97793bd2cb49ea8ce94c6bad8df5d7bc15b2b3c39d77759
|
File details
Details for the file py_secure_enclave-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl.
File metadata
- Download URL: py_secure_enclave-1.0.0-cp313-cp313-macosx_10_12_x86_64.whl
- Upload date:
- Size: 336.1 kB
- Tags: CPython 3.13, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8992a92ad8a74df2b79d176d99e36dee1444a92cd5bcccccce56796e8bf318ac
|
|
| MD5 |
3b0de1f5324d4c39f4667a2cf2394c93
|
|
| BLAKE2b-256 |
9ce7ceecbda16a604909fc00f537fdfea47ab907657d2bf85860712b145a0902
|
File details
Details for the file py_secure_enclave-1.0.0-cp312-cp312-macosx_11_0_arm64.whl.
File metadata
- Download URL: py_secure_enclave-1.0.0-cp312-cp312-macosx_11_0_arm64.whl
- Upload date:
- Size: 327.0 kB
- Tags: CPython 3.12, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bece4e53318c9f37aa4491475aac7471576a98af41d11c3e3dcb06eeb712fdd8
|
|
| MD5 |
95d1b485d2e5dcc35b83a46a4264ce39
|
|
| BLAKE2b-256 |
19be2dd533c6040b04a849c7a1eb63e82694c0268099605591d4cde4c3f16d88
|
File details
Details for the file py_secure_enclave-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl.
File metadata
- Download URL: py_secure_enclave-1.0.0-cp312-cp312-macosx_10_12_x86_64.whl
- Upload date:
- Size: 336.8 kB
- Tags: CPython 3.12, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c9c7acfbd51ee13df2f9e8a6d424586390a1825f9015d6bfc82daa2aa38ac37c
|
|
| MD5 |
6b0ac7dce1928fe49d736f4ee1824b15
|
|
| BLAKE2b-256 |
394640f2437fede0d61b418dad96e20b1d2b8f0427ca155b2cb37f8f4f468150
|
File details
Details for the file py_secure_enclave-1.0.0-cp311-cp311-macosx_11_0_arm64.whl.
File metadata
- Download URL: py_secure_enclave-1.0.0-cp311-cp311-macosx_11_0_arm64.whl
- Upload date:
- Size: 328.3 kB
- Tags: CPython 3.11, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
29ffc9fef7a49e3bffb33fd950cf4aa6c79dbcdf3db5d2158ffc7c51af20e6b2
|
|
| MD5 |
b27a00517d82c7b8ab1f8c4835be817b
|
|
| BLAKE2b-256 |
0851784a8cd1fe04449050b48cc476c025abfdb5f5882b33ccceb32cda4bd893
|
File details
Details for the file py_secure_enclave-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl.
File metadata
- Download URL: py_secure_enclave-1.0.0-cp311-cp311-macosx_10_12_x86_64.whl
- Upload date:
- Size: 342.0 kB
- Tags: CPython 3.11, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: maturin/1.13.1
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19d5dc21831e1f92319c00bfa8930b7745551332cdd76da84ec236133b42a009
|
|
| MD5 |
9b81d76d7da9efd599dc78cdb6d68ac3
|
|
| BLAKE2b-256 |
664136e5c08a456520f6f6ba977a9ec6921eace32c26e614b8faf299cf963dc2
|