How to do SSL pinning via self generated signed certificates in flutter?
Solution 1:
There isn't enough detail in the question, so this answer is based on some assumptions:
- Your APIs are HTTPS
- You are talking about validating a server-side self-signed HTTPS certificate
- You are using
package:http
as the http client - No client-side certificates
package:http
uses dart:io HttpClient
under the hood, and HttpClient
has a several features to allow for certificate validation. Since a self-signed server certificate will be untrusted by the client, the client will call the badCertificateCallback
allowing you to validate the server certificate yourself, for example:
HttpClient httpClient = new HttpClient()
..badCertificateCallback =
((X509Certificate cert, String host, int port) {
// tests that cert is self signed, correct subject and correct date(s)
return (cert.issuer == cert.subject &&
cert.subject == 'MySelfSignedCertCN' &&
cert.endValidity.millisecondsSinceEpoch == 1234567890);
});
IOClient ioClient = new IOClient(httpClient);
// use ioClient to perform get/post operations from package:http
// don't forget to call ioClient.close() when done
// note, this also closes the underlying HttpClient
Solution 2:
Create a self-signed certificate every year and have the clients trust only this year's and last year's certificates:
import 'dart:io'
show
BytesBuilder,
File,
HttpClient,
HttpClientRequest,
HttpClientResponse,
HttpHeaders,
HttpRequest,
HttpServer,
InternetAddress,
Process,
stderr,
stdout,
SecurityContext;
import 'dart:convert' show utf8;
Future<void> shellCommand(String command) async {
print('Executing command $command');
final Process process = await Process.start('sh', ['-c', command]);
stdout.addStream(process.stdout);
stderr.addStream(process.stderr);
final int exitCode = await process.exitCode;
if (exitCode != 0) {
throw new Exception('Process exited with status $exitCode');
}
}
void main() async {
// Last year's certificate:
await shellCommand(
'openssl req -newkey rsa:2048 -passout pass:password -keyout privatekey2018.pem -subj "/CN=localhost" -days 731 -x509 -out certificate2018.pem');
// This year's certificate:
await shellCommand(
'openssl req -newkey rsa:2048 -passout pass:password -keyout privatekey2019.pem -subj "/CN=localhost" -days 731 -x509 -out certificate2019.pem');
final SecurityContext serverSecurityContext = new SecurityContext();
serverSecurityContext.useCertificateChainBytes(
await new File('certificate2019.pem').readAsBytes());
serverSecurityContext.usePrivateKey('privatekey2019.pem',
password: 'password');
final HttpServer httpServer = await HttpServer.bindSecure(
InternetAddress.loopbackIPv4, 0, serverSecurityContext);
httpServer.listen((HttpRequest request) {
request.response.write('body1');
request.response.close();
});
print('Server listening at https://localhost:${httpServer.port}/');
print('Making request.');
final SecurityContext clientSecurityContext =
new SecurityContext(withTrustedRoots: false);
clientSecurityContext.setTrustedCertificatesBytes(
await new File('certificate2018.pem').readAsBytes());
clientSecurityContext.setTrustedCertificatesBytes(
await new File('certificate2019.pem').readAsBytes());
final HttpClient httpClient = new HttpClient(context: clientSecurityContext);
final HttpClientRequest request = await httpClient.getUrl(Uri(
scheme: 'https', host: 'localhost', port: httpServer.port, path: '/'));
final HttpClientResponse response = await request.close();
final List<int> bytes = await response.fold(new BytesBuilder(),
(BytesBuilder bytesBuilder, List<int> bytes) {
bytesBuilder.add(bytes);
return bytesBuilder;
}).then((BytesBuilder bytesBuilder) => bytesBuilder.takeBytes());
final String contenType =
response.headers.value(HttpHeaders.contentTypeHeader) ?? '';
print('${response.statusCode} ${response.reasonPhrase} '
'content-type="$contenType" body="${utf8.decode(bytes)}"');
httpServer.close(force: true);
}
Create self-signed certificates and have the client trust only those certificates, and ignore the hostname in the request. This is useful when using Terraform to deploy the server to AWS Elastic Beanstalk. The server binary blob must contain the certificates, yet the server's hostname is not known until the deployment completes.
import 'dart:io'
show
BytesBuilder,
File,
HttpClient,
HttpClientRequest,
HttpClientResponse,
HttpHeaders,
HttpRequest,
HttpServer,
InternetAddress,
Process,
stderr,
stdout,
SecurityContext,
X509Certificate;
import 'dart:convert' show utf8;
Future<void> shellCommand(String command) async {
print('Executing command $command');
final Process process = await Process.start('sh', ['-c', command]);
stdout.addStream(process.stdout);
stderr.addStream(process.stderr);
final int exitCode = await process.exitCode;
if (exitCode != 0) {
throw new Exception('Process exited with status $exitCode');
}
}
void main() async {
// Last year's certificate:
await shellCommand(
'openssl req -newkey rsa:2048 -passout pass:password -keyout privatekey2018.pem -subj "/CN=localhost" -days 731 -x509 -out certificate2018.pem');
// This year's certificate:
await shellCommand(
'openssl req -newkey rsa:2048 -passout pass:password -keyout privatekey2019.pem -subj "/CN=localhost" -days 731 -x509 -out certificate2019.pem');
final SecurityContext serverSecurityContext = new SecurityContext();
serverSecurityContext.useCertificateChainBytes(
await new File('certificate2019.pem').readAsBytes());
serverSecurityContext.usePrivateKey('privatekey2019.pem',
password: 'password');
final HttpServer httpServer = await HttpServer.bindSecure(
InternetAddress.loopbackIPv4, 0, serverSecurityContext);
httpServer.listen((HttpRequest request) {
request.response.write('body1');
request.response.close();
});
print('Server listening at https://localhost:${httpServer.port}/');
print('Making request.');
final SecurityContext clientSecurityContext =
new SecurityContext(withTrustedRoots: false);
final HttpClient httpClient = new HttpClient(context: clientSecurityContext);
final List<String> certificatePemStrings = [
await new File('certificate2018.pem').readAsString(),
await new File('certificate2019.pem').readAsString()
];
httpClient.badCertificateCallback =
(X509Certificate cert, String host, int port) => certificatePemStrings
.any((certificatePemString) => cert.pem == certificatePemString);
final HttpClientRequest request = await httpClient.getUrl(Uri(
scheme: 'https', host: 'localhost', port: httpServer.port, path: '/'));
final HttpClientResponse response = await request.close();
final List<int> bytes = await response.fold(new BytesBuilder(),
(BytesBuilder bytesBuilder, List<int> bytes) {
bytesBuilder.add(bytes);
return bytesBuilder;
}).then((BytesBuilder bytesBuilder) => bytesBuilder.takeBytes());
final String contenType =
response.headers.value(HttpHeaders.contentTypeHeader) ?? '';
print('${response.statusCode} ${response.reasonPhrase} '
'content-type="$contenType" body="${utf8.decode(bytes)}"');
httpServer.close(force: true);
}
Create certificate authority files on a secure laptop and keep them on a removable drive in a safe. Whenever you need a new certificate, get the removable drive and generate and sign a new server certificate. Here's an example of the openssl
commands to run and how to configure Dart to trust only certificates signed by your certificate authority:
import 'dart:io'
show
BytesBuilder,
File,
HttpClient,
HttpClientRequest,
HttpClientResponse,
HttpHeaders,
HttpRequest,
HttpServer,
InternetAddress,
Process,
SecurityContext,
stderr,
stdout;
import 'dart:convert' show utf8;
Future<void> shellCommand(String command) async {
print('Executing command $command');
final Process process = await Process.start('sh', ['-c', command]);
stdout.addStream(process.stdout);
stderr.addStream(process.stderr);
final int exitCode = await process.exitCode;
if (exitCode != 0) {
throw new Exception('Process exited with status $exitCode');
}
}
void main() async {
// Last year's certificates:
await shellCommand(
'openssl req -newkey rsa:2048 -nodes -keyout ca2018.privatekey.pem -subj "/OU=CA" -days 731 -x509 -out ca2018.certificate.pem');
// This year's certificates:
await shellCommand(
'openssl req -newkey rsa:2048 -nodes -keyout ca2019.privatekey.pem -subj "/OU=CA" -days 731 -x509 -out ca2019.certificate.pem');
await shellCommand(
'openssl req -newkey rsa:2048 -passout pass:password -keyout privatekey.pem -subj "/CN=localhost" -days 731 -sha256 -new -out csr2019.pem');
await shellCommand(
'openssl x509 -req -in csr2019.pem -CA ca2019.certificate.pem -CAkey ca2019.privatekey.pem -set_serial 1 -days 730 -sha256 -out certificate2019.pem');
await shellCommand(
'cat certificate2019.pem ca2019.certificate.pem > certificate2019.chain.pem');
final SecurityContext serverSecurityContext = new SecurityContext();
serverSecurityContext.useCertificateChainBytes(
await new File('certificate2019.chain.pem').readAsBytes());
serverSecurityContext.usePrivateKey('privatekey.pem', password: 'password');
final HttpServer httpServer = await HttpServer.bindSecure(
InternetAddress.loopbackIPv4, 0, serverSecurityContext);
httpServer.listen((HttpRequest request) {
request.response.write('body1');
request.response.close();
});
print('Server listening at https://localhost:${httpServer.port}/');
print('Making request.');
final SecurityContext clientSecurityContext =
new SecurityContext(withTrustedRoots: false);
clientSecurityContext.setTrustedCertificatesBytes(
await new File('ca2018.certificate.pem').readAsBytes());
clientSecurityContext.setTrustedCertificatesBytes(
await new File('ca2019.certificate.pem').readAsBytes());
final HttpClient httpClient = new HttpClient(context: clientSecurityContext);
final HttpClientRequest request = await httpClient.getUrl(Uri(
scheme: 'https', host: 'localhost', port: httpServer.port, path: '/'));
final HttpClientResponse response = await request.close();
final List<int> bytes = await response.fold(new BytesBuilder(),
(BytesBuilder bytesBuilder, List<int> bytes) {
bytesBuilder.add(bytes);
return bytesBuilder;
}).then((BytesBuilder bytesBuilder) => bytesBuilder.takeBytes());
final String contenType =
response.headers.value(HttpHeaders.contentTypeHeader) ?? '';
print('${response.statusCode} ${response.reasonPhrase} '
'content-type="$contenType" body="${utf8.decode(bytes)}"');
httpServer.close(force: true);
}