From cf097758bfbfaf5c313ff6e386ed6fcd5ea628b4 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 26 Oct 2015 14:20:32 -0400 Subject: [PATCH] Change updater to use shared flx package. This changes flutter's updater package to depend on and use the new flx package. I also did a little cleanup of the Bundle interface. --- sky/packages/flx/lib/bundle.dart | 85 +++++++++++++++++++++--- sky/packages/flx/lib/signing.dart | 97 ++++++++++++++++++++++++---- sky/packages/flx/pubspec.yaml | 2 +- sky/packages/updater/lib/bundle.dart | 75 --------------------- sky/packages/updater/lib/main.dart | 80 ++--------------------- sky/packages/updater/pubspec.yaml | 11 ++-- 6 files changed, 171 insertions(+), 179 deletions(-) delete mode 100644 sky/packages/updater/lib/bundle.dart diff --git a/sky/packages/flx/lib/bundle.dart b/sky/packages/flx/lib/bundle.dart index 9e72a259215..52485c8efa1 100644 --- a/sky/packages/flx/lib/bundle.dart +++ b/sky/packages/flx/lib/bundle.dart @@ -7,7 +7,16 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -const String kBundleMagic = '#!mojo '; +import 'package:bignum/bignum.dart'; + +import 'signing.dart'; + +// Magic string we put at the top of all bundle files. +const String kBundleMagic = '#!mojo mojo:sky_viewer\n'; + +// Prefix of the above, used when reading bundle files. This allows us to be +// more flexbile about what we accept. +const String kBundleMagicPrefix = '#!mojo '; Future> _readBytesWithLength(RandomAccessFile file) async { ByteData buffer = new ByteData(4); @@ -29,6 +38,16 @@ Future _readLine(RandomAccessFile file) async { return line; } +// Writes a 32-bit length followed by the content of [bytes]. +void _writeBytesWithLengthSync(File outputFile, List bytes) { + if (bytes == null) + bytes = new Uint8List(0); + assert(bytes.length < 0xffffffff); + ByteData length = new ByteData(4)..setUint32(0, bytes.length, Endianness.LITTLE_ENDIAN); + outputFile.writeAsBytesSync(length.buffer.asUint8List(), mode: FileMode.APPEND); + outputFile.writeAsBytesSync(bytes, mode: FileMode.APPEND); +} + // Represents a parsed .flx Bundle. Contains information from the bundle's // header, as well as an open File handle positioned where the zip content // begins. @@ -46,30 +65,78 @@ Future _readLine(RandomAccessFile file) async { // ECDSA public key that was used to sign this manifest. // content-hash: an integer SHA-256 hash value of the . class Bundle { - Bundle(this.path); + Bundle._fromFile(this.path); + Bundle.fromContent({ + this.path, + this.manifest, + contentBytes, + KeyPair keyPair: null + }) : _contentBytes = contentBytes { + assert(path != null); + assert(manifest != null); + assert(_contentBytes != null); + manifestBytes = serializeManifest(manifest, keyPair?.publicKey, _contentBytes); + signatureBytes = signManifest(manifestBytes, keyPair?.privateKey); + } final String path; List signatureBytes; List manifestBytes; Map manifest; - RandomAccessFile content; + + // File byte offset of the start of the zip content. Only valid when opened + // from a file. + int _contentOffset; + + // Zip content bytes. Only valid when created in memory. + List _contentBytes; Future _readHeader() async { - content = await new File(path).open(); - String magic = await _readLine(content); - if (!magic.startsWith(kBundleMagic)) + RandomAccessFile file = await new File(path).open(); + String magic = await _readLine(file); + if (!magic.startsWith(kBundleMagicPrefix)) { + file.close(); return false; - signatureBytes = await _readBytesWithLength(content); - manifestBytes = await _readBytesWithLength(content); + } + signatureBytes = await _readBytesWithLength(file); + manifestBytes = await _readBytesWithLength(file); + _contentOffset = await file.position(); + file.close(); + String manifestString = UTF8.decode(manifestBytes); manifest = JSON.decode(manifestString); return true; } static Future readHeader(String path) async { - Bundle bundle = new Bundle(path); + Bundle bundle = new Bundle._fromFile(path); if (!await bundle._readHeader()) return null; return bundle; } + + // When opened from a file, verifies that the package has a valid signature + // and content. + Future verifyContent() async { + assert(_contentOffset != null); + if (!verifyManifestSignature(manifest, manifestBytes, signatureBytes)) + return false; + + Stream> content = await new File(path).openRead(_contentOffset); + BigInteger expectedHash = new BigInteger(manifest['content-hash'], 10); + if (!await verifyContentHash(expectedHash, content)) + return false; + + return true; + } + + // Writes the in-memory representation to disk. + void writeSync() { + assert(_contentBytes != null); + File outputFile = new File(path); + outputFile.writeAsStringSync('#!mojo mojo:sky_viewer\n'); + _writeBytesWithLengthSync(outputFile, signatureBytes); + _writeBytesWithLengthSync(outputFile, manifestBytes); + outputFile.writeAsBytesSync(_contentBytes, mode: FileMode.APPEND, flush: true); + } } diff --git a/sky/packages/flx/lib/signing.dart b/sky/packages/flx/lib/signing.dart index d3390a6d590..a95e0766791 100644 --- a/sky/packages/flx/lib/signing.dart +++ b/sky/packages/flx/lib/signing.dart @@ -4,20 +4,23 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math'; import 'dart:io'; import 'dart:typed_data'; import 'package:asn1lib/asn1lib.dart'; import 'package:bignum/bignum.dart'; import 'package:cipher/cipher.dart'; +import 'package:cipher/impl/client.dart'; // The ECDSA algorithm parameters we're using. These match the parameters used // by the Flutter updater package. -final ECDomainParameters _ecDomain = new ECDomainParameters('prime256v1'); -final String kSignerAlgorithm = 'SHA-256/ECDSA'; -final String kHashAlgorithm = 'SHA-256'; - -final SecureRandom _random = _initRandom(); +class CipherParameters { + final String signerAlgorithm = 'SHA-256/ECDSA'; + final String hashAlgorithm = 'SHA-256'; + final ECDomainParameters domain = new ECDomainParameters('prime256v1'); + final SecureRandom random = _initRandom(); +} SecureRandom _initRandom() { // TODO(mpcomplete): Provide a better seed here. External entropy source? @@ -29,6 +32,13 @@ SecureRandom _initRandom() { return random; } +CipherParameters _initParams() { + initCipher(); + return new CipherParameters(); +} + +final CipherParameters _params = _initParams(); + // Returns a serialized manifest, with the public key and hash of the content // included. Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8List zipBytes) { @@ -48,7 +58,7 @@ Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8 if (publicKey != null) outputManifest['key'] = BASE64.encode(publicKey.Q.getEncoded()); - Uint8List zipHash = new Digest(kHashAlgorithm).process(zipBytes); + Uint8List zipHash = new Digest(_params.hashAlgorithm).process(zipBytes); BigInteger zipHashInt = new BigInteger.fromBytes(1, zipHash); outputManifest['content-hash'] = zipHashInt.intValue(); @@ -59,9 +69,9 @@ Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8 List signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) { if (manifestBytes == null || privateKey == null) return []; - Signer signer = new Signer(kSignerAlgorithm); + Signer signer = new Signer(_params.signerAlgorithm); PrivateKeyParameter params = new PrivateKeyParameter(privateKey); - signer.init(true, new ParametersWithRandom(params, _random)); + signer.init(true, new ParametersWithRandom(params, _params.random)); ECSignature signature = signer.generateSignature(manifestBytes); ASN1Sequence asn1 = new ASN1Sequence() ..add(new ASN1Integer(signature.r)) @@ -69,6 +79,37 @@ List signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) { return asn1.encodedBytes; } +bool verifyManifestSignature(Map manifest, + Uint8List manifestBytes, + Uint8List signatureBytes) { + ECSignature signature = _asn1ParseSignature(signatureBytes); + if (signature == null) + return false; + + List keyBytes = BASE64.decode(manifest['key']); + ECPoint q = _params.domain.curve.decodePoint(keyBytes); + ECPublicKey publicKey = new ECPublicKey(q, _params.domain); + + Signer signer = new Signer(_params.signerAlgorithm); + signer.init(false, new PublicKeyParameter(publicKey)); + return signer.verifySignature(manifestBytes, signature); +} + +Future verifyContentHash(BigInteger expectedHash, Stream> content) async { + // Hash the file incrementally. + Digest hasher = new Digest(_params.hashAlgorithm); + await content.forEach((List chunk) { + hasher.update(chunk, 0, chunk.length); + }); + Uint8List hashBytes = new Uint8List(hasher.digestSize); + int len = hasher.doFinal(hashBytes, 0); + hashBytes = hashBytes.sublist(0, len); + BigInteger actualHash = new BigInteger.fromBytes(1, hashBytes); + + return expectedHash == actualHash; +} + +// Parses a DER-encoded ASN.1 ECDSA private key block. ECPrivateKey _asn1ParsePrivateKey(ECDomainParameters ecDomain, Uint8List privateKey) { ASN1Parser parser = new ASN1Parser(privateKey); ASN1Sequence seq = parser.nextObject(); @@ -78,17 +119,47 @@ ECPrivateKey _asn1ParsePrivateKey(ECDomainParameters ecDomain, Uint8List private return new ECPrivateKey(d, ecDomain); } -Future loadPrivateKey(String privateKeyPath) async { +// 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) + 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(_ecDomain, new Uint8List.fromList(bytes)); + return _asn1ParsePrivateKey(_params.domain, new Uint8List.fromList(bytes)); } -ECPublicKey publicKeyFromPrivateKey(ECPrivateKey privateKey) { - if (privateKey == null) - return null; +ECPublicKey _publicKeyFromPrivateKey(ECPrivateKey privateKey) { ECPoint Q = privateKey.parameters.G * privateKey.d; return new ECPublicKey(Q, privateKey.parameters); } + +class KeyPair { + KeyPair(this.publicKey, this.privateKey); + + ECPublicKey publicKey; + ECPrivateKey privateKey; + + static KeyPair readFromPrivateKeySync(String path) { + ECPrivateKey privateKey = _readPrivateKeySync(path); + if (privateKey == null) + return null; + + ECPublicKey publicKey = _publicKeyFromPrivateKey(privateKey); + return new KeyPair(publicKey, privateKey); + } +} diff --git a/sky/packages/flx/pubspec.yaml b/sky/packages/flx/pubspec.yaml index 323cd8c5cdb..d7a947fea73 100644 --- a/sky/packages/flx/pubspec.yaml +++ b/sky/packages/flx/pubspec.yaml @@ -4,7 +4,7 @@ author: Flutter Authors description: Library for dealing with Flutter bundle (.flx) files homepage: http://flutter.io dependencies: - sky_services: 0.0.38 + sky_services: ^0.0.40 yaml: ^2.1.3 asn1lib: ^0.4.1 cipher: ^0.7.1 diff --git a/sky/packages/updater/lib/bundle.dart b/sky/packages/updater/lib/bundle.dart deleted file mode 100644 index 9e72a259215..00000000000 --- a/sky/packages/updater/lib/bundle.dart +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -const String kBundleMagic = '#!mojo '; - -Future> _readBytesWithLength(RandomAccessFile file) async { - ByteData buffer = new ByteData(4); - await file.readInto(buffer.buffer.asUint8List()); - int length = buffer.getUint32(0, Endianness.LITTLE_ENDIAN); - return await file.read(length); -} - -const int kMaxLineLen = 10*1024; -const int kNewline = 0x0A; -Future _readLine(RandomAccessFile file) async { - String line = ''; - while (line.length < kMaxLineLen) { - int byte = await file.readByte(); - if (byte == -1 || byte == kNewline) - break; - line += new String.fromCharCode(byte); - } - return line; -} - -// Represents a parsed .flx Bundle. Contains information from the bundle's -// header, as well as an open File handle positioned where the zip content -// begins. -// The bundle format is: -// #!mojo \n -// <32-bit length> -// <32-bit length> -// -// -// The manifest is a JSON string containing the following keys: -// (optional) name: the name of the package. -// version: the package version. -// update-url: the base URL to download a new manifest and bundle. -// key: a BASE-64 encoded DER-encoded ASN.1 representation of the Q point of the -// ECDSA public key that was used to sign this manifest. -// content-hash: an integer SHA-256 hash value of the . -class Bundle { - Bundle(this.path); - - final String path; - List signatureBytes; - List manifestBytes; - Map manifest; - RandomAccessFile content; - - Future _readHeader() async { - content = await new File(path).open(); - String magic = await _readLine(content); - if (!magic.startsWith(kBundleMagic)) - return false; - signatureBytes = await _readBytesWithLength(content); - manifestBytes = await _readBytesWithLength(content); - String manifestString = UTF8.decode(manifestBytes); - manifest = JSON.decode(manifestString); - return true; - } - - static Future readHeader(String path) async { - Bundle bundle = new Bundle(path); - if (!await bundle._readHeader()) - return null; - return bundle; - } -} diff --git a/sky/packages/updater/lib/main.dart b/sky/packages/updater/lib/main.dart index 307984c0d04..9f2e2a740e9 100644 --- a/sky/packages/updater/lib/main.dart +++ b/sky/packages/updater/lib/main.dart @@ -9,33 +9,18 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:mojo/core.dart'; -// TODO(mpcomplete): Remove this 'hide' when we remove the conflicting -// UpdateService from activity.mojom. -import 'package:flutter/services.dart' hide UpdateServiceProxy; +import 'package:flutter/services.dart'; +import 'package:flx/bundle.dart'; import 'package:sky_services/updater/update_service.mojom.dart'; import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart' as yaml; -import 'package:asn1lib/asn1lib.dart'; -import 'package:bignum/bignum.dart'; -import 'package:cipher/cipher.dart'; -import 'package:cipher/impl/client.dart'; -import 'bundle.dart'; import 'pipe_to_file.dart'; import 'version.dart'; const String kManifestFile = 'sky.yaml'; const String kBundleFile = 'app.flx'; -// Number of bytes to read at a time from a file. -const int kReadBlockSize = 32*1024; - -// The ECDSA algorithm parameters we're using. These match the parameters used -// by the signing tool in flutter_tools. -final ECDomainParameters _ecDomain = new ECDomainParameters('prime256v1'); -final String kSignerAlgorithm = 'SHA-256/ECDSA'; -final String kHashAlgorithm = 'SHA-256'; - UpdateServiceProxy _initUpdateService() { UpdateServiceProxy updateService = new UpdateServiceProxy.unbound(); shell.requestService(null, updateService); @@ -51,22 +36,6 @@ Future getDataDir() async { return cachedDataDir; } -// 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) - 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); -} - class UpdateFailure extends Error { UpdateFailure(this._message); String _message; @@ -112,7 +81,6 @@ class UpdateTask { String bundlePath = path.join(_dataDir, kBundleFile); Bundle bundle = await Bundle.readHeader(bundlePath); _currentManifest = bundle.manifest; - bundle.content.close(); } Future _fetchManifest() async { @@ -144,47 +112,8 @@ class UpdateTask { throw new UpdateFailure('Remote package not a valid FLX file.'); if (bundle.manifest['key'] != _currentManifest['key']) throw new UpdateFailure('Remote package key does not match.'); - - await _verifyManifestSignature(bundle); - await _verifyContentHash(bundle); - - bundle.content.close(); - } - - Future _verifyManifestSignature(Bundle bundle) async { - ECSignature ecSignature = _asn1ParseSignature(bundle.signatureBytes); - if (ecSignature == null) - throw new UpdateFailure('Corrupt package signature.'); - - List keyBytes = BASE64.decode(_currentManifest['key']); - ECPoint q = _ecDomain.curve.decodePoint(keyBytes); - ECPublicKey ecPublicKey = new ECPublicKey(q, _ecDomain); - - Signer signer = new Signer(kSignerAlgorithm); - signer.init(false, new PublicKeyParameter(ecPublicKey)); - if (!signer.verifySignature(bundle.manifestBytes, ecSignature)) - throw new UpdateFailure('Invalid package signature. This package has been tampered with.'); - } - - Future _verifyContentHash(Bundle bundle) async { - // Hash the bundle contents. - Digest hasher = new Digest(kHashAlgorithm); - RandomAccessFile content = bundle.content; - int remainingLen = await content.length() - await content.position(); - while (remainingLen > 0) { - List chunk = await content.read(min(remainingLen, kReadBlockSize)); - hasher.update(chunk, 0, chunk.length); - remainingLen -= chunk.length; - } - Uint8List hashBytes = new Uint8List(hasher.digestSize); - int len = hasher.doFinal(hashBytes, 0); - hashBytes = hashBytes.sublist(0, len); - BigInteger actualHash = new BigInteger.fromBytes(1, hashBytes); - - // Compare to our expected hash from the manifest. - BigInteger expectedHash = new BigInteger(bundle.manifest['content-hash'], 10); - if (expectedHash != actualHash) - throw new UpdateFailure('Invalid package content hash. This package has been tampered with.'); + if (!await bundle.verifyContent()) + throw new UpdateFailure('Invalid package signature or hash. This package has been tampered with.'); } Future _replaceBundle() async { @@ -194,7 +123,6 @@ class UpdateTask { } void main() { - initCipher(); UpdateTask task = new UpdateTask(); task.run(); } diff --git a/sky/packages/updater/pubspec.yaml b/sky/packages/updater/pubspec.yaml index caced20b6da..0608368dddc 100644 --- a/sky/packages/updater/pubspec.yaml +++ b/sky/packages/updater/pubspec.yaml @@ -6,13 +6,14 @@ homepage: http://flutter.io dependencies: mojo: 0.3.0 flutter: ">=0.0.3 <0.1.0" - sky_services: any - path: any - yaml: any - cipher: any - asn1lib: any + sky_services: ^0.0.40 + yaml: ^2.1.3 + path: ^1.3.0 + flx: 0.0.1 dependency_overrides: flutter: path: ../sky + flx: + path: ../flx environment: sdk: '>=1.12.0 <2.0.0'