Skip to content

Commit

Permalink
WIP: refactor for supporting revoking on Windows.
Browse files Browse the repository at this point in the history
  • Loading branch information
certaintls committed Jul 26, 2020
1 parent e825903 commit dc81a69
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 83 deletions.
23 changes: 10 additions & 13 deletions lib/certificate_detail.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class CertificateDetail extends StatelessWidget {
floatingActionButton: FloatingActionButton.extended(
onPressed: () => _handleDisableAction(context, cert),
icon: Icon(Icons.delete_forever),
label: Text('Disable'),
label: Text('Distrust'),
backgroundColor: Colors.red),
floatingActionButtonLocation: FloatingActionButtonLocation.endTop,
appBar: AppBar(
Expand Down Expand Up @@ -68,7 +68,7 @@ class CertificateDetail extends StatelessWidget {
Text(data.subject[X509Utils.DN['countryName']] ?? ''),
SizedBox(height: 10),
Text('Serial number:'),
Text(data.serialNumber?.toString()),
Text(data.serialNumber?.toString() ?? ''),
SizedBox(height: 20),
Text('Issued by:',
style: TextStyle(fontWeight: FontWeight.bold)),
Expand Down Expand Up @@ -102,7 +102,7 @@ class CertificateDetail extends StatelessWidget {
SizedBox(height: 5),
Text('SHA-256 fingerprint:'),
Text(StringUtils.addCharAtPosition(
data.publicKeyData.sha256Thumbprint, ' ', 2,
data.publicKeyData?.sha256Thumbprint ?? '', ' ', 2,
repeat: true)),
SizedBox(height: 10),
Text('SHA-1 fingerprint:'),
Expand All @@ -112,7 +112,7 @@ class CertificateDetail extends StatelessWidget {
SizedBox(height: 10),
Text('Subject Public Key Info (SPKI) fingerprint:'),
Text(StringUtils.addCharAtPosition(
data.publicKeyData?.sha256Thumbprint, ' ', 2,
data.publicKeyData?.sha256Thumbprint ?? '', ' ', 2,
repeat: true)),
SizedBox(height: 10),
]),
Expand All @@ -127,9 +127,10 @@ class CertificateDetail extends StatelessWidget {
context: ctx,
builder: (_) => AlertDialog(
title: Text('Disable ' + getTitle(cert.data) + '?'),
content: Text(
'Disabling certificate on Android through third party is not supported by the system.\n\n'
'However, CertainTLS cannot detect if you have disabled any certificates on Android.'),
content: Text(Platform.isAndroid
? 'Disabling certificate on Android through third party is not supported by the system.\n\n'
'However, CertainTLS cannot detect if you have disabled any certificates on Android.'
: 'Confirm?'),
actions: [
FlatButton(
onPressed: () => {Navigator.pop(ctx)}, child: Text('No')),
Expand All @@ -154,12 +155,8 @@ class CertificateDetail extends StatelessWidget {
Text('Failed to distrust ' + getTitle(cert.data) + '!'),
content: Text('Error: ' +
result.stderr +
'\n\n'
'You can either \n'
'1. Close CertainTLS and re-run it as root or \n'
'2. Execute the below command manually:\n\n'
'sudo security delete-certificate -Z ' +
cert.data.sha1Thumbprint),
'\n\n' +
distruster.getManualInstruction(cert)),
actions: [
FlatButton(
onPressed: () => {Navigator.pop(ctx)},
Expand Down
1 change: 1 addition & 0 deletions lib/certificate_distruster.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ import 'x509certificate.dart';
/// Windows calls the action "Revoke", but there is no "Reinstate"
abstract class CertificateDistruster {
ProcessResult distrust(X509Certificate cert);
String getManualInstruction(X509Certificate cert);
}
82 changes: 43 additions & 39 deletions lib/certificate_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,39 @@ class CertificateTile extends StatelessWidget {
Widget build(BuildContext context) {
X509CertificateData data = cert.data;
String commonName = getCommonName(data);
String org = getOrg(data);
String country = getCountry(data) != null ? ' (' + getCountry(data)+ ')' : '';
var title = Text(org + country);
var title = Text(getTitle(data));
var subtitle = Text(commonName);

return ListTile(title: title, subtitle: subtitle,
leading: _generateStatusIcon(cert.status),
trailing: Container(
width: 56,
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 6.0,
crossAxisSpacing: 6.0,
padding: const EdgeInsets.all(8.0),
children: [
Image.asset('images/google.png', color: cert.programs.contains('google') ? null : Colors.grey[300]),
Image.asset('images/microsoft.png', color: cert.programs.contains('microsoft') ? null : Colors.grey[300]),
Image.asset('images/apple.png', color: cert.programs.contains('apple') ? null : Colors.grey[300]),
Image.asset('images/mozilla.png', color: cert.programs.contains('mozilla') ? null : Colors.grey[300]),
],
)
)
);
return ListTile(
title: title,
subtitle: subtitle,
leading: _generateStatusIcon(cert.status),
trailing: Container(
width: 56,
child: GridView.count(
crossAxisCount: 2,
mainAxisSpacing: 6.0,
crossAxisSpacing: 6.0,
padding: const EdgeInsets.all(8.0),
children: [
Image.asset('images/google.png',
color: cert.programs.contains('google')
? null
: Colors.grey[300]),
Image.asset('images/microsoft.png',
color: cert.programs.contains('microsoft')
? null
: Colors.grey[300]),
Image.asset('images/apple.png',
color: cert.programs.contains('apple')
? null
: Colors.grey[300]),
Image.asset('images/mozilla.png',
color: cert.programs.contains('mozilla')
? null
: Colors.grey[300]),
],
)));
}

Widget _generateStatusIcon(String status) {
Expand All @@ -57,25 +67,19 @@ class CertificateTile extends StatelessWidget {
iconDisplay = Icon(Icons.priority_high, color: Colors.yellow[500]);
break;
}
return Stack(
alignment: Alignment.center,
children: [
IconButton(
icon: iconDisplay,
onPressed: null,
),
SizedBox(
return Stack(alignment: Alignment.center, children: [
IconButton(
icon: iconDisplay,
onPressed: null,
),
SizedBox(
height: 24.0,
width: 24.0,
child: Visibility(
visible: isInProgress,
child: CircularProgressIndicator(
strokeWidth: 2,
)
)
)
]
);
visible: isInProgress,
child: CircularProgressIndicator(
strokeWidth: 2,
)))
]);
}

}
}
16 changes: 3 additions & 13 deletions lib/certs_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'android_certificate_finder.dart';
import 'certaintls_server_verifier.dart';
import 'certificate_finder.dart';
import 'certificate_verifier.dart';
import 'macos_certificate_manager.dart';
import 'windows_certificate_finder.dart';
import 'windows_certificate_manager.dart';
import 'x509certificate.dart';

/// storeCerts[0] is the root certs
Expand All @@ -21,11 +20,9 @@ class CertsModel extends ChangeNotifier {
// The third list contains the problematic certs
List<List<X509Certificate>> storeCerts = List(3);
List<int> progress = [0, 0];
CertificateVerifier verifier;
CertificateDistruster distruster;
Map<String, String> stores;
String noUserCertsHelperText = 'No user installed certificates are found!';
String deleteCertConfirmText = '';

/// Back end entity reference
List<Identifier> certsRef = [];
Expand All @@ -38,21 +35,14 @@ class CertsModel extends ChangeNotifier {
noUserCertsHelperText =
"Due to Android security model, third party apps like CertainTLS do not have access to the user installed certificates.\n\n"
"However, a user can view the installer certificates via Android system UI:\n\n";
deleteCertConfirmText =
'Disabling certificate on Android through third party is not supported by the system.\n\n'
'However, CertainTLS cannot detect if you have disabled any certificates on Android.';
} else if (Platform.isMacOS) {
var manager = MacOSCertificateManager();
finder = manager;
verifier = manager;
distruster = manager;
deleteCertConfirmText =
'Deleting a certificate on MacOS requires root permission. You can either close and re-run CertainTLS as root or'
' manually run the command at the next step.';
} else if (Platform.isWindows) {
var manager = WindowsCertificateFinder();
var manager = WindowsCertificateManager();
finder = manager;
verifier = manager;
distruster = manager;
}
stores = finder.getCertStores();
storeCerts[0] = finder.getCertsByStore(stores.values.toList()[0]);
Expand Down
9 changes: 9 additions & 0 deletions lib/macos_certificate_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ class MacOSCertificateManager
return Process.runSync(
'security', ['delete-certificate', '-Z', cert.data.sha1Thumbprint]);
}

@override
String getManualInstruction(X509Certificate cert) {
return 'You can either \n'
'1. Close CertainTLS and re-run it as root or \n'
'2. Execute the below command manually:\n\n'
'sudo security delete-certificate -Z ' +
cert.data.sha1Thumbprint;
}
}

class AppleCertificateInfo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io';
import 'package:async/async.dart';
import 'package:basic_utils/basic_utils.dart';
import 'package:certaintls/certificate_distruster.dart';
import 'package:certaintls/certificate_finder.dart';
import 'package:certaintls/certificate_verifier.dart';
import 'package:certaintls/x509certificate.dart';
Expand All @@ -10,8 +11,8 @@ import 'package:html/parser.dart';
import 'package:http/http.dart';
import 'package:pem/pem.dart';

class WindowsCertificateFinder
implements CertificateFinder, CertificateVerifier {
class WindowsCertificateManager
implements CertificateFinder, CertificateVerifier, CertificateDistruster {
static String systemTrustedCertsPath = 'Root';
static String userInstalledCertsPath = 'My';
static RegExp delimiter =
Expand Down Expand Up @@ -44,10 +45,11 @@ class WindowsCertificateFinder
subject.putIfAbsent('2.5.4.11', () => tokenize(s, 'OU='));
subject.putIfAbsent('2.5.4.6', () => tokenize(s, ' C='));
X509CertificateData data = X509CertificateData(
sha1Thumbprint: tokenize(s, 'Cert Hash(sha1): ').toUpperCase(),
sha256Thumbprint: tokenize(s, 'Cert Hash(sha256): ').toUpperCase(),
subject: subject,
);
sha1Thumbprint: tokenize(s, 'Cert Hash(sha1): ').toUpperCase(),
sha256Thumbprint: tokenize(s, 'Cert Hash(sha256): ').toUpperCase(),
subject: subject,
serialNumber:
BigInt.parse(tokenize(s, 'Serial Number: '), radix: 16));
certs.add(X509Certificate(data: data));
}
});
Expand Down Expand Up @@ -161,6 +163,22 @@ class WindowsCertificateFinder
} else
return null;
}

