Commit dff3fa7e authored by Matt Perry's avatar Matt Perry

Support verifying .flx signatures when updating.

Adds a step to the updater to verify that the new .flx package is signed and
untampered.

Each .flx contains a signed manifest file. The manifest contains a SHA-256 hash
of the .flx contents. See bundle.dart for a description of the new .flx format.
parent 88bcfa2d
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
......
name: stocks
version: 0.0.2
update-url: http://localhost:9888/examples/stocks/
material-design-icons:
- name: action/account_balance
- name: action/assessment
......
......@@ -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:
......
// 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;
}
}
......@@ -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();
......@@ -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();
......@@ -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);
}
......@@ -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();
}
......@@ -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;
......@@ -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();
}
}
......@@ -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;
......@@ -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);
}
......@@ -9,6 +9,8 @@ dependencies:
sky_services: any
path: any
yaml: any
cipher: any
asn1lib: any
dependency_overrides:
flutter:
path: ../sky
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment