Flutter – TLS Certificate Pinning Sample

บทนำ (Overview)

TLS Certificate Pinning ช่วยทำให้แอปสามารถตรวจสอบได้ว่าบริการ TLS/SSL ที่กำลังจะเชื่อมต่อนั้นเป็นเป็นบริการที่รันอยู่บน server เดียวกันกับที่แอปรู้จักไหม เพื่อป้องกันการดัก traffic ด้วย proxy ในเบื้องต้นก่อน (ความเป็นจริงแล้วการ implement security control ฝั่ง client side มันย่อมมีแนวโน้มว่าจะถูก bypass ได้ แต่ทำไว้ยังดีกว่าไม่ทำเลย) หลายครั้งที่ต้องเรสประเด็นว่าแอปไม่มีการตรวจสอบ TLS certificate ของบริการ TLS/SSL ที่ตัวมันต้องเชื่อมต่อยิ่งเป็นแอปที่มีความสำคัญพวก financial app แล้วยิ่งต้องถูกกำกับด้วยข้อบังคับต่าง ๆ นานา ผู้พัฒนาแอปจึงจำเป็นที่ต้องพัฒนาแอปให้สอดคล้องกับข้อบังคับเหล่านั้น

ในบทความนี้ขอนำเสนอวิธีการทำ TLS certificate pinning บนแอปที่พัฒนาโดยใช้ Flutter framework กันอย่างง่าย ๆ ด้วยการใช้เมธอด setTrustedCertificatesBytes() ของคลาส SecurityContext

บทความโดย
Mr.Juttikhun Jirathanan
Cyber Security Researcher

Flutter – TLS Certificate Pinning

เก็บ Certificate ของ Server ตามมาตรฐานตระกูล X.509 ในรูปแบบ PEM

หลังจากที่พัฒนาแอป ติดตั้ง HTTPS service และตั้งค่า cipher suite ที่รองรับบน server เรียบร้อยแล้ว เราต้องรู้ก่อนว่า TLS certificate ของ service HTTPS ที่แอปต้องเชื่อมต่อนั้นคืออะไร ใช้คำสั่ง openssl ต่อไปนี้ในการเก็บ TLS certificate ในรูปแบบ PEM

โค้ดต่อไปนี้เป็นตัวอย่างการเก็บ​ TLS/SSL Certificate ของ https://httpbin.org

$ echo -n | openssl s_client -connect httpbin.org:443 -servername httpbin.org | openssl x509 > httpbin.org.cer 

ต่อไปนี้เป็นหน้าตาของ TLS/SSL certificate ที่ได้ในรูปแบบ PEM หน้าตาของมันจะประมาณนี้

