Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [6.3.0] - 2026-08-26

### Added

- Embedded configuration: a signed configuration bundled with the app, used
only when no online source and no previously fetched configuration is
available, typically the app's very first start during an outage. Expo apps
set the config-plugin prop `embeddedConfigurationFile`, which copies the file
into both native projects and writes `EmbeddedConfigurationFile` /
`embedded_configuration_asset` into the generated config files; bare apps
ship the file and add the key themselves. Intended only for apps whose
bundled resources are protected by RASP; see "Embedded configuration" in the
README.

### Changed

- Updated iOS native SDK to 6.3.0 and Android native SDK to 6.3.0.
- A configuration the SDK has fetched and validated is now retained on the
device and remains usable after a process restart when every configuration
source is unreachable.

## [6.2.0] - 2026-07-24

### Changed
Expand Down
64 changes: 64 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ holds no lever that disables or reconfigures pinning.
- [Installation](#installation)
- [Setup — Expo](#setup--expo)
- [Setup — bare React Native](#setup--bare-react-native)
- [Embedded configuration](#embedded-configuration)
- [Using the SDK](#using-the-sdk)
- [API reference](#api-reference)
- [Error handling](#error-handling)
Expand Down Expand Up @@ -97,6 +98,7 @@ The config plugin accepts these props:
| `mode` | `strict` \| `permissive` | Defaults to `strict`. |
| `configurationUrl` | string | Optional. HTTPS endpoint for a self-hosted signed config. |
| `logLevel` | `none` \| `error` \| `info` \| `debug` | Passed to the native init helper, so it also covers startup logging. |
| `embeddedConfigurationFile` | string | Optional. Path to a signed configuration bundled as a last-resort fallback. See [Embedded configuration](#embedded-configuration). |
| `ios.configFile` | string | Path to an existing `TrustPin-Info.plist` instead of generating one. |
| `android.configFile` | string | Path to an existing `trustpin.json` instead of generating one. |
| `android.allowNonOemImages` | boolean | Default `false`. Allows release builds on non-OEM device OS images (real devices only, not emulators). |
Expand Down Expand Up @@ -151,6 +153,7 @@ automatically):
| public key | yes | base64-encoded verification key |
| mode | no | `strict` (default, production) or `permissive` |
| configuration URL | no | HTTPS URL for a self-hosted signed config |
| embedded configuration | no | `EmbeddedConfigurationFile` (plist) / `embedded_configuration_asset` (JSON). See [Embedded configuration](#embedded-configuration) |

### 2. Call the native init helper

Expand Down Expand Up @@ -203,6 +206,67 @@ buildscript {

Then `cd ios && pod install`, and rebuild the app.

## Embedded configuration

TrustPin fetches its signed pinning configuration online and keeps the last
validated one on the device. For the one case where neither exists, the app's
**very first start while every configuration source is unreachable**, you can
ship a signed configuration inside the app as a last-resort fallback.

Download the signed configuration for your project from the TrustPin dashboard,
then:

**Expo**: point the plugin at it; prebuild copies it into both native projects
and adds the matching key to the generated config files:

```json
["@trustpin/react-native", {
"organizationId": "your-org-id",
"projectId": "your-project-id",
"publicKey": "LS0tLS1CRUdJTi...",
"embeddedConfigurationFile": "./trustpin-seed.b64"
}]
```

It cannot be combined with `ios.configFile` / `android.configFile`: the plugin
only adds the key to files it generates. With your own config files, declare
the key yourself and ship the payload as shown below.

**Bare React Native**: ship the file and reference it by name:

- **iOS**: add `ios/<YourApp>/trustpin-seed.b64` to the app target's **Copy
Bundle Resources**, then add to `TrustPin-Info.plist`:
```xml
<key>EmbeddedConfigurationFile</key>
<string>trustpin-seed.b64</string>
```
- **Android**: add `android/app/src/main/assets/trustpin-seed.b64`, then add to
`trustpin.json`:
```json
"embedded_configuration_asset": "trustpin-seed.b64"
```

### Requirements

- **Use it only in apps protected by RASP** (runtime application
self-protection) that guards bundled resources against modification. An
unprotected app must not ship an embedded configuration.
- **The file must be the unmodified signed payload** from the dashboard. It is
verified against `publicKey` during native setup; a file that is missing,
unreadable, or fails verification fails startup with
`INVALID_PROJECT_CONFIG`.
- **Regenerate it in CI on every release**, so it is never older than the app
that ships it. Pins expire on their own schedule, and an embedded configuration
whose pins have all expired is equivalent to having no fallback.

### Behaviour

- It is never preferred over an online source or over a configuration the SDK
has already fetched and validated.
- It is subject to the same integrity checks as any other configuration: a
device that has already trusted a newer configuration will not accept an
older embedded one.

## Using the SDK

Pinning is already active — ordinary requests are validated with no extra code:
Expand Down
4 changes: 2 additions & 2 deletions TrustPinReactNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ Pod::Spec.new do |s|
]
s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }

# Native SDK, locked to the 6.2.x.
s.dependency "TrustPinKit", "~> 6.2.0"
# Native SDK, locked to the 6.3.x.
s.dependency "TrustPinKit", "~> 6.3.0"

# React Native core + New Architecture (TurboModule codegen) dependencies.
install_modules_dependencies(s)
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ dependencies {
// the example passes the typed `.debug`.
api("cloud.trustpin:kotlin-sdk") {
version {
strictly("[6.2.0, 6.3.0)")
strictly("[6.3.0, 6.4.0)")
}
}

Expand Down
3 changes: 2 additions & 1 deletion example-expo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1856,8 +1856,8 @@ PODS:
- React-utils (= 0.85.3)
- ReactNativeDependencies
- ReactNativeDependencies (0.85.3)
- TrustPinKit (6.2.0)
- TrustPinReactNative (6.2.0-dev):
- TrustPinKit (6.3.0)
- TrustPinReactNative (6.3.0):
- hermes-engine
- RCTRequired
- RCTTypeSafety
Expand All @@ -1878,7 +1878,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- TrustPinKit (~> 6.2.0)
- TrustPinKit (~> 6.3.0)
- Yoga
- Yoga (0.0.0)

Expand Down Expand Up @@ -2122,7 +2122,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
FBLazyVector: 24e62c765683b8d89006a88a2c8f5cf019f0074d
hermes-engine: b7f913a6c7eadb06fbe2a9519e80b92bbb1fd12a
hermes-engine: 30cc42d16a9911849b72f5ac9af24a9df5f49904
RCTDeprecation: a4c521821fab57cbb125b36effe84d897d0dfa12
RCTRequired: 9f3a7e5645d4bc3f551593de7550bb66ab6e42bc
RCTSwiftUI: 239ed2eb9e73de5a6f518810630f0c95e01c8702
Expand All @@ -2131,7 +2131,7 @@ SPEC CHECKSUMS:
React: e2dc35338068bbd299c66f043ae0d7f25de8499e
React-callinvoker: 28b25d21b124c26cebaea713ba7d801b9351dc48
React-Core: 02ed7d2ffb70437bdf2aba074a13078a7b0b9ff0
React-Core-prebuilt: d1315d0975403bd506aae44147a96bfcb5feed8c
React-Core-prebuilt: e1da106deb995557515f0684b0ee594ff47f4e08
React-CoreModules: b3a5a42dadcde3b5d47b325bd912eb2ced89e146
React-cxxreact: fe8f88dda044e5905e99a00f41b7a874c3908716
React-debug: 92944dc4d89f56d640e75498266cbde557a48189
Expand Down Expand Up @@ -2194,11 +2194,11 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: 25c9c516839be2c5e3d3344f95dc7da5f7e63fc2
ReactCodegen: c8f81e6c6f762dcf442a6203a1fb58f7dafc8014
ReactCommon: 7dfc3250793bf36cf221096ff59e1179e13eef7f
ReactNativeDependencies: 6d76144d940da9da93bcce9eb8a27c2a60659350
TrustPinKit: 77a9ad322e91f2276edcd1b5ebbff7f8df484542
TrustPinReactNative: 2215f3e95b7b8abd1ddbf6b9711e3888af81b921
ReactNativeDependencies: a623b4d347714b61ec3c6f6ca2b0bf8c8605d75e
TrustPinKit: d4ccf971fd513161a0a51a08104fa9581fb4472e
TrustPinReactNative: 0a14f95033a95b49e1dd966c5540a92c451ec1cd
Yoga: 77dfa8673de2874e1855002ae59c68b8be9b007b

PODFILE CHECKSUM: b873e498c279153c00367a6291b174423836ab1b

COCOAPODS: 1.16.2
COCOAPODS: 1.17.0
11 changes: 10 additions & 1 deletion example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@trustpin/react-native",
"version": "6.2.0",
"version": "6.3.0",
"description": "TrustPin SSL/TLS certificate pinning for React Native — in-handshake enforcement via the native TrustPin SDKs",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
39 changes: 39 additions & 0 deletions plugin/src/__tests__/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ describe('prop validation', () => {
expect(() => resolveProps(undefined)).toThrow(TrustPinPluginError);
});

it('rejects an embedded configuration alongside a user-supplied config file', () => {
expect(() =>
resolveProps({
embeddedConfigurationFile: './trustpin-seed.b64',
ios: { configFile: './TrustPin-Info.plist' },
android: { configFile: './trustpin.json' },
}),
).toThrow(TrustPinPluginError);
});

it('accepts an embedded configuration alongside inline credentials', () => {
expect(() =>
resolveProps({ ...CREDENTIALS, embeddedConfigurationFile: './trustpin-seed.b64' }),
).not.toThrow();
});

it('rejects invalid enum values and non-https configuration URLs', () => {
expect(() => resolveProps({ ...CREDENTIALS, mode: 'loose' as 'strict' })).toThrow(/mode/);
expect(() => resolveProps({ ...CREDENTIALS, logLevel: 'verbose' as 'debug' })).toThrow(
Expand All @@ -113,6 +129,29 @@ describe('generated config files', () => {
expect(plist).toContain('a&amp;b&lt;c&gt;');
});

it('points the plist at the embedded configuration by file name only', () => {
const plist = buildPlist({
...CREDENTIALS,
embeddedConfigurationFile: './config/trustpin-seed.b64',
});
expect(plist).toContain('<key>EmbeddedConfigurationFile</key>');
// The native loader resolves a resource name in the bundle, not a path.
expect(plist).toContain('<string>trustpin-seed.b64</string>');
expect(plist).not.toContain('config/trustpin-seed.b64');
});

it('omits the embedded key when no file is configured', () => {
expect(buildPlist({ ...CREDENTIALS })).not.toContain('EmbeddedConfigurationFile');
expect(buildAssetJson({ ...CREDENTIALS })).not.toContain('embedded_configuration_asset');
});

it('points the Android asset JSON at the embedded configuration by file name only', () => {
const json = JSON.parse(
buildAssetJson({ ...CREDENTIALS, embeddedConfigurationFile: './config/trustpin-seed.b64' }),
);
expect(json.embedded_configuration_asset).toBe('trustpin-seed.b64');
});

it('writes snake_case JSON keys for Android', () => {
const json = JSON.parse(
buildAssetJson({ ...CREDENTIALS, configurationUrl: 'https://cdn.example.com' }),
Expand Down
15 changes: 15 additions & 0 deletions plugin/src/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export function buildAssetJson(props: TrustPinPluginProps): string {
if (props.configurationUrl) {
config.configuration_url = props.configurationUrl;
}
if (props.embeddedConfigurationFile) {
// The native loader resolves this as an asset name, so only the file name
// travels into the JSON.
config.embedded_configuration_asset = path.basename(props.embeddedConfigurationFile);
}
return `${JSON.stringify(config, null, 2)}\n`;
}

Expand Down Expand Up @@ -159,6 +164,16 @@ const withTrustPinAsset: ConfigPlugin<TrustPinPluginProps> = (config, props) =>

fs.mkdirSync(assetsDir, { recursive: true });
fs.writeFileSync(path.join(assetsDir, ASSET_FILE_NAME), contents);

// Gradle bundles everything under assets/ into the APK, so copying the
// signed configuration here is all the native asset loader needs.
if (props.embeddedConfigurationFile) {
const source = path.resolve(projectRoot, props.embeddedConfigurationFile);
if (!fs.existsSync(source)) {
throw new TrustPinPluginError(`embeddedConfigurationFile not found: ${source}`);
}
fs.copyFileSync(source, path.join(assetsDir, path.basename(source)));
}
return modConfig;
},
]);
Expand Down
Loading
Loading