Skip to content

Commit

Permalink
Merge pull request flutter#1612 from mpcomplete/signer.cipher
Browse files Browse the repository at this point in the history
Add support for verifying .flx signatures when updating
  • Loading branch information
mpcomplete committed Oct 19, 2015
2 parents 586d89b + 9f9605d commit d939abf
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 33 deletions.
2 changes: 1 addition & 1 deletion examples/fitness/sky.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: fitness
version: 0.0.1
update_url: http://localhost:9888/examples/fitness/
update-url: http://localhost:9888/examples/fitness/
material-design-icons:
- name: action/assessment
- name: action/help
Expand Down
3 changes: 3 additions & 0 deletions examples/stocks/sky.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
name: stocks
version: 0.0.2
update-url: http://localhost:9888/examples/stocks/
material-design-icons:
- name: action/account_balance
- name: action/assessment
Expand Down
2 changes: 1 addition & 1 deletion sky/packages/sky/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dependencies:
newton: '>=0.1.4 <0.2.0'
sky_engine: 0.0.38
sky_services: 0.0.38
sky_tools: '>=0.0.20 <0.1.0'
sky_tools: '>=0.0.25 <0.1.0'
vector_math: '>=1.4.3 <2.0.0'
intl: '>=0.12.4+2 <0.13.0'
environment:
Expand Down
75 changes: 75 additions & 0 deletions sky/packages/updater/lib/bundle.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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<List<int>> _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<String> _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 <any string>\n
// <32-bit length><signature of the manifest data>
// <32-bit length><manifest data>
// <zip content>
//
// 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 <zip content>.
class Bundle {
Bundle(this.path);

final String path;
List<int> signatureBytes;
List<int> manifestBytes;
Map<String, dynamic> manifest;
RandomAccessFile content;

Future<bool> _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<Bundle> readHeader(String path) async {
Bundle bundle = new Bundle(path);
if (!await bundle._readHeader())
return null;
return bundle;
}
}
143 changes: 119 additions & 24 deletions sky/packages/updater/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,38 @@
// found in the LICENSE file.

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:io';
import 'dart:typed_data';

import 'package:mojo/core.dart';
import 'package:flutter/services.dart';
// TODO(mpcomplete): Remove this 'hide' when we remove the conflicting
// UpdateService from activity.mojom.
import 'package:flutter/services.dart' hide UpdateServiceProxy;
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 'version.dart';
import 'bundle.dart';
import 'pipe_to_file.dart';
import 'version.dart';

const String kManifestFile = 'sky.yaml';
const String kBundleFile = 'app.flx';

const String kManifestFile = 'ui.yaml';
const String kBundleFile = 'app.skyx';
// 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();
Expand All @@ -32,20 +51,45 @@ Future<String> 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;
String toString() => _message;
}