-----BEGIN CERTIFICATE-----
MIIF3DCCBMSgAwIBAgIQAVjzCtWdYq8gnxXuT8K7MzANBgkqhkiG9w0BAQsFADBG
MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRUwEwYDVQQLEwxTZXJ2ZXIg
Q0EgMUIxDzANBgNVBAMTBkFtYXpvbjAeFw0yMTExMjEwMDAwMDBaFw0yMjEyMTky
MzU5NTlaMBYxFDASBgNVBAMTC2h0dHBiaW4ub3JnMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAhOQnpezrwA0vHzf47Pa+O84fWue/562TqQrVirtf+3fs
GQd3MmwnId+ksAGQvWN4M1/hSelYJb246pFqGB7t+ZI+vjBYH4/J6CiFsKwzusqk
SF63ftQh8Ox0OasB9HvRlOPHT/B5Dskh8HNiJ+1lExSZEaO9zsQ9wO62bsGHsMX/
UP3VQByXLVBZu0DMKsl2hGaUNy9+LgZv4/iVpWDPQ1+khpfxP9x1H+mMlUWBgYPq
7jG5ceTbltIoF/sUQPNR+yKIBSnuiISXFHO9HEnk5ph610hWmVQKIrCAPsAUMM9m
6+iDb64NjrMjWV/bkm36r+FBMz9L8HfEB4hxlwwg5QIDAQABo4IC9DCCAvAwHwYD
VR0jBBgwFoAUWaRmBlKge5WSPKOUByeWdFv5PdAwHQYDVR0OBBYEFM8HhLgDSKzJ
rNsRZQp9Kf/Wl0uzMCUGA1UdEQQeMByCC2h0dHBiaW4ub3Jngg0qLmh0dHBiaW4u
b3JnMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
AwIwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2NybC5zY2ExYi5hbWF6b250cnVz
dC5jb20vc2NhMWItMS5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwdQYIKwYBBQUH
AQEEaTBnMC0GCCsGAQUFBzABhiFodHRwOi8vb2NzcC5zY2ExYi5hbWF6b250cnVz
dC5jb20wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuc2NhMWIuYW1hem9udHJ1c3Qu
Y29tL3NjYTFiLmNydDAMBgNVHRMBAf8EAjAAMIIBfQYKKwYBBAHWeQIEAgSCAW0E
ggFpAWcAdgApeb7wnjk5IfBWc59jpXflvld9nGAK+PlNXSZcJV3HhAAAAX1Acwi1
AAAEAwBHMEUCIF3Y8cwUmZ9cmYSclNrSYOhYDeCzSTAYHwpIhp6oAHKBAiEA/8nK
wN0G3SwUiS3/NdzlVuuakr4a7oviAzN7zXiSmzcAdgBRo7D1/QF5nFZtuDd4jwyk
eswbJ8v3nohCmg3+1IsF5QAAAX1AcwjJAAAEAwBHMEUCIH4pZ7551jQOJ/lV20sD
KNCWOLdt+cS2pjUIFEpI8nwlAiEAj2hxpivIPtX9tReEcJCaAuC5Gh90mZz9lWQy
usID0VQAdQDfpV6raIJPH2yt7rhfTj5a6s2iEqRqXo47EsAgRFwqcwAAAX1Acwii
AAAEAwBGMEQCIEyeaMOsy5LSsKkIod2CfBML3J/+CwjvJekdMBI4QYI2AiAYKdpD
ptDftXG7GSOz8SgpqRtUoWIHs1woSj7uwEJwuzANBgkqhkiG9w0BAQsFAAOCAQEA
L2Qd0308BkF7ahyUYJkxkrfr4WyyrO7SW/TsNpSmxqPF+D/QqQcBt8tPHWg1oNEc
UYinl5qtA4kyHqpAlgzYl04FUpShkNDjcwd1GikgmNMIhSGx3EHaQeyHvrKIgCRe
TK1fPPxDvFU2ao9nnEfiQ0OossRVC6EaJsQ+/CEnTir0BEPjWRjW2C/g9YOHRyP9
PO4R/58KOy8pdJZwWkOyGKylZemsLy6sR8h3UE0KW0TawMwGO+sjWU2eB/uOJ6Yc
aAS0og1S7NrLDqT3HUWSf81g7qlNeC3hNgI8fMxFLPTkhn8+v220SJipi6ignkJR
VFoC1aTFolXMq/oMqRqUHA==
-----END CERTIFICATE-----

ตัวอย่าง Dart code เพื่อทำ TLS/SSL certificate pinning

ต่อไปนี้เป็นโค้ดภาษา Dart ที่ใช้ใน Flutter framework เพื่อทำ TLS/SSL certificate pinning

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/adapter.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
     return MaterialApp(
         home: Home()
      );
  }
}

class Home extends  StatefulWidget {
  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  late Response response;
  Dio dio = Dio();

  bool error = false; //for error status
  bool loading = false; //for data featching status
  String errmsg = ""; //to assing any error message from API/runtime
  var apidata; //for decoded JSON data
  
  @override
  void initState() {
    getData(); //fetching data
    super.initState();
  }