@override
ProcessResult distrust(X509Certificate cert) {
// Command doc: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/certutil#-revoke
return Process.runSync(
'CertUtil', ['-revoke', cert.data.serialNumber.toString()]);
}

@override
String getManualInstruction(X509Certificate cert) {
return 'You can either \n'
'1. Close CertainTLS and re-run it as root or \n'
'2. Execute the below command manually:\n\n'
'CertUtil -revoke ' +
cert.data.serialNumber.toString();
}
}

class MicroSoftCertificateInfo {
Expand Down
5 changes: 3 additions & 2 deletions lib/x509certificate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,9 @@ Future<X509CertificateData> certDownload(String url, {Client client}) async {
return null;
}

String getTitle(X509CertificateData data) =>
getOrg(data) + ' (' + (getCountry(data) ?? '') + ')';
String getTitle(X509CertificateData data) => getCountry(data) != null
? getOrg(data) + ' (' + getCountry(data) + ')'
: getOrg(data);

String getSubtitle(X509CertificateData data) => getCommonName(data);

Expand Down
18 changes: 12 additions & 6 deletions test/dependecy_test.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
@Timeout(const Duration(seconds: 1800))

import 'package:basic_utils/basic_utils.dart';
import 'package:certaintls/windows_certificate_finder.dart';
import 'package:certaintls/windows_certificate_manager.dart';
import 'package:certaintls/x509certificate.dart';
import 'package:test/test.dart';

void main() {
test('X509CertificateData can be constructed from imcomplete data returned by CertUtil on Windows', () async {
test(
'X509CertificateData can be constructed from imcomplete data returned by CertUtil on Windows',
() async {
var subject = <String, String>{};
subject.putIfAbsent('2.5.4.3', () => 'A-Trust-Root-07');
subject.putIfAbsent('2.5.4.10', () => 'A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH');
subject.putIfAbsent(
'2.5.4.10',
() =>
'A-Trust Ges. f. Sicherheitssysteme im elektr. Datenverkehr GmbH');
subject.putIfAbsent('2.5.4.11', () => 'A-Trust-Root-07');
subject.putIfAbsent('2.5.4.6', () => 'AT');
X509CertificateData data = X509CertificateData(
version: 3,
sha256Thumbprint: '8AC552AD577E37AD2C6808D72AA331D6A96B4B3FEBFF34CE9BC0578E08055EC3',
sha256Thumbprint:
'8AC552AD577E37AD2C6808D72AA331D6A96B4B3FEBFF34CE9BC0578E08055EC3',
subject: subject,
);

var finder = WindowsCertificateFinder();
var finder = WindowsCertificateManager();
var cert = X509Certificate(data: data);
await finder.verify(cert);
expect(cert.status, X509CertificateStatus.statusVerified);
});
}
}
8 changes: 4 additions & 4 deletions test/windows_check_certificates.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@Timeout(const Duration(seconds: 1800))

import 'package:certaintls/windows_certificate_finder.dart';
import 'package:certaintls/windows_certificate_manager.dart';
import 'package:certaintls/x509certificate.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:test/test.dart';
Expand All @@ -27,9 +27,9 @@ void main() async {
final program = 'microsoft';

test('Check Windows stock CA root certificates', () async {
var finder = WindowsCertificateFinder();
var certs =
finder.getCertsByStore(WindowsCertificateFinder.systemTrustedCertsPath);
var finder = WindowsCertificateManager();
var certs = finder
.getCertsByStore(WindowsCertificateManager.systemTrustedCertsPath);
if (!ignoreLocalCerts) {
await finder.verifyAll(certs);
print(
Expand Down

0 comments on commit dc81a69

Please sign in to comment.