# EllipticCurveKeyPair: hardware bound signing keys in a small Swift wrapper

> EllipticCurveKeyPair is a Swift wrapper that creates an elliptic curve key pair with the private key held in the Secure Enclave and the public key in the keychain, so every signature or decryption asks for FaceID, TouchID, a device passcode or an app password. It also exports the public key as X.509 DER so a server can verify the signature with openssl.

**agens-no/EllipticCurveKeyPair** — Sign, verify, encrypt and decrypt using the Secure Enclave

- Repository: https://github.com/agens-no/EllipticCurveKeyPair
- Stars: 723 · Forks: 121
- Language: Swift
- License: NOASSERTION
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/agens-no-ellipticcurvekeypair

## The problem is the Security framework's C shaped API, not the cryptography

EllipticCurveKeyPair exists because the Security framework can be confusing to use from Swift. The README names the specific obstacles: working out how to call SecKeyRawVerify, SecKeyGeneratePair and SecItemCopyMatching in Swift 3, where the implementation is described as straightforward thanks to Swift features. The author frames the project as example code and guidance that you can also treat as a micro framework, and the repository ships a demo app for iOS and a demo app for macOS plus a Tests directory, so it is a working library rather than a snippet collection. The scope is narrow and worth stating up front: this is about one cryptographic operation family, key pairs on Apple hardware, and it does not attempt to be a general purpose cryptography library.

## The private key stays in the enclave, the public key goes to the keychain

The mechanism has two halves. The private key is created and stored on the Secure Enclave, and the public key is stored in the keychain. The manager is configured with a public label and a private label, so a keypair manager can be set up for a purpose, for example a payment signing pair with a public label of payment.sign.public and a private label of payment.sign.private. The token field decides where the private key lives, and the default in the example is .secureEnclave. Access control is expressed as a separate object with a protection attribute and a set of flags, and the private side in the example uses kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly with the .userPresence and .privateKeyUsage flags, while the public side uses kSecAttrAccessibleAlwaysThisDeviceOnly. The practical effect is that the private key is not extractable as bytes by the app.

## Every private key use costs a user prompt

This is the feature that changes how an app feels. Each time the private key is used, the user is prompted with FaceID, TouchID, the device passcode or the application password, and the library lets you set the wording of that prompt, with Confirm payment in the example configuration. The signing use case in the README spells out the flow: a server sends a push with a session token that should be signed, the device signs the token with the private key while prompting the user, the signed token goes back to the server, and the server, which already holds the public key, verifies the signature. The server can then be confident the user approved that specific agreement. Because the prompt is per operation, an app that signs frequently will feel the cost, and a user who cancels the dialog has to be handled rather than assumed away.

## The keychain fallback is a different security model, not an equivalent one

The library allows a graceful fallback for hardware without a Secure Enclave, and the README shows how: check EllipticCurveKeyPair.Device.hasSecureEnclave and choose the flags accordingly, then set the token in the Config object to .secureEnclaveIfAvailable. The key difference is that the fallback path drops the .privateKeyUsage flag and uses .userPresence alone, which means the key lives in the keychain rather than in enclave hardware. That is a real change in the threat model, not a cosmetic one: a keychain-resident private key is extractable under the right conditions, while an enclave key is not. The documentation does not explain the trade-off in those words, so a team should read the access control flags it selects as a security decision rather than a configuration detail, and it should decide deliberately per keypair, since the demo falls back for all of them at once.

## Installing is a drag and drop, a pod, a Carthage dependency, or SwiftPM

There are four ways in, and the manual one is unusually simple because the library is two files. The README tells you to drag Sources/EllipticCurveKeyPair.swift and Sources/SHA256.swift into your Xcode project. The CocoaPods route is a one line podspec install, and the Carthage route is the usual GitHub dependency declaration.

```ruby
pod EllipticCurveKeyPair
```

```ruby
github "agens-no/EllipticCurveKeyPair"
```

The repository root also carries a Package.swift file, so Swift Package Manager is available as well, along with an EllipticCurveKeyPair.podspec for the pod path. The two-file manual route is worth knowing about for an app that has a strict dependency review, since it means the audit surface is a single source file plus a SHA256 helper rather than a dependency tree.

