@umutcansu/react-native-pinvault 2.6.1 → 2.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.8.0 — 2026-10-15
4
+
5
+ ### Changed
6
+
7
+ - **Release builds take a block's addresses only from the native security
8
+ file**: a declared block names its `url` there, and `enrollmentUrl` /
9
+ `renewalUrl` from JS are refused where the file names none (the device's
10
+ identity goes to all three, its token to `url`). iOS `ios.resolve` follows the
11
+ same rule (top-level `"ios": {"resolve": {…}}` in the file; fixed wherever the
12
+ file gives it). Add them to `pinvault_security.json` if a release build now
13
+ refuses to start.
14
+ - `PinVault.fetch` refuses `Host`, `Connection`, `Keep-Alive`,
15
+ `Proxy-Connection`, `TE`, `Trailer`, `Transfer-Encoding`, `Upgrade` and
16
+ `Content-Length` from JS: the HTTP client sets them.
17
+
18
+ ## 2.7.0 — 2026-10-15
19
+
20
+ ### Changed
21
+
22
+ - **Release builds take `tokenHosts` only from the native security file**,
23
+ as they already did `clientCertHosts`: where the file has the block but
24
+ names no `tokenHosts`, a `tokenHosts` from JS is refused
25
+ (`E_INVALID_CONFIG`, "declare tokenHosts for Config API … there"). The
26
+ library's default sends `PinVault-Token` to the block's own Config API only,
27
+ so a `tokenHosts` from JS would add hosts that receive the device's token.
28
+ Debug builds and files that name the hosts are unchanged. Move your
29
+ `tokenHosts` into `pinvault_security.json` (Android: `assets`, iOS: the
30
+ bundle) if a release build now refuses to start.
31
+
3
32
  ## 2.6.1 — 2026-10-15
4
33
 
5
34
  ### Fixed
package/README.md CHANGED
@@ -29,9 +29,9 @@ npm install @umutcansu/react-native-pinvault
29
29
  cd ios && pod install
30
30
  ```
31
31
 
32
- The native libraries come with it, at the package's own version: npm 2.6.1
33
- uses `io.github.umutcansu:pinvault:2.6.1` from Maven Central and the Swift
34
- package at tag `v2.6.1`. The New Architecture must stay on (React Native's
32
+ The native libraries come with it, at the package's own version: npm 2.8.0
33
+ uses `io.github.umutcansu:pinvault:2.8.0` from Maven Central and the Swift
34
+ package at tag `v2.8.0`. The New Architecture must stay on (React Native's
35
35
  default since 0.76).
36
36
 
37
37
  **iOS.** The pod depends on the PinVault Swift package through React Native's
@@ -165,7 +165,8 @@ names), enum values are the Kotlin constant names (`'TOKEN_MTLS'`,
165
165
  requirePinnedReactNativeNetworking, // see Networking
166
166
  android: { requireUnlockedDeviceAllowFallback, pinGlobalNetworking,
167
167
  keepReactNativeHttpCache, keepReactNativeCookies },
168
- ios: { resolve: { 'mock-tls.sample': '10.0.0.12' }, expectedBundleIds, expectedTeamIds, userAuthStrength,
168
+ ios: { resolve: { 'mock-tls.sample': '10.0.0.12' }, // release: only from the native security file
169
+ expectedBundleIds, expectedTeamIds, userAuthStrength,
169
170
  reactNativeMaxResponseBytes },
170
171
  }
171
172
  ```
@@ -236,6 +237,7 @@ signature. Ship them natively as well, in `pinvault_security.json`:
236
237
  "allowUnsigned": false, "allowUnpinnedConfigApi": false, "allowServerGeneratedKey": false
237
238
  }],
238
239
  "staticPins": { "pins": [ … ], "version": 1 },
