diff --git a/packages/artifact_proxy/pubspec.lock b/packages/artifact_proxy/pubspec.lock index 0e883c387..efcae040a 100644 --- a/packages/artifact_proxy/pubspec.lock +++ b/packages/artifact_proxy/pubspec.lock @@ -578,4 +578,4 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0-256.0.dev <4.0.0" + dart: ">=3.4.0 <4.0.0" diff --git a/packages/discord_gcp_alerts/pubspec.lock b/packages/discord_gcp_alerts/pubspec.lock index 679da24b9..a5a5daa19 100644 --- a/packages/discord_gcp_alerts/pubspec.lock +++ b/packages/discord_gcp_alerts/pubspec.lock @@ -570,4 +570,4 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0-256.0.dev <4.0.0" + dart: ">=3.4.0 <4.0.0" diff --git a/packages/shorebird_cli/bin/shorebird.dart b/packages/shorebird_cli/bin/shorebird.dart index 01870ebbc..cf895fe49 100644 --- a/packages/shorebird_cli/bin/shorebird.dart +++ b/packages/shorebird_cli/bin/shorebird.dart @@ -8,6 +8,7 @@ import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/cache.dart'; import 'package:shorebird_cli/src/code_push_client_wrapper.dart'; +import 'package:shorebird_cli/src/code_signer.dart'; import 'package:shorebird_cli/src/command_runner.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/engine_config.dart'; @@ -42,6 +43,7 @@ Future main(List args) async { bundletoolRef, cacheRef, codePushClientWrapperRef, + codeSignerRef, devicectlRef, doctorRef, engineConfigRef, diff --git a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart index 14b68198d..9c392d63b 100644 --- a/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart +++ b/packages/shorebird_cli/lib/src/code_push_client_wrapper.dart @@ -30,6 +30,7 @@ class PatchArtifactBundle { required this.path, required this.hash, required this.size, + this.hashSignature, }); /// The corresponding architecture. @@ -43,6 +44,9 @@ class PatchArtifactBundle { /// The size in bytes of the artifact. final int size; + + /// The signature of the artifact hash. + final String? hashSignature; } // A reference to a [CodePushClientWrapper] instance. @@ -687,6 +691,7 @@ aar artifact already exists, continuing...''', arch: artifact.arch, platform: platform, hash: artifact.hash, + hashSignature: artifact.hashSignature, ); } catch (error) { _handleErrorAndExit(error, progress: createArtifactProgress); diff --git a/packages/shorebird_cli/lib/src/code_signer.dart b/packages/shorebird_cli/lib/src/code_signer.dart new file mode 100644 index 000000000..0a3175230 --- /dev/null +++ b/packages/shorebird_cli/lib/src/code_signer.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:pem/pem.dart'; +import 'package:pointycastle/pointycastle.dart'; +import 'package:scoped_deps/scoped_deps.dart'; + +/// A reference to a [CodeSigner] instance. +final codeSignerRef = create(CodeSigner.new); + +/// The [CodeSigner] instance available in the current zone. +CodeSigner get codeSigner => read(codeSignerRef); + +/// {@template code_signer} +/// Manages code signing operations. +/// {@endtemplate} +class CodeSigner { + /// Signs a [message] using the provided [privateKeyPemFile] using + /// SHA-256/RSA. + /// + /// This is the equivalent of: + /// $ openssl dgst -sha256 -sign privateKey.pem -out signature message + String sign({required String message, required File privateKeyPemFile}) { + final privateKeyData = _privateKeyBytes(pemFile: privateKeyPemFile); + final privateKey = RSAPrivateKeyFromInt.from(privateKeyData); + + final signer = Signer('SHA-256/RSA') + ..init(true, PrivateKeyParameter(privateKey)); + + final signature = + signer.generateSignature(utf8.encode(message)) as RSASignature; + return base64.encode(signature.bytes); + } + + /// Decodes a PEM file containing a private key and returns its contents as + /// bytes. + List _privateKeyBytes({required File pemFile}) { + final privateKeyString = pemFile.readAsStringSync(); + final pemCodec = PemCodec(PemLabel.privateKey); + return pemCodec.decode(privateKeyString); + } +} + +extension RSAPrivateKeyFromInt on RSAPrivateKey { + /// Converts an RSA private key bytes to a pointycastle [RSAPrivateKey]. + /// + /// Based on https://github.com/konstantinullrich/crypton/blob/trunk/lib/src/rsa/private_key.dart + static RSAPrivateKey from(List privateKeyBytes) { + var asn1Parser = ASN1Parser(Uint8List.fromList(privateKeyBytes)); + final topLevelSeq = asn1Parser.nextObject() as ASN1Sequence; + final privateKey = topLevelSeq.elements![2]; + + asn1Parser = ASN1Parser(privateKey.valueBytes); + final pkSeq = asn1Parser.nextObject() as ASN1Sequence; + + final modulus = pkSeq.elements![1] as ASN1Integer; + final privateExponent = pkSeq.elements![3] as ASN1Integer; + final p = pkSeq.elements![4] as ASN1Integer; + final q = pkSeq.elements![5] as ASN1Integer; + + return RSAPrivateKey( + modulus.integer!, + privateExponent.integer!, + p.integer, + q.integer, + ); + } +} diff --git a/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart b/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart index 634e779d3..a2d76dcc8 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart @@ -6,8 +6,11 @@ import 'package:shorebird_cli/src/archive_analysis/archive_differ.dart'; import 'package:shorebird_cli/src/artifact_builder.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_signer.dart'; import 'package:shorebird_cli/src/commands/patch/patcher.dart'; import 'package:shorebird_cli/src/doctor.dart'; +import 'package:shorebird_cli/src/extensions/arg_results.dart'; +import 'package:shorebird_cli/src/extensions/file.dart'; import 'package:shorebird_cli/src/logger.dart'; import 'package:shorebird_cli/src/patch_diff_checker.dart'; import 'package:shorebird_cli/src/platform.dart'; @@ -31,6 +34,11 @@ class AndroidPatcher extends Patcher { required super.target, }); + @override + Future assertArgsAreValid() async { + argResults.file('private-key-path')?.assertExists(); + } + @override ReleaseType get releaseType => ReleaseType.android; @@ -148,6 +156,15 @@ Looked in: logger.detail('Creating artifact for $patchArtifactPath'); final patchArtifact = File(patchArtifactPath); final hash = sha256.convert(await patchArtifact.readAsBytes()).toString(); + + final privateKeyFile = argResults.file('private-key-path'); + final hashSignature = privateKeyFile != null + ? codeSigner.sign( + message: hash, + privateKeyPemFile: privateKeyFile, + ) + : null; + try { final diffPath = await artifactManager.createDiff( releaseArtifactPath: releaseArtifactPath.value, @@ -158,6 +175,7 @@ Looked in: path: diffPath, hash: hash, size: await File(diffPath).length(), + hashSignature: hashSignature, ); } catch (error) { createDiffProgress.fail('$error'); diff --git a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart index 24b51940a..e274a9394 100644 --- a/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart +++ b/packages/shorebird_cli/lib/src/commands/patch/patch_command.dart @@ -92,6 +92,13 @@ of the iOS app that is using this module.''', abbr: 'n', negatable: false, help: 'Validate but do not upload the patch.', + ) + ..addOption( + 'private-key-path', + hide: true, + help: ''' +The path for a private key file that will be used to sign the patch artifact. +''', ); } diff --git a/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart b/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart index ed15a5982..23fbcdf2b 100644 --- a/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart +++ b/packages/shorebird_cli/lib/src/commands/release/android_releaser.dart @@ -96,15 +96,11 @@ Please comment and upvote ${link(uri: Uri.parse('https://github.com/shorebirdtec final File aab; - final publicKeyPath = argResults['public-key-path'] as String?; + final publicKeyFile = argResults.file('public-key-path'); + final base64PublicKey = publicKeyFile != null + ? base64Encode(publicKeyFile.readAsBytesSync()) + : null; - String? base64PublicKey; - if (publicKeyPath != null) { - final publicKeyFile = File(publicKeyPath); - final rawPublicKey = publicKeyFile.readAsBytesSync(); - - base64PublicKey = base64Encode(rawPublicKey); - } try { aab = await artifactBuilder.buildAppBundle( flavor: flavor, diff --git a/packages/shorebird_cli/pubspec.lock b/packages/shorebird_cli/pubspec.lock index d44624890..0e9dfe47b 100644 --- a/packages/shorebird_cli/pubspec.lock +++ b/packages/shorebird_cli/pubspec.lock @@ -26,10 +26,10 @@ packages: dependency: "direct main" description: name: archive - sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 + sha256: "6bd38d335f0954f5fad9c79e614604fbf03a0e5b975923dd001b6ea965ef5b4b" url: "https://pub.dev" source: hosted - version: "3.5.1" + version: "3.6.0" args: dependency: "direct main" description: @@ -485,6 +485,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + pem: + dependency: "direct main" + description: + name: pem + sha256: "3dfb24524f805ad694ba3cdbb6387ab31ab661fdb8ea873052ed88487fcfef86" + url: "https://pub.dev" + source: hosted + version: "2.0.5" petitparser: dependency: transitive description: @@ -502,7 +510,7 @@ packages: source: hosted version: "3.1.4" pointycastle: - dependency: transitive + dependency: "direct main" description: name: pointycastle sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" @@ -561,10 +569,10 @@ packages: dependency: "direct main" description: name: scoped_deps - sha256: "7416a9026f3b457dda634a8e83c8e47d7ece33bef360ffba114c2d89255166ae" + sha256: bc54cece4fed785157dc53b7554d31107f574897f4b2d1196db905a38c084e31 url: "https://pub.dev" source: hosted - version: "0.1.0+1" + version: "0.1.0+2" shelf: dependency: transitive description: @@ -695,10 +703,10 @@ packages: dependency: transitive description: name: string_validator - sha256: "54d4f42cd6878ae72793a58a529d9a18ebfdfbfebd9793bbe55c9b28935e8543" + sha256: a278d038104aa2df15d0e09c47cb39a49f907260732067d0034dc2f2e4e2ac94 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" term_glyph: dependency: transitive description: @@ -799,10 +807,10 @@ packages: dependency: transitive description: name: web_socket - sha256: bfe704c186c6e32a46f6607f94d079cd0b747b9a489fceeecc93cd3adb98edd5 + sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" web_socket_channel: dependency: transitive description: diff --git a/packages/shorebird_cli/pubspec.yaml b/packages/shorebird_cli/pubspec.yaml index 5a2c7b69b..67b7195ec 100644 --- a/packages/shorebird_cli/pubspec.yaml +++ b/packages/shorebird_cli/pubspec.yaml @@ -28,7 +28,9 @@ dependencies: mason_logger: ^0.2.15 meta: ^1.14.0 path: ^1.9.0 + pem: ^2.0.5 platform: ^3.1.4 + pointycastle: ^3.9.1 propertylistserialization: ^1.3.0 pub_semver: ^2.1.4 pubspec_parse: ^1.2.3 diff --git a/packages/shorebird_cli/test/fixtures/crypto/private.pem b/packages/shorebird_cli/test/fixtures/crypto/private.pem new file mode 100644 index 000000000..7d98bf5c4 --- /dev/null +++ b/packages/shorebird_cli/test/fixtures/crypto/private.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDbB2kQZu6+U+xv +2LSpit8x58mcTDUEdOLxJhlMqtc68laYSk8TWFZ9uS9jNe7lr3qBXXKhwXcMzCfT +hWZGUqELgCGwPQ0vRQ2FiGi1sob3UrCLW8BekeEJ3PmBAQHDQrW4HgnP7MrpYr7f +U+vJinAvBvJc2pehjwgABRDhJOwdhXnD4ExKLyl6lYxF3sNH1Edxs05mUm90FDk3 +G8Hgk3h1EyrxwLvd7PU/13sN/C/dNZj6F70Sa5ctPZSK9lKUcisYFrswV+rJR7au +jQXtN78HSyLXaK0FtYirJy+pxeN4482fpYSmo3shaNv0tSHXrYnJhrPktv9V54lf +wsq0SVxzAgMBAAECggEAI+rwre8Qn/yM4tXji9lepZqa7hB0h0ZlwDbyYiddQ+jh +Y3hCxGuG73dn+nnUiIJAxtMct8L382IioSYYnK1IhJAetih6L4dq4rwjs7F9lDRR +XYu5HakILCu6bLnOC10MBg/tpapxUgQj7zqeCX8JdjofkjOJ4cVeAfwoBaEePpl1 +ruqmn7AZZHA63gw1C+pkhFKEihPmViFVc5W8fyUO/bfC7bSni+sUCXG0vDyyJ4pG +oZODe3JMDsMUyl/FLLCJyu78hpPa8OyNjs3rdYdDpNuiMlY0YvQ4eN2x5WcKNtKe +46Vf8r2NH3eY0JRrL7QYKEzTdPZaJUIcmydH/rEW4QKBgQDw2i8L6BKREygKnEWJ ++KLMZxAKOdlp4glGX3j17cx5PQKmjy6Hi8TRBMrn1tePeWTcscVl8V/BAEorCK3H +JXZQxb0+StT3UPsaSYroFyz5ZBS77MsCBIQnrfKRvyfRDQ2Dp05Tc7l2ioaxSujx +X+rYUTzkT22PkBElkTIZDAs34QKBgQDozdzGKVjEbUfY7N56nA3/fyxnFX7gKf/4 +MSDGpa+RByjXj+YnHGI1WE96VB44sd84146jzq9i4FZyjcFeHtbrgzrfNYO9mxCl +28rX5rDp/gQ9QW5bDmQFvl4lKVVs59lgZf6la0TS63fEUcSzEDQMkaMtomqWfGnV +F/Q9OXwO0wKBgGuRpq10qsYsfhevD8e9SkhsR1ep2pZVo7rQbR+5YzdKrmJhVHCp +Ve/cahr9cyzbFNcUdos/MHrsfDOYHrTw4FTW29x0Y4VJn7xv2CAsKaQAtNnxugFe +rv9hyxKZA1l0sPJ5yJuw9cYhvGJ2iG81XZfbQIzfhJk3yNC0dmGFZYVBAoGBAInV +qMsim83grdM/mxGY56jIEPAPiAkMlOLLo445dtM1G/dU2X16jqLq4FObDjGfDnzH +E0rlCm5OSKCWUVB6jeDu16JkOtW9w4OPuG9PxJslrDjgTohW4t2Lso3qBQvv0YID +oVsrQZpnk4eGqiEijM6MQ8K3EMh8bOSfxBmjuVHFAoGBAMF57jTOZwJ/66MNqYJi +wZdX9VxTLbX4YGBSeWi5X+qT5AVsKUlR6yxCzLBxJMDXn472RkNurscdakvI3nnt +h8PU5ur9uFKEcYsO9HPtvkuSZsQ8VEN3fMr8XdaTtH30eF6xKqNQdCMzqnWmWjM0 +UT/B/sUKZUuxZ3Y2NH59Ba+J +-----END PRIVATE KEY----- diff --git a/packages/shorebird_cli/test/fixtures/crypto/public.pem b/packages/shorebird_cli/test/fixtures/crypto/public.pem new file mode 100644 index 000000000..9b06c72e9 --- /dev/null +++ b/packages/shorebird_cli/test/fixtures/crypto/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2wdpEGbuvlPsb9i0qYrf +MefJnEw1BHTi8SYZTKrXOvJWmEpPE1hWfbkvYzXu5a96gV1yocF3DMwn04VmRlKh +C4AhsD0NL0UNhYhotbKG91Kwi1vAXpHhCdz5gQEBw0K1uB4Jz+zK6WK+31PryYpw +LwbyXNqXoY8IAAUQ4STsHYV5w+BMSi8pepWMRd7DR9RHcbNOZlJvdBQ5NxvB4JN4 +dRMq8cC73ez1P9d7Dfwv3TWY+he9EmuXLT2UivZSlHIrGBa7MFfqyUe2ro0F7Te/ +B0si12itBbWIqycvqcXjeOPNn6WEpqN7IWjb9LUh162JyYaz5Lb/VeeJX8LKtElc +cwIDAQAB +-----END PUBLIC KEY----- diff --git a/packages/shorebird_cli/test/src/code_signer_test.dart b/packages/shorebird_cli/test/src/code_signer_test.dart new file mode 100644 index 000000000..f80736acf --- /dev/null +++ b/packages/shorebird_cli/test/src/code_signer_test.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +import 'package:path/path.dart' as p; +import 'package:shorebird_cli/src/code_signer.dart'; +import 'package:shorebird_cli/src/third_party/flutter_tools/lib/flutter_tools.dart'; +import 'package:test/test.dart'; + +void main() { + group( + CodeSigner, + () { + final cryptoFixturesBasePath = p.join('test', 'fixtures', 'crypto'); + final privateKeyFile = File( + p.join(cryptoFixturesBasePath, 'private.pem'), + ); + + late CodeSigner codeSigner; + + setUp(() { + codeSigner = CodeSigner(); + }); + + group('sign', () { + const message = + '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b'; + + test('signature matches openssl output', () async { + final outputDir = Directory.systemTemp.createTempSync(); + final messageFile = File(p.join(outputDir.path, 'message')) + ..writeAsStringSync(message); + final signatureFile = File(p.join(outputDir.path, 'signature')); + await Process.run('openssl', [ + 'dgst', + '-sha256', + '-sign', + privateKeyFile.path, + '-out', + signatureFile.path, + messageFile.path, + ]); + + final expectedSignature = + base64Encode(signatureFile.readAsBytesSync()); + final actualSignature = codeSigner.sign( + message: message, + privateKeyPemFile: privateKeyFile, + ); + expect(actualSignature, equals(expectedSignature)); + }); + }); + }, + onPlatform: { + 'windows': const Skip('Does not have openssl installed by default'), + }, + ); +} diff --git a/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart index a6fe6a241..384af69bc 100644 --- a/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/android_patcher_test.dart @@ -1,4 +1,5 @@ import 'package:args/args.dart'; +import 'package:crypto/crypto.dart'; import 'package:mason_logger/mason_logger.dart'; import 'package:mocktail/mocktail.dart'; import 'package:path/path.dart' as p; @@ -8,6 +9,7 @@ import 'package:shorebird_cli/src/archive_analysis/archive_analysis.dart'; import 'package:shorebird_cli/src/artifact_builder.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_signer.dart'; import 'package:shorebird_cli/src/commands/patch/patch.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/engine_config.dart'; @@ -35,6 +37,7 @@ void main() { late ArtifactBuilder artifactBuilder; late ArtifactManager artifactManager; late CodePushClientWrapper codePushClientWrapper; + late CodeSigner codeSigner; late Doctor doctor; late Platform platform; late Directory projectRoot; @@ -49,9 +52,12 @@ void main() { late AndroidPatcher patcher; - void setUpProjectRootArtifacts({String? flavor}) { - for (final archMetadata in Arch.values) { - final artifactPath = p.join( + File patchArtifactForArch( + Arch arch, { + String? flavor, + }) { + return File( + p.join( projectRoot.path, 'build', 'app', @@ -60,10 +66,17 @@ void main() { flavor != null ? '${flavor}Release' : 'release', 'out', 'lib', - archMetadata.androidBuildPath, + arch.androidBuildPath, 'libapp.so', - ); - File(artifactPath).createSync(recursive: true); + ), + ); + } + + void setUpProjectRootArtifacts({String? flavor}) { + for (final arch in Arch.values) { + patchArtifactForArch(arch, flavor: flavor) + ..createSync(recursive: true) + ..writeAsStringSync(arch.arch); } } @@ -74,6 +87,7 @@ void main() { artifactBuilderRef.overrideWith(() => artifactBuilder), artifactManagerRef.overrideWith(() => artifactManager), codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), + codeSignerRef.overrideWith(() => codeSigner), doctorRef.overrideWith(() => doctor), engineConfigRef.overrideWith(() => const EngineConfig.empty()), loggerRef.overrideWith(() => logger), @@ -90,6 +104,7 @@ void main() { setUpAll(() { registerFallbackValue(Directory('')); + registerFallbackValue(File('')); registerFallbackValue(ReleasePlatform.android); registerFallbackValue(Uri.parse('https://example.com')); setExitFunctionForTests(); @@ -102,6 +117,7 @@ void main() { artifactBuilder = MockArtifactBuilder(); artifactManager = MockArtifactManager(); codePushClientWrapper = MockCodePushClientWrapper(); + codeSigner = MockCodeSigner(); doctor = MockDoctor(); platform = MockPlatform(); progress = MockProgress(); @@ -436,6 +452,56 @@ Looked in: ); expect(result, hasLength(Arch.values.length)); + for (final bundle in result.values) { + expect(bundle.hashSignature, isNull); + } + }); + + group('when a private key is provided', () { + setUp(() { + final privateKey = File( + p.join( + Directory.systemTemp.createTempSync().path, + 'test-private.pem', + ), + )..createSync(); + + when(() => argResults['private-key-path']) + .thenReturn(privateKey.path); + + when( + () => codeSigner.sign( + message: any(named: 'message'), + privateKeyPemFile: any(named: 'privateKeyPemFile'), + ), + ).thenAnswer((invocation) { + final message = invocation.namedArguments[#message] as String; + return '$message-signature'; + }); + }); + + test('returns patch artifact bundles with proper hash signatures', + () async { + final result = await runWithOverrides( + () => patcher.createPatchArtifacts( + appId: 'appId', + releaseId: 0, + releaseArtifact: File('release.aab'), + ), + ); + + // Hash the patch artifacts and append '-signature' to get the + // expected signatures, per the mock of [codeSigner.sign] above. + final expectedSignatures = Arch.values + .map(patchArtifactForArch) + .map((f) => sha256.convert(f.readAsBytesSync()).toString()) + .map((hash) => '$hash-signature') + .toList(); + + final signatures = + result.values.map((bundle) => bundle.hashSignature).toList(); + expect(signatures, equals(expectedSignatures)); + }); }); }); }); diff --git a/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart b/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart index 7d4448894..4d1ea9376 100644 --- a/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart +++ b/packages/shorebird_cli/test/src/commands/patch/ios_patcher_test.dart @@ -138,6 +138,12 @@ void main() { ); }); + group('assertArgsAreValid', () { + test('has no specific validations', () { + expect(patcher.assertArgsAreValid, returnsNormally); + }); + }); + group('archiveDiffer', () { test('is an IosArchiveDiffer', () { expect(patcher.archiveDiffer, isA()); diff --git a/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart b/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart index badee3b10..0140f9195 100644 --- a/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart +++ b/packages/shorebird_cli/test/src/commands/release/android_releaser_test.dart @@ -8,6 +8,7 @@ import 'package:platform/platform.dart'; import 'package:scoped_deps/scoped_deps.dart'; import 'package:shorebird_cli/src/artifact_builder.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/android_releaser.dart'; import 'package:shorebird_cli/src/doctor.dart'; import 'package:shorebird_cli/src/engine_config.dart'; @@ -35,6 +36,7 @@ void main() { late ArgResults argResults; late ArtifactBuilder artifactBuilder; late CodePushClientWrapper codePushClientWrapper; + late CodeSigner codeSigner; late Doctor doctor; late Platform platform; late Directory projectRoot; @@ -55,6 +57,7 @@ void main() { values: { artifactBuilderRef.overrideWith(() => artifactBuilder), codePushClientWrapperRef.overrideWith(() => codePushClientWrapper), + codeSignerRef.overrideWith(() => codeSigner), doctorRef.overrideWith(() => doctor), engineConfigRef.overrideWith(() => const EngineConfig.empty()), loggerRef.overrideWith(() => logger), @@ -72,6 +75,7 @@ void main() { setUpAll(() { registerFallbackValue(Directory('')); + registerFallbackValue(File('')); registerFallbackValue(ReleasePlatform.android); setExitFunctionForTests(); }); @@ -82,6 +86,7 @@ void main() { argResults = MockArgResults(); artifactBuilder = MockArtifactBuilder(); codePushClientWrapper = MockCodePushClientWrapper(); + codeSigner = MockCodeSigner(); doctor = MockDoctor(); operatingSystemInterface = MockOperatingSystemInterface(); platform = MockPlatform(); diff --git a/packages/shorebird_cli/test/src/mocks.dart b/packages/shorebird_cli/test/src/mocks.dart index c602b167b..db2f7e431 100644 --- a/packages/shorebird_cli/test/src/mocks.dart +++ b/packages/shorebird_cli/test/src/mocks.dart @@ -16,6 +16,7 @@ import 'package:shorebird_cli/src/artifact_manager.dart'; import 'package:shorebird_cli/src/auth/auth.dart'; import 'package:shorebird_cli/src/cache.dart' show Cache; 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/release/releaser.dart'; import 'package:shorebird_cli/src/config/config.dart'; @@ -72,6 +73,8 @@ class MockCodePushClient extends Mock implements CodePushClient {} class MockCodePushClientWrapper extends Mock implements CodePushClientWrapper {} +class MockCodeSigner extends Mock implements CodeSigner {} + class MockDevicectl extends Mock implements Devicectl {} class MockDirectory extends Mock implements Directory {} diff --git a/packages/shorebird_code_push_client/lib/src/code_push_client.dart b/packages/shorebird_code_push_client/lib/src/code_push_client.dart index 7b4de7dae..00459c0ad 100644 --- a/packages/shorebird_code_push_client/lib/src/code_push_client.dart +++ b/packages/shorebird_code_push_client/lib/src/code_push_client.dart @@ -133,6 +133,7 @@ class CodePushClient { required String arch, required ReleasePlatform platform, required String hash, + String? hashSignature, }) async { final request = http.MultipartRequest( 'POST', @@ -144,6 +145,7 @@ class CodePushClient { 'platform': platform.name, 'hash': hash, 'size': '${file.length}', + if (hashSignature != null) 'hash_signature': hashSignature, }); final response = await _httpClient.send(request); final body = await response.stream.bytesToString(); diff --git a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart index e19a20f38..df1ea29be 100644 --- a/packages/shorebird_code_push_client/test/src/code_push_client_test.dart +++ b/packages/shorebird_code_push_client/test/src/code_push_client_test.dart @@ -185,13 +185,63 @@ void main() { final request = verify(() => httpClient.send(captureAny())) .captured - .single as http.BaseRequest; + .single as http.MultipartRequest; expect(request.method, equals('POST')); expect( request.url, equals(v1('apps/$appId/patches/$patchId/artifacts')), ); expect(request.hasHeaders(expectedHeaders), isTrue); + expect( + request.fields, + equals({ + 'arch': arch, + 'platform': platform.name, + 'hash': hash, + 'size': '${fixture.readAsBytesSync().lengthInBytes}', + }), + ); + }); + + group('when a hash signature is provided', () { + const hashSignature = 'hash_signature'; + test('makes the correct request', () async { + final tempDir = Directory.systemTemp.createTempSync(); + final fixture = File(path.join(tempDir.path, 'release.txt')) + ..createSync(); + + try { + await codePushClient.createPatchArtifact( + appId: appId, + artifactPath: fixture.path, + patchId: patchId, + arch: arch, + platform: platform, + hash: hash, + hashSignature: hashSignature, + ); + } catch (_) {} + + final request = verify(() => httpClient.send(captureAny())) + .captured + .single as http.MultipartRequest; + expect(request.method, equals('POST')); + expect( + request.url, + equals(v1('apps/$appId/patches/$patchId/artifacts')), + ); + expect(request.hasHeaders(expectedHeaders), isTrue); + expect( + request.fields, + equals({ + 'arch': arch, + 'platform': platform.name, + 'hash': hash, + 'size': '${fixture.readAsBytesSync().lengthInBytes}', + 'hash_signature': hashSignature, + }), + ); + }); }); test('throws an exception if the http request fails (unknown)', () async {