Commit b458935b authored by Matt Perry's avatar Matt Perry

Support signing flx packages with ECDSA key pair

Adds a --private-key option to the build command, which specifies an ECDSA
private key. When this is provided along with a manifest, the manifest is
prepended to the .flx package and signed with the private key. The manifest
also includes a SHA-256 hash of the zipped content portion of the .flx package.

This is used by the Flutter updater package, to verify that updates are
from the right publisher.
parent 5670243b
......@@ -4,10 +4,14 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:cipher/cipher.dart';
import 'package:cipher/impl/client.dart';
import 'package:yaml/yaml.dart';
import '../signing.dart';
import '../toolchain.dart';
import 'flutter_command.dart';
......@@ -96,6 +100,16 @@ ArchiveFile _createFile(String key, String assetBase) {
return new ArchiveFile.noCompress(key, content.length, content);
}
// Writes a 32-bit length followed by the content of [bytes].
void _writeBytesWithLength(File 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.writeAsBytesSync(length.buffer.asUint8List(), mode: FileMode.APPEND);
outputFile.writeAsBytesSync(bytes, mode: FileMode.APPEND);
}
ArchiveFile _createSnapshotFile(String snapshotPath) {
File file = new File(snapshotPath);
List<int> content = file.readAsBytesSync();
......@@ -106,6 +120,7 @@ const String _kDefaultAssetBase = 'packages/material_design_icons/icons';
const String _kDefaultMainPath = 'lib/main.dart';
const String _kDefaultOutputPath = 'app.flx';
const String _kDefaultSnapshotPath = 'snapshot_blob.bin';
const String _kDefaultPrivateKeyPath = 'privatekey.der';
class BuildCommand extends FlutterCommand {
final String name = 'build';
......@@ -113,15 +128,18 @@ class BuildCommand extends FlutterCommand {
BuildCommand() {
argParser.addOption('asset-base', defaultsTo: _kDefaultAssetBase);
argParser.addOption('compiler');
argParser.addOption('main', defaultsTo: _kDefaultMainPath);
argParser.addOption('manifest');
argParser.addOption('private-key', defaultsTo: _kDefaultPrivateKeyPath);
argParser.addOption('output-file', abbr: 'o', defaultsTo: _kDefaultOutputPath);
argParser.addOption('snapshot', defaultsTo: _kDefaultSnapshotPath);
}
@override
Future<int> run() async {
initCipher();
String compilerPath = argResults['compiler'];
if (compilerPath == null)
......@@ -134,7 +152,8 @@ class BuildCommand extends FlutterCommand {
mainPath: argResults['main'],
manifestPath: argResults['manifest'],
outputPath: argResults['output-file'],
snapshotPath: argResults['snapshot']
snapshotPath: argResults['snapshot'],
privateKeyPath: argResults['private-key']
);
}
......@@ -143,7 +162,8 @@ class BuildCommand extends FlutterCommand {
String mainPath: _kDefaultMainPath,
String manifestPath,
String outputPath: _kDefaultOutputPath,
String snapshotPath: _kDefaultSnapshotPath
String snapshotPath: _kDefaultSnapshotPath,
String privateKeyPath: _kDefaultPrivateKeyPath
}) async {
Map manifestDescriptor = _loadManifest(manifestPath);
......@@ -167,10 +187,16 @@ class BuildCommand extends FlutterCommand {
archive.addFile(file);
}
ECPrivateKey privateKey = await loadPrivateKey(privateKeyPath);
ECPublicKey publicKey = publicKeyFromPrivateKey(privateKey);
File outputFile = new File(outputPath);
outputFile.writeAsStringSync('#!mojo mojo:sky_viewer\n');
outputFile.writeAsBytesSync(
new ZipEncoder().encode(archive), mode: FileMode.APPEND, flush: true);
Uint8List zipBytes = new Uint8List.fromList(new ZipEncoder().encode(archive));
Uint8List manifestBytes = serializeManifest(manifestDescriptor, publicKey, zipBytes);
_writeBytesWithLength(outputFile, signManifest(manifestBytes, privateKey));
_writeBytesWithLength(outputFile, manifestBytes);
outputFile.writeAsBytesSync(zipBytes, mode: FileMode.APPEND, flush: true);
return 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';
import 'package:asn1lib/asn1lib.dart';
import 'package:bignum/bignum.dart';
import 'package:cipher/cipher.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();
SecureRandom _initRandom() {
// TODO(mpcomplete): Provide a better seed here. External entropy source?
final Uint8List key = new Uint8List(16);
final KeyParameter keyParam = new KeyParameter(key);
final ParametersWithIV params = new ParametersWithIV(keyParam, new Uint8List(16));
SecureRandom random = new SecureRandom('AES/CTR/AUTO-SEED-PRNG')
..seed(params);
return random;
}
// Returns a serialized manifest, with the public key and hash of the content
// included.
Uint8List serializeManifest(Map manifestDescriptor, ECPublicKey publicKey, Uint8List zipBytes) {
if (manifestDescriptor == null)
return null;
final List<String> kSavedKeys = [
'name',
'version',
'update-url'
];
Map outputManifest = new Map();
manifestDescriptor.forEach((key, value) {
if (kSavedKeys.contains(key))
outputManifest[key] = value;
});
if (publicKey != null)
outputManifest['key'] = BASE64.encode(publicKey.Q.getEncoded());
Uint8List zipHash = new Digest(kHashAlgorithm).process(zipBytes);
BigInteger zipHashInt = new BigInteger.fromBytes(1, zipHash);
outputManifest['content-hash'] = zipHashInt.intValue();
return new Uint8List.fromList(UTF8.encode(JSON.encode(outputManifest)));
}
// Returns the ASN.1 encoded signature of the input manifestBytes.
List<int> signManifest(Uint8List manifestBytes, ECPrivateKey privateKey) {
if (manifestBytes == null || privateKey == null)
return [];
Signer signer = new Signer(kSignerAlgorithm);
PrivateKeyParameter params = new PrivateKeyParameter(privateKey);
signer.init(true, new ParametersWithRandom(params, _random));
ECSignature signature = signer.generateSignature(manifestBytes);
ASN1Sequence asn1 = new ASN1Sequence()
..add(new ASN1Integer(signature.r))
..add(new ASN1Integer(signature.s));
return asn1.encodedBytes;
}
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);
}
Future<ECPrivateKey> loadPrivateKey(String privateKeyPath) async {
File file = new File(privateKeyPath);
if (!file.existsSync())
return null;
List<int> bytes = file.readAsBytesSync();
return _asn1ParsePrivateKey(_ecDomain, new Uint8List.fromList(bytes));
}
ECPublicKey publicKeyFromPrivateKey(ECPrivateKey privateKey) {
if (privateKey == null)
return null;
ECPoint Q = privateKey.parameters.G * privateKey.d;
return new ECPublicKey(Q, privateKey.parameters);
}
......@@ -10,6 +10,8 @@ environment:
dependencies:
archive: ^1.0.20
args: ^0.13.0
asn1lib: ^0.4.1
cipher: ^0.7.1
mustache4dart: ^1.0.0
path: ^1.3.0
shelf_route: ^0.13.4
......
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