react-native-dpop 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,373 @@
1
+ import Foundation
2
+ import Security
3
+ import React
4
+
5
+ final class DPoPModule {
6
+ static let shared = DPoPModule()
7
+
8
+ private let keyStore = DPoPKeyStore()
9
+ private let defaultAlias = "react-native-dpop"
10
+
11
+ private init() {}
12
+
13
+ func resolveAlias(_ alias: String?) -> String {
14
+ guard let alias, !alias.isEmpty else {
15
+ return defaultAlias
16
+ }
17
+ return alias
18
+ }
19
+
20
+ func assertHardwareBacked(alias: String?) throws {
21
+ let effectiveAlias = resolveAlias(alias)
22
+ guard keyStore.hasKeyPair(alias: effectiveAlias) else {
23
+ throw DPoPError.keyNotFound(alias: effectiveAlias)
24
+ }
25
+
26
+ guard keyStore.isHardwareBacked(alias: effectiveAlias) else {
27
+ throw DPoPError.notHardwareBacked(alias: effectiveAlias)
28
+ }
29
+ }
30
+
31
+ func calculateThumbprint(alias: String?) throws -> String {
32
+ let effectiveAlias = resolveAlias(alias)
33
+ if !keyStore.hasKeyPair(alias: effectiveAlias) {
34
+ try keyStore.generateKeyPair(alias: effectiveAlias)
35
+ }
36
+ let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
37
+ let coordinates = try DPoPUtils.getPublicCoordinates(fromRawPublicKey: try DPoPUtils.toRawPublicKey(keyPair.publicKey))
38
+ return DPoPUtils.calculateThumbprint(kty: "EC", crv: "P-256", x: coordinates.x, y: coordinates.y)
39
+ }
40
+
41
+ func deleteKeyPair(alias: String?) throws {
42
+ try keyStore.deleteKeyPair(alias: resolveAlias(alias))
43
+ }
44
+
45
+ func getKeyInfo(alias: String?) -> [String: Any] {
46
+ let effectiveAlias = resolveAlias(alias)
47
+ let secureEnclaveAvailable = keyStore.isSecureEnclaveAvailable()
48
+ let fallbackReason = keyStore.getSecureEnclaveFallbackReason(alias: effectiveAlias)
49
+ let secureEnclaveFallbackReason: Any = fallbackReason ?? NSNull()
50
+ let keyInfo = keyStore.getKeyInfo(alias: effectiveAlias)
51
+
52
+ if !keyInfo.hasKeyPair {
53
+ return [
54
+ "alias": effectiveAlias,
55
+ "hasKeyPair": false,
56
+ "hardware": [
57
+ "ios": [
58
+ "secureEnclaveAvailable": secureEnclaveAvailable,
59
+ "secureEnclaveBacked": false,
60
+ "securityLevel": NSNull(),
61
+ "secureEnclaveFallbackReason": secureEnclaveFallbackReason
62
+ ]
63
+ ]
64
+ ]
65
+ }
66
+
67
+ let secureEnclaveBacked = secureEnclaveAvailable && keyInfo.insideSecureHardware
68
+ let securityLevel = secureEnclaveBacked ? 2 : 1
69
+
70
+ return [
71
+ "alias": keyInfo.alias,
72
+ "algorithm": keyInfo.algorithm,
73
+ "curve": keyInfo.curve,
74
+ "hasKeyPair": true,
75
+ "insideSecureHardware": secureEnclaveBacked,
76
+ "hardware": [
77
+ "ios": [
78
+ "secureEnclaveAvailable": secureEnclaveAvailable,
79
+ "secureEnclaveBacked": secureEnclaveBacked,
80
+ "securityLevel": securityLevel,
81
+ "secureEnclaveFallbackReason": secureEnclaveFallbackReason
82
+ ]
83
+ ]
84
+ ]
85
+ }
86
+
87
+ func getPublicKeyDer(alias: String?) throws -> String {
88
+ let effectiveAlias = resolveAlias(alias)
89
+ if !keyStore.hasKeyPair(alias: effectiveAlias) {
90
+ try keyStore.generateKeyPair(alias: effectiveAlias)
91
+ }
92
+ let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
93
+ return DPoPUtils.base64UrlEncode(try DPoPUtils.toDerPublicKey(keyPair.publicKey))
94
+ }
95
+
96
+ func getPublicKeyJwk(alias: String?) throws -> [String: Any] {
97
+ let effectiveAlias = resolveAlias(alias)
98
+ if !keyStore.hasKeyPair(alias: effectiveAlias) {
99
+ try keyStore.generateKeyPair(alias: effectiveAlias)
100
+ }
101
+ let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
102
+ let coordinates = try DPoPUtils.getPublicCoordinates(fromRawPublicKey: try DPoPUtils.toRawPublicKey(keyPair.publicKey))
103
+ return [
104
+ "kty": "EC",
105
+ "crv": "P-256",
106
+ "x": coordinates.x,
107
+ "y": coordinates.y
108
+ ]
109
+ }
110
+
111
+ func getPublicKeyRaw(alias: String?) throws -> String {
112
+ let effectiveAlias = resolveAlias(alias)
113
+ if !keyStore.hasKeyPair(alias: effectiveAlias) {
114
+ try keyStore.generateKeyPair(alias: effectiveAlias)
115
+ }
116
+ let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
117
+ return DPoPUtils.base64UrlEncode(try DPoPUtils.toRawPublicKey(keyPair.publicKey))
118
+ }
119
+
120
+ func hasKeyPair(alias: String?) -> Bool {
121
+ keyStore.hasKeyPair(alias: resolveAlias(alias))
122
+ }
123
+
124
+ func isBoundToAlias(proof: String, alias: String?) throws -> Bool {
125
+ let effectiveAlias = resolveAlias(alias)
126
+ if !keyStore.hasKeyPair(alias: effectiveAlias) {
127
+ try keyStore.generateKeyPair(alias: effectiveAlias)
128
+ }
129
+ let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
130
+ return try DPoPUtils.isProofBoundToPublicKey(proof, publicKey: keyPair.publicKey)
131
+ }
132
+
133
+ func rotateKeyPair(alias: String?) throws {
134
+ try keyStore.generateKeyPair(alias: resolveAlias(alias))
135
+ }
136
+
137
+ func signWithDpopPrivateKey(payload: String, alias: String?) throws -> String {
138
+ let effectiveAlias = resolveAlias(alias)
139
+ if !keyStore.hasKeyPair(alias: effectiveAlias) {
140
+ try keyStore.generateKeyPair(alias: effectiveAlias)
141
+ }
142
+ let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
143
+ var error: Unmanaged<CFError>?
144
+ guard let derSignature = SecKeyCreateSignature(
145
+ keyPair.privateKey,
146
+ .ecdsaSignatureMessageX962SHA256,
147
+ Data(payload.utf8) as CFData,
148
+ &error
149
+ ) as Data? else {
150
+ throw DPoPError.securityError(error?.takeRetainedValue())
151
+ }
152
+ let joseSignature = try DPoPUtils.derToJose(derSignature, partLength: 32)
153
+ return DPoPUtils.base64UrlEncode(joseSignature)
154
+ }
155
+
156
+ func generateProof(
157
+ htu: String,
158
+ htm: String,
159
+ nonce: String?,
160
+ accessToken: String?,
161
+ additional: [String: Any]?,
162
+ kid: String?,
163
+ jti: String?,
164
+ iat: NSNumber?,
165
+ alias: String?
166
+ ) throws -> [String: Any] {
167
+ let effectiveAlias = resolveAlias(alias)
168
+ if !keyStore.hasKeyPair(alias: effectiveAlias) {
169
+ try keyStore.generateKeyPair(alias: effectiveAlias)
170
+ }
171
+ let keyPair = try keyStore.getKeyPair(alias: effectiveAlias)
172
+ let coordinates = try DPoPUtils.getPublicCoordinates(fromRawPublicKey: try DPoPUtils.toRawPublicKey(keyPair.publicKey))
173
+
174
+ var jwk: [String: Any] = [
175
+ "kty": "EC",
176
+ "crv": "P-256",
177
+ "x": coordinates.x,
178
+ "y": coordinates.y
179
+ ]
180
+
181
+ var header: [String: Any] = [
182
+ "typ": "dpop+jwt",
183
+ "alg": "ES256",
184
+ "jwk": jwk
185
+ ]
186
+
187
+ if let kid, !kid.isEmpty {
188
+ header["kid"] = kid
189
+ }
190
+
191
+ let issuedAt = iat?.int64Value ?? Int64(Date().timeIntervalSince1970)
192
+ let finalJti = (jti?.isEmpty == false) ? jti! : UUID().uuidString
193
+
194
+ var payload: [String: Any] = [
195
+ "jti": finalJti,
196
+ "htm": htm.uppercased(),
197
+ "htu": htu,
198
+ "iat": issuedAt
199
+ ]
200
+
201
+ if let nonce, !nonce.isEmpty {
202
+ payload["nonce"] = nonce
203
+ }
204
+
205
+ if let accessToken, !accessToken.isEmpty {
206
+ payload["ath"] = DPoPUtils.hashAccessToken(accessToken)
207
+ }
208
+
209
+ if let additional {
210
+ for (key, value) in additional {
211
+ payload[key] = value
212
+ }
213
+ }
214
+
215
+ let headerSegment = DPoPUtils.base64UrlEncode(try DPoPUtils.jsonData(header))
216
+ let payloadSegment = DPoPUtils.base64UrlEncode(try DPoPUtils.jsonData(payload))
217
+ let signingInput = "\(headerSegment).\(payloadSegment)"
218
+
219
+ var error: Unmanaged<CFError>?
220
+ guard let derSignature = SecKeyCreateSignature(
221
+ keyPair.privateKey,
222
+ .ecdsaSignatureMessageX962SHA256,
223
+ Data(signingInput.utf8) as CFData,
224
+ &error
225
+ ) as Data? else {
226
+ throw DPoPError.securityError(error?.takeRetainedValue())
227
+ }
228
+ let joseSignature = try DPoPUtils.derToJose(derSignature, partLength: 32)
229
+ let jwt = "\(signingInput).\(DPoPUtils.base64UrlEncode(joseSignature))"
230
+
231
+ let proofContext: [String: Any] = [
232
+ "htu": payload["htu"] as? String ?? htu,
233
+ "htm": payload["htm"] as? String ?? htm.uppercased(),
234
+ "nonce": payload["nonce"] ?? NSNull(),
235
+ "ath": payload["ath"] ?? NSNull(),
236
+ "kid": header["kid"] ?? NSNull(),
237
+ "jti": payload["jti"] as? String ?? finalJti,
238
+ "iat": Double(issuedAt),
239
+ "additional": additional ?? NSNull()
240
+ ]
241
+
242
+ return [
243
+ "proof": jwt,
244
+ "proofContext": proofContext
245
+ ]
246
+ }
247
+
248
+ }
249
+
250
+ @objc extension ReactNativeDPoP {
251
+ static func moduleName() -> String! {
252
+ "ReactNativeDPoP"
253
+ }
254
+
255
+ static func requiresMainQueueSetup() -> Bool {
256
+ false
257
+ }
258
+
259
+ func assertHardwareBacked(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
260
+ do {
261
+ try DPoPModule.shared.assertHardwareBacked(alias: alias)
262
+ resolve(nil)
263
+ } catch {
264
+ reject("ERR_DPOP_ASSERT_HARDWARE_BACKED", error.localizedDescription, error)
265
+ }
266
+ }
267
+
268
+ func calculateThumbprint(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
269
+ do {
270
+ resolve(try DPoPModule.shared.calculateThumbprint(alias: alias))
271
+ } catch {
272
+ reject("ERR_DPOP_CALCULATE_THUMBPRINT", error.localizedDescription, error)
273
+ }
274
+ }
275
+
276
+ func deleteKeyPair(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
277
+ do {
278
+ try DPoPModule.shared.deleteKeyPair(alias: alias)
279
+ resolve(nil)
280
+ } catch {
281
+ reject("ERR_DPOP_DELETE_KEY_PAIR", error.localizedDescription, error)
282
+ }
283
+ }
284
+
285
+ func getKeyInfo(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
286
+ resolve(DPoPModule.shared.getKeyInfo(alias: alias))
287
+ }
288
+
289
+ func getPublicKeyDer(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
290
+ do {
291
+ resolve(try DPoPModule.shared.getPublicKeyDer(alias: alias))
292
+ } catch {
293
+ reject("ERR_DPOP_PUBLIC_KEY", error.localizedDescription, error)
294
+ }
295
+ }
296
+
297
+ func getPublicKeyJwk(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
298
+ do {
299
+ resolve(try DPoPModule.shared.getPublicKeyJwk(alias: alias))
300
+ } catch {
301
+ reject("ERR_DPOP_PUBLIC_KEY", error.localizedDescription, error)
302
+ }
303
+ }
304
+
305
+ func getPublicKeyRaw(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
306
+ do {
307
+ resolve(try DPoPModule.shared.getPublicKeyRaw(alias: alias))
308
+ } catch {
309
+ reject("ERR_DPOP_PUBLIC_KEY", error.localizedDescription, error)
310
+ }
311
+ }
312
+
313
+ func hasKeyPair(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
314
+ resolve(DPoPModule.shared.hasKeyPair(alias: alias))
315
+ }
316
+
317
+ func isBoundToAlias(_ proof: String, alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
318
+ do {
319
+ resolve(try DPoPModule.shared.isBoundToAlias(proof: proof, alias: alias))
320
+ } catch {
321
+ reject("ERR_DPOP_IS_BOUND_TO_ALIAS", error.localizedDescription, error)
322
+ }
323
+ }
324
+
325
+ func rotateKeyPair(_ alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
326
+ do {
327
+ try DPoPModule.shared.rotateKeyPair(alias: alias)
328
+ resolve(nil)
329
+ } catch {
330
+ reject("ERR_DPOP_ROTATE_KEY_PAIR", error.localizedDescription, error)
331
+ }
332
+ }
333
+
334
+ func signWithDpopPrivateKey(_ payload: String, alias: String?, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
335
+ do {
336
+ resolve(try DPoPModule.shared.signWithDpopPrivateKey(payload: payload, alias: alias))
337
+ } catch {
338
+ reject("ERR_DPOP_SIGN_WITH_PRIVATE_KEY", error.localizedDescription, error)
339
+ }
340
+ }
341
+
342
+ func generateProof(
343
+ _ htu: String,
344
+ htm: String,
345
+ nonce: String?,
346
+ accessToken: String?,
347
+ additional: [String: Any]?,
348
+ kid: String?,
349
+ jti: String?,
350
+ iat: NSNumber?,
351
+ alias: String?,
352
+ resolve: @escaping RCTPromiseResolveBlock,
353
+ reject: @escaping RCTPromiseRejectBlock
354
+ ) {
355
+ do {
356
+ resolve(
357
+ try DPoPModule.shared.generateProof(
358
+ htu: htu,
359
+ htm: htm,
360
+ nonce: nonce,
361
+ accessToken: accessToken,
362
+ additional: additional,
363
+ kid: kid,
364
+ jti: jti,
365
+ iat: iat,
366
+ alias: alias
367
+ )
368
+ )
369
+ } catch {
370
+ reject("ERR_DPOP_GENERATE_PROOF", error.localizedDescription, error)
371
+ }
372
+ }
373
+ }
@@ -0,0 +1,93 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTUtils.h>
3
+
4
+ #ifdef RCT_NEW_ARCH_ENABLED
5
+ #if __has_include(<ReactNativeDPoPSpec/ReactNativeDPoPSpec.h>)
6
+ #import <ReactNativeDPoPSpec/ReactNativeDPoPSpec.h>
7
+ #else
8
+ #import "ReactNativeDPoPSpec.h"
9
+ #endif
10
+ #endif
11
+
12
+ #if __has_include(<ReactNativeDPoP/ReactNativeDPoP-Swift.h>)
13
+ #import <ReactNativeDPoP/ReactNativeDPoP-Swift.h>
14
+ #else
15
+ #import "ReactNativeDPoP-Swift.h"
16
+ #endif
17
+
18
+ @interface RCT_EXTERN_MODULE(ReactNativeDPoP, NSObject)
19
+
20
+ RCT_EXTERN_METHOD(assertHardwareBacked:(NSString * _Nullable)alias
21
+ resolve:(RCTPromiseResolveBlock)resolve
22
+ reject:(RCTPromiseRejectBlock)reject)
23
+
24
+ RCT_EXTERN_METHOD(calculateThumbprint:(NSString * _Nullable)alias
25
+ resolve:(RCTPromiseResolveBlock)resolve
26
+ reject:(RCTPromiseRejectBlock)reject)
27
+
28
+ RCT_EXTERN_METHOD(deleteKeyPair:(NSString * _Nullable)alias
29
+ resolve:(RCTPromiseResolveBlock)resolve
30
+ reject:(RCTPromiseRejectBlock)reject)
31
+
32
+ RCT_EXTERN_METHOD(getKeyInfo:(NSString * _Nullable)alias
33
+ resolve:(RCTPromiseResolveBlock)resolve
34
+ reject:(RCTPromiseRejectBlock)reject)
35
+
36
+ RCT_EXTERN_METHOD(getPublicKeyDer:(NSString * _Nullable)alias
37
+ resolve:(RCTPromiseResolveBlock)resolve
38
+ reject:(RCTPromiseRejectBlock)reject)
39
+
40
+ RCT_EXTERN_METHOD(getPublicKeyJwk:(NSString * _Nullable)alias
41
+ resolve:(RCTPromiseResolveBlock)resolve
42
+ reject:(RCTPromiseRejectBlock)reject)
43
+
44
+ RCT_EXTERN_METHOD(getPublicKeyRaw:(NSString * _Nullable)alias
45
+ resolve:(RCTPromiseResolveBlock)resolve
46
+ reject:(RCTPromiseRejectBlock)reject)
47
+
48
+ RCT_EXTERN_METHOD(hasKeyPair:(NSString * _Nullable)alias
49
+ resolve:(RCTPromiseResolveBlock)resolve
50
+ reject:(RCTPromiseRejectBlock)reject)
51
+
52
+ RCT_EXTERN_METHOD(isBoundToAlias:(NSString *)proof
53
+ alias:(NSString * _Nullable)alias
54
+ resolve:(RCTPromiseResolveBlock)resolve
55
+ reject:(RCTPromiseRejectBlock)reject)
56
+
57
+ RCT_EXTERN_METHOD(rotateKeyPair:(NSString * _Nullable)alias
58
+ resolve:(RCTPromiseResolveBlock)resolve
59
+ reject:(RCTPromiseRejectBlock)reject)
60
+
61
+ RCT_EXTERN_METHOD(signWithDpopPrivateKey:(NSString *)payload
62
+ alias:(NSString * _Nullable)alias
63
+ resolve:(RCTPromiseResolveBlock)resolve
64
+ reject:(RCTPromiseRejectBlock)reject)
65
+
66
+ RCT_EXTERN_METHOD(generateProof:(NSString *)htu
67
+ htm:(NSString *)htm
68
+ nonce:(NSString * _Nullable)nonce
69
+ accessToken:(NSString * _Nullable)accessToken
70
+ additional:(NSDictionary * _Nullable)additional
71
+ kid:(NSString * _Nullable)kid
72
+ jti:(NSString * _Nullable)jti
73
+ iat:(NSNumber * _Nullable)iat
74
+ alias:(NSString * _Nullable)alias
75
+ resolve:(RCTPromiseResolveBlock)resolve
76
+ reject:(RCTPromiseRejectBlock)reject)
77
+
78
+ @end
79
+
80
+ @implementation ReactNativeDPoP
81
+ @end
82
+
83
+ #if RCT_NEW_ARCH_ENABLED
84
+ @implementation ReactNativeDPoP (TurboModule)
85
+
86
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
87
+ (const facebook::react::ObjCTurboModule::InitParams &)params
88
+ {
89
+ return std::make_shared<facebook::react::NativeReactNativeDPoPSpecJSI>(params);
90
+ }
91
+
92
+ @end
93
+ #endif
@@ -0,0 +1,203 @@
1
+ import Foundation
2
+ import CryptoKit
3
+ import Security
4
+
5
+ enum DPoPUtils {
6
+ static func base64UrlEncode(_ data: Data) -> String {
7
+ data.base64EncodedString()
8
+ .replacingOccurrences(of: "+", with: "-")
9
+ .replacingOccurrences(of: "/", with: "_")
10
+ .replacingOccurrences(of: "=", with: "")
11
+ }
12
+
13
+ static func base64UrlDecode(_ value: String) -> Data? {
14
+ let remainder = value.count % 4
15
+ let padded = remainder == 0 ? value : value + String(repeating: "=", count: 4 - remainder)
16
+ let base64 = padded
17
+ .replacingOccurrences(of: "-", with: "+")
18
+ .replacingOccurrences(of: "_", with: "/")
19
+ return Data(base64Encoded: base64)
20
+ }
21
+
22
+ static func sha256(_ data: Data) -> Data {
23
+ Data(SHA256.hash(data: data))
24
+ }
25
+
26
+ static func hashAccessToken(_ accessToken: String) -> String {
27
+ let data = Data(accessToken.utf8)
28
+ return base64UrlEncode(sha256(data))
29
+ }
30
+
31
+ static func calculateThumbprint(kty: String, crv: String, x: String, y: String) -> String {
32
+ let canonical = "{\"crv\":\"\(crv)\",\"kty\":\"\(kty)\",\"x\":\"\(x)\",\"y\":\"\(y)\"}"
33
+ return base64UrlEncode(sha256(Data(canonical.utf8)))
34
+ }
35
+
36
+ static func jsonData(_ object: Any) throws -> Data {
37
+ if #available(iOS 11.0, *) {
38
+ return try JSONSerialization.data(withJSONObject: object, options: [.sortedKeys])
39
+ }
40
+ return try JSONSerialization.data(withJSONObject: object, options: [])
41
+ }
42
+
43
+ static func derToJose(_ derSignature: Data, partLength: Int = 32) throws -> Data {
44
+ let bytes = [UInt8](derSignature)
45
+ guard !bytes.isEmpty, bytes[0] == 0x30 else {
46
+ throw DPoPError.invalidDerSignature
47
+ }
48
+
49
+ var index = 1
50
+ let (_, seqLengthBytes) = try readDerLength(bytes, startIndex: index)
51
+ index += seqLengthBytes
52
+
53
+ guard index < bytes.count, bytes[index] == 0x02 else {
54
+ throw DPoPError.invalidDerSignature
55
+ }
56
+
57
+ index += 1
58
+ let (rLength, rLengthBytes) = try readDerLength(bytes, startIndex: index)
59
+ index += rLengthBytes
60
+ let rEnd = index + rLength
61
+ guard rEnd <= bytes.count else {
62
+ throw DPoPError.invalidDerSignature
63
+ }
64
+ let r = Data(bytes[index..<rEnd])
65
+ index = rEnd
66
+
67
+ guard index < bytes.count, bytes[index] == 0x02 else {
68
+ throw DPoPError.invalidDerSignature
69
+ }
70
+
71
+ index += 1
72
+ let (sLength, sLengthBytes) = try readDerLength(bytes, startIndex: index)
73
+ index += sLengthBytes
74
+ let sEnd = index + sLength
75
+ guard sEnd <= bytes.count else {
76
+ throw DPoPError.invalidDerSignature
77
+ }
78
+ let s = Data(bytes[index..<sEnd])
79
+
80
+ let rFixed = try toUnsignedFixedLength(r, length: partLength)
81
+ let sFixed = try toUnsignedFixedLength(s, length: partLength)
82
+ return rFixed + sFixed
83
+ }
84
+
85
+ static func getPublicCoordinates(fromRawPublicKey raw: Data) throws -> (x: String, y: String) {
86
+ guard raw.count == 65, raw.first == 0x04 else {
87
+ throw DPoPError.invalidPublicKey
88
+ }
89
+
90
+ let x = raw.subdata(in: 1..<33)
91
+ let y = raw.subdata(in: 33..<65)
92
+ return (base64UrlEncode(x), base64UrlEncode(y))
93
+ }
94
+
95
+ static func toRawPublicKey(_ publicKey: SecKey) throws -> Data {
96
+ var error: Unmanaged<CFError>?
97
+ guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
98
+ throw DPoPError.securityError(error?.takeRetainedValue())
99
+ }
100
+
101
+ guard publicKeyData.count == 65, publicKeyData.first == 0x04 else {
102
+ throw DPoPError.invalidPublicKey
103
+ }
104
+
105
+ return publicKeyData
106
+ }
107
+
108
+ static func toDerPublicKey(_ publicKey: SecKey) throws -> Data {
109
+ let raw = try toRawPublicKey(publicKey)
110
+ let cryptoKey = try P256.Signing.PublicKey(x963Representation: raw)
111
+ return cryptoKey.derRepresentation
112
+ }
113
+
114
+ static func isProofBoundToPublicKey(_ proof: String, publicKey: SecKey) throws -> Bool {
115
+ let parts = proof.split(separator: ".", omittingEmptySubsequences: false)
116
+ guard parts.count == 3 else {
117
+ throw DPoPError.invalidProofFormat
118
+ }
119
+
120
+ guard let headerData = base64UrlDecode(String(parts[0])) else {
121
+ throw DPoPError.invalidProofFormat
122
+ }
123
+
124
+ let json = try JSONSerialization.jsonObject(with: headerData, options: [])
125
+ guard let header = json as? [String: Any],
126
+ let jwk = header["jwk"] as? [String: Any],
127
+ let kty = jwk["kty"] as? String,
128
+ let crv = jwk["crv"] as? String,
129
+ let x = jwk["x"] as? String,
130
+ let y = jwk["y"] as? String else {
131
+ throw DPoPError.invalidProofFormat
132
+ }
133
+
134
+ let coordinates = try getPublicCoordinates(fromRawPublicKey: try toRawPublicKey(publicKey))
135
+ return kty == "EC" && crv == "P-256" && x == coordinates.x && y == coordinates.y
136
+ }
137
+
138
+ private static func readDerLength(_ input: [UInt8], startIndex: Int) throws -> (Int, Int) {
139
+ guard startIndex < input.count else {
140
+ throw DPoPError.invalidDerSignature
141
+ }
142
+
143
+ let first = Int(input[startIndex])
144
+ if (first & 0x80) == 0 {
145
+ return (first, 1)
146
+ }
147
+
148
+ let lengthBytesCount = first & 0x7F
149
+ guard lengthBytesCount > 0, lengthBytesCount <= 4, startIndex + lengthBytesCount < input.count else {
150
+ throw DPoPError.invalidDerSignature
151
+ }
152
+
153
+ var length = 0
154
+ for index in 0..<lengthBytesCount {
155
+ length = (length << 8) | Int(input[startIndex + 1 + index])
156
+ }
157
+
158
+ return (length, 1 + lengthBytesCount)
159
+ }
160
+
161
+ private static func toUnsignedFixedLength(_ value: Data, length: Int) throws -> Data {
162
+ let bytes = [UInt8](value)
163
+ if bytes.count == length {
164
+ return value
165
+ }
166
+
167
+ if bytes.count == length + 1, bytes.first == 0x00 {
168
+ return Data(bytes.dropFirst())
169
+ }
170
+
171
+ if bytes.count < length {
172
+ return Data(repeating: 0, count: length - bytes.count) + value
173
+ }
174
+
175
+ throw DPoPError.invalidDerSignature
176
+ }
177
+ }
178
+
179
+ enum DPoPError: LocalizedError {
180
+ case invalidDerSignature
181
+ case invalidPublicKey
182
+ case invalidProofFormat
183
+ case keyNotFound(alias: String)
184
+ case notHardwareBacked(alias: String)
185
+ case securityError(CFError?)
186
+
187
+ var errorDescription: String? {
188
+ switch self {
189
+ case .invalidDerSignature:
190
+ return "Invalid DER signature format"
191
+ case .invalidPublicKey:
192
+ return "Invalid P-256 public key"
193
+ case .invalidProofFormat:
194
+ return "Invalid DPoP proof format"
195
+ case .keyNotFound(let alias):
196
+ return "Key pair not found for alias: \(alias)"
197
+ case .notHardwareBacked(let alias):
198
+ return "Key pair is not hardware-backed for alias: \(alias)"
199
+ case .securityError(let error):
200
+ return (error as Error?)?.localizedDescription ?? "Security framework error"
201
+ }
202
+ }
203
+ }