  getData() async { 

    BaseOptions options = BaseOptions(
        baseUrl: "https://httpbin.org",
        connectTimeout: 3000,
        receiveTimeout: 3000,
      );

      Dio dio = Dio(options);

      // https://httpbin.org/ - leaf cert. 
      String certificate = '''
-----BEGIN CERTIFICATE-----
MIIF3DCCBMSgAwIBAgIQAVjzCtWdYq8gnxXuT8K7MzANBgkqhkiG9w0BAQsFADBG
MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRUwEwYDVQQLEwxTZXJ2ZXIg
Q0EgMUIxDzANBgNVBAMTBkFtYXpvbjAeFw0yMTExMjEwMDAwMDBaFw0yMjEyMTky
MzU5NTlaMBYxFDASBgNVBAMTC2h0dHBiaW4ub3JnMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAhOQnpezrwA0vHzf47Pa+O84fWue/562TqQrVirtf+3fs
GQd3MmwnId+ksAGQvWN4M1/hSelYJb246pFqGB7t+ZI+vjBYH4/J6CiFsKwzusqk
SF63ftQh8Ox0OasB9HvRlOPHT/B5Dskh8HNiJ+1lExSZEaO9zsQ9wO62bsGHsMX/
UP3VQByXLVBZu0DMKsl2hGaUNy9+LgZv4/iVpWDPQ1+khpfxP9x1H+mMlUWBgYPq
7jG5ceTbltIoF/sUQPNR+yKIBSnuiISXFHO9HEnk5ph610hWmVQKIrCAPsAUMM9m
6+iDb64NjrMjWV/bkm36r+FBMz9L8HfEB4hxlwwg5QIDAQABo4IC9DCCAvAwHwYD
VR0jBBgwFoAUWaRmBlKge5WSPKOUByeWdFv5PdAwHQYDVR0OBBYEFM8HhLgDSKzJ
rNsRZQp9Kf/Wl0uzMCUGA1UdEQQeMByCC2h0dHBiaW4ub3Jngg0qLmh0dHBiaW4u
b3JnMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH
AwIwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDovL2NybC5zY2ExYi5hbWF6b250cnVz
dC5jb20vc2NhMWItMS5jcmwwEwYDVR0gBAwwCjAIBgZngQwBAgEwdQYIKwYBBQUH
AQEEaTBnMC0GCCsGAQUFBzABhiFodHRwOi8vb2NzcC5zY2ExYi5hbWF6b250cnVz
dC5jb20wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuc2NhMWIuYW1hem9udHJ1c3Qu
Y29tL3NjYTFiLmNydDAMBgNVHRMBAf8EAjAAMIIBfQYKKwYBBAHWeQIEAgSCAW0E
ggFpAWcAdgApeb7wnjk5IfBWc59jpXflvld9nGAK+PlNXSZcJV3HhAAAAX1Acwi1
AAAEAwBHMEUCIF3Y8cwUmZ9cmYSclNrSYOhYDeCzSTAYHwpIhp6oAHKBAiEA/8nK
wN0G3SwUiS3/NdzlVuuakr4a7oviAzN7zXiSmzcAdgBRo7D1/QF5nFZtuDd4jwyk
eswbJ8v3nohCmg3+1IsF5QAAAX1AcwjJAAAEAwBHMEUCIH4pZ7551jQOJ/lV20sD
KNCWOLdt+cS2pjUIFEpI8nwlAiEAj2hxpivIPtX9tReEcJCaAuC5Gh90mZz9lWQy
usID0VQAdQDfpV6raIJPH2yt7rhfTj5a6s2iEqRqXo47EsAgRFwqcwAAAX1Acwii
AAAEAwBGMEQCIEyeaMOsy5LSsKkIod2CfBML3J/+CwjvJekdMBI4QYI2AiAYKdpD
ptDftXG7GSOz8SgpqRtUoWIHs1woSj7uwEJwuzANBgkqhkiG9w0BAQsFAAOCAQEA
L2Qd0308BkF7ahyUYJkxkrfr4WyyrO7SW/TsNpSmxqPF+D/QqQcBt8tPHWg1oNEc
UYinl5qtA4kyHqpAlgzYl04FUpShkNDjcwd1GikgmNMIhSGx3EHaQeyHvrKIgCRe
TK1fPPxDvFU2ao9nnEfiQ0OossRVC6EaJsQ+/CEnTir0BEPjWRjW2C/g9YOHRyP9
PO4R/58KOy8pdJZwWkOyGKylZemsLy6sR8h3UE0KW0TawMwGO+sjWU2eB/uOJ6Yc
aAS0og1S7NrLDqT3HUWSf81g7qlNeC3hNgI8fMxFLPTkhn8+v220SJipi6ignkJR
VFoC1aTFolXMq/oMqRqUHA==
-----END CERTIFICATE-----
''';
      

      (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
          SecurityContext sc = new SecurityContext();
          sc.setTrustedCertificatesBytes(certificate.codeUnits);
          HttpClient httpClient = new HttpClient(context: sc);
          return httpClient;
      };


      setState(() {
         loading = true;  
      });

      String url = "https://httpbin.org";


    try {
        Response response = await dio.get(url); 
        apidata = response.data; 
        
        print(apidata);
      } catch (e) {
        print("Exception: $e");
        errmsg = '$e';
        error = true;
      }
      
      loading = false;
      setState(() {}); //refresh UI 
  }

  
  @override
  Widget build(BuildContext context) {
    
    return Scaffold(
         appBar: AppBar(
            title: Text("TLS Certificate Pinning Demo"),
            backgroundColor: Colors.redAccent,
         ),

         body: Container(
           alignment: Alignment.topCenter,
           padding: EdgeInsets.all(20),
            child: loading?
             CircularProgressIndicator(): 
             Container( 
               child:error?Text("Error: $errmsg"):
               Text('$apidata'),
             )       
         )
    );
  } 
}

จริง ๆ แล้วเราสามารถใช้เมธอด setTrustedCertificates() ได้เช่นกัน และสามารถ pin ใบรับรองได้มากกว่า 1 ใบเผื่อว่าแอปต้องคุยกับหลาย server สิ่งที่ต้องระวังเวลาทำ certificate pinning คือ เราควร pin leaf certificate หรือ public key ของ certificate แทนการ pin CA certificate หรือ intermediate certificate เพราะมีโอกาสที่ leaf certificate คนละใบอาจถูกสร้างโดย CA เดียวกันได้

Ref.:

สามารถดาวน์โหลดตัวอย่าง Application และ Code ได้ที่