Commit d89823be authored by Jason Simmons's avatar Jason Simmons Committed by GitHub

Remove packages/flx and the FLX code signing scheme (#4612)

parent 3a88a973
......@@ -16,7 +16,6 @@ flutter analyze --flutter-repo
(cd packages/flutter_sprites; flutter test)
(cd packages/flutter_test; flutter test)
(cd packages/flutter_tools; dart -c test/all.dart)
(cd packages/flx; dart -c test/all.dart)
(cd dev/manual_tests; flutter test)
(cd examples/hello_world; flutter test)
......
......@@ -6,8 +6,6 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flx/bundle.dart';
import 'package:flx/signing.dart';
import 'package:json_schema/json_schema.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
......@@ -449,29 +447,10 @@ Future<int> assemble({
if (fontManifest != null)
zipBuilder.addEntry(fontManifest);
AsymmetricKeyPair<PublicKey, PrivateKey> keyPair = keyPairFromPrivateKeyFileSync(privateKeyPath);
printTrace('KeyPair from $privateKeyPath: $keyPair.');
if (keyPair != null) {
printTrace('Calling CipherParameters.seedRandom().');
CipherParameters.get().seedRandom();
}
File zipFile = new File(outputPath.substring(0, outputPath.length - 4) + '.zip');
printTrace('Encoding zip file to ${zipFile.path}');
zipBuilder.createZip(zipFile, new Directory(workingDirPath));
List<int> zipBytes = zipFile.readAsBytesSync();
ensureDirectoryExists(outputPath);
printTrace('Creating flx at $outputPath.');
Bundle bundle = new Bundle.fromContent(
path: outputPath,
manifest: manifestDescriptor,
contentBytes: zipBytes,
keyPair: keyPair
);
bundle.writeSync();
printTrace('Encoding zip file to $outputPath');
zipBuilder.createZip(new File(outputPath), new Directory(workingDirPath));
printTrace('Built $outputPath.');
......
......@@ -26,9 +26,6 @@ dependencies:
xml: ^2.4.1
yaml: ^2.1.3
flx:
path: ../flx
# We depend on very specific internal implementation details of the
# 'test' package, which change between versions, so here we pin it
# precisely.
......
// 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';
import 'package:bignum/bignum.dart';
import 'signing.dart';
// Magic string we put at the top of all bundle files.
const String kBundleMagic = '#!mojo mojo:flutter\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 ';
typedef Stream<List<int>> StreamOpener();
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;
}
// Writes a 32-bit length followed by the content of [bytes].
void _writeBytesWithLengthSync(RandomAccessFile outputFile, List<int> 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.writeFromSync(length.buffer.asUint8List());
outputFile.writeFromSync(bytes);
}
// 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._fromFile(this.path);
Bundle.fromContent({
this.path,
this.manifest,
List<int> contentBytes,
AsymmetricKeyPair<PublicKey, PrivateKey> keyPair
}) : _contentBytes = contentBytes {
assert(path != null);
assert(manifest != null);
assert(_contentBytes != null);
manifestBytes = serializeManifest(manifest, keyPair?.publicKey, _contentBytes);
signatureBytes = signManifest(manifestBytes, keyPair?.privateKey);
_openContentStream = () => new Stream<List<int>>.fromIterable(<List<int>>[_contentBytes]);
}
final String path;
List<int> signatureBytes;
List<int> manifestBytes;
Map<String, dynamic> manifest;
// Callback to open a Stream containing the bundle content data.
StreamOpener _openContentStream;
// Zip content bytes. Only valid when created in memory.
List<int> _contentBytes;
Future<bool> _readHeader() async {
RandomAccessFile file = await new File(path).open();
String magic = await _readLine(file);
if (!magic.startsWith(kBundleMagicPrefix)) {
file.close();
return false;
}
signatureBytes = await _readBytesWithLength(file);
manifestBytes = await _readBytesWithLength(file);
int contentOffset = await file.position();
_openContentStream = () => new File(path).openRead(contentOffset);
file.close();
String manifestString = UTF8.decode(manifestBytes);
manifest = JSON.decode(manifestString);
return true;
}
static Future<Bundle> readHeader(String path) async {
Bundle bundle = new Bundle._fromFile(path);
if (!await bundle._readHeader())
return null;
return bundle;
}
// Verifies that the package has a valid signature and content.
Future<bool> verifyContent() async {
if (!verifyManifestSignature(manifest, manifestBytes, signatureBytes))
return false;
Stream<List<int>> content = _openContentStream();
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);
RandomAccessFile outputFile = new File(path).openSync(mode: FileMode.WRITE);
outputFile.writeStringSync(kBundleMagic);
_writeBytesWithLengthSync(outputFile, signatureBytes);
_writeBytesWithLengthSync(outputFile, manifestBytes);
outputFile.writeFromSync(_contentBytes);
outputFile.close();
}
}
// 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';
import 'package:asn1lib/asn1lib.dart';
import 'package:bignum/bignum.dart';
import 'package:crypto/crypto.dart' hide Digest;
import 'package:pointycastle/pointycastle.dart';
export 'package:pointycastle/pointycastle.dart' show AsymmetricKeyPair, PublicKey, PrivateKey;
// The ECDSA algorithm parameters we're using. These match the parameters used
// by the Flutter updater package.
class CipherParameters {
final String signerAlgorithm = 'SHA-256/ECDSA';
final String hashAlgorithm = 'SHA-256';
final ECDomainParameters domain = new ECDomainParameters('prime256v1');
SecureRandom get random {
if (_random == null)
_initRandom(new Uint8List(16), new Uint8List(16));
return _random;
}
// Seeds our secure random number generator using data from /dev/urandom.
// Disclaimer: I don't really understand why we need 2 parameters for
// pointycastle's API.
void seedRandom() {
if (_random != null)
return;
try {
RandomAccessFile file = new File("/dev/urandom").openSync();
Uint8List key = new Uint8List.fromList(file.readSync(16));
Uint8List iv = new Uint8List.fromList(file.readSync(16));
_initRandom(key, iv);
file.closeSync();
} on FileSystemException {
// TODO(mpcomplete): need an entropy source on Windows. We might get this
// for free from Dart itself soon.
print("Warning: Failed to seed random number generator. No /dev/urandom.");
}
}
SecureRandom _random;
void _initRandom(Uint8List key, Uint8List iv) {
KeyParameter keyParam = new KeyParameter(key);
ParametersWithIV<KeyParameter> params = new ParametersWithIV<KeyParameter>(keyParam, iv);
_random = new SecureRandom('AES/CTR/AUTO-SEED-PRNG')
..seed(params);
}
static CipherParameters get() => _params;
static CipherParameters _init() {
return new CipherParameters();
}
}
final CipherParameters _params = CipherParameters._init();
// Returns a serialized manifest, with the public key and hash of the content
// included.
Uint8List serializeManifest(Map<String, dynamic> manifestDescriptor, ECPublicKey publicKey, Uint8List zipBytes) {
if (manifestDescriptor == null)
return null;
final List<String> kSavedKeys = <String>[
'name',
'version',
'update-url'
];
Map<String, dynamic> outputManifest = new Map<String, dynamic>();
manifestDescriptor.forEach((String key, dynamic value) {
if (kSavedKeys.contains(key))
outputManifest[key] = value;
});
if (publicKey != null)
outputManifest['key'] = BASE64.encode(publicKey.Q.getEncoded());
List<int> hash = sha256.convert(zipBytes).bytes;
BigInteger zipHashInt = new BigInteger.fromBytes(1, hash);
outputManifest['content-hash'] = zipHashInt.intValue();
return new Uint8List.fromList(UTF8.encode(JSON.encode(outputManifest)));
}
// Returns the ASN.1 encoded signature of the input manifestBytes.
Uint8List signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) {
if (manifestBytes == null || privateKey == null)
return new Uint8List(0);
Signer signer = new Signer(_params.signerAlgorithm);
PrivateKeyParameter<PrivateKey> params = new PrivateKeyParameter<PrivateKey>(privateKey);
signer.init(true, new ParametersWithRandom<PrivateKeyParameter<PrivateKey>>(params, _params.random));
ECSignature signature = signer.generateSignature(manifestBytes);
ASN1Sequence asn1 = new ASN1Sequence()
..add(new ASN1Integer(signature.r))
..add(new ASN1Integer(signature.s));
return asn1.encodedBytes;
}
bool verifyManifestSignature(Map<String, dynamic> manifest,
Uint8List manifestBytes,
Uint8List signatureBytes) {
ECSignature signature = _asn1ParseSignature(signatureBytes);
if (signature == null)
return false;
List<int> keyBytes = BASE64.decode(manifest['key']);
ECPoint q = _params.domain.curve.decodePoint(keyBytes);
PublicKey publicKey = new ECPublicKey(q, _params.domain);
Signer signer = new Signer(_params.signerAlgorithm);
signer.init(false, new PublicKeyParameter<PublicKey>(publicKey));
return signer.verifySignature(manifestBytes, signature);
}
Future<bool> verifyContentHash(BigInteger expectedHash, Stream<List<int>> content) async {
// Hash the file incrementally.
Digest hasher = new Digest(_params.hashAlgorithm);
await content.forEach((List<int> 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();
assert(seq.elements.length >= 2);
ASN1OctetString keyOct = seq.elements[1];
BigInteger d = new BigInteger.fromBytes(1, keyOct.octets);
return new ECPrivateKey(d, ecDomain);
}
// Parses a DER-encoded ASN.1 ECDSA signature block.
ECSignature _asn1ParseSignature(Uint8List signature) {
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;
}
}
PublicKey _publicKeyFromPrivateKey(ECPrivateKey privateKey) {
ECPoint Q = privateKey.parameters.G * privateKey.d;
return new ECPublicKey(Q, privateKey.parameters);
}
AsymmetricKeyPair<PublicKey, PrivateKey> keyPairFromPrivateKeyFileSync(String privateKeyPath) {
File file = new File(privateKeyPath);
if (!file.existsSync())
return null;
return keyPairFromPrivateKeyBytes(file.readAsBytesSync());
}
AsymmetricKeyPair<PublicKey, PrivateKey> keyPairFromPrivateKeyBytes(List<int> privateKeyBytes) {
ECPrivateKey privateKey = _asn1ParsePrivateKey(
_params.domain, new Uint8List.fromList(privateKeyBytes));
if (privateKey == null)
return null;
PublicKey publicKey = _publicKeyFromPrivateKey(privateKey);
return new AsymmetricKeyPair<PublicKey, PrivateKey>(publicKey, privateKey);
}
name: flx
version: 0.0.10
author: Flutter Authors <flutter-dev@googlegroups.com>
description: Library for dealing with Flutter bundle (.flx) files
homepage: https://github.com/flutter/flutter/tree/master/packages/flx
dependencies:
bignum: ^0.1.0
asn1lib: ^0.4.1
pointycastle: 0.10.0
crypto: '>=1.1.1 <3.0.0'
environment:
sdk: '>=1.16.0 <2.0.0'
dev_dependencies:
flutter_test:
path: ../flutter_test
# Exclude this package from the hosted API docs.
dartdoc:
nodoc: true
// Copyright (c) 2016 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 'bundle_test.dart' as bundle_test;
import 'signing_test.dart' as signing_test;
void main() {
bundle_test.main();
signing_test.main();
}
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flx/bundle.dart';
import 'package:flx/signing.dart';
import 'package:test/test.dart';
Future<Null> main() async {
// 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<int> kPrivateKeyDER = BASE64.decode(kPrivateKeyBase64);
// Test manifest.
final Map<String, dynamic> kManifest = <String, dynamic>{
'name': 'test app',
'version': '1.0.0'
};
// Simple test byte pattern.
final Uint8List kTestBytes = new Uint8List.fromList(<int>[1, 2, 3]);
// Create a temp dir and file for the bundle.
Directory tempDir = Directory.systemTemp.createTempSync('bundle_test');
String bundlePath = tempDir.path + '/bundle.flx';
AsymmetricKeyPair<PublicKey, PrivateKey> keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER);
Map<String, dynamic> manifest = JSON.decode(UTF8.decode(
serializeManifest(kManifest, keyPair.publicKey, kTestBytes)
));
test('verifyContent works', () async {
Bundle bundle = new Bundle.fromContent(
path: bundlePath,
manifest: manifest,
contentBytes: kTestBytes,
keyPair: keyPair
);
bool verifies = await bundle.verifyContent();
expect(verifies, equals(true));
});
test('write/read works', () async {
Bundle bundle = new Bundle.fromContent(
path: bundlePath,
manifest: manifest,
contentBytes: kTestBytes,
keyPair: keyPair
);
bundle.writeSync();
Bundle diskBundle = await Bundle.readHeader(bundlePath);
expect(diskBundle != null, equals(true));
expect(diskBundle.manifestBytes, equals(bundle.manifestBytes));
expect(diskBundle.signatureBytes, equals(bundle.signatureBytes));
expect(diskBundle.manifest['key'], equals(bundle.manifest['key']));
expect(diskBundle.manifest['key'], equals(manifest['key']));
bool verifies = await diskBundle.verifyContent();
expect(verifies, equals(true));
});
test('cleanup', () async {
tempDir.deleteSync(recursive: true);
});
}
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:bignum/bignum.dart';
import 'package:flx/signing.dart';
import 'package:pointycastle/pointycastle.dart' hide CipherParameters;
import 'package:test/test.dart';
Future<Null> main() async {
// 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<int> 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<String, dynamic> kManifest = <String, dynamic>{
'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(<int>[1, 2, 3]);
final List<Uint8List> kTestBytesList = <Uint8List>[
new Uint8List.fromList(<int>[1, 2]), new Uint8List.fromList(<int>[3])];
final int kTestHash = 0x039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81;
// Set up a key generator.
CipherParameters cipher = CipherParameters.get();
cipher.seedRandom();
ECKeyGeneratorParameters ecParams = new ECKeyGeneratorParameters(cipher.domain);
ParametersWithRandom<ECKeyGeneratorParameters> keyGeneratorParams =
new ParametersWithRandom<ECKeyGeneratorParameters>(ecParams, cipher.random);
KeyGenerator keyGenerator = new KeyGenerator('EC');
keyGenerator.init(keyGeneratorParams);
test('can read openssl key pair', () {
AsymmetricKeyPair<PublicKey, PrivateKey> keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER);
expect(keyPair != null, equals(true));
ECPrivateKey keyPairPrivateKey = keyPair.privateKey;
ECPublicKey keyPairPublicKey = keyPair.publicKey;
expect(keyPairPrivateKey.d.intValue(), equals(kPrivateKeyD));
expect(keyPairPublicKey.Q.x.toBigInteger().intValue(), equals(kPublicKeyQx));
expect(keyPairPublicKey.Q.y.toBigInteger().intValue(), equals(kPublicKeyQy));
});
test('serializeManifest adds key and content-hash', () {
AsymmetricKeyPair<PublicKey, PrivateKey> keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER);
ECPublicKey keyPairPublicKey = keyPair.publicKey;
Uint8List manifestBytes = serializeManifest(kManifest, keyPairPublicKey, kTestBytes);
Map<String, dynamic> decodedManifest = JSON.decode(UTF8.decode(manifestBytes));
String expectedKey = BASE64.encode(keyPairPublicKey.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', () {
AsymmetricKeyPair<PublicKey, PrivateKey> keyPair = keyPairFromPrivateKeyBytes(kPrivateKeyDER);
ECPrivateKey keyPairPrivateKey = keyPair.privateKey;
ECPublicKey keyPairPublicKey = keyPair.publicKey;
Map<String, dynamic> manifest = JSON.decode(UTF8.decode(
serializeManifest(kManifest, keyPairPublicKey, kTestBytes))
);
Uint8List signatureBytes = signManifest(kTestBytes, keyPairPrivateKey);
bool verifies = verifyManifestSignature(manifest, kTestBytes, signatureBytes);
expect(verifies, equals(true));
// Ensure it fails with invalid signature or content.
Uint8List badBytes = new Uint8List.fromList(<int>[42]);
verifies = verifyManifestSignature(manifest, kTestBytes, badBytes);
expect(verifies, equals(false));
verifies = verifyManifestSignature(manifest, badBytes, signatureBytes);
expect(verifies, equals(false));
});
test('signing works with arbitrary key', () {
AsymmetricKeyPair<PublicKey, PrivateKey> keyPair = keyGenerator.generateKeyPair();
ECPrivateKey keyPairPrivateKey = keyPair.privateKey;
ECPublicKey keyPairPublicKey = keyPair.publicKey;
String failReason = 'offending private key: ${keyPairPrivateKey.d}';
Map<String, dynamic> manifest = JSON.decode(UTF8.decode(
serializeManifest(kManifest, keyPairPublicKey, kTestBytes))
);
Uint8List signatureBytes = signManifest(kTestBytes, keyPairPrivateKey);
bool verifies = verifyManifestSignature(manifest, kTestBytes, signatureBytes);
expect(verifies, equals(true), reason: failReason);
// Ensure it fails with invalid signature or content.
Uint8List badBytes = new Uint8List.fromList(<int>[42]);
verifies = verifyManifestSignature(manifest, kTestBytes, badBytes);
expect(verifies, equals(false), reason: failReason);
verifies = verifyManifestSignature(manifest, badBytes, signatureBytes);
expect(verifies, equals(false), reason: failReason);
});
test('verifyContentHash works', () async {
Stream<Uint8List> contentStream = new Stream<Uint8List>.fromIterable(kTestBytesList);
bool verifies = await verifyContentHash(new BigInteger(kTestHash), contentStream);
expect(verifies, equals(true));
// Ensure it fails with invalid hash or content.
contentStream = new Stream<Uint8List>.fromIterable(kTestBytesList);
verifies = await verifyContentHash(new BigInteger(0xdeadbeef), contentStream);
expect(verifies, equals(false));
Stream<Uint8List> badContentStream = new Stream<Uint8List>.fromIterable(
<Uint8List>[new Uint8List.fromList(<int>[42])]
);
verifies = await verifyContentHash(new BigInteger(kTestHash), badContentStream);
expect(verifies, equals(false));
});
}
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