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/codesigning-plugin-auto-pubkey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@callstack/repack": minor
---

Add `publicKeyPath` and `nativeProjectPaths` options to `CodeSigningPlugin`. When `publicKeyPath` is set, the plugin automatically embeds the public key into `Info.plist` (iOS) and `strings.xml` (Android) during compilation, removing the need for manual native file setup. The `embedPublicKey` utility is also exported for standalone use.
100 changes: 94 additions & 6 deletions packages/repack/src/plugins/CodeSigningPlugin/CodeSigningPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import type { Compiler as RspackCompiler } from '@rspack/core';
import jwt from 'jsonwebtoken';
import type { Compiler as WebpackCompiler } from 'webpack';
import { type CodeSigningPluginConfig, validateConfig } from './config.js';
import { embedPublicKey } from './embedPublicKey.js';

function resolveProjectPath(
projectRoot: string,
configPath?: string
): string | undefined {
if (!configPath) return undefined;
return path.isAbsolute(configPath)
? configPath
: path.resolve(projectRoot, configPath);
}

export class CodeSigningPlugin {
private chunkFilenames: Set<string>;
Expand All @@ -26,7 +37,6 @@ export class CodeSigningPlugin {
mainOutputFilename: string,
excludedChunks: string[] | RegExp[]
): boolean {
/** Exclude non-chunks & main chunk as it's always local */
if (!this.chunkFilenames.has(file) || file === mainOutputFilename) {
return false;
}
Expand All @@ -39,6 +49,72 @@ export class CodeSigningPlugin {
});
}

private embedPublicKeyInNativeProjects(compiler: RspackCompiler) {
if (!this.config.publicKeyPath) {
return;
}

const logger = compiler.getInfrastructureLogger('RepackCodeSigningPlugin');
const projectRoot = compiler.context;

const publicKeyPath = resolveProjectPath(
projectRoot,
this.config.publicKeyPath
)!;

if (!fs.existsSync(publicKeyPath)) {
logger.warn(
`Public key not found at ${publicKeyPath}. ` +
'Skipping automatic embedding into native project files.'
);
return;
}

const result = embedPublicKey({
publicKeyPath,
projectRoot,
iosInfoPlistPath: resolveProjectPath(
projectRoot,
this.config.nativeProjectPaths?.ios
),
androidStringsXmlPath: resolveProjectPath(
projectRoot,
this.config.nativeProjectPaths?.android
),
});

if (result.error) {
logger.warn(result.error);
return;
}

if (result.ios.modified) {
logger.info(`Embedded public key in iOS Info.plist: ${result.ios.path}`);
} else if (result.ios.error) {
logger.warn(`Failed to embed public key in iOS: ${result.ios.error}`);
} else {
logger.warn(
'Could not find iOS Info.plist. Skipping auto-embedding for iOS. ' +
'Use nativeProjectPaths.ios or manually add the public key to Info.plist.'
);
}

if (result.android.modified) {
logger.info(
`Embedded public key in Android strings.xml: ${result.android.path}`
);
} else if (result.android.error) {
logger.warn(
`Failed to embed public key in Android: ${result.android.error}`
);
} else {
logger.warn(
'Could not find Android strings.xml. Skipping auto-embedding for Android. ' +
'Use nativeProjectPaths.android or manually add the public key to strings.xml.'
);
}
}

apply(compiler: RspackCompiler): void;
apply(compiler: WebpackCompiler): void;

Expand All @@ -62,15 +138,27 @@ export class CodeSigningPlugin {
*/
const TOKEN_BUFFER_SIZE = 1280;
/**
* Used to denote beginning of the code-signing section of the bundle
* Used to denote the beginning of the code-signing section of the bundle
* alias for "Repack Code-Signing Signature Begin"
*/
const BEGIN_CS_MARK = '/* RCSSB */';

const privateKeyPath = path.isAbsolute(this.config.privateKeyPath)
? this.config.privateKeyPath
: path.resolve(compiler.context, this.config.privateKeyPath);
const privateKey = fs.readFileSync(privateKeyPath);
const privateKeyPath = resolveProjectPath(
compiler.context,
this.config.privateKeyPath
)!;

let privateKey: Buffer;
try {
privateKey = fs.readFileSync(privateKeyPath);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(
`Failed to read private key from ${privateKeyPath}: ${message}`
);
}

this.embedPublicKeyInNativeProjects(compiler);

const excludedChunks = Array.isArray(this.config.excludeChunks)
? this.config.excludeChunks
Expand Down
29 changes: 28 additions & 1 deletion packages/repack/src/plugins/CodeSigningPlugin/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ export interface CodeSigningPluginConfig {
privateKeyPath: string;
/** Names of chunks to exclude from being signed. */
excludeChunks?: string[] | RegExp | RegExp[];
/**
* Path to the public key file. When provided, the plugin will automatically
* embed the public key into native project files (Info.plist for iOS,
* strings.xml for Android) so that the runtime can verify signed bundles.
*
* Relative paths are resolved from the project root (compiler context).
*/
publicKeyPath?: string;
/**
* Override auto-detected paths to native project files where the public key
* should be embedded. Only used when `publicKeyPath` is set.
*/
nativeProjectPaths?: {
Comment thread
dannyhw marked this conversation as resolved.
/** Path to iOS Info.plist. Auto-detected if not provided. */
ios?: string;
/** Path to Android strings.xml. Auto-detected if not provided. */
android?: string;
};
}

type Schema = Parameters<typeof validate>[0];
Expand All @@ -18,7 +36,7 @@ export const optionsSchema: Schema = {
type: 'object',
properties: {
enabled: { type: 'boolean' },
privateKeyPath: { type: 'string' },
privateKeyPath: { type: 'string', minLength: 1 },
excludeChunks: {
anyOf: [
{
Expand All @@ -38,6 +56,15 @@ export const optionsSchema: Schema = {
},
],
},
publicKeyPath: { type: 'string', minLength: 1 },
nativeProjectPaths: {
type: 'object',
properties: {
ios: { type: 'string', minLength: 1 },
android: { type: 'string', minLength: 1 },
},
additionalProperties: false,
},
},
required: ['privateKeyPath'],
additionalProperties: false,
Expand Down
Loading
Loading