feat(shorebird_cli): add --public-key-cmd and --sign-cmd support (#3605)
This commit is contained in:
@@ -0,0 +1,29 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all tests (must run from packages/ directory to avoid cache conflicts)
|
||||||
|
cd packages && very_good test -r
|
||||||
|
|
||||||
|
# Run tests for a single package (can use -r failures-only to reduce output)
|
||||||
|
dart test packages/shorebird_cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Dart monorepo. Main package is `shorebird_cli`.
|
||||||
|
|
||||||
|
**Platform-specific operations** use the Releaser/Patcher pattern:
|
||||||
|
- `commands/release/` - `Releaser` base class with platform implementations
|
||||||
|
- `commands/patch/` - `Patcher` base class with platform implementations
|
||||||
|
|
||||||
|
**Dependency injection** uses `scoped_deps` with zone-based refs (see any `*Ref` variable).
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- PR titles must follow semantic commit format (enforced in CI)
|
||||||
|
- CSpell: use inline `// cspell:words` for 1-2 files; add to global config for more
|
||||||
|
- Prefer new commits over amending in PRs - history gets squashed anyway
|
||||||
@@ -6,6 +6,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:pem/pem.dart';
|
import 'package:pem/pem.dart';
|
||||||
import 'package:pointycastle/pointycastle.dart';
|
import 'package:pointycastle/pointycastle.dart';
|
||||||
import 'package:scoped_deps/scoped_deps.dart';
|
import 'package:scoped_deps/scoped_deps.dart';
|
||||||
|
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||||
|
|
||||||
/// A reference to a [CodeSigner] instance.
|
/// A reference to a [CodeSigner] instance.
|
||||||
final codeSignerRef = create(CodeSigner.new);
|
final codeSignerRef = create(CodeSigner.new);
|
||||||
@@ -53,8 +54,13 @@ class CodeSigner {
|
|||||||
/// simply the modulus and exponent of the public key, without information
|
/// simply the modulus and exponent of the public key, without information
|
||||||
/// about the algorithm or or ASN1 object type identifier.
|
/// about the algorithm or or ASN1 object type identifier.
|
||||||
String base64PublicKey(File publicKeyPemFile) {
|
String base64PublicKey(File publicKeyPemFile) {
|
||||||
|
return base64PublicKeyFromPem(publicKeyPemFile.readAsStringSync());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the base64 encoded DER from a PEM-encoded public key string.
|
||||||
|
String base64PublicKeyFromPem(String publicKeyPem) {
|
||||||
final publicKey = _RSAPublicKeyFromBytes.rsaPublicKeyFromBytes(
|
final publicKey = _RSAPublicKeyFromBytes.rsaPublicKeyFromBytes(
|
||||||
_pemBytes(pemFile: publicKeyPemFile, type: PemLabel.publicKey),
|
PemCodec(PemLabel.publicKey).decode(publicKeyPem),
|
||||||
);
|
);
|
||||||
|
|
||||||
final publicKeySeq = ASN1Sequence()
|
final publicKeySeq = ASN1Sequence()
|
||||||
@@ -64,11 +70,104 @@ class CodeSigner {
|
|||||||
return base64.encode(publicKeySeq.encodedBytes!);
|
return base64.encode(publicKeySeq.encodedBytes!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reads a PEM file containing a key of type [type] and returns its contents
|
/// Runs a command and returns its stdout, expected to be a PEM public key.
|
||||||
/// as bytes.
|
Future<String> runPublicKeyCmd(String command) async {
|
||||||
List<int> _pemBytes({required File pemFile, required PemLabel type}) {
|
final result = await process.run(
|
||||||
final privateKeyString = pemFile.readAsStringSync();
|
'sh',
|
||||||
return PemCodec(type).decode(privateKeyString);
|
['-c', command],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
throw ProcessException(
|
||||||
|
command,
|
||||||
|
[],
|
||||||
|
'Command failed with exit code ${result.exitCode}: ${result.stderr}',
|
||||||
|
result.exitCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final output = '${result.stdout}'.trim();
|
||||||
|
if (!output.contains('-----BEGIN') || !output.contains('PUBLIC KEY')) {
|
||||||
|
if (output.isEmpty) {
|
||||||
|
throw const FormatException(
|
||||||
|
'Command produced no output. '
|
||||||
|
'Expected a PEM-encoded public key.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final preview = output.length > 100
|
||||||
|
? '${output.substring(0, 100)}...'
|
||||||
|
: output;
|
||||||
|
throw FormatException(
|
||||||
|
'Command output does not appear to be a PEM-encoded public key: '
|
||||||
|
'$preview',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs data by piping it to a command's stdin and reading the base64
|
||||||
|
/// signature from stdout.
|
||||||
|
Future<String> signWithCmd({
|
||||||
|
required String data,
|
||||||
|
required String command,
|
||||||
|
}) async {
|
||||||
|
final proc = await process.start(
|
||||||
|
'sh',
|
||||||
|
['-c', command],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write data to stdin and close it
|
||||||
|
proc.stdin.write(data);
|
||||||
|
await proc.stdin.close();
|
||||||
|
|
||||||
|
// Read stdout and stderr concurrently to avoid potential deadlock
|
||||||
|
final results = await Future.wait([
|
||||||
|
proc.stdout.transform(utf8.decoder).join(),
|
||||||
|
proc.stderr.transform(utf8.decoder).join(),
|
||||||
|
]);
|
||||||
|
final stdout = results[0];
|
||||||
|
final stderr = results[1];
|
||||||
|
final exitCode = await proc.exitCode;
|
||||||
|
|
||||||
|
if (exitCode != 0) {
|
||||||
|
throw ProcessException(
|
||||||
|
command,
|
||||||
|
[],
|
||||||
|
'Sign command failed with exit code $exitCode: $stderr',
|
||||||
|
exitCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final signature = stdout.trim();
|
||||||
|
if (signature.isEmpty) {
|
||||||
|
throw const FormatException('Sign command produced no output');
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verifies a signature against a message using a PEM-encoded public key.
|
||||||
|
bool verify({
|
||||||
|
required String message,
|
||||||
|
required String signature,
|
||||||
|
required String publicKeyPem,
|
||||||
|
}) {
|
||||||
|
final publicKey = _RSAPublicKeyFromBytes.rsaPublicKeyFromBytes(
|
||||||
|
PemCodec(PemLabel.publicKey).decode(publicKeyPem),
|
||||||
|
);
|
||||||
|
|
||||||
|
final signer = Signer('SHA-256/RSA')
|
||||||
|
..init(false, PublicKeyParameter<RSAPublicKey>(publicKey));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return signer.verifySignature(
|
||||||
|
utf8.encode(message),
|
||||||
|
RSASignature(base64.decode(signature)),
|
||||||
|
);
|
||||||
|
} on Exception {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses a PEM-encoded private key string and returns the key bytes along
|
/// Parses a PEM-encoded private key string and returns the key bytes along
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import 'package:shorebird_cli/src/archive_analysis/android_archive_differ.dart';
|
|||||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
import 'package:shorebird_cli/src/code_signer.dart';
|
|
||||||
import 'package:shorebird_cli/src/commands/patch/patch.dart';
|
import 'package:shorebird_cli/src/commands/patch/patch.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
|
||||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||||
@@ -143,13 +141,7 @@ class AarPatcher extends Patcher {
|
|||||||
logger.detail('Creating artifact for $artifactPath');
|
logger.detail('Creating artifact for $artifactPath');
|
||||||
final patchArtifact = File(artifactPath);
|
final patchArtifact = File(artifactPath);
|
||||||
final hash = sha256.convert(await patchArtifact.readAsBytes()).toString();
|
final hash = sha256.convert(await patchArtifact.readAsBytes()).toString();
|
||||||
|
final hashSignature = await signHash(hash);
|
||||||
final privateKeyFile = argResults.file(
|
|
||||||
CommonArguments.privateKeyArg.name,
|
|
||||||
);
|
|
||||||
final hashSignature = privateKeyFile != null
|
|
||||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final diffPath = await artifactManager.createDiff(
|
final diffPath = await artifactManager.createDiff(
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import 'package:shorebird_cli/src/archive_analysis/android_archive_differ.dart';
|
|||||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
import 'package:shorebird_cli/src/code_signer.dart';
|
|
||||||
import 'package:shorebird_cli/src/commands/patch/patcher.dart';
|
import 'package:shorebird_cli/src/commands/patch/patcher.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
|
||||||
import 'package:shorebird_cli/src/doctor.dart';
|
import 'package:shorebird_cli/src/doctor.dart';
|
||||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||||
@@ -189,13 +187,7 @@ Please refer to ${link(uri: Uri.parse('https://github.com/shorebirdtech/shorebir
|
|||||||
logger.detail('Creating artifact for $patchArtifactPath');
|
logger.detail('Creating artifact for $patchArtifactPath');
|
||||||
final patchArtifact = File(patchArtifactPath);
|
final patchArtifact = File(patchArtifactPath);
|
||||||
final hash = sha256.convert(await patchArtifact.readAsBytes()).toString();
|
final hash = sha256.convert(await patchArtifact.readAsBytes()).toString();
|
||||||
|
final hashSignature = await signHash(hash);
|
||||||
final privateKeyFile = argResults.file(
|
|
||||||
CommonArguments.privateKeyArg.name,
|
|
||||||
);
|
|
||||||
final hashSignature = privateKeyFile != null
|
|
||||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final diffPath = await artifactManager.createDiff(
|
final diffPath = await artifactManager.createDiff(
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ import 'package:shorebird_cli/src/archive_analysis/apple_archive_differ.dart';
|
|||||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
import 'package:shorebird_cli/src/code_signer.dart';
|
|
||||||
import 'package:shorebird_cli/src/commands/patch/patch.dart';
|
import 'package:shorebird_cli/src/commands/patch/patch.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
|
||||||
import 'package:shorebird_cli/src/doctor.dart';
|
import 'package:shorebird_cli/src/doctor.dart';
|
||||||
import 'package:shorebird_cli/src/executables/aot_tools.dart';
|
import 'package:shorebird_cli/src/executables/aot_tools.dart';
|
||||||
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
|
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
|
||||||
@@ -232,11 +230,8 @@ class IosFrameworkPatcher extends Patcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final patchFileSize = patchFile.statSync().size;
|
final patchFileSize = patchFile.statSync().size;
|
||||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
|
||||||
final hash = sha256.convert(patchBuildFile.readAsBytesSync()).toString();
|
final hash = sha256.convert(patchBuildFile.readAsBytesSync()).toString();
|
||||||
final hashSignature = privateKeyFile != null
|
final hashSignature = await signHash(hash);
|
||||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Arch.arm64: PatchArtifactBundle(
|
Arch.arm64: PatchArtifactBundle(
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart';
|
|||||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
import 'package:shorebird_cli/src/code_signer.dart';
|
|
||||||
import 'package:shorebird_cli/src/commands/patch/patcher.dart';
|
import 'package:shorebird_cli/src/commands/patch/patcher.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
|
||||||
import 'package:shorebird_cli/src/doctor.dart';
|
import 'package:shorebird_cli/src/doctor.dart';
|
||||||
import 'package:shorebird_cli/src/executables/executables.dart';
|
import 'package:shorebird_cli/src/executables/executables.dart';
|
||||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||||
@@ -304,11 +302,8 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
|
|||||||
}
|
}
|
||||||
|
|
||||||
final patchFileSize = patchFile.statSync().size;
|
final patchFileSize = patchFile.statSync().size;
|
||||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
|
||||||
final hash = sha256.convert(patchBuildFile.readAsBytesSync()).toString();
|
final hash = sha256.convert(patchBuildFile.readAsBytesSync()).toString();
|
||||||
final hashSignature = privateKeyFile != null
|
final hashSignature = await signHash(hash);
|
||||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Arch.arm64: PatchArtifactBundle(
|
Arch.arm64: PatchArtifactBundle(
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import 'package:shorebird_cli/src/archive_analysis/linux_bundle_differ.dart';
|
|||||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
import 'package:shorebird_cli/src/code_signer.dart';
|
|
||||||
import 'package:shorebird_cli/src/commands/commands.dart';
|
import 'package:shorebird_cli/src/commands/commands.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
|
||||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||||
import 'package:shorebird_cli/src/logging/logging.dart';
|
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||||
@@ -85,10 +83,7 @@ class LinuxPatcher extends Patcher {
|
|||||||
// build/linux/x64/release/bundle
|
// build/linux/x64/release/bundle
|
||||||
final appSoPath = p.join(tempDir.path, 'lib', 'libapp.so');
|
final appSoPath = p.join(tempDir.path, 'lib', 'libapp.so');
|
||||||
|
|
||||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
final hashSignature = await signHash(hash);
|
||||||
final hashSignature = privateKeyFile != null
|
|
||||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final String diffPath;
|
final String diffPath;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import 'package:shorebird_cli/src/archive_analysis/apple_archive_differ.dart';
|
|||||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
import 'package:shorebird_cli/src/code_signer.dart';
|
|
||||||
import 'package:shorebird_cli/src/commands/patch/patch.dart';
|
import 'package:shorebird_cli/src/commands/patch/patch.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
|
||||||
import 'package:shorebird_cli/src/doctor.dart';
|
import 'package:shorebird_cli/src/doctor.dart';
|
||||||
import 'package:shorebird_cli/src/executables/executables.dart';
|
import 'package:shorebird_cli/src/executables/executables.dart';
|
||||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||||
@@ -201,11 +199,8 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}''');
|
|||||||
|
|
||||||
final patchFile = File(patchFilePath);
|
final patchFile = File(patchFilePath);
|
||||||
final patchFileSize = patchFile.statSync().size;
|
final patchFileSize = patchFile.statSync().size;
|
||||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
|
||||||
final hash = sha256.convert(patchArtifact.readAsBytesSync()).toString();
|
final hash = sha256.convert(patchArtifact.readAsBytesSync()).toString();
|
||||||
final hashSignature = privateKeyFile != null
|
final hashSignature = await signHash(hash);
|
||||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return PatchArtifactBundle(
|
return PatchArtifactBundle(
|
||||||
arch: arch.arch,
|
arch: arch.arch,
|
||||||
|
|||||||
@@ -140,6 +140,14 @@ To target the latest release (e.g. the release that was most recently updated) u
|
|||||||
CommonArguments.publicKeyArg.name,
|
CommonArguments.publicKeyArg.name,
|
||||||
help: CommonArguments.publicKeyArg.description,
|
help: CommonArguments.publicKeyArg.description,
|
||||||
)
|
)
|
||||||
|
..addOption(
|
||||||
|
CommonArguments.publicKeyCmd.name,
|
||||||
|
help: CommonArguments.publicKeyCmd.description,
|
||||||
|
)
|
||||||
|
..addOption(
|
||||||
|
CommonArguments.signCmd.name,
|
||||||
|
help: CommonArguments.signCmd.description,
|
||||||
|
)
|
||||||
..addOption(
|
..addOption(
|
||||||
CommonArguments.splitDebugInfoArg.name,
|
CommonArguments.splitDebugInfoArg.name,
|
||||||
help: CommonArguments.splitDebugInfoArg.description,
|
help: CommonArguments.splitDebugInfoArg.description,
|
||||||
@@ -287,7 +295,7 @@ NOTE: this is ${styleBold.wrap('not')} recommended. Asset changes cannot be incl
|
|||||||
Future<void> createPatch(Patcher patcher) async {
|
Future<void> createPatch(Patcher patcher) async {
|
||||||
await patcher.assertPreconditions();
|
await patcher.assertPreconditions();
|
||||||
await patcher.assertArgsAreValid();
|
await patcher.assertArgsAreValid();
|
||||||
results.assertAbsentOrValidKeyPair();
|
results.assertAbsentOrValidKeyPairOrCommands();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await shorebirdValidator.validateFlavors(
|
await shorebirdValidator.validateFlavors(
|
||||||
@@ -444,7 +452,9 @@ Building patch with Flutter $flutterVersionString
|
|||||||
usedIgnoreNativeChangesFlag: allowNativeDiffs,
|
usedIgnoreNativeChangesFlag: allowNativeDiffs,
|
||||||
hasNativeChanges: diffStatus.hasNativeChanges,
|
hasNativeChanges: diffStatus.hasNativeChanges,
|
||||||
inferredReleaseVersion: inferredReleaseVersion,
|
inferredReleaseVersion: inferredReleaseVersion,
|
||||||
isSigned: results.wasParsed(CommonArguments.privateKeyArg.name),
|
isSigned:
|
||||||
|
results.wasParsed(CommonArguments.privateKeyArg.name) ||
|
||||||
|
results.wasParsed(CommonArguments.signCmd.name),
|
||||||
environment: BuildEnvironmentMetadata(
|
environment: BuildEnvironmentMetadata(
|
||||||
flutterRevision: shorebirdEnv.flutterRevision,
|
flutterRevision: shorebirdEnv.flutterRevision,
|
||||||
operatingSystem: platform.operatingSystem,
|
operatingSystem: platform.operatingSystem,
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ import 'package:args/args.dart';
|
|||||||
import 'package:mason_logger/mason_logger.dart';
|
import 'package:mason_logger/mason_logger.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
|
import 'package:shorebird_cli/src/code_signer.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||||
import 'package:shorebird_cli/src/deployment_track.dart';
|
import 'package:shorebird_cli/src/deployment_track.dart';
|
||||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||||
import 'package:shorebird_cli/src/extensions/iterable.dart';
|
import 'package:shorebird_cli/src/extensions/iterable.dart';
|
||||||
|
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||||
import 'package:shorebird_cli/src/metadata/metadata.dart';
|
import 'package:shorebird_cli/src/metadata/metadata.dart';
|
||||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||||
import 'package:shorebird_cli/src/release_type.dart';
|
import 'package:shorebird_cli/src/release_type.dart';
|
||||||
import 'package:shorebird_cli/src/shorebird_documentation.dart';
|
import 'package:shorebird_cli/src/shorebird_documentation.dart';
|
||||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||||
|
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||||
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
import 'package:shorebird_code_push_protocol/shorebird_code_push_protocol.dart';
|
||||||
|
|
||||||
@@ -127,6 +130,90 @@ More info: ${troubleshootingUrl.toLink()}.
|
|||||||
/// Whether to allow changes in native code (--allow-native-diffs).
|
/// Whether to allow changes in native code (--allow-native-diffs).
|
||||||
bool get allowNativeDiffs => argResults['allow-native-diffs'] == true;
|
bool get allowNativeDiffs => argResults['allow-native-diffs'] == true;
|
||||||
|
|
||||||
|
/// Returns a function that signs data, or null if no signing is configured.
|
||||||
|
Future<String> Function(String)? _resolveSigner() {
|
||||||
|
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
||||||
|
if (privateKeyFile != null) {
|
||||||
|
return (hash) async =>
|
||||||
|
codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
final signCmd = argResults[CommonArguments.signCmd.name] as String?;
|
||||||
|
if (signCmd != null) {
|
||||||
|
return (hash) async {
|
||||||
|
try {
|
||||||
|
return await codeSigner.signWithCmd(data: hash, command: signCmd);
|
||||||
|
} on ProcessException catch (e) {
|
||||||
|
logger.err(
|
||||||
|
'Failed to run --${CommonArguments.signCmd.name}: ${e.message}',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.software.code);
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
logger.err(
|
||||||
|
'--${CommonArguments.signCmd.name} produced invalid output: '
|
||||||
|
'${e.message}',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.software.code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the public key PEM from the configured source, or null.
|
||||||
|
Future<String?> _resolvePublicKeyPem() async {
|
||||||
|
try {
|
||||||
|
return await argResults.resolvePublicKeyPem();
|
||||||
|
} on ProcessException catch (e) {
|
||||||
|
logger.err(
|
||||||
|
'Failed to run '
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}: ${e.message}',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.software.code);
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
logger.err(
|
||||||
|
'--${CommonArguments.publicKeyCmd.name} produced invalid output: '
|
||||||
|
'${e.message}',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.software.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Signs a hash using the configured signing method.
|
||||||
|
///
|
||||||
|
/// Returns null if no signing is configured.
|
||||||
|
Future<String?> signHash(String hash) async {
|
||||||
|
final signer = _resolveSigner();
|
||||||
|
if (signer == null) return null;
|
||||||
|
|
||||||
|
final signature = await signer(hash);
|
||||||
|
final publicKeyPem = await _resolvePublicKeyPem();
|
||||||
|
|
||||||
|
if (publicKeyPem == null) {
|
||||||
|
logger.err(
|
||||||
|
'A public key is required for code signing. '
|
||||||
|
'Provide --${CommonArguments.publicKeyArg.name} or '
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}.',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.usage.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!codeSigner.verify(
|
||||||
|
message: hash,
|
||||||
|
signature: signature,
|
||||||
|
publicKeyPem: publicKeyPem,
|
||||||
|
)) {
|
||||||
|
logger.err(
|
||||||
|
'Signature verification failed. The signature does not match '
|
||||||
|
'the provided public key.',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.software.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
/// The link percentage for the generated patch artifact if applicable.
|
/// The link percentage for the generated patch artifact if applicable.
|
||||||
/// Returns `null` if the platform does not use a linker or if the linking
|
/// Returns `null` if the platform does not use a linker or if the linking
|
||||||
/// step has not yet been run.
|
/// step has not yet been run.
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ import 'package:shorebird_cli/src/archive_analysis/windows_archive_differ.dart';
|
|||||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
import 'package:shorebird_cli/src/code_signer.dart';
|
|
||||||
import 'package:shorebird_cli/src/commands/patch/patcher.dart';
|
import 'package:shorebird_cli/src/commands/patch/patcher.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
|
||||||
import 'package:shorebird_cli/src/doctor.dart';
|
import 'package:shorebird_cli/src/doctor.dart';
|
||||||
import 'package:shorebird_cli/src/executables/executables.dart';
|
import 'package:shorebird_cli/src/executables/executables.dart';
|
||||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||||
@@ -109,10 +107,7 @@ class WindowsPatcher extends Patcher {
|
|||||||
// build/windows/x64/runner/Release
|
// build/windows/x64/runner/Release
|
||||||
final appSoPath = p.join(tempDir.path, 'data', 'app.so');
|
final appSoPath = p.join(tempDir.path, 'data', 'app.so');
|
||||||
|
|
||||||
final privateKeyFile = argResults.file(CommonArguments.privateKeyArg.name);
|
final hashSignature = await signHash(hash);
|
||||||
final hashSignature = privateKeyFile != null
|
|
||||||
? codeSigner.sign(message: hash, privateKeyPemFile: privateKeyFile)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final String diffPath;
|
final String diffPath;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -74,11 +74,12 @@ class AarReleaser extends Releaser {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
||||||
|
final base64PublicKey = await getEncodedPublicKey();
|
||||||
await artifactBuilder.buildAar(
|
await artifactBuilder.buildAar(
|
||||||
buildNumber: buildNumber,
|
buildNumber: buildNumber,
|
||||||
targetPlatforms: architectures,
|
targetPlatforms: architectures,
|
||||||
args: argResults.forwardedArgs,
|
args: argResults.forwardedArgs,
|
||||||
base64PublicKey: argResults.encodedPublicKey,
|
base64PublicKey: base64PublicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Copy release AAR to a new directory to avoid overwriting with
|
// Copy release AAR to a new directory to avoid overwriting with
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ Please comment and upvote ${link(uri: Uri.parse('https://github.com/shorebirdtec
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
||||||
final base64PublicKey = argResults.encodedPublicKey;
|
final base64PublicKey = await getEncodedPublicKey();
|
||||||
final aab = await artifactBuilder.buildAppBundle(
|
final aab = await artifactBuilder.buildAppBundle(
|
||||||
flavor: flavor,
|
flavor: flavor,
|
||||||
target: target,
|
target: target,
|
||||||
|
|||||||
@@ -78,9 +78,10 @@ class IosFrameworkReleaser extends Releaser {
|
|||||||
shorebirdSupplementDir!.deleteSync(recursive: true);
|
shorebirdSupplementDir!.deleteSync(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final base64PublicKey = await getEncodedPublicKey();
|
||||||
await artifactBuilder.buildIosFramework(
|
await artifactBuilder.buildIosFramework(
|
||||||
args: argResults.forwardedArgs,
|
args: argResults.forwardedArgs,
|
||||||
base64PublicKey: argResults.encodedPublicKey,
|
base64PublicKey: base64PublicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Copy release xcframework to a new directory to avoid overwriting with
|
// Copy release xcframework to a new directory to avoid overwriting with
|
||||||
|
|||||||
@@ -102,12 +102,13 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
shorebirdSupplementDir!.deleteSync(recursive: true);
|
shorebirdSupplementDir!.deleteSync(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final base64PublicKey = await getEncodedPublicKey();
|
||||||
await artifactBuilder.buildIpa(
|
await artifactBuilder.buildIpa(
|
||||||
codesign: codesign,
|
codesign: codesign,
|
||||||
flavor: flavor,
|
flavor: flavor,
|
||||||
target: target,
|
target: target,
|
||||||
args: argResults.forwardedArgs,
|
args: argResults.forwardedArgs,
|
||||||
base64PublicKey: argResults.encodedPublicKey,
|
base64PublicKey: base64PublicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
final xcarchiveDirectory = artifactManager.getXcarchiveDirectory();
|
final xcarchiveDirectory = artifactManager.getXcarchiveDirectory();
|
||||||
|
|||||||
@@ -64,10 +64,11 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
||||||
|
final base64PublicKey = await getEncodedPublicKey();
|
||||||
await artifactBuilder.buildLinuxApp(
|
await artifactBuilder.buildLinuxApp(
|
||||||
target: target,
|
target: target,
|
||||||
args: argResults.forwardedArgs,
|
args: argResults.forwardedArgs,
|
||||||
base64PublicKey: argResults.encodedPublicKey,
|
base64PublicKey: base64PublicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
return artifactManager.linuxBundleDirectory;
|
return artifactManager.linuxBundleDirectory;
|
||||||
|
|||||||
@@ -93,12 +93,13 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final base64PublicKey = await getEncodedPublicKey();
|
||||||
await artifactBuilder.buildMacos(
|
await artifactBuilder.buildMacos(
|
||||||
codesign: codesign,
|
codesign: codesign,
|
||||||
flavor: flavor,
|
flavor: flavor,
|
||||||
target: target,
|
target: target,
|
||||||
args: argResults.forwardedArgs,
|
args: argResults.forwardedArgs,
|
||||||
base64PublicKey: argResults.encodedPublicKey,
|
base64PublicKey: base64PublicKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
final appDirectory = artifactManager.getMacOSAppDirectory(flavor: flavor);
|
final appDirectory = artifactManager.getMacOSAppDirectory(flavor: flavor);
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ of the iOS app that is using this module. (aar and ios-framework only)''',
|
|||||||
CommonArguments.publicKeyArg.name,
|
CommonArguments.publicKeyArg.name,
|
||||||
help: CommonArguments.publicKeyArg.description,
|
help: CommonArguments.publicKeyArg.description,
|
||||||
)
|
)
|
||||||
|
..addOption(
|
||||||
|
CommonArguments.publicKeyCmd.name,
|
||||||
|
help: CommonArguments.publicKeyCmd.description,
|
||||||
|
)
|
||||||
..addOption(
|
..addOption(
|
||||||
CommonArguments.splitDebugInfoArg.name,
|
CommonArguments.splitDebugInfoArg.name,
|
||||||
help: CommonArguments.splitDebugInfoArg.description,
|
help: CommonArguments.splitDebugInfoArg.description,
|
||||||
@@ -367,14 +371,18 @@ of the iOS app that is using this module. (aar and ios-framework only)''',
|
|||||||
|
|
||||||
/// Validates arguments that are common to all release types.
|
/// Validates arguments that are common to all release types.
|
||||||
Future<void> assertArgsAreValid(Releaser releaser) async {
|
Future<void> assertArgsAreValid(Releaser releaser) async {
|
||||||
results.assertAbsentOrValidPublicKey();
|
results.assertAbsentOrValidPublicKeyOrCmd();
|
||||||
|
|
||||||
final shorebirdYaml = shorebirdEnv.getShorebirdYaml();
|
final shorebirdYaml = shorebirdEnv.getShorebirdYaml();
|
||||||
if (shorebirdYaml?.patchVerification != null &&
|
final hasPublicKey =
|
||||||
!results.wasParsed(CommonArguments.publicKeyArg.name)) {
|
results.wasParsed(CommonArguments.publicKeyArg.name) ||
|
||||||
|
results.wasParsed(CommonArguments.publicKeyCmd.name);
|
||||||
|
if (shorebirdYaml?.patchVerification != null && !hasPublicKey) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
'patch_verification is set in shorebird.yaml but '
|
'patch_verification is set in shorebird.yaml but '
|
||||||
'--${CommonArguments.publicKeyArg.name} was not provided.\n'
|
'no public key was provided '
|
||||||
|
'(--${CommonArguments.publicKeyArg.name} '
|
||||||
|
'or --${CommonArguments.publicKeyCmd.name}).\n'
|
||||||
'patch_verification configuration will have no effect.',
|
'patch_verification configuration will have no effect.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -554,10 +562,13 @@ ${summary.join('\n')}
|
|||||||
required Release release,
|
required Release release,
|
||||||
required Releaser releaser,
|
required Releaser releaser,
|
||||||
}) async {
|
}) async {
|
||||||
|
final hasPublicKey =
|
||||||
|
results.wasParsed(CommonArguments.publicKeyArg.name) ||
|
||||||
|
results.wasParsed(CommonArguments.publicKeyCmd.name);
|
||||||
final baseMetadata = UpdateReleaseMetadata(
|
final baseMetadata = UpdateReleaseMetadata(
|
||||||
releasePlatform: releaser.releaseType.releasePlatform,
|
releasePlatform: releaser.releaseType.releasePlatform,
|
||||||
flutterVersionOverride: flutterVersionArg,
|
flutterVersionOverride: flutterVersionArg,
|
||||||
includesPublicKey: results.wasParsed(CommonArguments.publicKeyArg.name),
|
includesPublicKey: hasPublicKey,
|
||||||
environment: BuildEnvironmentMetadata(
|
environment: BuildEnvironmentMetadata(
|
||||||
flutterRevision: shorebirdEnv.flutterRevision,
|
flutterRevision: shorebirdEnv.flutterRevision,
|
||||||
operatingSystem: platform.operatingSystem,
|
operatingSystem: platform.operatingSystem,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
import 'package:pub_semver/pub_semver.dart';
|
import 'package:pub_semver/pub_semver.dart';
|
||||||
|
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||||
import 'package:shorebird_cli/src/metadata/metadata.dart';
|
import 'package:shorebird_cli/src/metadata/metadata.dart';
|
||||||
import 'package:shorebird_cli/src/release_type.dart';
|
import 'package:shorebird_cli/src/release_type.dart';
|
||||||
import 'package:shorebird_cli/src/shorebird_env.dart';
|
import 'package:shorebird_cli/src/shorebird_env.dart';
|
||||||
@@ -81,4 +82,9 @@ abstract class Releaser {
|
|||||||
Future<String> getReleaseVersion({
|
Future<String> getReleaseVersion({
|
||||||
required FileSystemEntity releaseArtifactRoot,
|
required FileSystemEntity releaseArtifactRoot,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Gets the base64-encoded public key from either file or command.
|
||||||
|
///
|
||||||
|
/// Returns null if no public key is configured.
|
||||||
|
Future<String?> getEncodedPublicKey() => argResults.getEncodedPublicKey();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,11 +66,12 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<FileSystemEntity> buildReleaseArtifacts() {
|
Future<FileSystemEntity> buildReleaseArtifacts() async {
|
||||||
|
final base64PublicKey = await getEncodedPublicKey();
|
||||||
return artifactBuilder.buildWindowsApp(
|
return artifactBuilder.buildWindowsApp(
|
||||||
target: target,
|
target: target,
|
||||||
args: argResults.forwardedArgs,
|
args: argResults.forwardedArgs,
|
||||||
base64PublicKey: argResults.encodedPublicKey,
|
base64PublicKey: base64PublicKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,25 @@ The path for a private key .pem file that will be used to sign the patch artifac
|
|||||||
''',
|
''',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// An argument that allows the user to specify a command that outputs a
|
||||||
|
/// PEM-encoded public key to stdout.
|
||||||
|
static const publicKeyCmd = ArgumentDescriber(
|
||||||
|
name: 'public-key-cmd',
|
||||||
|
description: '''
|
||||||
|
Command that outputs a PEM-encoded public key to stdout for patch signature validation.
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// An argument that allows the user to specify a command that signs data.
|
||||||
|
/// The command reads data from stdin and outputs a base64 signature to
|
||||||
|
/// stdout.
|
||||||
|
static const signCmd = ArgumentDescriber(
|
||||||
|
name: 'sign-cmd',
|
||||||
|
description: '''
|
||||||
|
Command that reads data from stdin and outputs a base64 signature to stdout.
|
||||||
|
''',
|
||||||
|
);
|
||||||
|
|
||||||
/// An argument that allows the user to specify a release version. You will
|
/// An argument that allows the user to specify a release version. You will
|
||||||
/// most likely want to provide a custom description for this argument that
|
/// most likely want to provide a custom description for this argument that
|
||||||
/// more thoroughly explains what the release version is used for.
|
/// more thoroughly explains what the release version is used for.
|
||||||
|
|||||||
@@ -73,6 +73,25 @@ extension CodeSign on ArgResults {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the public key PEM string from the configured source.
|
||||||
|
///
|
||||||
|
/// Returns null if no public key is configured. Throws
|
||||||
|
/// [ProcessException] or [FormatException] if a command-based key
|
||||||
|
/// fails.
|
||||||
|
Future<String?> resolvePublicKeyPem() async {
|
||||||
|
final publicKeyFile = file(CommonArguments.publicKeyArg.name);
|
||||||
|
if (publicKeyFile != null) {
|
||||||
|
return publicKeyFile.readAsStringSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
final publicKeyCmd = this[CommonArguments.publicKeyCmd.name] as String?;
|
||||||
|
if (publicKeyCmd != null && wasParsed(CommonArguments.publicKeyCmd.name)) {
|
||||||
|
return codeSigner.runPublicKeyCmd(publicKeyCmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the public key file and encode it to base64 if any.
|
/// Read the public key file and encode it to base64 if any.
|
||||||
String? get encodedPublicKey {
|
String? get encodedPublicKey {
|
||||||
final publicKeyFile = file(CommonArguments.publicKeyArg.name);
|
final publicKeyFile = file(CommonArguments.publicKeyArg.name);
|
||||||
@@ -81,6 +100,121 @@ extension CodeSign on ArgResults {
|
|||||||
? codeSigner.base64PublicKey(publicKeyFile)
|
? codeSigner.base64PublicKey(publicKeyFile)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates key arguments for patch commands.
|
||||||
|
///
|
||||||
|
/// Valid configurations:
|
||||||
|
/// - No signing (nothing provided)
|
||||||
|
/// - File-based: --public-key-path + --private-key-path
|
||||||
|
/// - Command-based: --public-key-cmd + --sign-cmd
|
||||||
|
/// - Mixed: --public-key-path + --sign-cmd
|
||||||
|
///
|
||||||
|
/// Invalid configurations:
|
||||||
|
/// - Both --public-key-path and --public-key-cmd (ambiguous public key)
|
||||||
|
/// - Both --private-key-path and --sign-cmd (ambiguous signing method)
|
||||||
|
/// - --sign-cmd without a public key source
|
||||||
|
/// - --private-key-path without --public-key-path
|
||||||
|
void assertAbsentOrValidKeyPairOrCommands() {
|
||||||
|
final hasPublicKeyFile = wasParsed(CommonArguments.publicKeyArg.name);
|
||||||
|
final hasPrivateKeyFile = wasParsed(CommonArguments.privateKeyArg.name);
|
||||||
|
final hasPublicKeyCmd = wasParsed(CommonArguments.publicKeyCmd.name);
|
||||||
|
final hasSignCmd = wasParsed(CommonArguments.signCmd.name);
|
||||||
|
|
||||||
|
// Can't have two public key sources
|
||||||
|
if (hasPublicKeyFile && hasPublicKeyCmd) {
|
||||||
|
logger.err(
|
||||||
|
'Cannot specify both --${CommonArguments.publicKeyArg.name} and '
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}.',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.usage.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't have two signing methods
|
||||||
|
if (hasPrivateKeyFile && hasSignCmd) {
|
||||||
|
logger.err(
|
||||||
|
'Cannot specify both --${CommonArguments.privateKeyArg.name} and '
|
||||||
|
'--${CommonArguments.signCmd.name}.',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.usage.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File-based signing requires both file args
|
||||||
|
if (hasPrivateKeyFile || (hasPublicKeyFile && !hasSignCmd)) {
|
||||||
|
assertAbsentOrValidKeyPair();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --sign-cmd requires a public key source
|
||||||
|
if (hasSignCmd && !hasPublicKeyFile && !hasPublicKeyCmd) {
|
||||||
|
logger.err(
|
||||||
|
'--${CommonArguments.signCmd.name} requires a public key '
|
||||||
|
'(--${CommonArguments.publicKeyArg.name} or '
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}).',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.usage.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the public key file exists if provided
|
||||||
|
if (hasPublicKeyFile && hasSignCmd) {
|
||||||
|
assertAbsentOrValidPublicKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates public key arguments for release commands.
|
||||||
|
///
|
||||||
|
/// Valid configurations:
|
||||||
|
/// - No public key (no signing)
|
||||||
|
/// - --public-key-path with valid file
|
||||||
|
/// - --public-key-cmd
|
||||||
|
///
|
||||||
|
/// Invalid: mixing --public-key-path and --public-key-cmd
|
||||||
|
void assertAbsentOrValidPublicKeyOrCmd() {
|
||||||
|
final hasFilePath = wasParsed(CommonArguments.publicKeyArg.name);
|
||||||
|
final hasCmd = wasParsed(CommonArguments.publicKeyCmd.name);
|
||||||
|
|
||||||
|
if (hasFilePath && hasCmd) {
|
||||||
|
logger.err(
|
||||||
|
'Cannot specify both --${CommonArguments.publicKeyArg.name} and '
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}.',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.usage.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasFilePath) {
|
||||||
|
assertAbsentOrValidPublicKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get base64-encoded public key from either file or command.
|
||||||
|
///
|
||||||
|
/// Returns null if no public key is configured.
|
||||||
|
Future<String?> getEncodedPublicKey() async {
|
||||||
|
try {
|
||||||
|
final pem = await resolvePublicKeyPem();
|
||||||
|
if (pem == null) return null;
|
||||||
|
return codeSigner.base64PublicKeyFromPem(pem);
|
||||||
|
} on ProcessException catch (e) {
|
||||||
|
logger.err(
|
||||||
|
'Failed to run '
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}: ${e.message}',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.software.code);
|
||||||
|
} on FormatException catch (e) {
|
||||||
|
logger.err(
|
||||||
|
'--${CommonArguments.publicKeyCmd.name} produced invalid output: '
|
||||||
|
'${e.message}',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.software.code);
|
||||||
|
// Malformed PEM content causes ASN1 parsing errors (RangeError, etc.)
|
||||||
|
// ignore: avoid_catching_errors
|
||||||
|
} on Error catch (e) {
|
||||||
|
// ASN1 parsing errors for malformed PEM content
|
||||||
|
logger.err(
|
||||||
|
'--${CommonArguments.publicKeyCmd.name} output is not a valid '
|
||||||
|
'public key: $e',
|
||||||
|
);
|
||||||
|
throw ProcessExit(ExitCode.software.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extension on [ArgResults] to provide file related extensions.
|
/// Extension on [ArgResults] to provide file related extensions.
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
// cspell:words pubin dgst outform
|
// cspell:words pubin dgst outform
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:scoped_deps/scoped_deps.dart';
|
||||||
import 'package:shorebird_cli/src/code_signer.dart';
|
import 'package:shorebird_cli/src/code_signer.dart';
|
||||||
|
import 'package:shorebird_cli/src/shorebird_process.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class MockShorebirdProcess extends Mock implements ShorebirdProcess {}
|
||||||
|
|
||||||
|
class MockShorebirdProcessResult extends Mock
|
||||||
|
implements ShorebirdProcessResult {}
|
||||||
|
|
||||||
|
class MockProcess extends Mock implements Process {}
|
||||||
|
|
||||||
|
class MockIOSink extends Mock implements IOSink {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group(
|
group(
|
||||||
CodeSigner,
|
CodeSigner,
|
||||||
@@ -119,9 +132,250 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('base64PublicKeyFromPem', () {
|
||||||
|
test('output matches base64PublicKey from file', () {
|
||||||
|
final publicKeyPem = publicKeyFile.readAsStringSync();
|
||||||
|
expect(
|
||||||
|
codeSigner.base64PublicKeyFromPem(publicKeyPem),
|
||||||
|
equals(codeSigner.base64PublicKey(publicKeyFile)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('verify', () {
|
||||||
|
const message =
|
||||||
|
'6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b';
|
||||||
|
|
||||||
|
test('returns true for valid signature', () {
|
||||||
|
final signature = codeSigner.sign(
|
||||||
|
message: message,
|
||||||
|
privateKeyPemFile: privateKeyFile,
|
||||||
|
);
|
||||||
|
final publicKeyPem = publicKeyFile.readAsStringSync();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
codeSigner.verify(
|
||||||
|
message: message,
|
||||||
|
signature: signature,
|
||||||
|
publicKeyPem: publicKeyPem,
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for invalid signature', () {
|
||||||
|
final publicKeyPem = publicKeyFile.readAsStringSync();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
codeSigner.verify(
|
||||||
|
message: message,
|
||||||
|
signature: 'invalid-signature',
|
||||||
|
publicKeyPem: publicKeyPem,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for wrong message', () {
|
||||||
|
final signature = codeSigner.sign(
|
||||||
|
message: message,
|
||||||
|
privateKeyPemFile: privateKeyFile,
|
||||||
|
);
|
||||||
|
final publicKeyPem = publicKeyFile.readAsStringSync();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
codeSigner.verify(
|
||||||
|
message: 'wrong-message',
|
||||||
|
signature: signature,
|
||||||
|
publicKeyPem: publicKeyPem,
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onPlatform: {
|
onPlatform: {
|
||||||
'windows': const Skip('Does not have openssl installed by default'),
|
'windows': const Skip('Does not have openssl installed by default'),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
group('CodeSigner command-based signing', () {
|
||||||
|
late ShorebirdProcess shorebirdProcess;
|
||||||
|
late CodeSigner codeSigner;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
shorebirdProcess = MockShorebirdProcess();
|
||||||
|
codeSigner = CodeSigner();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('runPublicKeyCmd', () {
|
||||||
|
test('returns trimmed stdout on success', () async {
|
||||||
|
final result = MockShorebirdProcessResult();
|
||||||
|
when(() => result.exitCode).thenReturn(0);
|
||||||
|
when(() => result.stdout).thenReturn('''
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
''');
|
||||||
|
when(
|
||||||
|
() => shorebirdProcess.run(any(), any()),
|
||||||
|
).thenAnswer((_) async => result);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
final output = await codeSigner.runPublicKeyCmd('cat key.pem');
|
||||||
|
expect(output, contains('-----BEGIN PUBLIC KEY-----'));
|
||||||
|
expect(output, contains('-----END PUBLIC KEY-----'));
|
||||||
|
},
|
||||||
|
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws ProcessException on non-zero exit code', () async {
|
||||||
|
final result = MockShorebirdProcessResult();
|
||||||
|
when(() => result.exitCode).thenReturn(1);
|
||||||
|
when(() => result.stderr).thenReturn('command not found');
|
||||||
|
when(
|
||||||
|
() => shorebirdProcess.run(any(), any()),
|
||||||
|
).thenAnswer((_) async => result);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
() => codeSigner.runPublicKeyCmd('invalid-command'),
|
||||||
|
throwsA(isA<ProcessException>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws FormatException when output is empty', () async {
|
||||||
|
final result = MockShorebirdProcessResult();
|
||||||
|
when(() => result.exitCode).thenReturn(0);
|
||||||
|
when(() => result.stdout).thenReturn('');
|
||||||
|
when(
|
||||||
|
() => shorebirdProcess.run(any(), any()),
|
||||||
|
).thenAnswer((_) async => result);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
() => codeSigner.runPublicKeyCmd('empty-cmd'),
|
||||||
|
throwsA(
|
||||||
|
isA<FormatException>().having(
|
||||||
|
(e) => e.message,
|
||||||
|
'message',
|
||||||
|
contains('produced no output'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws FormatException when output is not a PEM key', () async {
|
||||||
|
final result = MockShorebirdProcessResult();
|
||||||
|
when(() => result.exitCode).thenReturn(0);
|
||||||
|
when(() => result.stdout).thenReturn('not a pem key');
|
||||||
|
when(
|
||||||
|
() => shorebirdProcess.run(any(), any()),
|
||||||
|
).thenAnswer((_) async => result);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
() => codeSigner.runPublicKeyCmd('echo "not a pem key"'),
|
||||||
|
throwsA(isA<FormatException>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('signWithCmd', () {
|
||||||
|
late MockProcess proc;
|
||||||
|
late MockIOSink stdin;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
proc = MockProcess();
|
||||||
|
stdin = MockIOSink();
|
||||||
|
when(() => proc.stdin).thenReturn(stdin);
|
||||||
|
when(() => stdin.close()).thenAnswer((_) async {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns trimmed stdout on success', () async {
|
||||||
|
when(() => proc.stdout).thenAnswer(
|
||||||
|
(_) => Stream.value(utf8.encode('base64signature\n')),
|
||||||
|
);
|
||||||
|
when(() => proc.stderr).thenAnswer((_) => const Stream.empty());
|
||||||
|
when(() => proc.exitCode).thenAnswer((_) async => 0);
|
||||||
|
when(
|
||||||
|
() => shorebirdProcess.start(any(), any()),
|
||||||
|
).thenAnswer((_) async => proc);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
final signature = await codeSigner.signWithCmd(
|
||||||
|
data: 'hash-to-sign',
|
||||||
|
command: 'sign-script.sh',
|
||||||
|
);
|
||||||
|
expect(signature, equals('base64signature'));
|
||||||
|
},
|
||||||
|
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws ProcessException on non-zero exit code', () async {
|
||||||
|
when(() => proc.stdout).thenAnswer((_) => const Stream.empty());
|
||||||
|
when(() => proc.stderr).thenAnswer(
|
||||||
|
(_) => Stream.value(utf8.encode('error')),
|
||||||
|
);
|
||||||
|
when(() => proc.exitCode).thenAnswer((_) async => 1);
|
||||||
|
when(
|
||||||
|
() => shorebirdProcess.start(any(), any()),
|
||||||
|
).thenAnswer((_) async => proc);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
() => codeSigner.signWithCmd(
|
||||||
|
data: 'hash-to-sign',
|
||||||
|
command: 'failing-script.sh',
|
||||||
|
),
|
||||||
|
throwsA(isA<ProcessException>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws FormatException when output is empty', () async {
|
||||||
|
when(() => proc.stdout).thenAnswer(
|
||||||
|
(_) => Stream.value(utf8.encode(' \n ')),
|
||||||
|
);
|
||||||
|
when(() => proc.stderr).thenAnswer((_) => const Stream.empty());
|
||||||
|
when(() => proc.exitCode).thenAnswer((_) async => 0);
|
||||||
|
when(
|
||||||
|
() => shorebirdProcess.start(any(), any()),
|
||||||
|
).thenAnswer((_) async => proc);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
() => codeSigner.signWithCmd(
|
||||||
|
data: 'hash-to-sign',
|
||||||
|
command: 'empty-output-script.sh',
|
||||||
|
),
|
||||||
|
throwsA(isA<FormatException>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {processRef.overrideWith(() => shorebirdProcess)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -551,16 +551,18 @@ void main() {
|
|||||||
|
|
||||||
group('when a private key is provided', () {
|
group('when a private key is provided', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
final privateKey = File(
|
final tempDir = Directory.systemTemp.createTempSync();
|
||||||
p.join(
|
final privateKey = File(p.join(tempDir.path, 'test-private.pem'))
|
||||||
Directory.systemTemp.createTempSync().path,
|
..createSync();
|
||||||
'test-private.pem',
|
final publicKey = File(p.join(tempDir.path, 'test-public.pem'))
|
||||||
),
|
..writeAsStringSync('public-key-pem');
|
||||||
)..createSync();
|
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.privateKeyArg.name],
|
() => argResults[CommonArguments.privateKeyArg.name],
|
||||||
).thenReturn(privateKey.path);
|
).thenReturn(privateKey.path);
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
|
).thenReturn(publicKey.path);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => codeSigner.sign(
|
() => codeSigner.sign(
|
||||||
@@ -571,6 +573,13 @@ void main() {
|
|||||||
final message = invocation.namedArguments[#message] as String;
|
final message = invocation.namedArguments[#message] as String;
|
||||||
return '$message-signature';
|
return '$message-signature';
|
||||||
});
|
});
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
@@ -594,16 +594,18 @@ Looked in:
|
|||||||
|
|
||||||
group('when a private key is provided', () {
|
group('when a private key is provided', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
final privateKey = File(
|
final tempDir = Directory.systemTemp.createTempSync();
|
||||||
p.join(
|
final privateKey = File(p.join(tempDir.path, 'test-private.pem'))
|
||||||
Directory.systemTemp.createTempSync().path,
|
..createSync();
|
||||||
'test-private.pem',
|
final publicKey = File(p.join(tempDir.path, 'test-public.pem'))
|
||||||
),
|
..writeAsStringSync('public-key-pem');
|
||||||
)..createSync();
|
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.privateKeyArg.name],
|
() => argResults[CommonArguments.privateKeyArg.name],
|
||||||
).thenReturn(privateKey.path);
|
).thenReturn(privateKey.path);
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
|
).thenReturn(publicKey.path);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => codeSigner.sign(
|
() => codeSigner.sign(
|
||||||
@@ -614,6 +616,13 @@ Looked in:
|
|||||||
final message = invocation.namedArguments[#message] as String;
|
final message = invocation.namedArguments[#message] as String;
|
||||||
return '$message-signature';
|
return '$message-signature';
|
||||||
});
|
});
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
@@ -872,16 +872,19 @@ void main() {
|
|||||||
|
|
||||||
group('when code signing the patch', () {
|
group('when code signing the patch', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
final tempDir = Directory.systemTemp.createTempSync();
|
||||||
final privateKey = File(
|
final privateKey = File(
|
||||||
p.join(
|
p.join(tempDir.path, 'test-private.pem'),
|
||||||
Directory.systemTemp.createTempSync().path,
|
|
||||||
'test-private.pem',
|
|
||||||
),
|
|
||||||
)..createSync();
|
)..createSync();
|
||||||
|
final publicKey = File(p.join(tempDir.path, 'test-public.pem'))
|
||||||
|
..writeAsStringSync('public-key-pem');
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.privateKeyArg.name],
|
() => argResults[CommonArguments.privateKeyArg.name],
|
||||||
).thenReturn(privateKey.path);
|
).thenReturn(privateKey.path);
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
|
).thenReturn(publicKey.path);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => codeSigner.sign(
|
() => codeSigner.sign(
|
||||||
@@ -892,6 +895,13 @@ void main() {
|
|||||||
final message = invocation.namedArguments[#message] as String;
|
final message = invocation.namedArguments[#message] as String;
|
||||||
return '$message-signature';
|
return '$message-signature';
|
||||||
});
|
});
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
@@ -1169,16 +1169,19 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
|
|||||||
|
|
||||||
group('when code signing the patch', () {
|
group('when code signing the patch', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
final tempDir = Directory.systemTemp.createTempSync();
|
||||||
final privateKey = File(
|
final privateKey = File(
|
||||||
p.join(
|
p.join(tempDir.path, 'test-private.pem'),
|
||||||
Directory.systemTemp.createTempSync().path,
|
|
||||||
'test-private.pem',
|
|
||||||
),
|
|
||||||
)..createSync();
|
)..createSync();
|
||||||
|
final publicKey = File(p.join(tempDir.path, 'test-public.pem'))
|
||||||
|
..writeAsStringSync('public-key-pem');
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.privateKeyArg.name],
|
() => argResults[CommonArguments.privateKeyArg.name],
|
||||||
).thenReturn(privateKey.path);
|
).thenReturn(privateKey.path);
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
|
).thenReturn(publicKey.path);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => codeSigner.sign(
|
() => codeSigner.sign(
|
||||||
@@ -1189,6 +1192,13 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
|
|||||||
final message = invocation.namedArguments[#message] as String;
|
final message = invocation.namedArguments[#message] as String;
|
||||||
return '$message-signature';
|
return '$message-signature';
|
||||||
});
|
});
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
@@ -366,6 +366,12 @@ void main() {
|
|||||||
|
|
||||||
group('when signing keys are provided', () {
|
group('when signing keys are provided', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
final publicKeyFile = File(
|
||||||
|
p.join(
|
||||||
|
Directory.systemTemp.createTempSync().path,
|
||||||
|
'public-key.pem',
|
||||||
|
),
|
||||||
|
)..createSync();
|
||||||
when(
|
when(
|
||||||
() => artifactManager.createDiff(
|
() => artifactManager.createDiff(
|
||||||
releaseArtifactPath: any(named: 'releaseArtifactPath'),
|
releaseArtifactPath: any(named: 'releaseArtifactPath'),
|
||||||
@@ -374,7 +380,7 @@ void main() {
|
|||||||
).thenAnswer((_) async => diffFile.path);
|
).thenAnswer((_) async => diffFile.path);
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.publicKeyArg.name],
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
).thenReturn('public-key.pem');
|
).thenReturn(publicKeyFile.path);
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.privateKeyArg.name],
|
() => argResults[CommonArguments.privateKeyArg.name],
|
||||||
).thenReturn('private-key.pem');
|
).thenReturn('private-key.pem');
|
||||||
@@ -384,6 +390,13 @@ void main() {
|
|||||||
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||||
),
|
),
|
||||||
).thenReturn('signature');
|
).thenReturn('signature');
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('signs patch', () async {
|
test('signs patch', () async {
|
||||||
|
|||||||
@@ -952,6 +952,13 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
|
|||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.privateKeyArg.name],
|
() => argResults[CommonArguments.privateKeyArg.name],
|
||||||
).thenReturn(createTempFile('private.pem').path);
|
).thenReturn(createTempFile('private.pem').path);
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
|
).thenReturn(
|
||||||
|
(createTempFile(
|
||||||
|
'public.pem',
|
||||||
|
)..writeAsStringSync('public-key-pem')).path,
|
||||||
|
);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => codeSigner.sign(
|
() => codeSigner.sign(
|
||||||
@@ -959,6 +966,13 @@ For more information see: ${supportedFlutterVersionsUrl.toLink()}'''),
|
|||||||
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||||
),
|
),
|
||||||
).thenReturn('my-signature');
|
).thenReturn('my-signature');
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('returns artifact bundles with non-null hash signature', () async {
|
test('returns artifact bundles with non-null hash signature', () async {
|
||||||
|
|||||||
@@ -194,6 +194,12 @@ void main() {
|
|||||||
when(
|
when(
|
||||||
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||||
).thenReturn(false);
|
).thenReturn(false);
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||||
|
).thenReturn(false);
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.signCmd.name),
|
||||||
|
).thenReturn(false);
|
||||||
|
|
||||||
when(aotTools.isLinkDebugInfoSupported).thenAnswer((_) async => true);
|
when(aotTools.isLinkDebugInfoSupported).thenAnswer((_) async => true);
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,31 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
import 'package:scoped_deps/scoped_deps.dart';
|
import 'package:scoped_deps/scoped_deps.dart';
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
|
import 'package:shorebird_cli/src/code_signer.dart';
|
||||||
import 'package:shorebird_cli/src/commands/commands.dart';
|
import 'package:shorebird_cli/src/commands/commands.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||||
import 'package:shorebird_cli/src/deployment_track.dart';
|
import 'package:shorebird_cli/src/deployment_track.dart';
|
||||||
|
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||||
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
import 'package:shorebird_cli/src/patch_diff_checker.dart';
|
||||||
import 'package:shorebird_cli/src/platform/platform.dart';
|
import 'package:shorebird_cli/src/platform/platform.dart';
|
||||||
import 'package:shorebird_cli/src/release_type.dart';
|
import 'package:shorebird_cli/src/release_type.dart';
|
||||||
|
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||||
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
import 'package:shorebird_code_push_client/shorebird_code_push_client.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../../mocks.dart';
|
import '../../mocks.dart';
|
||||||
|
|
||||||
|
class FakeFile extends Fake implements File {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group(Patcher, () {
|
group(Patcher, () {
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
registerFallbackValue(ReleasePlatform.android);
|
registerFallbackValue(ReleasePlatform.android);
|
||||||
registerFallbackValue(DeploymentTrack.stable);
|
registerFallbackValue(DeploymentTrack.stable);
|
||||||
|
registerFallbackValue(FakeFile());
|
||||||
});
|
});
|
||||||
|
|
||||||
group('linkPercentage', () {
|
group('linkPercentage', () {
|
||||||
@@ -236,6 +243,358 @@ void main() {
|
|||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('signHash', () {
|
||||||
|
final cryptoFixturesBasePath = p.join('test', 'fixtures', 'crypto');
|
||||||
|
final privateKeyFile = File(
|
||||||
|
p.join(cryptoFixturesBasePath, 'private.pem'),
|
||||||
|
);
|
||||||
|
|
||||||
|
late ArgParser argParser;
|
||||||
|
late ArgResults argResults;
|
||||||
|
late CodeSigner codeSigner;
|
||||||
|
late ShorebirdLogger logger;
|
||||||
|
late File publicKeyTempFile;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
argParser = ArgParser()
|
||||||
|
..addOption(CommonArguments.publicKeyArg.name)
|
||||||
|
..addOption(CommonArguments.privateKeyArg.name)
|
||||||
|
..addOption(CommonArguments.publicKeyCmd.name)
|
||||||
|
..addOption(CommonArguments.signCmd.name);
|
||||||
|
codeSigner = MockCodeSigner();
|
||||||
|
logger = MockShorebirdLogger();
|
||||||
|
publicKeyTempFile = File(
|
||||||
|
p.join(
|
||||||
|
Directory.systemTemp.createTempSync().path,
|
||||||
|
'public.pem',
|
||||||
|
),
|
||||||
|
)..writeAsStringSync('fake-public-key-pem');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when no signing is configured', () async {
|
||||||
|
argResults = argParser.parse([]);
|
||||||
|
|
||||||
|
final patcher = _TestPatcher(
|
||||||
|
argParser: argParser,
|
||||||
|
argResults: argResults,
|
||||||
|
flavor: null,
|
||||||
|
target: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
final result = await patcher.signHash('test-hash');
|
||||||
|
expect(result, isNull);
|
||||||
|
},
|
||||||
|
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns signature from file-based signing', () async {
|
||||||
|
argResults = argParser.parse([
|
||||||
|
'--${CommonArguments.publicKeyArg.name}=${publicKeyTempFile.path}',
|
||||||
|
'--${CommonArguments.privateKeyArg.name}=${privateKeyFile.path}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.sign(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||||
|
),
|
||||||
|
).thenReturn('file-signature');
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
final patcher = _TestPatcher(
|
||||||
|
argParser: argParser,
|
||||||
|
argResults: argResults,
|
||||||
|
flavor: null,
|
||||||
|
target: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
final result = await patcher.signHash('test-hash');
|
||||||
|
expect(result, equals('file-signature'));
|
||||||
|
},
|
||||||
|
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'throws ProcessExit when signer present but no public key',
|
||||||
|
() async {
|
||||||
|
argResults = argParser.parse([
|
||||||
|
'--${CommonArguments.privateKeyArg.name}=${privateKeyFile.path}',
|
||||||
|
]);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.sign(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||||
|
),
|
||||||
|
).thenReturn('file-signature');
|
||||||
|
|
||||||
|
final patcher = _TestPatcher(
|
||||||
|
argParser: argParser,
|
||||||
|
argResults: argResults,
|
||||||
|
flavor: null,
|
||||||
|
target: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
() => patcher.signHash('test-hash'),
|
||||||
|
throwsA(isA<ProcessExit>()),
|
||||||
|
);
|
||||||
|
verify(
|
||||||
|
() => logger.err(
|
||||||
|
any(that: contains('public key is required')),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
codeSignerRef.overrideWith(() => codeSigner),
|
||||||
|
loggerRef.overrideWith(() => logger),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('returns signature from command-based signing when valid', () async {
|
||||||
|
argResults = argParser.parse([
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||||
|
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||||
|
]);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.signWithCmd(
|
||||||
|
data: any(named: 'data'),
|
||||||
|
command: any(named: 'command'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => 'cmd-signature');
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-public-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
final patcher = _TestPatcher(
|
||||||
|
argParser: argParser,
|
||||||
|
argResults: argResults,
|
||||||
|
flavor: null,
|
||||||
|
target: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
final result = await patcher.signHash('test-hash');
|
||||||
|
expect(result, equals('cmd-signature'));
|
||||||
|
verify(
|
||||||
|
() => codeSigner.signWithCmd(
|
||||||
|
data: 'test-hash',
|
||||||
|
command: 'sign-cmd',
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
verify(() => codeSigner.runPublicKeyCmd('get-key-cmd')).called(1);
|
||||||
|
verify(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: 'test-hash',
|
||||||
|
signature: 'cmd-signature',
|
||||||
|
publicKeyPem: 'pem-public-key',
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws ProcessExit when sign-cmd fails', () async {
|
||||||
|
argResults = argParser.parse([
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||||
|
'--${CommonArguments.signCmd.name}=bad-cmd',
|
||||||
|
]);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.signWithCmd(
|
||||||
|
data: any(named: 'data'),
|
||||||
|
command: any(named: 'command'),
|
||||||
|
),
|
||||||
|
).thenThrow(
|
||||||
|
const ProcessException('bad-cmd', [], 'command not found', 127),
|
||||||
|
);
|
||||||
|
|
||||||
|
final patcher = _TestPatcher(
|
||||||
|
argParser: argParser,
|
||||||
|
argResults: argResults,
|
||||||
|
flavor: null,
|
||||||
|
target: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
() => patcher.signHash('test-hash'),
|
||||||
|
throwsA(isA<ProcessExit>()),
|
||||||
|
);
|
||||||
|
verify(
|
||||||
|
() => logger.err(any(that: contains('--sign-cmd'))),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
codeSignerRef.overrideWith(() => codeSigner),
|
||||||
|
loggerRef.overrideWith(() => logger),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws ProcessExit when public-key-cmd fails', () async {
|
||||||
|
argResults = argParser.parse([
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}=bad-key-cmd',
|
||||||
|
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||||
|
]);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.signWithCmd(
|
||||||
|
data: any(named: 'data'),
|
||||||
|
command: any(named: 'command'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => 'signature');
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenThrow(
|
||||||
|
const ProcessException(
|
||||||
|
'bad-key-cmd',
|
||||||
|
[],
|
||||||
|
'command not found',
|
||||||
|
127,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final patcher = _TestPatcher(
|
||||||
|
argParser: argParser,
|
||||||
|
argResults: argResults,
|
||||||
|
flavor: null,
|
||||||
|
target: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
() => patcher.signHash('test-hash'),
|
||||||
|
throwsA(isA<ProcessExit>()),
|
||||||
|
);
|
||||||
|
verify(
|
||||||
|
() => logger.err(any(that: contains('--public-key-cmd'))),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
codeSignerRef.overrideWith(() => codeSigner),
|
||||||
|
loggerRef.overrideWith(() => logger),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports mixed signing (public key file + sign cmd)', () async {
|
||||||
|
argResults = argParser.parse([
|
||||||
|
'--${CommonArguments.publicKeyArg.name}=${publicKeyTempFile.path}',
|
||||||
|
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||||
|
]);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.signWithCmd(
|
||||||
|
data: any(named: 'data'),
|
||||||
|
command: any(named: 'command'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => 'cmd-signature');
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
final patcher = _TestPatcher(
|
||||||
|
argParser: argParser,
|
||||||
|
argResults: argResults,
|
||||||
|
flavor: null,
|
||||||
|
target: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
final result = await patcher.signHash('test-hash');
|
||||||
|
expect(result, equals('cmd-signature'));
|
||||||
|
verifyNever(() => codeSigner.runPublicKeyCmd(any()));
|
||||||
|
verify(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: 'test-hash',
|
||||||
|
signature: 'cmd-signature',
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws ProcessExit when signature verification fails', () async {
|
||||||
|
argResults = argParser.parse([
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||||
|
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||||
|
]);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.signWithCmd(
|
||||||
|
data: any(named: 'data'),
|
||||||
|
command: any(named: 'command'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => 'bad-signature');
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-public-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(false);
|
||||||
|
|
||||||
|
final patcher = _TestPatcher(
|
||||||
|
argParser: argParser,
|
||||||
|
argResults: argResults,
|
||||||
|
flavor: null,
|
||||||
|
target: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
await expectLater(
|
||||||
|
() => patcher.signHash('test-hash'),
|
||||||
|
throwsA(isA<ProcessExit>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {
|
||||||
|
codeSignerRef.overrideWith(() => codeSigner),
|
||||||
|
loggerRef.overrideWith(() => logger),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -467,6 +467,12 @@ void main() {
|
|||||||
|
|
||||||
group('when signing keys are provided', () {
|
group('when signing keys are provided', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
final publicKeyFile = File(
|
||||||
|
p.join(
|
||||||
|
Directory.systemTemp.createTempSync().path,
|
||||||
|
'public-key.pem',
|
||||||
|
),
|
||||||
|
)..createSync();
|
||||||
when(
|
when(
|
||||||
() => artifactManager.createDiff(
|
() => artifactManager.createDiff(
|
||||||
releaseArtifactPath: any(named: 'releaseArtifactPath'),
|
releaseArtifactPath: any(named: 'releaseArtifactPath'),
|
||||||
@@ -475,7 +481,7 @@ void main() {
|
|||||||
).thenAnswer((_) async => diffFile.path);
|
).thenAnswer((_) async => diffFile.path);
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.publicKeyArg.name],
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
).thenReturn('public-key.pem');
|
).thenReturn(publicKeyFile.path);
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.privateKeyArg.name],
|
() => argResults[CommonArguments.privateKeyArg.name],
|
||||||
).thenReturn('private-key.pem');
|
).thenReturn('private-key.pem');
|
||||||
@@ -485,6 +491,13 @@ void main() {
|
|||||||
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
privateKeyPemFile: any(named: 'privateKeyPemFile'),
|
||||||
),
|
),
|
||||||
).thenReturn('signature');
|
).thenReturn('signature');
|
||||||
|
when(
|
||||||
|
() => codeSigner.verify(
|
||||||
|
message: any(named: 'message'),
|
||||||
|
signature: any(named: 'signature'),
|
||||||
|
publicKeyPem: any(named: 'publicKeyPem'),
|
||||||
|
),
|
||||||
|
).thenReturn(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('signs patch', () async {
|
test('signs patch', () async {
|
||||||
|
|||||||
@@ -326,6 +326,9 @@ void main() {
|
|||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.publicKeyArg.name],
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
).thenReturn(patchSigningPublicKeyFile.path);
|
).thenReturn(patchSigningPublicKeyFile.path);
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => artifactBuilder.buildAar(
|
() => artifactBuilder.buildAar(
|
||||||
@@ -337,7 +340,7 @@ void main() {
|
|||||||
).thenAnswer((_) async => File(''));
|
).thenAnswer((_) async => File(''));
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => codeSigner.base64PublicKey(any()),
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
).thenReturn(base64PublicKey);
|
).thenReturn(base64PublicKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -357,6 +360,57 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when a public-key-cmd is provided', () {
|
||||||
|
const base64PublicKey = 'base64PublicKeyFromCmd';
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||||
|
).thenReturn('get-key-cmd');
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => artifactBuilder.buildAar(
|
||||||
|
buildNumber: any(named: 'buildNumber'),
|
||||||
|
targetPlatforms: any(named: 'targetPlatforms'),
|
||||||
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: any(named: 'base64PublicKey'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => File(''));
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-public-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
|
).thenReturn(base64PublicKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'runs public key cmd and forwards encoded key to buildAar',
|
||||||
|
() async {
|
||||||
|
await runWithOverrides(() => aarReleaser.buildReleaseArtifacts());
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => artifactBuilder.buildAar(
|
||||||
|
buildNumber: buildNumber,
|
||||||
|
targetPlatforms: any(named: 'targetPlatforms'),
|
||||||
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: base64PublicKey,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -417,6 +417,9 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.publicKeyArg.name],
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
).thenReturn(patchSigningPublicKeyFile.path);
|
).thenReturn(patchSigningPublicKeyFile.path);
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => artifactBuilder.buildAppBundle(
|
() => artifactBuilder.buildAppBundle(
|
||||||
@@ -438,7 +441,7 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
).thenAnswer((_) async => File(''));
|
).thenAnswer((_) async => File(''));
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => codeSigner.base64PublicKey(any()),
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
).thenReturn(base64PublicKey);
|
).thenReturn(base64PublicKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -496,6 +499,61 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when a public-key-cmd is provided', () {
|
||||||
|
const base64PublicKey = 'base64PublicKeyFromCmd';
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||||
|
).thenReturn('get-key-cmd');
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => artifactBuilder.buildAppBundle(
|
||||||
|
flavor: any(named: 'flavor'),
|
||||||
|
target: any(named: 'target'),
|
||||||
|
targetPlatforms: any(named: 'targetPlatforms'),
|
||||||
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: any(named: 'base64PublicKey'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => aabFile);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-public-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
|
).thenReturn(base64PublicKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'runs public key cmd and forwards encoded key to buildAab',
|
||||||
|
() async {
|
||||||
|
await runWithOverrides(
|
||||||
|
() => androidReleaser.buildReleaseArtifacts(),
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => artifactBuilder.buildAppBundle(
|
||||||
|
flavor: any(named: 'flavor'),
|
||||||
|
target: any(named: 'target'),
|
||||||
|
targetPlatforms: any(named: 'targetPlatforms'),
|
||||||
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: base64PublicKey,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('getReleaseVersion', () {
|
group('getReleaseVersion', () {
|
||||||
|
|||||||
@@ -293,6 +293,9 @@ void main() {
|
|||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.publicKeyArg.name],
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
).thenReturn(patchSigningPublicKeyFile.path);
|
).thenReturn(patchSigningPublicKeyFile.path);
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => artifactBuilder.buildIosFramework(
|
() => artifactBuilder.buildIosFramework(
|
||||||
@@ -304,7 +307,7 @@ void main() {
|
|||||||
AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
||||||
);
|
);
|
||||||
when(
|
when(
|
||||||
() => codeSigner.base64PublicKey(any()),
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
).thenReturn(base64PublicKey);
|
).thenReturn(base64PublicKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -326,6 +329,59 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when a public-key-cmd is provided', () {
|
||||||
|
const base64PublicKey = 'base64PublicKeyFromCmd';
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||||
|
).thenReturn('get-key-cmd');
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => artifactBuilder.buildIosFramework(
|
||||||
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: any(named: 'base64PublicKey'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async =>
|
||||||
|
AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-public-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
|
).thenReturn(base64PublicKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'runs public key cmd and forwards encoded key to '
|
||||||
|
'buildIosFramework',
|
||||||
|
() async {
|
||||||
|
await runWithOverrides(
|
||||||
|
() => iosFrameworkReleaser.buildReleaseArtifacts(),
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => artifactBuilder.buildIosFramework(
|
||||||
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: base64PublicKey,
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
group('when stale build/ios/shorebird directory exists', () {
|
group('when stale build/ios/shorebird directory exists', () {
|
||||||
late Directory shorebirdSupplementDir;
|
late Directory shorebirdSupplementDir;
|
||||||
|
|
||||||
|
|||||||
@@ -310,7 +310,7 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
).thenReturn(xcarchiveDirectory);
|
).thenReturn(xcarchiveDirectory);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => codeSigner.base64PublicKey(any()),
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
).thenReturn(base64PublicKey);
|
).thenReturn(base64PublicKey);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
@@ -332,6 +332,9 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.publicKeyArg.name],
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
).thenReturn(patchSigningPublicKeyFile.path);
|
).thenReturn(patchSigningPublicKeyFile.path);
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
when(
|
when(
|
||||||
() => artifactBuilder.buildIpa(
|
() => artifactBuilder.buildIpa(
|
||||||
@@ -365,6 +368,60 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when a public-key-cmd is provided', () {
|
||||||
|
setUp(() {
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||||
|
).thenReturn('get-key-cmd');
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => artifactBuilder.buildIpa(
|
||||||
|
codesign: any(named: 'codesign'),
|
||||||
|
flavor: any(named: 'flavor'),
|
||||||
|
target: any(named: 'target'),
|
||||||
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: any(named: 'base64PublicKey'),
|
||||||
|
),
|
||||||
|
).thenAnswer(
|
||||||
|
(_) async =>
|
||||||
|
AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
||||||
|
);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-public-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
|
).thenReturn('base64PublicKeyFromCmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'runs public key cmd and forwards encoded key to buildIpa',
|
||||||
|
() async {
|
||||||
|
await runWithOverrides(() => iosReleaser.buildReleaseArtifacts());
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => artifactBuilder.buildIpa(
|
||||||
|
codesign: any(named: 'codesign'),
|
||||||
|
flavor: any(named: 'flavor'),
|
||||||
|
target: any(named: 'target'),
|
||||||
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: 'base64PublicKeyFromCmd',
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
group('when not codesigning', () {
|
group('when not codesigning', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
when(() => argResults['codesign']).thenReturn(false);
|
when(() => argResults['codesign']).thenReturn(false);
|
||||||
|
|||||||
@@ -296,14 +296,20 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
|
|
||||||
group('when public key is passed as an arg', () {
|
group('when public key is passed as an arg', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
final publicKeyFile = File(
|
||||||
|
p.join(
|
||||||
|
Directory.systemTemp.createTempSync().path,
|
||||||
|
'public-key.pem',
|
||||||
|
),
|
||||||
|
)..createSync(recursive: true);
|
||||||
when(
|
when(
|
||||||
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
() => argResults.wasParsed(CommonArguments.publicKeyArg.name),
|
||||||
).thenReturn(true);
|
).thenReturn(true);
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.publicKeyArg.name],
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
).thenReturn('public_key');
|
).thenReturn(publicKeyFile.path);
|
||||||
when(
|
when(
|
||||||
() => codeSigner.base64PublicKey(any()),
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
).thenReturn('encoded_public_key');
|
).thenReturn('encoded_public_key');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,6 +324,38 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when a public-key-cmd is provided', () {
|
||||||
|
setUp(() {
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||||
|
).thenReturn('get-key-cmd');
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-public-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
|
).thenReturn('encoded_public_key_from_cmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes public key to buildLinuxApp', () async {
|
||||||
|
await runWithOverrides(releaser.buildReleaseArtifacts);
|
||||||
|
verify(
|
||||||
|
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => artifactBuilder.buildLinuxApp(
|
||||||
|
base64PublicKey: 'encoded_public_key_from_cmd',
|
||||||
|
target: any(named: 'target'),
|
||||||
|
args: any(named: 'args'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('getReleaseVersion', () {
|
group('getReleaseVersion', () {
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import 'package:scoped_deps/scoped_deps.dart';
|
|||||||
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
import 'package:shorebird_cli/src/artifact_builder/artifact_builder.dart';
|
||||||
import 'package:shorebird_cli/src/artifact_manager.dart';
|
import 'package:shorebird_cli/src/artifact_manager.dart';
|
||||||
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
import 'package:shorebird_cli/src/code_push_client_wrapper.dart';
|
||||||
|
import 'package:shorebird_cli/src/code_signer.dart';
|
||||||
import 'package:shorebird_cli/src/commands/release/release.dart';
|
import 'package:shorebird_cli/src/commands/release/release.dart';
|
||||||
|
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||||
import 'package:shorebird_cli/src/config/config.dart';
|
import 'package:shorebird_cli/src/config/config.dart';
|
||||||
import 'package:shorebird_cli/src/doctor.dart';
|
import 'package:shorebird_cli/src/doctor.dart';
|
||||||
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
|
import 'package:shorebird_cli/src/executables/xcodebuild.dart';
|
||||||
@@ -35,6 +37,7 @@ void main() {
|
|||||||
late ArtifactBuilder artifactBuilder;
|
late ArtifactBuilder artifactBuilder;
|
||||||
late ArtifactManager artifactManager;
|
late ArtifactManager artifactManager;
|
||||||
late CodePushClientWrapper codePushClientWrapper;
|
late CodePushClientWrapper codePushClientWrapper;
|
||||||
|
late CodeSigner codeSigner;
|
||||||
late Directory projectRoot;
|
late Directory projectRoot;
|
||||||
late Doctor doctor;
|
late Doctor doctor;
|
||||||
late FlavorValidator flavorValidator;
|
late FlavorValidator flavorValidator;
|
||||||
@@ -54,6 +57,7 @@ void main() {
|
|||||||
artifactBuilderRef.overrideWith(() => artifactBuilder),
|
artifactBuilderRef.overrideWith(() => artifactBuilder),
|
||||||
artifactManagerRef.overrideWith(() => artifactManager),
|
artifactManagerRef.overrideWith(() => artifactManager),
|
||||||
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
|
codePushClientWrapperRef.overrideWith(() => codePushClientWrapper),
|
||||||
|
codeSignerRef.overrideWith(() => codeSigner),
|
||||||
doctorRef.overrideWith(() => doctor),
|
doctorRef.overrideWith(() => doctor),
|
||||||
loggerRef.overrideWith(() => logger),
|
loggerRef.overrideWith(() => logger),
|
||||||
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
|
shorebirdEnvRef.overrideWith(() => shorebirdEnv),
|
||||||
@@ -69,6 +73,7 @@ void main() {
|
|||||||
artifactBuilder = MockArtifactBuilder();
|
artifactBuilder = MockArtifactBuilder();
|
||||||
artifactManager = MockArtifactManager();
|
artifactManager = MockArtifactManager();
|
||||||
codePushClientWrapper = MockCodePushClientWrapper();
|
codePushClientWrapper = MockCodePushClientWrapper();
|
||||||
|
codeSigner = MockCodeSigner();
|
||||||
doctor = MockDoctor();
|
doctor = MockDoctor();
|
||||||
flavorValidator = MockFlavorValidator();
|
flavorValidator = MockFlavorValidator();
|
||||||
projectRoot = Directory.systemTemp.createTempSync();
|
projectRoot = Directory.systemTemp.createTempSync();
|
||||||
@@ -269,6 +274,7 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
flavor: any(named: 'flavor'),
|
flavor: any(named: 'flavor'),
|
||||||
target: any(named: 'target'),
|
target: any(named: 'target'),
|
||||||
args: any(named: 'args'),
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: any(named: 'base64PublicKey'),
|
||||||
),
|
),
|
||||||
).thenAnswer(
|
).thenAnswer(
|
||||||
(_) async => AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
(_) async => AppleBuildResult(kernelFile: File('/path/to/app.dill')),
|
||||||
@@ -356,6 +362,41 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when a public-key-cmd is provided', () {
|
||||||
|
setUp(() {
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||||
|
).thenReturn('get-key-cmd');
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-public-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
|
).thenReturn('base64PublicKeyFromCmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runs public key cmd and forwards encoded key', () async {
|
||||||
|
await runWithOverrides(releaser.buildReleaseArtifacts);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem('pem-public-key'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => artifactBuilder.buildMacos(
|
||||||
|
base64PublicKey: 'base64PublicKeyFromCmd',
|
||||||
|
args: any(named: 'args'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('verifies artifacts exist and returns app path', () async {
|
test('verifies artifacts exist and returns app path', () async {
|
||||||
expect(
|
expect(
|
||||||
await runWithOverrides(releaser.buildReleaseArtifacts),
|
await runWithOverrides(releaser.buildReleaseArtifacts),
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ void main() {
|
|||||||
when(() => argResults['platforms']).thenReturn(['android']);
|
when(() => argResults['platforms']).thenReturn(['android']);
|
||||||
when(() => argResults['flutter-version']).thenReturn('latest');
|
when(() => argResults['flutter-version']).thenReturn('latest');
|
||||||
when(() => argResults.wasParsed(any())).thenReturn(true);
|
when(() => argResults.wasParsed(any())).thenReturn(true);
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||||
|
).thenReturn(false);
|
||||||
|
|
||||||
when(cache.updateAll).thenAnswer((_) async => {});
|
when(cache.updateAll).thenAnswer((_) async => {});
|
||||||
|
|
||||||
@@ -699,7 +702,9 @@ $exception'''),
|
|||||||
verify(
|
verify(
|
||||||
() => logger.warn(
|
() => logger.warn(
|
||||||
'patch_verification is set in shorebird.yaml but '
|
'patch_verification is set in shorebird.yaml but '
|
||||||
'--${CommonArguments.publicKeyArg.name} was not provided.\n'
|
'no public key was provided '
|
||||||
|
'(--${CommonArguments.publicKeyArg.name} '
|
||||||
|
'or --${CommonArguments.publicKeyCmd.name}).\n'
|
||||||
'patch_verification configuration will have no effect.',
|
'patch_verification configuration will have no effect.',
|
||||||
),
|
),
|
||||||
).called(1);
|
).called(1);
|
||||||
|
|||||||
@@ -297,6 +297,12 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
|
|
||||||
group('when public key is passed as an arg', () {
|
group('when public key is passed as an arg', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
final publicKeyFile = File(
|
||||||
|
p.join(
|
||||||
|
Directory.systemTemp.createTempSync().path,
|
||||||
|
'public-key.pem',
|
||||||
|
),
|
||||||
|
)..createSync(recursive: true);
|
||||||
when(
|
when(
|
||||||
() => artifactBuilder.buildWindowsApp(
|
() => artifactBuilder.buildWindowsApp(
|
||||||
target: any(named: 'target'),
|
target: any(named: 'target'),
|
||||||
@@ -309,9 +315,9 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
).thenReturn(true);
|
).thenReturn(true);
|
||||||
when(
|
when(
|
||||||
() => argResults[CommonArguments.publicKeyArg.name],
|
() => argResults[CommonArguments.publicKeyArg.name],
|
||||||
).thenReturn('public_key');
|
).thenReturn(publicKeyFile.path);
|
||||||
when(
|
when(
|
||||||
() => codeSigner.base64PublicKey(any()),
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
).thenReturn('encoded_public_key');
|
).thenReturn('encoded_public_key');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -326,6 +332,45 @@ To change the version of this release, change your app's version in your pubspec
|
|||||||
).called(1);
|
).called(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('when a public-key-cmd is provided', () {
|
||||||
|
setUp(() {
|
||||||
|
when(
|
||||||
|
() => artifactBuilder.buildWindowsApp(
|
||||||
|
target: any(named: 'target'),
|
||||||
|
args: any(named: 'args'),
|
||||||
|
base64PublicKey: any(named: 'base64PublicKey'),
|
||||||
|
),
|
||||||
|
).thenAnswer((_) async => projectRoot);
|
||||||
|
when(
|
||||||
|
() => argResults[CommonArguments.publicKeyCmd.name],
|
||||||
|
).thenReturn('get-key-cmd');
|
||||||
|
when(
|
||||||
|
() => argResults.wasParsed(CommonArguments.publicKeyCmd.name),
|
||||||
|
).thenReturn(true);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-public-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
|
).thenReturn('encoded_public_key_from_cmd');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('passes public key to buildWindowsApp', () async {
|
||||||
|
await runWithOverrides(releaser.buildReleaseArtifacts);
|
||||||
|
verify(
|
||||||
|
() => codeSigner.runPublicKeyCmd('get-key-cmd'),
|
||||||
|
).called(1);
|
||||||
|
verify(
|
||||||
|
() => artifactBuilder.buildWindowsApp(
|
||||||
|
base64PublicKey: 'encoded_public_key_from_cmd',
|
||||||
|
target: any(named: 'target'),
|
||||||
|
args: any(named: 'args'),
|
||||||
|
),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('getReleaseVersion', () {
|
group('getReleaseVersion', () {
|
||||||
|
|||||||
@@ -1,11 +1,29 @@
|
|||||||
// cspell:ignore qwer
|
// cspell:ignore qwer
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:args/args.dart';
|
import 'package:args/args.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
import 'package:scoped_deps/scoped_deps.dart';
|
||||||
|
import 'package:shorebird_cli/src/code_signer.dart';
|
||||||
import 'package:shorebird_cli/src/common_arguments.dart';
|
import 'package:shorebird_cli/src/common_arguments.dart';
|
||||||
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
import 'package:shorebird_cli/src/extensions/arg_results.dart';
|
||||||
|
import 'package:shorebird_cli/src/logging/logging.dart';
|
||||||
import 'package:shorebird_cli/src/release_type.dart';
|
import 'package:shorebird_cli/src/release_type.dart';
|
||||||
|
import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class MockCodeSigner extends Mock implements CodeSigner {}
|
||||||
|
|
||||||
|
class MockShorebirdLogger extends Mock implements ShorebirdLogger {}
|
||||||
|
|
||||||
|
class FakeFile extends Fake implements File {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
setUpAll(() {
|
||||||
|
registerFallbackValue(FakeFile());
|
||||||
|
});
|
||||||
|
|
||||||
group('OptionFinder', () {
|
group('OptionFinder', () {
|
||||||
late ArgParser argParser;
|
late ArgParser argParser;
|
||||||
|
|
||||||
@@ -300,4 +318,225 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('CodeSign', () {
|
||||||
|
final cryptoFixturesBasePath = p.join('test', 'fixtures', 'crypto');
|
||||||
|
final publicKeyFile = File(p.join(cryptoFixturesBasePath, 'public.pem'));
|
||||||
|
|
||||||
|
late ArgParser parser;
|
||||||
|
late ShorebirdLogger logger;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
logger = MockShorebirdLogger();
|
||||||
|
parser = ArgParser()
|
||||||
|
..addOption(CommonArguments.publicKeyArg.name)
|
||||||
|
..addOption(CommonArguments.privateKeyArg.name)
|
||||||
|
..addOption(CommonArguments.publicKeyCmd.name)
|
||||||
|
..addOption(CommonArguments.signCmd.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('assertAbsentOrValidKeyPairOrCommands', () {
|
||||||
|
test('succeeds when no signing arguments provided', () {
|
||||||
|
final args = <String>[];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
expect(result.assertAbsentOrValidKeyPairOrCommands, returnsNormally);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when both public key sources provided', () {
|
||||||
|
final args = [
|
||||||
|
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||||
|
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||||
|
];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
|
||||||
|
runScoped(
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
result.assertAbsentOrValidKeyPairOrCommands,
|
||||||
|
throwsA(isA<ProcessExit>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {loggerRef.overrideWith(() => logger)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when both signing methods provided', () {
|
||||||
|
final privateKeyFile = File(
|
||||||
|
p.join(cryptoFixturesBasePath, 'private.pem'),
|
||||||
|
);
|
||||||
|
final args = [
|
||||||
|
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||||
|
'--${CommonArguments.privateKeyArg.name}=${privateKeyFile.path}',
|
||||||
|
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||||
|
];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
|
||||||
|
runScoped(
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
result.assertAbsentOrValidKeyPairOrCommands,
|
||||||
|
throwsA(isA<ProcessExit>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {loggerRef.overrideWith(() => logger)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when sign-cmd provided without any public key', () {
|
||||||
|
final args = ['--${CommonArguments.signCmd.name}=sign-cmd'];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
|
||||||
|
runScoped(
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
result.assertAbsentOrValidKeyPairOrCommands,
|
||||||
|
throwsA(isA<ProcessExit>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {loggerRef.overrideWith(() => logger)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('succeeds when both cmd arguments provided', () {
|
||||||
|
final args = [
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||||
|
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||||
|
];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
expect(result.assertAbsentOrValidKeyPairOrCommands, returnsNormally);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('succeeds with public-key-path + sign-cmd (mixed)', () {
|
||||||
|
final args = [
|
||||||
|
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||||
|
'--${CommonArguments.signCmd.name}=sign-cmd',
|
||||||
|
];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
expect(result.assertAbsentOrValidKeyPairOrCommands, returnsNormally);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('succeeds when both file arguments provided with valid files', () {
|
||||||
|
final privateKeyFile = File(
|
||||||
|
p.join(cryptoFixturesBasePath, 'private.pem'),
|
||||||
|
);
|
||||||
|
final args = [
|
||||||
|
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||||
|
'--${CommonArguments.privateKeyArg.name}=${privateKeyFile.path}',
|
||||||
|
];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
expect(result.assertAbsentOrValidKeyPairOrCommands, returnsNormally);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('assertAbsentOrValidPublicKeyOrCmd', () {
|
||||||
|
test('succeeds when no public key arguments provided', () {
|
||||||
|
final args = <String>[];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
expect(result.assertAbsentOrValidPublicKeyOrCmd, returnsNormally);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('succeeds when only public-key-path provided', () {
|
||||||
|
final args = [
|
||||||
|
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||||
|
];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
expect(result.assertAbsentOrValidPublicKeyOrCmd, returnsNormally);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('succeeds when only public-key-cmd provided', () {
|
||||||
|
final args = ['--${CommonArguments.publicKeyCmd.name}=get-key-cmd'];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
expect(result.assertAbsentOrValidPublicKeyOrCmd, returnsNormally);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws when both public-key-path and public-key-cmd provided', () {
|
||||||
|
final args = [
|
||||||
|
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||||
|
'--${CommonArguments.publicKeyCmd.name}=get-key-cmd',
|
||||||
|
];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
|
||||||
|
runScoped(
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
result.assertAbsentOrValidPublicKeyOrCmd,
|
||||||
|
throwsA(isA<ProcessExit>()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
values: {loggerRef.overrideWith(() => logger)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('getEncodedPublicKey', () {
|
||||||
|
late CodeSigner codeSigner;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
codeSigner = MockCodeSigner();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null when no public key configured', () async {
|
||||||
|
final args = <String>[];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
final encoded = await result.getEncodedPublicKey();
|
||||||
|
expect(encoded, isNull);
|
||||||
|
},
|
||||||
|
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'returns encoded key from file when public-key-path provided',
|
||||||
|
() async {
|
||||||
|
final args = [
|
||||||
|
'--${CommonArguments.publicKeyArg.name}=${publicKeyFile.path}',
|
||||||
|
];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
|
).thenReturn('encoded-key');
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
final encoded = await result.getEncodedPublicKey();
|
||||||
|
expect(encoded, equals('encoded-key'));
|
||||||
|
},
|
||||||
|
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'returns encoded key from cmd when public-key-cmd provided',
|
||||||
|
() async {
|
||||||
|
final args = ['--${CommonArguments.publicKeyCmd.name}=get-key-cmd'];
|
||||||
|
final result = parser.parse(args);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => codeSigner.runPublicKeyCmd(any()),
|
||||||
|
).thenAnswer((_) async => 'pem-key');
|
||||||
|
when(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem(any()),
|
||||||
|
).thenReturn('encoded-key');
|
||||||
|
|
||||||
|
await runScoped(
|
||||||
|
() async {
|
||||||
|
final encoded = await result.getEncodedPublicKey();
|
||||||
|
expect(encoded, equals('encoded-key'));
|
||||||
|
verify(() => codeSigner.runPublicKeyCmd('get-key-cmd')).called(1);
|
||||||
|
verify(
|
||||||
|
() => codeSigner.base64PublicKeyFromPem('pem-key'),
|
||||||
|
).called(1);
|
||||||
|
},
|
||||||
|
values: {codeSignerRef.overrideWith(() => codeSigner)},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user