@umutcansu/react-native-pinvault 2.7.0 → 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 +16 -1
- package/README.md +16 -11
- package/RNPinVault.podspec +1 -1
- package/android/build.gradle +1 -1
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ConfigParser.kt +14 -0
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/NativeSecurity.kt +16 -5
- package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinnedFetch.kt +8 -0
- package/ios/Core/ConfigParser.swift +37 -3
- package/ios/Core/NativeSecurity.swift +20 -1
- package/ios/Core/PinnedFetch.swift +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
3
18
|
## 2.7.0 — 2026-10-15
|
|
4
19
|
|
|
5
20
|
### Changed
|
|
@@ -11,7 +26,7 @@
|
|
|
11
26
|
library's default sends `PinVault-Token` to the block's own Config API only,
|
|
12
27
|
so a `tokenHosts` from JS would add hosts that receive the device's token.
|
|
13
28
|
Debug builds and files that name the hosts are unchanged. Move your
|
|
14
|
-
`tokenHosts` into `
|
|
29
|
+
`tokenHosts` into `pinvault_security.json` (Android: `assets`, iOS: the
|
|
15
30
|
bundle) if a release build now refuses to start.
|
|
16
31
|
|
|
17
32
|
## 2.6.1 — 2026-10-15
|
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.
|
|
33
|
-
uses `io.github.umutcansu:pinvault:2.
|
|
34
|
-
package at tag `v2.
|
|
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' },
|
|
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`)
|
|
266
|
-
|
|
267
|
-
|
|
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,10 +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 `
|
|
277
|
-
the file names them (the bundle
|
|
278
|
-
`PinVault-Token` to another host; the
|
|
279
|
-
block's
|
|
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+);
|
|
280
285
|
- `require` only tightens: a protection it turns on stays on whatever JS says,
|
|
281
286
|
its `requireCaTrust` hosts are added to JS's, the expected signer / bundle /
|
|
282
287
|
team ids and iOS `userAuthStrength` are fixed, and `expiredConfigGraceSeconds`
|
package/RNPinVault.podspec
CHANGED
|
@@ -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.
|
|
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"
|
package/android/build.gradle
CHANGED
|
@@ -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
|
}
|
|
@@ -48,11 +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`
|
|
52
|
-
* file gives them, and `attestation: true` /
|
|
53
|
-
* cannot be switched off; in a release build
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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;
|
|
56
58
|
* - `require` only tightens: a protection it turns on stays on whatever JS
|
|
57
59
|
* says, `requireCaTrust` hosts are added to JS's, `expectedSignerSha256` is
|
|
58
60
|
* fixed, and `expiredConfigGraceSeconds` is the most JS may ask for;
|
|
@@ -192,6 +194,11 @@ internal class NativeSecurity(
|
|
|
192
194
|
)
|
|
193
195
|
b.finish()
|
|
194
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
|
+
}
|
|
195
202
|
val staticPins = root.obj("staticPins")?.let(ConfigParser::staticPins)
|
|
196
203
|
val require = root.obj("require")?.let { r ->
|
|
197
204
|
Require(
|
|
@@ -309,6 +316,10 @@ internal object SecurityPolicy {
|
|
|
309
316
|
"declare $key for Config API '$id' there (or $relaxation)")
|
|
310
317
|
if (block.bootstrapPins == null && !block.allowUnpinnedConfigApi) anchor("bootstrapPins", "allowUnpinnedConfigApi")
|
|
311
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")
|
|
312
323
|
}
|
|
313
324
|
|
|
314
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
|
-
|
|
127
|
-
|
|
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 }
|
|
@@ -317,6 +340,17 @@ public enum ConfigParser {
|
|
|
317
340
|
if let j = renewalUrl, j != n { throw fixed("renewalUrl") }
|
|
318
341
|
fixedRenewalUrl = n
|
|
319
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
|
+
}
|
|
320
354
|
// Who gets the device's identity is the file's call in a release build: JS cannot
|
|
321
355
|
// add a host the file does not name.
|
|
322
356
|
if release, let nativeBlock, nativeBlock.clientCertHosts == nil, clientCertHosts != nil {
|
|
@@ -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;
|
|
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.
|
|
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",
|