Skip to content

Commit

Permalink
feat: adding private key argument to patch command (#2133)
Browse files Browse the repository at this point in the history
Co-authored-by: Bryan Oltman <[email protected]>
  • Loading branch information
erickzanardo and bryanoltman authored May 23, 2024
1 parent 83e0908 commit b93f5fd
Show file tree
Hide file tree
Showing 19 changed files with 358 additions and 26 deletions.
2 changes: 1 addition & 1 deletion packages/artifact_proxy/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 1 addition & 1 deletion packages/discord_gcp_alerts/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
2 changes: 2 additions & 0 deletions packages/shorebird_cli/bin/shorebird.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,6 +43,7 @@ Future<void> main(List<String> args) async {
bundletoolRef,
cacheRef,
codePushClientWrapperRef,
codeSignerRef,
devicectlRef,
doctorRef,
engineConfigRef,
Expand Down
5 changes: 5 additions & 0 deletions packages/shorebird_cli/lib/src/code_push_client_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class PatchArtifactBundle {
required this.path,
required this.hash,
required this.size,
this.hashSignature,
});

/// The corresponding architecture.
Expand All @@ -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.
Expand Down Expand Up @@ -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);
Expand Down
69 changes: 69 additions & 0 deletions packages/shorebird_cli/lib/src/code_signer.dart
Original file line number Diff line number Diff line change
@@ -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<RSAPrivateKey>(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<int> _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<int> 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,
);
}
}
18 changes: 18 additions & 0 deletions packages/shorebird_cli/lib/src/commands/patch/android_patcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -31,6 +34,11 @@ class AndroidPatcher extends Patcher {
required super.target,
});

@override
Future<void> assertArgsAreValid() async {
argResults.file('private-key-path')?.assertExists();
}

@override
ReleaseType get releaseType => ReleaseType.android;

Expand Down Expand Up @@ -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,
Expand All @@ -158,6 +175,7 @@ Looked in:
path: diffPath,
hash: hash,
size: await File(diffPath).length(),
hashSignature: hashSignature,
);
} catch (error) {
createDiffProgress.fail('$error');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
''',
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
26 changes: 17 additions & 9 deletions packages/shorebird_cli/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -502,7 +510,7 @@ packages:
source: hosted
version: "3.1.4"
pointycastle:
dependency: transitive
dependency: "direct main"
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions packages/shorebird_cli/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions packages/shorebird_cli/test/fixtures/crypto/private.pem
Original file line number Diff line number Diff line change
@@ -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-----
9 changes: 9 additions & 0 deletions packages/shorebird_cli/test/fixtures/crypto/public.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2wdpEGbuvlPsb9i0qYrf
MefJnEw1BHTi8SYZTKrXOvJWmEpPE1hWfbkvYzXu5a96gV1yocF3DMwn04VmRlKh
C4AhsD0NL0UNhYhotbKG91Kwi1vAXpHhCdz5gQEBw0K1uB4Jz+zK6WK+31PryYpw
LwbyXNqXoY8IAAUQ4STsHYV5w+BMSi8pepWMRd7DR9RHcbNOZlJvdBQ5NxvB4JN4
dRMq8cC73ez1P9d7Dfwv3TWY+he9EmuXLT2UivZSlHIrGBa7MFfqyUe2ro0F7Te/
B0si12itBbWIqycvqcXjeOPNn6WEpqN7IWjb9LUh162JyYaz5Lb/VeeJX8LKtElc
cwIDAQAB
-----END PUBLIC KEY-----
56 changes: 56 additions & 0 deletions packages/shorebird_cli/test/src/code_signer_test.dart
Original file line number Diff line number Diff line change
@@ -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'),
},
);
}
Loading

0 comments on commit b93f5fd

Please sign in to comment.