diff --git a/sky/packages/flx/lib/signing.dart b/sky/packages/flx/lib/signing.dart index a95e0766791..c9a1a0668fc 100644 --- a/sky/packages/flx/lib/signing.dart +++ b/sky/packages/flx/lib/signing.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:math'; import 'dart:io'; import 'dart:typed_data'; @@ -44,7 +43,7 @@ final CipherParameters _params = _initParams(); Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8List zipBytes) { if (manifestDescriptor == null) return null; - final List kSavedKeys = [ + final List kSavedKeys = [ 'name', 'version', 'update-url' @@ -66,9 +65,9 @@ Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8 } // Returns the ASN.1 encoded signature of the input manifestBytes. -List signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) { +Uint8List signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) { if (manifestBytes == null || privateKey == null) - return []; + return new Uint8List(0); Signer signer = new Signer(_params.signerAlgorithm); PrivateKeyParameter params = new PrivateKeyParameter(privateKey); signer.init(true, new ParametersWithRandom(params, _params.random)); @@ -121,26 +120,22 @@ ECPrivateKey _asn1ParsePrivateKey(ECDomainParameters ecDomain, Uint8List private // Parses a DER-encoded ASN.1 ECDSA signature block. ECSignature _asn1ParseSignature(Uint8List signature) { - ASN1Parser parser = new ASN1Parser(signature); - ASN1Object object = parser.nextObject(); - if (object is! ASN1Sequence) + try { + ASN1Parser parser = new ASN1Parser(signature); + ASN1Object object = parser.nextObject(); + if (object is! ASN1Sequence) + return null; + ASN1Sequence sequence = object; + if (!(sequence.elements.length == 2 && + sequence.elements[0] is ASN1Integer && + sequence.elements[1] is ASN1Integer)) + return null; + ASN1Integer r = sequence.elements[0]; + ASN1Integer s = sequence.elements[1]; + return new ECSignature(r.valueAsPositiveBigInteger, s.valueAsPositiveBigInteger); + } on ASN1Exception { return null; - ASN1Sequence sequence = object; - if (!(sequence.elements.length == 2 && - sequence.elements[0] is ASN1Integer && - sequence.elements[1] is ASN1Integer)) - return null; - ASN1Integer r = sequence.elements[0]; - ASN1Integer s = sequence.elements[1]; - return new ECSignature(r.valueAsPositiveBigInteger, s.valueAsPositiveBigInteger); -} - -ECPrivateKey _readPrivateKeySync(String privateKeyPath) { - File file = new File(privateKeyPath); - if (!file.existsSync()) - return null; - List bytes = file.readAsBytesSync(); - return _asn1ParsePrivateKey(_params.domain, new Uint8List.fromList(bytes)); + } } ECPublicKey _publicKeyFromPrivateKey(ECPrivateKey privateKey) { @@ -154,8 +149,16 @@ class KeyPair { ECPublicKey publicKey; ECPrivateKey privateKey; - static KeyPair readFromPrivateKeySync(String path) { - ECPrivateKey privateKey = _readPrivateKeySync(path); + static KeyPair readFromPrivateKeySync(String privateKeyPath) { + File file = new File(privateKeyPath); + if (!file.existsSync()) + return null; + return fromPrivateKeyBytes(file.readAsBytesSync()); + } + + static KeyPair fromPrivateKeyBytes(List privateKeyBytes) { + ECPrivateKey privateKey = _asn1ParsePrivateKey( + _params.domain, new Uint8List.fromList(privateKeyBytes)); if (privateKey == null) return null; diff --git a/sky/packages/flx/pubspec.yaml b/sky/packages/flx/pubspec.yaml index d7a947fea73..acaae9e47fa 100644 --- a/sky/packages/flx/pubspec.yaml +++ b/sky/packages/flx/pubspec.yaml @@ -1,5 +1,5 @@ name: flx -version: 0.0.1 +version: 0.0.2 author: Flutter Authors description: Library for dealing with Flutter bundle (.flx) files homepage: http://flutter.io diff --git a/sky/packages/workbench/pubspec.yaml b/sky/packages/workbench/pubspec.yaml index 57040de79aa..759be72f636 100644 --- a/sky/packages/workbench/pubspec.yaml +++ b/sky/packages/workbench/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: flutter_rendering_examples: any flutter_sprites: any playfair: any + flx: any dependency_overrides: material_design_icons: path: ../material_design_icons @@ -20,5 +21,7 @@ dependency_overrides: path: ../../../examples/rendering flutter_sprites: path: ../../../skysprites + flx: + path: ../flx environment: sdk: '>=1.8.0 <2.0.0' diff --git a/sky/unit/pubspec.yaml b/sky/unit/pubspec.yaml index 1e6dbe2ca95..62c6b7ef682 100644 --- a/sky/unit/pubspec.yaml +++ b/sky/unit/pubspec.yaml @@ -4,8 +4,11 @@ dependencies: sky_tools: any test: any quiver: any + flx: ^0.0.2 dependency_overrides: material_design_icons: path: ../packages/material_design_icons flutter: path: ../packages/sky + flx: + path: ../packages/flx diff --git a/sky/unit/test/flx/signing_test.dart b/sky/unit/test/flx/signing_test.dart new file mode 100644 index 00000000000..d7cec6e37c9 --- /dev/null +++ b/sky/unit/test/flx/signing_test.dart @@ -0,0 +1,100 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:bignum/bignum.dart'; +import 'package:flx/signing.dart'; +import 'package:quiver/testing/async.dart'; +import 'package:test/test.dart'; + +void main() { + // The following constant was generated via the openssl shell commands: + // openssl ecparam -genkey -name prime256v1 -out privatekey.pem + // openssl ec -in privatekey.pem -outform DER | base64 + const String kPrivateKeyBase64 = 'MHcCAQEEIG4Xt+MgsdP/o89kAHz7EVVLKkN+DUfpaBtZfMyFGbUgoAoGCCqGSM49AwEHoUQDQgAElPtbBVPPqKHYXYAgHaxB2hL6sXeFc99YLijTAuAPe2Nbhywan+v4k+nFm0TJJW/mkV+nH+fyBZ98t4UcFCqkOg=='; + final List kPrivateKeyDER = BASE64.decode(kPrivateKeyBase64); + + // Unpacked values of the above private key. + const int kPrivateKeyD = 0x6e17b7e320b1d3ffa3cf64007cfb11554b2a437e0d47e9681b597ccc8519b520; + const int kPublicKeyQx = 0x94fb5b0553cfa8a1d85d80201dac41da12fab1778573df582e28d302e00f7b63; + const int kPublicKeyQy = 0x5b872c1a9febf893e9c59b44c9256fe6915fa71fe7f2059f7cb7851c142aa43a; + + // Test manifest. + final Map kManifest = { + 'name': 'test app', + 'version': '1.0.0' + }; + + // Simple test byte pattern (flat and in chunked form) and its SHA-256 hash. + final Uint8List kTestBytes = new Uint8List.fromList([1, 2, 3]); + final List kTestBytesList = [ + new Uint8List.fromList([1, 2]), new Uint8List.fromList([3])]; + final int kTestHash = 0x039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81; + + test('can read openssl key pair', () { + KeyPair keyPair = KeyPair.fromPrivateKeyBytes(kPrivateKeyDER); + expect(keyPair != null, equals(true)); + expect(keyPair.privateKey.d.intValue(), equals(kPrivateKeyD)); + expect(keyPair.publicKey.Q.x.toBigInteger().intValue(), equals(kPublicKeyQx)); + expect(keyPair.publicKey.Q.y.toBigInteger().intValue(), equals(kPublicKeyQy)); + }); + + test('serializeManifest adds key and content-hash', () { + KeyPair keyPair = KeyPair.fromPrivateKeyBytes(kPrivateKeyDER); + Uint8List manifestBytes = serializeManifest(kManifest, keyPair.publicKey, kTestBytes); + Map decodedManifest = JSON.decode(UTF8.decode(manifestBytes)); + String expectedKey = BASE64.encode(keyPair.publicKey.Q.getEncoded()); + expect(decodedManifest != null, equals(true)); + expect(decodedManifest['name'], equals(kManifest['name'])); + expect(decodedManifest['version'], equals(kManifest['version'])); + expect(decodedManifest['key'], equals(expectedKey)); + expect(decodedManifest['content-hash'], equals(kTestHash)); + }); + + test('signManifest and verifyManifestSignature work', () { + KeyPair keyPair = KeyPair.fromPrivateKeyBytes(kPrivateKeyDER); + Map manifest = JSON.decode(UTF8.decode( + serializeManifest(kManifest, keyPair.publicKey, kTestBytes))); + Uint8List signatureBytes = signManifest(kTestBytes, keyPair.privateKey); + + bool verifies = verifyManifestSignature(manifest, kTestBytes, signatureBytes); + expect(verifies, equals(true)); + + // Ensure it fails with invalid signature or content. + Uint8List badBytes = new Uint8List.fromList([42]); + verifies = verifyManifestSignature(manifest, kTestBytes, badBytes); + expect(verifies, equals(false)); + verifies = verifyManifestSignature(manifest, badBytes, signatureBytes); + expect(verifies, equals(false)); + }); + + test('verifyContentHash works', () { + new FakeAsync().run((FakeAsync async) { + bool verifies; + Stream contentStream = new Stream.fromIterable(kTestBytesList); + verifyContentHash(new BigInteger(kTestHash), contentStream).then((bool rv) { + verifies = rv; + }); + async.elapse(Duration.ZERO); + expect(verifies, equals(true)); + + // Ensure it fails with invalid hash or content. + verifies = null; + contentStream = new Stream.fromIterable(kTestBytesList); + verifyContentHash(new BigInteger(0xdeadbeef), contentStream).then((bool rv) { + verifies = rv; + }); + async.elapse(Duration.ZERO); + expect(verifies, equals(false)); + + verifies = null; + Stream badContentStream = + new Stream.fromIterable([new Uint8List.fromList([42])]); + verifyContentHash(new BigInteger(kTestHash), badContentStream).then((bool rv) { + verifies = rv; + }); + async.elapse(Duration.ZERO); + expect(verifies, equals(false)); + }); + }); +}