240
+ "ios": { "resolve": { "api.example.com": "10.0.0.12" } },
239
241
  "require": {
240
242
  "requireUnlockedDevice": true, "requireHardwareBackedKeys": true,
241
243
  "managedTrustRoots": true, "wipeVaultFilesOnRevocation": true,
@@ -262,9 +264,9 @@ parsed as strictly; a broken file rejects `start` with `E_INVALID_CONFIG`
262
264
  applies) or repeat it (lists in any order); a different value rejects `start`
263
265
  with `E_INVALID_CONFIG` naming the field;
264
266
  - in a release build a declared block must carry its anchors: `bootstrapPins`
265
- (or `allowUnpinnedConfigApi`) and `signaturePublicKeys` (or `allowUnsigned`);
266
- a block that only names itself rejects `start` — otherwise the bundle would
267
- supply the pins and keys;
267
+ (or `allowUnpinnedConfigApi`), `signaturePublicKeys` (or `allowUnsigned`) and
268
+ its `url` (2.7.1+: the device's token and identity go there); a block that
269
+ only names itself rejects `start` — otherwise the bundle would supply them;
268
270
  - `allowUnsigned`, `allowUnpinnedConfigApi` and `allowServerGeneratedKey` from
269
271
  JS are refused unless the file allows them for that block; a release build
270
272
  of the native libraries (2.4.2+) refuses the first two unless the block also
@@ -273,9 +275,13 @@ parsed as strictly; a broken file rejects `start` with `E_INVALID_CONFIG`
273
275
  - a block's `url`, `enrollmentUrl`, `renewalUrl`, `tokenHosts` and
274
276
  `clientCertHosts` are fixed where the file gives them (where the block talks to, and who gets its token and identity),
275
277
  and `attestation: true` / `proofOfPossession: true` there cannot be turned off from JS;
276
- in a release build JS may give `clientCertHosts` only where the file names
277
- them (the bundle cannot hand the device's identity to another host;
278
- `tokenHosts` from JS only narrows the library's default);
278
+ in a release build JS may give `enrollmentUrl`, `renewalUrl` (2.7.1+),
279
+ `clientCertHosts` and `tokenHosts` only where the file names them (the bundle
280
+ cannot hand the device's identity or its `PinVault-Token` to another host; the
281
+ library presents the identity to the block's url, enrollment and renewal
282
+ addresses, and its default sends the token to the block's own Config API only);
283
+ - iOS `ios.resolve` (where a pinned host's connection goes) is fixed where the
284
+ file gives it, and a release build takes it only from the file (2.7.1+);
279
285
  - `require` only tightens: a protection it turns on stays on whatever JS says,
280
286
  its `requireCaTrust` hosts are added to JS's, the expected signer / bundle /
281
287
  team ids and iOS `userAuthStrength` are fixed, and `expiredConfigGraceSeconds`
@@ -4,7 +4,7 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
4
 
5
5
  # PinVault for iOS is a Swift package (Package.swift at the repository root,
6
6
  # product "PinVault"). React Native's spm_dependency adds it to the Pods project:
7
- # - consumers: the git URL at the tag of this package's version (v2.6.1);
7
+ # - consumers: the git URL at the tag of this package's version (v2.8.0);
8
8
  # - development: PINVAULT_IOS_PACKAGE_PATH = a PinVault checkout (the sample's
9
9
  # Podfile sets it to this repository), the counterpart of pinvault.localPath.
10
10
  pinvault_url = "https://github.com/umutcansu/PinVault.git"
@@ -9,7 +9,7 @@ buildscript {
9
9
  kotlinVersion: "2.2.0",
10
10
  minSdkVersion: 24,
11
11
  compileSdkVersion: 36,
12
- pinvaultVersion: "2.6.1",
12
+ pinvaultVersion: "2.8.0",
13
13
  ]
14
14
  ext.pinvaultRnExt = { prop ->
15
15
  rootProject.ext.has(prop) ? rootProject.ext.get(prop) : PinVaultRN[prop]
@@ -278,6 +278,20 @@ internal object ConfigParser {
278
278
  val blockUrl = nativeBlock?.url?.also { if (it != url) fixed("url") } ?: url
279
279
  val fixedEnrollmentUrl = nativeBlock?.enrollmentUrl?.also { if (enrollmentUrl != null && it != enrollmentUrl) fixed("enrollmentUrl") } ?: enrollmentUrl
280
280
  val fixedRenewalUrl = nativeBlock?.renewalUrl?.also { if (renewalUrl != null && it != renewalUrl) fixed("renewalUrl") } ?: renewalUrl
281
+ // The device's identity goes to the enrollment and renewal addresses as well as to url:
282
+ // in a release build JS may give them only where the file names them (url itself must be
283
+ // in the file, SecurityPolicy.apply), like tokenHosts and clientCertHosts below.
284
+ if (release && nativeBlock != null) {
285
+ for ((key, given, declared) in listOf(
286
+ Triple("enrollmentUrl", enrollmentUrl, nativeBlock.enrollmentUrl),
287
+ Triple("renewalUrl", renewalUrl, nativeBlock.renewalUrl),
288
+ )) {
289
+ if (declared == null && given != null) {
290
+ throw BridgeInputException("${b.path}.$key: a release build takes it only from the app's native security file " +
291
+ "(${native?.source}); declare $key for Config API '$id' there")
292
+ }
293
+ }
294
+ }
281
295
  if (nativeBlock?.attestation == true && jsAttestation == false) {
282
296
  throw BridgeInputException("${b.path}.attestation: the app's native security file turns it on; JS cannot turn it off")
283
297
  }
@@ -286,11 +300,18 @@ internal object ConfigParser {
286
300
  throw BridgeInputException("${b.path}.proofOfPossession: the app's native security file turns it on; JS cannot turn it off")
287
301
  }
288
302
  val proof = jsProof == true || nativeBlock?.proofOfPossession == true
303
+ // Who gets the device's token is the file's call in a release build, like the identity
304
+ // below: the library's default is the block's own Config API only, so a tokenHosts from
305
+ // JS would ADD hosts that receive the token.
306
+ if (release && nativeBlock != null && nativeBlock.tokenHosts == null && jsTokenHosts != null) {
307
+ throw BridgeInputException("${b.path}.tokenHosts: a release build takes it only from the app's native security file " +
308
+ "(${native?.source}); declare tokenHosts for Config API '$id' there")
309
+ }
289
310
  val tokenHosts = nativeBlock?.tokenHosts?.also { n ->
290
311
  if (jsTokenHosts != null && !NativeSecurity.sameKeys(jsTokenHosts.map(String::lowercase), n.map(String::lowercase))) fixed("tokenHosts")
291
312
  } ?: jsTokenHosts
292
313
  // Who gets the device's identity is the file's call in a release build: JS cannot
293
- // add a host the file does not name (tokenHosts only narrows the library's default).
314
+ // add a host the file does not name.
294
315
  if (release && nativeBlock != null && nativeBlock.clientCertHosts == null && clientCertHosts != null) {
295
316
  throw BridgeInputException("${b.path}.clientCertHosts: a release build takes it only from the app's native security file " +
296
317
  "(${native?.source}); declare clientCertHosts for Config API '$id' there")
@@ -48,9 +48,13 @@ import java.io.IOException
48
48
  * a different value is refused (`E_INVALID_CONFIG`);
49
49
  * - `allowUnsigned`, `allowUnpinnedConfigApi`, `allowServerGeneratedKey` from
50
50
  * JS are refused unless the file allows them for that block;
51
- * - `url`, `tokenHosts` and `clientCertHosts` of a block are fixed where the
52
- * file gives them, and `attestation: true` / `proofOfPossession: true` there
53
- * cannot be switched off;
51
+ * - `url`, `enrollmentUrl`, `renewalUrl`, `tokenHosts` and `clientCertHosts`
52
+ * of a block are fixed where the file gives them, and `attestation: true` /
53
+ * `proofOfPossession: true` there cannot be switched off; in a release build
54
+ * a declared block must name its `url`, and JS may give `enrollmentUrl`,
55
+ * `renewalUrl`, `tokenHosts` and `clientCertHosts` only where the file names
56
+ * them (where the device's token and identity go is the file's call);
57
+ * - `ios.resolve` (top level) is the iOS side's; here it is read for the shape;
54
58
  * - `require` only tightens: a protection it turns on stays on whatever JS
55
59
  * says, `requireCaTrust` hosts are added to JS's, `expectedSignerSha256` is
56
60
  * fixed, and `expiredConfigGraceSeconds` is the most JS may ask for;
@@ -190,6 +194,11 @@ internal class NativeSecurity(
190
194
  )
191
195
  b.finish()
192
196
  }
197
+ // iOS-only (connection addresses, `ios.resolve`): read for the shape, used by the iOS side.
198
+ root.obj("ios")?.let { ios ->
199
+ ios.stringMap("resolve", 64, 255, 255)
200
+ ios.finish()
201
+ }
193
202
  val staticPins = root.obj("staticPins")?.let(ConfigParser::staticPins)
194
203
  val require = root.obj("require")?.let { r ->
195
204
  Require(
@@ -307,6 +316,10 @@ internal object SecurityPolicy {
307
316
  "declare $key for Config API '$id' there (or $relaxation)")
308
317
  if (block.bootstrapPins == null && !block.allowUnpinnedConfigApi) anchor("bootstrapPins", "allowUnpinnedConfigApi")
309
318
  if (block.signaturePublicKeys == null && !block.allowUnsigned) anchor("signaturePublicKeys", "allowUnsigned")
319
+ // Where the block talks to: the library hands it the device's token and identity.
320
+ if (block.url == null) throw BridgeInputException(
321
+ "$path.url: a release build takes it only from the app's native security file (${native.source}); " +
322
+ "declare url for Config API '$id' there")
310
323
  }
311
324
 
312
325
  val pins = block.bootstrapPins?.also { n ->
@@ -45,6 +45,11 @@ internal object PinnedFetch {
45
45
  /** The attestation headers: only the library sets them, for its token hosts (and a proof only it can sign). */
46
46
  val LIBRARY_HEADERS = listOf("PinVault-Token", "PinVault-Proof")
47
47
 
48
+ /** The HTTP client's own headers: the host the URL names, the connection and the body's framing. */
49
+ val CLIENT_HEADERS = listOf(
50
+ "Host", "Connection", "Keep-Alive", "Proxy-Connection", "TE", "Trailer", "Transfer-Encoding", "Upgrade", "Content-Length",
51
+ )
52
+
48
53
  fun parse(json: String): FetchRequest {
49
54
  // Base64 inflates by 4/3; leave room for the other fields.
50
55
  val f = StrictJson.parseObject(json, "request", maxChars = (MAX_REQUEST_BYTES * 4 / 3 + 64 * 1024).toInt())
@@ -59,6 +64,9 @@ internal object PinnedFetch {
59
64
  if (LIBRARY_HEADERS.any { it.equals(name, ignoreCase = true) }) {
60
65
  throw BridgeInputException("request.headers.$name: set by PinVault itself (a token host's requests carry it)")
61
66
  }
67
+ if (CLIENT_HEADERS.any { it.equals(name, ignoreCase = true) }) {
68
+ throw BridgeInputException("request.headers.$name: set by the HTTP client (host, connection and framing headers)")
69
+ }
62
70
  if (value.any { it == '\r' || it == '\n' || it.code == 0 }) {
63
71
  throw BridgeInputException("request.headers.$name: line breaks are not allowed")
64
72
  }
@@ -123,9 +123,8 @@ public enum ConfigParser {
123
123
  var maxRN = ReactNetworking.defaultMaxResponseBytes
124
124
  if let ios = try root.object("ios") {
125
125
  if let v = try ios.int64("reactNativeMaxResponseBytes", min: 1, max: ReactNetworking.maxResponseBytesLimit) { maxRN = v }
126
- if let resolve = try ios.stringMap("resolve", maxItems: 64, maxKeyLength: 255, maxValueLength: 255) {
127
- for (host, address) in resolve.sorted(by: { $0.key < $1.key }) { builder.resolve(host: host, to: address) }
128
- }
126
+ try applyResolve(try ios.stringMap("resolve", maxItems: 64, maxKeyLength: 255, maxValueLength: 255),
127
+ native: native, release: release, to: builder)
129
128
  if let v = try fixedList("config.ios.expectedBundleIds", require.expectedBundleIds,
130
129
  try ios.stringList("expectedBundleIds", maxItems: 16, maxLength: 255), native) {
131
130
  builder.expectedBundleId(v)
@@ -143,6 +142,7 @@ public enum ConfigParser {
143
142
  }
144
143
 
145
144
  else {
145
+ try applyResolve(nil, native: native, release: release, to: builder)
146
146
  if let v = require.expectedBundleIds { builder.expectedBundleId(v) }
147
147
  if let v = require.expectedTeamIds { builder.expectedTeamId(v) }
148
148
  if let v = require.userAuthStrength { builder.userAuthStrength(v) }
@@ -157,6 +157,29 @@ public enum ConfigParser {
157
157
  return parsed
158
158
  }
159
159
 
160
+ /// `config.ios.resolve` decides where a pinned host's TCP connection goes. The native
161
+ /// security file's map is the value (JS may leave it out or repeat it); a release build
162
+ /// takes it only from the file, so an OTA bundle cannot send the app's connections elsewhere.
163
+ static func applyResolve(_ given: [String: String]?, native: NativeSecurity?, release: Bool,
164
+ to builder: PinVaultConfig.Builder) throws {
165
+ let lowered: ([String: String]) -> [String: String] = { map in
166
+ Dictionary(map.map { ($0.key.lowercased(), $0.value) }, uniquingKeysWith: { first, _ in first })
167
+ }
168
+ var map = given
169
+ if let native {
170
+ if let fixed = native.resolve {
171
+ if let given, lowered(given) != lowered(fixed) {
172
+ throw BridgeInputError("config.ios.resolve: differs from the app's native security file (\(native.source)); the native value is fixed")
173
+ }
174
+ map = fixed
175
+ } else if release, given != nil {
176
+ throw BridgeInputError("config.ios.resolve: a release build takes it only from the app's native security file "
177
+ + "(\(native.source)); declare ios.resolve there")
178
+ }
179
+ }
180
+ for (host, address) in (map ?? [:]).sorted(by: { $0.key < $1.key }) { builder.resolve(host: host, to: address) }
181
+ }
182
+
160
183
  /// A JS list against the native file's: fixed where the file gives it.
161
184
  static func fixedList(_ path: String, _ native: [String]?, _ js: [String]?, _ file: NativeSecurity?) throws -> [String]? {
162
185
  guard let native else { return js }
@@ -295,6 +318,13 @@ public enum ConfigParser {
295
318
  }
296
319
  let proof = jsProof == true || nativeBlock?.proofOfPossession == true
297
320
  let lower: ([String]) -> [String] = { $0.map { $0.lowercased() } }
321
+ // Who gets the device's token is the file's call in a release build, like the identity
322
+ // below: the library's default is the block's own Config API only, so a tokenHosts from
323
+ // JS would ADD hosts that receive the token.
324
+ if release, let nativeBlock, nativeBlock.tokenHosts == nil, jsTokenHosts != nil {
325
+ throw BridgeInputError("\(b.path).tokenHosts: a release build takes it only from the app's native security file "
326
+ + "(\(native?.source ?? "")); declare tokenHosts for Config API '\(id)' there")
327
+ }
298
328
  var tokenHosts = jsTokenHosts
299
329
  if let n = nativeBlock?.tokenHosts {
300
330
  if let j = jsTokenHosts, !NativeSecurity.sameKeys(lower(j), lower(n)) { throw fixed("tokenHosts") }
@@ -310,8 +340,19 @@ public enum ConfigParser {
310
340
  if let j = renewalUrl, j != n { throw fixed("renewalUrl") }
311
341
  fixedRenewalUrl = n
312
342
  }
343
+ // The device's identity goes to the enrollment and renewal addresses as well as to url:
344
+ // in a release build JS may give them only where the file names them (url itself must be
345
+ // in the file, SecurityPolicy.apply), like tokenHosts and clientCertHosts.
346
+ if release, let nativeBlock {
347
+ for (key, given, declared) in [("enrollmentUrl", enrollmentUrl, nativeBlock.enrollmentUrl),
348
+ ("renewalUrl", renewalUrl, nativeBlock.renewalUrl)]
349
+ where declared == nil && given != nil {
350
+ throw BridgeInputError("\(b.path).\(key): a release build takes it only from the app's native security file "
351
+ + "(\(native?.source ?? "")); declare \(key) for Config API '\(id)' there")
352
+ }
353
+ }
313
354
  // Who gets the device's identity is the file's call in a release build: JS cannot
314
- // add a host the file does not name (tokenHosts only narrows the library's default).
355
+ // add a host the file does not name.
315
356
  if release, let nativeBlock, nativeBlock.clientCertHosts == nil, clientCertHosts != nil {
316
357
  throw BridgeInputError("\(b.path).clientCertHosts: a release build takes it only from the app's native security file "
317
358
  + "(\(native?.source ?? "")); declare clientCertHosts for Config API '\(id)' there")
@@ -11,6 +11,7 @@
11
11
  // "clientCertHosts",
12
12
  // "enrollmentUrl", "renewalUrl" }],
13
13
  // "staticPins": { "pins": [...], "version": 1 },
14
+ // "ios": { "resolve": { "<host>": "<address>" } },
14
15
  // "require": { "requireUnlockedDevice", "requireHardwareBackedKeys",
15
16
  // "requirePinnedReactNativeNetworking", "managedTrustRoots",
16
17
  // "wipeVaultFilesOnRevocation", "requireCaTrust", "expectedBundleIds",
@@ -21,7 +22,11 @@
21
22
  // When the file is there: every JS Config API (and JS `staticPins`) must be
22
23
  // declared in it; a declared field is the value (JS may omit or repeat it, a
23
24
  // different value is refused); the three relaxations need the file's consent;
24
- // `attestation: true` and `proofOfPossession: true` cannot be switched off; `require` only tightens (its
25
+ // `attestation: true` and `proofOfPossession: true` cannot be switched off; in a
26
+ // release build a declared block names its `url`, and JS may give `enrollmentUrl`,
27
+ // `renewalUrl`, `tokenHosts`, `clientCertHosts` and `ios.resolve` only where the
28
+ // file names them (where the device's token, identity and connections go is the
29
+ // file's call); `require` only tightens (its
25
30
  // protections stay on, its `requireCaTrust` hosts are added to JS's, its
26
31
  // expected ids are fixed, its grace is the most JS may ask for); `vaultFiles`
27
32
  // fixes those files' signature key, encryption and lock, and a JS vault file
@@ -100,6 +105,8 @@ public struct NativeSecurity {
100
105
  var require = Require()
101
106
  /// Nil when the file has no `vaultFiles` section.
102
107
  var vaultFiles: [String: VaultFilePolicy]? = nil
108
+ /// `ios.resolve` (logical host → connection address); nil when the file has none.
109
+ var resolve: [String: String]? = nil
103
110
 
104
111
  /// True when the app's Info.plist accepts a release start without the file.
105
112
  public static func noFileAllowed(_ bundle: Bundle = .main) -> Bool {
@@ -147,6 +154,12 @@ public struct NativeSecurity {
147
154
  )
148
155
  try b.finish()
149
156
  }
157
+ // Where a pinned host's connection goes (`ios.resolve`): fixed where the file gives it.
158
+ var resolve: [String: String]? = nil
159
+ if let ios = try root.object("ios") {
160
+ resolve = try ios.stringMap("resolve", maxItems: 64, maxKeyLength: 255, maxValueLength: 255)
161
+ try ios.finish()
162
+ }
150
163
  let staticPins = try root.object("staticPins").map(ConfigParser.staticPins)
151
164
  var require = Require()
152
165
  if let r = try root.object("require") {
@@ -188,6 +201,7 @@ public struct NativeSecurity {
188
201
  var native = NativeSecurity(source: source, blocks: blocks, staticPins: staticPins)
189
202
  native.require = require
190
203
  native.vaultFiles = vaultFiles
204
+ native.resolve = resolve
191
205
  return native
192
206
  } catch let e as BridgeInputError {
193
207
  throw e.message.hasPrefix("native security file") ? e : BridgeInputError("native security file \(e.message)")
@@ -268,6 +282,11 @@ enum SecurityPolicy {
268
282
  }
269
283
  if block.bootstrapPins == nil && !block.allowUnpinnedConfigApi { throw anchor("bootstrapPins", "allowUnpinnedConfigApi") }
270
284
  if block.signaturePublicKeys == nil && !block.allowUnsigned { throw anchor("signaturePublicKeys", "allowUnsigned") }
285
+ // Where the block talks to: the library hands it the device's token and identity.
286
+ if block.url == nil {
287
+ throw BridgeInputError("\(path).url: a release build takes it only from the app's native security file (\(native.source)); "
288
+ + "declare url for Config API '\(id)' there")
289
+ }
271
290
  }
272
291
 
273
292
  var out = js
@@ -29,6 +29,9 @@ public enum PinnedFetch {
29
29
  static let tokenChars = CharacterSet(charactersIn: "!#$%&'*+.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-")
30
30
  /// The attestation headers: only the library sets them, for its token hosts (and a proof only it can sign).
31
31
  static let libraryHeaders = ["PinVault-Token", "PinVault-Proof"]
32
+ /// The HTTP client's own headers: the host the URL names, the connection and the body's framing.
33
+ static let clientHeaders = ["Host", "Connection", "Keep-Alive", "Proxy-Connection", "TE", "Trailer",
34
+ "Transfer-Encoding", "Upgrade", "Content-Length"]
32
35
 
33
36
  public static func parse(_ json: String) throws -> FetchRequest {
34
37
  let f = try StrictJSON.parseObject(json, path: "request", maxChars: Int(maxRequestBytes * 4 / 3) + 64 * 1024)
@@ -47,6 +50,9 @@ public enum PinnedFetch {
47
50
  if libraryHeaders.contains(where: { $0.caseInsensitiveCompare(name) == .orderedSame }) {
48
51
  throw BridgeInputError("request.headers.\(name): set by PinVault itself (a token host's requests carry it)")
49
52
  }
53
+ if clientHeaders.contains(where: { $0.caseInsensitiveCompare(name) == .orderedSame }) {
54
+ throw BridgeInputError("request.headers.\(name): set by the HTTP client (host, connection and framing headers)")
55
+ }
50
56
  if value.unicodeScalars.contains(where: { $0 == "\r" || $0 == "\n" || $0.value == 0 }) {
51
57
  throw BridgeInputError("request.headers.\(name): line breaks are not allowed")
52
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umutcansu/react-native-pinvault",
3
- "version": "2.6.1",
3
+ "version": "2.8.0",
4
4
  "description": "PinVault for React Native: dynamic certificate pinning with signed remote configs, mTLS enrollment and signed vault files, as a thin bridge over the native PinVault libraries",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",