class UpdateTask {
UpdateTask() {}
UpdateTask();

run() async {
Future run() async {
try {
await _runImpl();
} catch(e) {
} on UpdateFailure catch (e) {
print('Update failed: $e');
} catch (e, stackTrace) {
print('Update failed: $e');
print('Stack: $stackTrace');
} finally {
_updateService.ptr.notifyUpdateCheckComplete();
}
}

_runImpl() async {
Future _runImpl() async {
_dataDir = await getDataDir();

await _readLocalManifest();
Expand All @@ -54,27 +98,25 @@ class UpdateTask {
print('Update skipped. No new version.');
return;
}
MojoResult result = await _fetchBundle();
if (!result.isOk) {
print('Update failed while fetching new skyx bundle.');
return;
}
await _fetchBundle();
await _validateBundle();
await _replaceBundle();
print('Update success.');
}

yaml.YamlMap _currentManifest;
Map _currentManifest;
String _dataDir;
String _tempPath;

_readLocalManifest() async {
String manifestPath = path.join(_dataDir, kManifestFile);
String manifestData = await new File(manifestPath).readAsString();
_currentManifest = yaml.loadYaml(manifestData, sourceUrl: manifestPath);
Future _readLocalManifest() async {
String bundlePath = path.join(_dataDir, kBundleFile);
Bundle bundle = await Bundle.readHeader(bundlePath);
_currentManifest = bundle.manifest;
bundle.content.close();
}

Future<yaml.YamlMap> _fetchManifest() async {
String manifestUrl = _currentManifest['update_url'] + '/' + kManifestFile;
String manifestUrl = _currentManifest['update-url'] + '/' + kManifestFile;
String manifestData = await fetchString(manifestUrl);
return yaml.loadYaml(manifestData, sourceUrl: manifestUrl);
}
Expand All @@ -85,21 +127,74 @@ class UpdateTask {
return (currentVersion < remoteVersion);
}

Future<MojoResult> _fetchBundle() async {
Future _fetchBundle() async {
// TODO(mpcomplete): Use the cache dir. We need an equivalent of mkstemp().
_tempPath = path.join(_dataDir, 'tmp.skyx');
String bundleUrl = _currentManifest['update_url'] + '/' + kBundleFile;
String bundleUrl = _currentManifest['update-url'] + '/' + kBundleFile;
UrlResponse response = await fetchUrl(bundleUrl);
return PipeToFile.copyToFile(response.body, _tempPath);
MojoResult result = await PipeToFile.copyToFile(response.body, _tempPath);
if (!result.isOk)
throw new UpdateFailure('Failure fetching new package: ${response.statusLine}');
}

Future _validateBundle() async {
Bundle bundle = await Bundle.readHeader(_tempPath);

if (bundle == null)
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<int> 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.');
}

_replaceBundle() async {
Future _replaceBundle() async {
String bundlePath = path.join(_dataDir, kBundleFile);
await new File(_tempPath).rename(bundlePath);
}
}

void main() {
var task = new UpdateTask();
initCipher();
UpdateTask task = new UpdateTask();
task.run();
}
10 changes: 5 additions & 5 deletions sky/packages/updater/lib/pipe_to_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ class PipeToFile {
return _consumer.endRead(thisRead.lengthInBytes);
}

Future<MojoResult> drain() async {
var completer = new Completer();
Future drain() async {
Completer completer = new Completer();
// TODO(mpcomplete): Is it legit to pass an async callback to listen?
_eventStream.listen((List<int> event) async {
var mojoSignals = new MojoHandleSignals(event[1]);
MojoHandleSignals mojoSignals = new MojoHandleSignals(event[1]);
if (mojoSignals.isReadable) {
var result = await _doRead();
MojoResult result = await _doRead();
if (!result.isOk) {
_eventStream.close();
_eventStream = null;
Expand All @@ -58,7 +58,7 @@ class PipeToFile {
}

static Future<MojoResult> copyToFile(MojoDataPipeConsumer consumer, String outputPath) {
var drainer = new PipeToFile(consumer, outputPath);
PipeToFile drainer = new PipeToFile(consumer, outputPath);
return drainer.drain();
}
}
4 changes: 2 additions & 2 deletions sky/packages/updater/lib/version.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import 'dart:math';
// Usage: assert(new Version('1.1.0') < new Version('1.2.1'));
class Version {
Version(String versionStr) :
_parts = versionStr.split('.').map((val) => int.parse(val)).toList();
_parts = versionStr.split('.').map((String val) => int.parse(val)).toList();

List<int> _parts;

Expand All @@ -28,5 +28,5 @@ class Version {
return _parts.length - other._parts.length; // results in 1.0 < 1.0.0
}

int get hashCode => _parts.fold(373, (acc, part) => 37*acc + part);
int get hashCode => _parts.fold(373, (int acc, int part) => 37*acc + part);
}
2 changes: 2 additions & 0 deletions sky/packages/updater/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ dependencies:
sky_services: any
path: any
yaml: any
cipher: any
asn1lib: any
dependency_overrides:
flutter:
path: ../sky
Expand Down
2 changes: 2 additions & 0 deletions sky/packages/workbench/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ homepage: https://github.com/flutter/engine/tree/master/sky/packages/workbench
dependencies:
flutter: ">=0.0.3 <0.1.0"
sky_tools: any
cipher: any
asn1lib: any
dependency_overrides:
material_design_icons:
path: ../material_design_icons
Expand Down
1 change: 1 addition & 0 deletions sky/tools/sky_build.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def main():
'--output-file', os.path.abspath(args.output_file),
'--package-root', os.path.abspath(args.package_root),
'--snapshot', os.path.abspath(args.snapshot),
'--private-key', os.path.abspath(os.path.join(args.package_root, '..', 'privatekey.der')),
]

if args.manifest:
Expand Down

0 comments on commit d939abf

Please sign in to comment.