-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SSL Public key Pinning support for SecurityContext #35981
Comments
I'm also in need of such a feature. Even certificate pinning I found that is only possible in dart to add a self-signed certificate as trusted (in the badCertificateCallback), but I couldn't find any way of validating myself in every call the certificate of the server I'm connecting to. In a MITM attack the default security context implementation wouldn't be enough. |
@rsilvr Are you saying that certificate pinning doesn't work with arbitrary certificates? Can anyone confirm this? This would make this functionality pretty much useless. |
@i-schuetz factory SecurityContext({bool withTrustedRoots: false}); by setting withTrustedRoots to false will deny all request even if they are root trusted certificates and will fallback to badCertificateCallback. Then you can verify your self signed certificates. |
@sandeepcmsm Thank you a lot for making this clear! I've tried something similar to this some months ago but I have no idea why it wasn't working before... |
@i-schuetz what do you mean by "This would make this functionality pretty much useless." There is no functionality to pin a key at all. At least that i am aware of. The only thing that comes close is to use All you can do currently is "first check the public key" with a webservice call and validation deactivated (a "pilot" call) and after that activate it (with The only "right" way is to either have a callback for all requests the gives us the Certificate (like in Because what we want is to validate AND check the public key on the same webservice call uninterrupted (roughly on the same connection). |
I think you missed a point. Public key cryptography works because the public and private keys are linked by math. If you encrypt with the public key, only someone holding the private key can decrypt. If an attacker makes a new certificate using the same public key, they cannot use it because they don't know the corresponding private key. SSL/TLS connection setup will fail.
So I believe one can implement certificate pinning just by checking the public key of the certificate. That's the only thing that matters for security. |
I believe that certificate pinning is already possible without this feature request:
It's possible that
|
Key rotation reduces risk. When an attacker obtains an old server hard drive or backup file and gets an old server private key from it, they cannot impersonate the current server if the key has been rotated. Therefore always generate a new key when updating certificates. Configure the client to trust the old key and the new key. Wait for your users to update to the new version of the client. Then deploy the new key to your servers. Then you can remove the old key from the client. The sole purpose of this feature request is to facilitate key pinning without rotation, a bad security practice. Please comment and explain if you disagree. Otherwise, let's close this feature request. Below are several examples certificate pinning, supporting the good security practice of annual key rotation. 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);
} An alternative to pinning the public key is to use a certificate authority. Create the 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 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);
} |
Hi, relatively new to Flutter here (and programming in general). Only familiar with the more basic stuffs but I've now encountered the need to use a CertificatePinner such as this in flutter: All I have is the public key in the form of a string like shown below, nothing else: How do I go about achieving this? Sorry if there's already an answer because I don't seem to fully understand the finer details of what's been written above in order to achieve what I could in java with these few lines of code:
Thanks in advance |
Ideally, it should be possible to get the hash of the SubjectPublicKeyInfo string as described in https://www.imperialviolet.org/2011/05/04/pinning.html to avoid possible reinterpretation attacks when the raw public key is used on a different curve e.g. This most likely is already possible by manually parsing the provided DER/PEM representation in the |
@nioncode, Ideally it should contain the whole keychain with leaf, intermediate and root certificates. |
@mleonhard, I agree with you. Having key rotation enabled is the way to go. It is the recommended way and should be used in such scenarios. The other solution where we let Other Platforms still allow some way of Public Key pinning. The only option we have in dart is to pin the actual certificate (or create a self signed certificate). Even though it is the best solution, there should be an alternative. |
@SahilPatel16 are you sure that the certificate returned in the EDIT: seems to be correct and the corresponding issue is #39425 |
Any update on this? It's impossible to pin our own certificate, the httpClient only returns CA certificate |
Certificate Pinning is possible using setTrustedCertificatesBytes. However, HPKP is not possible as you've already mentioned. |
@yapcwed if you have figured out how to get certificate pinning to work in dartlang, please share details on how you accomplished it. When I tried to get it working, I was thwarted by these bugs:
|
@mleonhard only way I found to retrieve the correct certificate was using HttpClientResponse object
Here you can access:
Etc and it has the correct certificate, instead of CA certificate only. I somehow made this to be compatible with "http" by returning response like this:
I would prefer to do the pinning normally at badCertificateCallback, but until it is fixed, this was the only way I found to perform certificate pinning, at response level |
@TMSantos Thanks for sharing your workaround. It checks the certificate after sending the request data. I want to verify the certificate before that happens. Is there a way to make the TLS connection first, check the server certificate, and then do an HTTP request over the TLS channel? |
@mleonhard Ah I forgot to mention that my use case is probably different. I have a flutter app that connects to a specific endpoint. To prevent MITM attacks I pin the gateway's certificate using static http.Client getSecureClient(List<List<int>> certificates) {
final context = SecurityContext(withTrustedRoots: false);
certificates.forEach((cert) {
context.setTrustedCertificatesBytes(cert);
});
return IOClient(HttpClient(context: context));
} |
@mleonhard that's the optimal scenario, is also what I want, but didn't find an workaround to do the check before sending the request data yet |
Any progress on this front? I've tried all the suggestions and if I'm understanding correctly public key pinning is not possible right now if you don't have the possibility to use your own CA, which I unfortunately don't. The two aforementioned bugs seem to render this impossible for now? |
Any progress on this issue? public key pinning is a very common way to aviod MIMT, suggest Dart could implement it ASAP officially. Thanks. |
Just to hang in and connect these two issues : #47695 Access to full certificate chain would be great. |
I just released a package that does public key pinning (SHA-256 of SPKI): https://pub.dev/packages/certificate_pinning_httpclient But it only works on Android and iOS right now. It downloads the certificate chain once via a method channel and uses them |
We wanted to check the sha1 of the intermediate certificate & the common name of the leaf certificate. But this is also not possible |
Support for pinning base64 SHA-256 hashes as in HTTP Public Key Pinning (HPKP) or SHA-1 base64 hashes in SecurityContext.
Support for Subject Public Key Info in X509Certificate.
normally in native android and ios apps key pinning done similar like this https://square.github.io/okhttp/3.x/okhttp/okhttp3/CertificatePinner.html
The text was updated successfully, but these errors were encountered: