Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/gold-jokes-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@callstack/repack": major
---

Add support for passing a per-script `publicKey` from `ScriptManager` resolvers so signed bundles can be verified with a runtime-provided public key instead of only the app-embedded `RepackPublicKey`
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,18 @@ class CodeSigningUtils {
private fun parsePublicKey(stringPublicKey: String): PublicKey? {
val formattedPublicKey = stringPublicKey.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replace(System.getProperty("line.separator")!!, "")
.replace("\\s".toRegex(), "")

val byteKey: ByteArray = Base64.decode(formattedPublicKey.toByteArray(), Base64.DEFAULT)
val x509Key = X509EncodedKeySpec(byteKey)
val kf = KeyFactory.getInstance("RSA")
if (formattedPublicKey.isBlank()) {
return null
}

return kf.generatePublic(x509Key)
return runCatching {
val byteKey: ByteArray = Base64.decode(formattedPublicKey.toByteArray(), Base64.DEFAULT)
val x509Key = X509EncodedKeySpec(byteKey)
val kf = KeyFactory.getInstance("RSA")
kf.generatePublic(x509Key)
}.getOrNull()
}

private fun verifyAndDecodeToken(
Expand Down Expand Up @@ -83,15 +88,17 @@ class CodeSigningUtils {
return null
}

fun verifyBundle(context: Context, token: String?, fileContent: ByteArray?) {
fun verifyBundle(
context: Context, token: String?, fileContent: ByteArray?, stringPublicKey: String?
) {
if (token == null) {
throw Exception("The bundle verification failed because no token for the bundle was found.")
}

val stringPublicKey = getPublicKeyFromStringsIfExist(context)
val resolvedPublicKey = stringPublicKey ?: getPublicKeyFromStringsIfExist(context)
?: throw Exception("The bundle verification failed because PublicKey was not found in the bundle. Make sure you've added the PublicKey to the res/values/strings.xml under RepackPublicKey key.")

val publicKey = parsePublicKey(stringPublicKey)
val publicKey = parsePublicKey(resolvedPublicKey)
?: throw Exception("The bundle verification failed because the PublicKey is invalid.")

val claims: Map<String, Any?> = verifyAndDecodeToken(token, publicKey)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class FileSystemScriptLoader(private val reactContext: ReactContext, private val
}

if (config.verifyScriptSignature == "strict" || (config.verifyScriptSignature == "lax" && token != null)) {
CodeSigningUtils.verifyBundle(reactContext, token, bundle)
CodeSigningUtils.verifyBundle(reactContext, token, bundle, config.publicKey)
}

return bundle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class RemoteScriptLoader(val reactContext: ReactContext, private val nativeLoade
} ?: Pair(null, null)

if (config.verifyScriptSignature == "strict" || (config.verifyScriptSignature == "lax" && token != null)) {
CodeSigningUtils.verifyBundle(reactContext, token, bundle)
CodeSigningUtils.verifyBundle(reactContext, token, bundle, config.publicKey)
}

if (bundle == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ data class ScriptConfig(
val timeout: Int,
val headers: Headers,
val verifyScriptSignature: String,
val publicKey: String?,
val uniqueId: String,
val sourceUrl: String
) {
Expand All @@ -33,6 +34,7 @@ data class ScriptConfig(
val headersMap = value.getMap("headers")
val timeout = value.getInt("timeout")
val verifyScriptSignature = requireNotNull(value.getString("verifyScriptSignature"))
val publicKey = value.getString("publicKey")
val uniqueId = requireNotNull(value.getString("uniqueId"))

val initialUrl = URL(urlString)
Expand Down Expand Up @@ -79,6 +81,7 @@ data class ScriptConfig(
timeout,
headers.build(),
verifyScriptSignature,
publicKey,
uniqueId,
sourceUrl
)
Expand Down
32 changes: 22 additions & 10 deletions packages/repack/ios/CodeSigningUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,33 @@ public class CodeSigningUtils: NSObject {
}

let signatureB64 = convertBase64URLtoBase64(jwtSignature)
let signature = Signature(data: Data(base64Encoded: signatureB64)!)
guard let signatureData = Data(base64Encoded: signatureB64) else {
throw CodeSigningError.tokenInvalid
}
let signature = Signature(data: signatureData)

guard let pk = try? PublicKey(pemEncoded: publicKey) else {
guard let pk = try? PublicKey(
pemEncoded: publicKey.trimmingCharacters(in: .whitespacesAndNewlines)
) else {
throw CodeSigningError.publicKeyInvalid
}

// use b64-encoded header and payload for signature verification
let tokenWithoutSignature = token.components(separatedBy: ".").dropLast().joined(separator: ".")
let clearMessage = try? ClearMessage(string: tokenWithoutSignature, using: .utf8)

let isSuccesfullyVerified = try? clearMessage?.verify(with: pk, signature: signature, digestType: .sha256)
guard let clearMessage = try? ClearMessage(string: tokenWithoutSignature, using: .utf8) else {
throw CodeSigningError.tokenInvalid
}

if isSuccesfullyVerified! {
return jwt
} else {
do {
let isSuccesfullyVerified = try clearMessage.verify(with: pk, signature: signature, digestType: .sha256)
if isSuccesfullyVerified {
return jwt
} else {
throw CodeSigningError.tokenVerificationFailed
}
} catch let error as CodeSigningError {
throw error
} catch {
throw CodeSigningError.tokenVerificationFailed
}
}
Expand Down Expand Up @@ -80,12 +92,12 @@ public class CodeSigningUtils: NSObject {
}

@objc
public static func verifyBundle(token: String?, fileContent: NSData?) throws {
public static func verifyBundle(token: String?, fileContent: NSData?, publicKey: String?) throws {
guard let token = token else {
throw CodeSigningError.tokenNotFound
}

guard let publicKey = getPublicKey() else {
guard let publicKey = publicKey ?? getPublicKey() else {
throw CodeSigningError.publicKeyNotFound
}

Expand Down
2 changes: 2 additions & 0 deletions packages/repack/ios/ScriptConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly, nullable) NSDictionary *headers;
@property (nonatomic, readonly) NSNumber *timeout;
@property (nonatomic, readonly) NSString *verifyScriptSignature;
@property (nonatomic, readonly, nullable) NSString *publicKey;
@property (nonatomic, readonly) NSString *uniqueId;
@property (nonatomic, readonly) NSString *sourceUrl;

Expand All @@ -39,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN
withBody:(nullable NSData *)body
withTimeout:(NSNumber *)timeout
withVerifyScriptSignature:(NSString *)verifyScriptSignature
withPublicKey:(nullable NSString *)publicKey
withUniqueId:(NSString *)uniqueId
withSourceUrl:(NSString *)sourceUrl;

Expand Down
5 changes: 5 additions & 0 deletions packages/repack/ios/ScriptConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ @implementation ScriptConfig
@synthesize headers = _headers;
@synthesize timeout = _timeout;
@synthesize verifyScriptSignature = _verifyScriptSignature;
@synthesize publicKey = _publicKey;
@synthesize uniqueId = _uniqueId;
@synthesize sourceUrl = _sourceUrl;

Expand Down Expand Up @@ -42,6 +43,7 @@ + (ScriptConfig *)fromConfig:(JS::NativeScriptManager::NormalizedScriptLocator &
withBody:[config.body() dataUsingEncoding:NSUTF8StringEncoding]
withTimeout:[NSNumber numberWithDouble:config.timeout()]
withVerifyScriptSignature:config.verifyScriptSignature()
withPublicKey:config.publicKey()
withUniqueId:config.uniqueId()
withSourceUrl:sourceUrl];
}
Expand All @@ -67,6 +69,7 @@ + (ScriptConfig *)fromConfig:(NSDictionary *)config withScriptId:(nonnull NSStri
withBody:[config[@"body"] dataUsingEncoding:NSUTF8StringEncoding]
withTimeout:config[@"timeout"]
withVerifyScriptSignature:config[@"verifyScriptSignature"]
withPublicKey:config[@"publicKey"]
withUniqueId:config[@"uniqueId"]
withSourceUrl:sourceUrl];
}
Expand All @@ -90,6 +93,7 @@ - (ScriptConfig *)initWithScript:(NSString *)scriptId
withBody:(nullable NSData *)body
withTimeout:(nonnull NSNumber *)timeout
withVerifyScriptSignature:(NSString *)verifyScriptSignature
withPublicKey:(nullable NSString *)publicKey
withUniqueId:(NSString *)uniqueId
withSourceUrl:(nonnull NSString *)sourceUrl
{
Expand All @@ -103,6 +107,7 @@ - (ScriptConfig *)initWithScript:(NSString *)scriptId
_headers = headers;
_timeout = timeout;
_verifyScriptSignature = verifyScriptSignature;
_publicKey = publicKey;
_uniqueId = uniqueId;
_sourceUrl = sourceUrl;
return self;
Expand Down
10 changes: 8 additions & 2 deletions packages/repack/ios/ScriptManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,10 @@ - (void)downloadAndCache:(ScriptConfig *)config completionHandler:(void (^)(NSEr
if ([config.verifyScriptSignature isEqualToString:@"strict"] ||
([config.verifyScriptSignature isEqualToString:@"lax"] && token != nil)) {
NSError *codeSigningError = nil;
[CodeSigningUtils verifyBundleWithToken:token fileContent:bundle error:&codeSigningError];
[CodeSigningUtils verifyBundleWithToken:token
fileContent:bundle
publicKey:config.publicKey
error:&codeSigningError];
if (codeSigningError != nil) {
callback(codeSigningError);
return;
Expand Down Expand Up @@ -315,7 +318,10 @@ - (void)executeFromFilesystem:(ScriptConfig *)config
if ([config.verifyScriptSignature isEqualToString:@"strict"] ||
([config.verifyScriptSignature isEqualToString:@"lax"] && token != nil)) {
NSError *codeSigningError = nil;
[CodeSigningUtils verifyBundleWithToken:token fileContent:bundle error:&codeSigningError];
[CodeSigningUtils verifyBundleWithToken:token
fileContent:bundle
publicKey:config.publicKey
error:&codeSigningError];
if (codeSigningError != nil) {
reject(CodeExecutionFailure, codeSigningError.localizedDescription, nil);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export interface NormalizedScriptLocator {
headers: { [key: string]: string } | undefined;
body: string | undefined;
verifyScriptSignature: NormalizedScriptLocatorSignatureVerificationMode;
publicKey?: string;
}

export interface Spec extends TurboModule {
Expand Down
24 changes: 17 additions & 7 deletions packages/repack/src/modules/ScriptManager/Script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
NormalizedScriptLocatorHTTPMethod,
NormalizedScriptLocatorSignatureVerificationMode,
} from './NativeScriptManager.js';
import { normalizePublicKey } from './normalizePublicKey.js';
import type { ScriptLocator } from './types.js';

/**
Expand Down Expand Up @@ -117,6 +118,14 @@ export class Script {
throw new Error('Property url as a function is not support');
}

const verifyScriptSignature =
(locator.verifyScriptSignature as NormalizedScriptLocatorSignatureVerificationMode) ??
NormalizedScriptLocatorSignatureVerificationMode.OFF;
const publicKey = normalizePublicKey(
locator.publicKey,
verifyScriptSignature
);

return new Script(
key.scriptId,
key.caller,
Expand All @@ -134,9 +143,8 @@ export class Script {
body,
headers: Object.keys(headers).length ? headers : undefined,
fetch: locator.cache === false ? true : fetch,
verifyScriptSignature:
(locator.verifyScriptSignature as NormalizedScriptLocatorSignatureVerificationMode) ??
NormalizedScriptLocatorSignatureVerificationMode.OFF,
verifyScriptSignature,
...(publicKey ? { publicKey } : {}),
},
locator.cache
);
Expand Down Expand Up @@ -170,7 +178,7 @@ export class Script {
shouldUpdateCache(
cachedData: Pick<
NormalizedScriptLocator,
'method' | 'url' | 'query' | 'headers' | 'body'
'method' | 'url' | 'query' | 'headers' | 'body' | 'publicKey'
>
) {
if (!this.cache || !cachedData) {
Expand All @@ -191,7 +199,7 @@ export class Script {
shouldRefetch(
cachedData: Pick<
NormalizedScriptLocator,
'method' | 'url' | 'query' | 'headers' | 'body'
'method' | 'url' | 'query' | 'headers' | 'body' | 'publicKey'
>
) {
if (!this.cache) {
Expand All @@ -211,15 +219,16 @@ export class Script {
checkIfCacheDataOutdated(
cachedData: Pick<
NormalizedScriptLocator,
'method' | 'url' | 'query' | 'headers' | 'body'
'method' | 'url' | 'query' | 'headers' | 'body' | 'publicKey'
>
) {
return (
cachedData.method !== this.locator.method ||
cachedData.url !== this.locator.url ||
cachedData.query !== this.locator.query ||
!shallowEqual(cachedData.headers, this.locator.headers) ||
cachedData.body !== this.locator.body
cachedData.body !== this.locator.body ||
cachedData.publicKey !== this.locator.publicKey
);
}

Expand All @@ -235,6 +244,7 @@ export class Script {
query: this.locator.query,
headers: this.locator.headers,
body: this.locator.body,
publicKey: this.locator.publicKey,
};
}

Expand Down
Loading
Loading