## DER export is what lets a server verify the signature

The feature list mentions exporting the public key as X.509 DER with a proper ASN.1 header and structure, and verifying the signature with openssl on the command line. The API is a single call returning a Data value for DER and a String for PEM, so the same public key can be handed to a backend in either shape.

```swift
do {
    let key = try KeyPair.manager.publicKey().data().DER // Data
} catch {
    // handle error
}
```

Signing is the same shape, with a digest and a hash algorithm, and you may pass an LAContext object to control the authentication context.

```swift
do {
    let digest = "some text to sign".data(using: .utf8)!
    let signature = try KeyPair.manager.sign(digest, hash: .sha256)
} catch {
    // handle error
}
```

Encryption and decryption follow the same manager API, and the encryption path is documented as available only on iOS 10 and above. For encrypting on a different device or operating system with the public key, the README points to a write up on the Secure Enclave ECIES format, which tells you how to produce ciphertext the enclave can read.

## Error handling is the part you will actually need to write

The README names the three failures you will hit in practice: the Secure Enclave not being available, the user cancelling the fingerprint dialog, and no fingerprints enrolled. The library surfaces these as typed errors, with an underlying case carrying the Security framework code and an authentication case carrying the authentication error, and the examples show how to match on them.

```swift
do {
    let decrypted = try KeyPair.manager.decrypt(encrypted)
} catch EllipticCurveKeyPair.Error.underlying(_, let underlying) where underlying.code == errSecUnimplemented {
    print("Unsupported device")
} catch EllipticCurveKeyPair.Error.authentication(let authenticationError) where authenticationError.code == .userCancel {
    print("User cancelled/dismissed authentication dialog")
} catch {
    print("Some other error occurred. Error \(error)")
}
```

There is also a debugging aid: assign a closure to EllipticCurveKeyPair.logger to print every mutation the library makes in the keychain, which is the fastest way to see what a misconfigured access control is doing. The badges in the README describe the target platform range as iOS 9.0 and macOS 10.12.1 with Swift 3 and 4, which dates the project, and the last push was on 2026-06-23 with no GitHub releases, so there is no tagged version to pin and the licence is a custom one that GitHub cannot classify.

## Conclusion

EllipticCurveKeyPair fits a team shipping an iOS or macOS app that must prove a user approved a specific transaction, and that already has a backend able to verify an X.509 public key with openssl. It does not fit a cross platform service, because the private key never leaves the enclave, and it is not a good foundation for a new project without checking the toolchain it targets. Before you adopt it, read the LICENSE file, since the project uses a custom licence GitHub cannot classify, and confirm your deployment target still supports the platform versions the badges list, iOS 9.0 and macOS 10.12.1, given that the encryption path needs iOS 10 or above.

## FAQ

### How do I install EllipticCurveKeyPair in a Swift project?

The README lists four ways: drag Sources/EllipticCurveKeyPair.swift and Sources/SHA256.swift into the Xcode project, use the podspec with pod EllipticCurveKeyPair, use the Carthage dependency github "agens-no/EllipticCurveKeyPair", or add the package, since the repository root contains a Package.swift.

### What happens on a device without a Secure Enclave?

The library surfaces it as an underlying error whose code is errSecUnimplemented, which the example catches and reports as an unsupported device. For a graceful fallback, check EllipticCurveKeyPair.Device.hasSecureEnclave, use the .userPresence flag alone, and set the token in the Config object to .secureEnclaveIfAvailable, which stores the key in the keychain instead.

### How can a server verify a signature produced by this library?

Export the public key as X.509 DER, or as PEM, and verify the signature with openssl on the command line, which the feature list calls out as the reason for the DER export. The demo app contains a working example of the round trip.

## Sources

- [agens-no/EllipticCurveKeyPair on GitHub](https://github.com/agens-no/EllipticCurveKeyPair)
- [Issues](https://github.com/agens-no/EllipticCurveKeyPair/issues)
- [README](https://github.com/agens-no/EllipticCurveKeyPair/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/agens-no-ellipticcurvekeypair
