|
1 | 1 | # TurnkeyStamper |
2 | 2 |
|
3 | | -This Swift package provides a unified interface for signing payloads using either API keys or WebAuthn passkeys. It abstracts over the differences between raw keypair signing and passkey-based assertion, and provides a simple method to produce verifiable cryptographic stamps. |
| 3 | +This Swift package provides a unified interface for signing payloads using API keys, on-device keys (Secure Enclave or Keychain), or WebAuthn passkeys. It abstracts over the differences between various signing methods and provides a simple `stamp()` method to produce verifiable cryptographic stamps. |
4 | 4 |
|
5 | | -It is designed to work seamlessly with Turnkey’s backend APIs that expect either `X-Stamp` or `X-Stamp-WebAuthn` headers. |
| 5 | +It is designed to work seamlessly with Turnkey's backend APIs that expect either `X-Stamp` or `X-Stamp-WebAuthn` headers. |
6 | 6 |
|
7 | 7 | ## Features |
8 | 8 |
|
9 | | -* Supports both API key-based and WebAuthn passkey-based stamping. |
10 | | -* Unified `stamp()` method returns the correct header name and value. |
11 | | -* Uses P-256 ECDSA signatures (DER for API keys, WebAuthn-compliant for passkeys). |
| 9 | +* **API Key Signing**: Sign with raw P-256 keypairs |
| 10 | +* **On-Device Key Signing**: Sign with keys stored in Secure Enclave or Keychain |
| 11 | + * Automatic backend selection (prefers Secure Enclave when available) |
| 12 | + * Manual backend selection for specific requirements |
| 13 | +* **Passkey Signing**: WebAuthn-compliant passkey authentication |
| 14 | +* **Unified Interface**: Single `stamp()` method returns the correct header name and value |
| 15 | +* **Key Management**: Create, list, and delete on-device key pairs |
12 | 16 |
|
13 | 17 | --- |
14 | 18 |
|
15 | | -## Requirements |
| 19 | +## Usage |
16 | 20 |
|
17 | | -* iOS 16.0+ / macOS 13.0+ |
18 | | -* Swift 5.7+ |
| 21 | +### 1. API Key Signing |
19 | 22 |
|
20 | | ---- |
| 23 | +Sign with a raw P-256 key pair (both public and private key provided): |
21 | 24 |
|
22 | | -## Usage |
| 25 | +```swift |
| 26 | +import TurnkeyStamper |
| 27 | + |
| 28 | +let stamper = Stamper(apiPublicKey: "<public-key-hex>", apiPrivateKey: "<private-key-hex>") |
| 29 | +let (headerName, headerValue) = try await stamper.stamp(payload: jsonPayload) |
| 30 | +// headerName: "X-Stamp" |
| 31 | +``` |
23 | 32 |
|
24 | | -### API Key Signing |
| 33 | +### 2. On-Device Key Signing |
| 34 | + |
| 35 | +Sign with a key stored in Secure Enclave or Keychain (only public key needed): |
25 | 36 |
|
26 | 37 | ```swift |
27 | 38 | import TurnkeyStamper |
28 | 39 |
|
29 | | -let stamper = Stamper(apiPublicKey: "<public-key>", apiPrivateKey: "<private-key>") |
| 40 | +// Create a new on-device key pair |
| 41 | +let publicKey = try Stamper.createOnDeviceKeyPair() |
| 42 | + |
| 43 | +// Sign with automatic backend selection (prefers Secure Enclave) |
| 44 | +let stamper = try Stamper(apiPublicKey: publicKey) |
| 45 | +let (headerName, headerValue) = try await stamper.stamp(payload: jsonPayload) |
| 46 | +// headerName: "X-Stamp" |
30 | 47 | ``` |
31 | 48 |
|
32 | | -### Passkey Signing |
| 49 | +#### Manual Backend Selection |
| 50 | + |
| 51 | +You can explicitly choose which backend to use: |
| 52 | + |
| 53 | +```swift |
| 54 | +// Force Secure Enclave |
| 55 | +let stamper = try Stamper(apiPublicKey: publicKey, onDevicePreference: .secureEnclave) |
| 56 | + |
| 57 | +// Force Secure Storage (Keychain) |
| 58 | +let stamper = try Stamper(apiPublicKey: publicKey, onDevicePreference: .secureStorage) |
| 59 | + |
| 60 | +// Auto (default) - prefers Secure Enclave when available |
| 61 | +let stamper = try Stamper(apiPublicKey: publicKey, onDevicePreference: .auto) |
| 62 | +``` |
| 63 | + |
| 64 | +#### Key Management |
| 65 | + |
| 66 | +```swift |
| 67 | +// Create a new key pair |
| 68 | +let publicKey = try Stamper.createOnDeviceKeyPair(preference: .auto) |
| 69 | + |
| 70 | +// List existing key pairs |
| 71 | +let keys = try Stamper.listOnDeviceKeyPairs(preference: .auto) |
| 72 | + |
| 73 | +// Delete a key pair |
| 74 | +try Stamper.deleteOnDeviceKeyPair(publicKeyHex: publicKey, preference: .auto) |
| 75 | +``` |
| 76 | + |
| 77 | +#### Advanced: Secure Enclave with Biometric Protection |
| 78 | + |
| 79 | +For Secure Enclave, you can set an authentication policy at key creation time. The policy is embedded in the key and enforced by the hardware: |
33 | 80 |
|
34 | 81 | ```swift |
35 | 82 | import TurnkeyStamper |
36 | 83 |
|
37 | | -let stamper = Stamper(rpId: "your.site.com", presentationAnchor: anchor) |
| 84 | +// Create config with biometric requirement |
| 85 | +let config = SecureEnclaveStamper.SecureEnclaveConfig(authPolicy: .biometryAny) |
| 86 | + |
| 87 | +// Create key with biometric protection |
| 88 | +let publicKey = try SecureEnclaveStamper.createKeyPair(config: config) |
| 89 | + |
| 90 | +// All subsequent operations work normally - the biometric prompt happens automatically |
| 91 | +let stamp = try SecureEnclaveStamper.stamp(payload: jsonPayload, publicKeyHex: publicKey) |
| 92 | +// User is prompted for biometric authentication when signing |
38 | 93 | ``` |
39 | 94 |
|
40 | | -The resulting header can be attached to any HTTP request for authenticated interaction with Turnkey services. |
| 95 | +**Note**: Unlike Secure Storage, Secure Enclave config is only used at key creation. Subsequent operations (list, stamp, delete) don't need the config because the auth policy is permanently embedded in the key by the hardware. |
| 96 | + |
| 97 | +#### Advanced: Secure Storage with Custom Configuration |
| 98 | + |
| 99 | +For Secure Storage (Keychain), you can customize storage attributes like access groups, iCloud sync, or biometric protection: |
| 100 | + |
| 101 | +```swift |
| 102 | +import TurnkeyStamper |
| 103 | + |
| 104 | +// Create custom configuration |
| 105 | +let config = SecureStorageStamper.SecureStorageConfig( |
| 106 | + accessibility: .afterFirstUnlockThisDeviceOnly, |
| 107 | + accessControlPolicy: .biometryAny, // Require biometric authentication |
| 108 | + authPrompt: "Authenticate to sign", |
| 109 | + biometryReuseWindowSeconds: 30, |
| 110 | + synchronizable: false, // Don't sync to iCloud |
| 111 | + accessGroup: "com.example.shared" // Share keys between apps |
| 112 | +) |
| 113 | + |
| 114 | +// Create key with custom config |
| 115 | +let publicKey = try SecureStorageStamper.createKeyPair(config: config) |
| 116 | + |
| 117 | +// IMPORTANT: All subsequent operations MUST use the same config |
| 118 | +let keys = try SecureStorageStamper.listKeyPairs(config: config) |
| 119 | +let stamp = try SecureStorageStamper.stamp(payload: jsonPayload, publicKeyHex: publicKey, config: config) |
| 120 | +try SecureStorageStamper.deleteKeyPair(publicKeyHex: publicKey, config: config) |
| 121 | +``` |
| 122 | + |
| 123 | +**Note**: Keychain queries must match how items were stored. If you create a key with custom config attributes (especially `accessGroup`, `synchronizable`, or `accessControlPolicy`), you must pass that same config to all subsequent operations (`listKeyPairs`, `stamp`, `deleteKeyPair`). If you use default settings, the no-config methods work fine. |
| 124 | + |
| 125 | +### 3. Passkey Signing |
| 126 | + |
| 127 | +Sign with WebAuthn passkeys: |
| 128 | + |
| 129 | +```swift |
| 130 | +import TurnkeyStamper |
| 131 | + |
| 132 | +let stamper = Stamper(rpId: "your.site.com", presentationAnchor: window) |
| 133 | +let (headerName, headerValue) = try await stamper.stamp(payload: jsonPayload) |
| 134 | +// headerName: "X-Stamp-WebAuthn" |
| 135 | +``` |
| 136 | + |
| 137 | +--- |
| 138 | + |
| 139 | +## Architecture |
| 140 | + |
| 141 | +### Secure Enclave Stamper |
| 142 | + |
| 143 | +The **Secure Enclave** is Apple's dedicated secure coprocessor: |
| 144 | + |
| 145 | +* Private keys are generated and stored inside the Secure Enclave |
| 146 | +* Keys never leave the secure enclave - signing happens inside |
| 147 | +* Available on iPhone 5s and later, iPad Air and later, Macs with Apple Silicon or T2 chip |
| 148 | +* Metadata stored in iCloud Keychain for persistence |
| 149 | + |
| 150 | +### Secure Storage Stamper |
| 151 | + |
| 152 | +The **Secure Storage** stamper uses the device's Keychain: |
| 153 | + |
| 154 | +* Private keys stored in local Keychain |
| 155 | +* Available after first device unlock (no biometric protection by default) |
| 156 | +* Used as fallback when Secure Enclave is unavailable |
| 157 | +* Works on all Apple devices |
| 158 | + |
| 159 | +### Automatic Selection |
| 160 | + |
| 161 | +When using `.auto` preference (default), the stamper: |
| 162 | +1. Checks if Secure Enclave is available |
| 163 | +2. Uses Secure Enclave if supported, otherwise falls back to Secure Storage |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +## Requirements |
| 168 | + |
| 169 | +* iOS 17.0+ / macOS 14.0+ |
| 170 | +* Swift 5.9+ |
41 | 171 |
|
42 | 172 | --- |
43 | 173 |
|
0 commit comments