AndroidでのMQTTのTLS/SSLによる認証ガイダンス

 # AndroidでのMQTTのTLS/SSLによる認証についてのステップ・バイ・ステップ・ガイド

![AndroidでのMQTTのTLS/SSLによる認証についてのステップ・バイ・ステップ・ガイド](https://assets.emqx.com/images/fc83ede6d17442ed06a074f078db008a.png?imageMogr2/thumbnail/1520x684)

[MQTT](https://en.wikipedia.org/wiki/MQTT)は、IoTデバイス開発者のために柔軟性とハードウェア/ネットワークリソースのバランスを取ることを目的とした、軽量かつ柔軟なIoTメッセージ交換とデータ転送プロトコルです。安全な通信を確保するために、通信暗号化にTLS/SSLがよく利用されます。

この記事では、主にAndroidとMQTTを用いてTLS/SSLの一方向および双方向認証を行う方法について説明します。

### 準備

この記事では、[Eclipse Paho Android Service](https://github.com/eclipse/paho.mqtt.android) と `BouncyCastle` を使用して依存関係を追加します。

```groovy
dependencies {
    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.1.0'
    implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
    implementation 'org.bouncycastle:bcpkix-jdk15on:1.59' 
}
```

AndroidがTLS/SSLを使って接続するためのコアコードセクションは以下の通りです。

```java
MqttConnectOptions options = new MqttConnectOptions();
SSLSocketFactory sslSocketFactory = ...
options.setSocketFactory(sslSocketFactory);
```

フォーカスは`SSLSocketFactory`の取得方法です。一方向認証と双方向認証について以下で説明します。

### 一方向認証 

一方向認証は、サーバ側がクライアントを認証することを意味します。コアコードは次の通りです。

```java
public static SSLSocketFactory getSingleSocketFactory(InputStream caCrtFileInputStream) throws Exception {
    Security.addProvider(new BouncyCastleProvider());
    X509Certificate caCert = null;

    BufferedInputStream bis = new BufferedInputStream(caCrtFileInputStream); 
    CertificateFactory cf = CertificateFactory.getInstance("X.509");

    while (bis.available() > 0) {
        caCert = (X509Certificate) cf.generateCertificate(bis);
    }
    KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
    caKs.load(null, null);
    caKs.setCertificateEntry("cert-certificate", caCert);
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(caKs);
    SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
    sslContext.init(null, tmf.getTrustManagers(), null);
    return sslContext.getSocketFactory();
}
```

`res/raw` に`ca.crt`を置き、次のように呼び出します。

```java
try {
    InputStream caCrtFileI = context.getResources().openRawResource(R.raw.ca); 
    options.setSocketFactory(getSingleSocketFactory(caCrtFile));
} catch (Exception e) {
    e.printStackTrace();
}
```

### 双方向認証

双方向認証は、サーバ側とクライアントが互いを認証することを意味します。コアコードは次の通りです。

```java
public static SSLSocketFactory getSocketFactory(InputStream caCrtFile, InputStream crtFile, InputStream keyFile, 
                                                    String password) throws Exception {
    Security.addProvider(new BouncyCastleProvider());

    // CA証明書のロード
    X509Certificate caCert = null; 

    BufferedInputStream bis = new BufferedInputStream(caCrtFile);
    CertificateFactory cf = CertificateFactory.getInstance("X.509");

    while (bis.available() > 0) {
        caCert = (X509Certificate) cf.generateCertificate(bis);
    }
    
    // クライアント証明書のロード 
    bis = new BufferedInputStream(crtFile);
    X509Certificate cert = null;
    while (bis.available() > 0) {
        cert = (X509Certificate) cf.generateCertificate(bis);
    }

    // クライアント秘密鍵のロード
    PEMParser pemParser = new PEMParser(new InputStreamReader(keyFile));
    Object object = pemParser.readObject();
    JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
    KeyPair key = converter.getKeyPair((PEMKeyPair) object);

    KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
    caKs.load(null, null);
    caKs.setCertificateEntry("cert-certificate", caCert);
    TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    tmf.init(caKs);

    KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
    ks.load(null, null);
    ks.setCertificateEntry("certificate", cert);
    ks.setKeyEntry("private-cert", key.getPrivate(), password.toCharArray(), 
            new java.security.cert.Certificate[]{cert});
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(ks, password.toCharArray());

    SSLContext context = SSLContext.getInstance("TLSv1.2");
    context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

    return context.getSocketFactory();
}
```

サーバ側の証明書が必要で、クライアントの証明書と秘密鍵を`res/raw`配下に置き、次のように呼び出します。パスワードは空文字列に設定することに注意してください。

```java
try {
  InputStream caCrtFile = context.getResources().openRawResource(R.raw.ca);
  InputStream crtFile = context.getResources().openRawResource(R.raw.cert); 
  InputStream keyFile = context.getResources().openRawResource(R.raw.key);
  options.setSocketFactory(getSocketFactory(caCrtFile, crtFile, keyFile, "")); 
} catch (Exception e) {
  e.printStackTrace();
}
```

以上が、[AndroidのMQTT](https://www.emqx.com/en/blog/android-connects-mqtt-using-kotlin)でTLS/SSLの一方向および双方向認証を実行する方法です。