Mctrain's Blog

What I learned in IT, as well as thought about life

OpenSSL Authenticated Android Accessory Protocol

| Comments

前段时间在研究Android Auto(关于什么是Auto请自行google),里面涉及到两个比较关键的数据传输和加密协议:Android Accessory ProtocolOpenSSL。具体来说,auto和车载系统(之后称为headunit)之间数据的传输以及最初连接的建立是基于Android Open Accessory (AOA)协议的,而它们两个之间的认证过程以及数据的加密是基于OpenSSL的握手和加密协议。

在研究这两个协议的过程中,“如何将AOA协议和SSL协议结合起来”是一个很关键的问题。我在网上找了很多资料,但是并没有一个比较完整的教程,所以打算在这篇博客中做一个详细的介绍,并且将相关代码开源。

github上的源码

关于Android Auto

关于什么是Android Auto,可以到google的官方网站上去查询。简单来说,就是Google开发的一套机制,可以将手机上的应用(包括地图、音乐、通话等)和车载系统进行交互,使得车载系统的功能更加丰富。如果要描述它的机制的话,可以用下面一张图来表示:

Androi Auto Mechanism

其中,负责和headunit进行交互的是GMS (Google Mobile Service)的Car Service,然后它会和Google开发的Auto应用联系,Auto应用负责和其它第三方应用程序交互,现在支持Android Auto的第三方应用程序有这些,可以看到大部分还是一些音乐和社交类的应用。这里有一个特殊的应用,那就是Google Map,它是直接整合在GMS里面的,可以直接和Car service进行交互,应该不需要经过Auto(当然这还仅仅是我的推测)。

关于两个协议

由于这篇博文主要介绍的是手机和车载的交互协议,因此我们主要关注的是GMS car service和headunit之间的交互,所以,我们把上面那张图简化一下:

AA and SSL Protocols

其中Android Open Accessory(AOA)协议发生在两台设备通过USB进行连接,其中一台作为Accessory,一台作为Device。在Auto的例子中,headunit的角色为Accessory,手机的角色为DeviceAOA协议主要作用是关于AccessoryDevice在初始化连接时候的互相识别,以及之后数据的传输。

关于AccessoryDevice的概念可以看这篇博文,这里就不详述了。

AccessoryDevice建立连接之后,两边就可以进行数据的传输了,但是由于一些隐私问题,传输的数据需要进行加密,因此就引入了OpenSSL协议。在OpenSSL协议中连接两端的实体被分为了ServerClient,这两个角色有什么区别会在之后提到。在这里我们只需要知道在Auto的例子中,headunit的角色为Client,手机的角色为Server。因此我们的图又被抽象为如下:

AA and SSL Protocols 2

好了,到现在为止,我们就完全和Auto撇清关系了,我们接下来要介绍的,就是两个实体,它们通过USB连接,一个作为AOA协议的Accessory和OpenSSL协议的Client,另一个作为AOA协议的Device和OpenSSL协议的Server

Android Open Accessory(AOA)协议

当两个实体通过USB进行连接之后,最先做出反应的是Accessory,它会做以下几件事情:

步骤1:获得和它连接的Device的VendorID和ProductID;

步骤2:判断它们是否匹配相应的数字;

比如在Auto的例子中,headunit需要判断VendorID是否匹配0x18D1,ProductID是否匹配0x2D00或者0x2D01?)

  • 如果匹配,则表示该设备支持Android accessory模式,并且当前已经处于该模,所以Accessory可以直接和Device进行通信(直接跳到步骤5);
  • 否则,则表示该设备目前不处在Android accessory模式,但是不清楚其是否支持该模式,需要进行确认(继续执行步骤3~4)。

步骤3:Android accessory模式确认和重新连接;

  • 通过USB发送一个请求:

    requestType:    USB_DIR_IN | USB_TYPE_VENDOR
    request:        51
    value:          0
    index:          0
    data:           protocol version number (16 bits little endian sent from the device to the accessory)
    
  • 如果对方返回一个非零整数,则表示该设备支持Android accessory模式,该返回值表示支持的协议版本号;

  • 发送另外的请求,该请求中包含一些字符串,用来表示Device中哪些应用程序可以来和Accessory进行交互:

    requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
    request:        52
    value:          0
    index:          string ID
    data            zero terminated UTF8 string sent from accessory to device
    
  • 有效的string ID包含以下几类:

    manufacturer name:  0
    model name:         1
    description:        2
    version:            3
    URI:                4
    serial number:      5
    
  • 在Auto的例子中,headUnit在这个过程中会发送两个string ID:manufacturer name = "Android"model name = "Android Auto"。该string ID会触发手机设备中com.google.android.gms.car.FirstActivityonCreate()函数,从而使得GMS car service和headUnit进行accessory的连接;

  • Accessory最后发送一个请求,告诉Device开始进入Android accessory模式,并且重新建立连接:

    requestType:    USB_DIR_OUT | USB_TYPE_VENDOR
    request:        53
    value:          0
    index:          0
    data:           none
    

步骤4:重新检查;

步骤3结束之后,Device会重新和Accessory进行连接,这时Accessory回到步骤1进行检查,如果检查通过,则进入步骤5,如果Device不支持Android accessory模式,或者没有匹配的应用程序,则Device会返回信息告诉Accessory,这时Accessory就只能等待下一个手机设备的接入。

步骤5:开始通信.

从这之后,AccessoryDevice将通过Android Accessory协议进行通信,Accessory首先获得该USB连接中的一些配置元数据,包括接口类型(UsbInterface),端点信息(UsbEndpoint)等,从而获得对应的bulk endpoints,进行之后的通信过程。

在数据通信的过程中,Accessory通过libusb库提供的libusb_control_transferlibusb_bulk_transfer接口进行数据的传输,其中,libusb_control_transfer用于传输一些指令数据,而libusb_bulk_transfer用于传输一些比较大的数据,比如音频数据,图像数据等; 而Device则通过Android USBManager提供的openAccessory接口获得一个文件描述符,然后通过其对应的FileInputStreamFileOutputStream进行数据的读写:

1
2
3
4
5
6
ParcelFileDescriptor mFD = mUSBManager.openAccessory(acc);
if (mFD != null) {
    FileDescripter fd = mFD.getFileDescriptor();
    mIS = new FileInputStream(fd);  // use this to receive messages
    mOS = new FileOutputStream(fd); // use this to send commands
}

OpenSSL协议

AccessoryDevice建立连接,并且可以传输数据之后,它们就要开始建立OpenSSL的连接,对数据进行加解密了。这里主要分为了两个过程:握手过程和数据加解密过程。这里简单介绍下握手协议:

OpenSSL握手协议

握手协议的作用是身份的认证,该过程由Client端发起,这个协议的过程如下:

OpenSSL Handshake Protocol

在这个过程中,Client首先会对Server提供的证书(Certificate)进行验证,Server也会对Client提供的证书进行验证。同时它们会用Server的公钥(包含在Server的证书中)和存在Server端的私钥进行秘钥的协商,最后通过这个协商好的秘钥(master key)对数据进行加解密。

这里推荐StackOverflow的一个帖子,里面的前两个回答对OpenSSL握手协议进行了一个很棒的解释。


代码分析

在进行了背景介绍之后,我们开始来分析下如何实现这整个过程。

源码可以在这里下载。

里面有两个目录:aoa-dev-ssl-serveraoa-acc-ssl-client,分别代表上面描述的两个实体。这两个目录是两个不同的Android应用,编译完之后可以通过adb install安装在Android平台的手机或者平板上。

AOA协议的实现

首先由aoa-acc-ssl-client发起,代码在src/cn/sjtu/ipads/uas/UasTransport.java文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  private void usb_acc_string_send(UsbDeviceConnection connection, int index, String string) {
    byte[] buffer = (string + "\0").getBytes();
    int len = connection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR,
        OAP_SEND_STRING, 0, index, buffer, buffer.length, 10000);
  }

  private void usb_acc_strings_send() {
    usb_acc_string_send(m_usb_dev_conn, OAP_STR_MANUFACTURE, "SJTU");
    usb_acc_string_send(m_usb_dev_conn, OAP_STR_MODEL, "SJTU IPADS");
  }

  private void acc_mode_switch() {
    int acc_ver = usb_acc_version_get(m_usb_dev_conn);
    usb_acc_strings_send();
    m_usb_dev_conn.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, OAP_START, 0, 0, null, 0, 10000);
  }

  private void usb_connect(UsbDevice device) {
    if (usb_open(device) < 0) {
      usb_disconnect();
      return;
    }
    int dev_vend_id = device.getVendorId();
    int dev_prod_id = device.getProductId();
    if (dev_vend_id == USB_VID_GOO && (dev_prod_id == USB_PID_OAP_NUL || dev_prod_id == USB_PID_OAP_ADB)) {
      int ret = acc_mode_connect();
      ...
      return;
    }
    acc_mode_switch();
    usb_disconnect();
  }

这个可以参照我之前讲的AOA协议来对照,这里当调用usb_acc_strings_send()将两个字符串发送出去之后,在Device端就会有相应的应用被唤醒,因为在该应用中定义了如下内容(在aoa-dev-ssl-server目录的res/xml/usb_accessory_filter文件中):

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <usb-accessory manufacturer="SJTU" model="SJTU IPADS" />
</resources>

而在aoa-dev-ssl-server目录的AndroidManifest.xml文件中定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="cn.sjtu.ipads.ual">

  <uses-feature android:name="android.hardware.usb.accessory" android:required="true"/>

  <application>
    <uses-library android:name="com.android.future.usb.accessory" />
    ...
    <activity
      android:name="UalTraActivity">
      <intent-filter>
        <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"/>
      </intent-filter>
      <meta-data
        android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
        android:resource="@xml/usb_accessory_filter"/>
    </activity>
    ...
  </application>
</manifest>

所以,aoa-dev-ssl-server这个应用会被唤醒,进入UalTraActivityonCreate()函数。在该类中,会进行USB accessory的连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mDeviceHandler = new Handler(this);
    mUSBManager = (UsbManager) getSystemService(Context.USB_SERVICE);
    connectToAccessory();
  }

  public void connectToAccessory() {
    // bail out if we're already connected
    if (mConnection != null)
      return;

    Log.v(TAG, "connectToAccessory");
    // assume only one accessory (currently safe assumption)
    UsbAccessory[] accessories = mUSBManager.getAccessoryList();
    UsbAccessory accessory = (accessories == null ? null
        : accessories[0]);
    if (accessory != null) {
      if (mUSBManager.hasPermission(accessory)) {
        openAccessory(accessory);
      } else {
        Log.v(TAG, "no permission for accessory");
      }
    } else {
      Log.d(TAG, "mAccessory is null");
    }
  }

  private void openAccessory(UsbAccessory accessory) {
    Log.v(TAG, "openAccessory");
    mConnection = new UsbConnection(this, mUSBManager, accessory);
  if (mConnection == null) {
      Log.d(TAG, "mConnection is null");
    finish();
  }
    performPostConnectionTasks();
  }

UsbConnection这个类中会通过UsbManageropenAccessory接口得到一个文件描述符mFileDescriptor,之后的数据传输就是通过对这个mFileDescriptor的读写来进行的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  public UsbConnection(Activity activity, UsbManager usbManager,
      UsbAccessory accessory) {
    mActivity = activity;
    mFileDescriptor = usbManager.openAccessory(accessory);
    if (mFileDescriptor != null) {
      Log.v("UsbConnection", "mFileDescriptor");
      mAccessory = accessory;
      FileDescriptor fd = mFileDescriptor.getFileDescriptor();
      mInputStream = new FileInputStream(fd);
      mOutputStream = new FileOutputStream(fd);
    }
    IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
    filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
    mActivity.registerReceiver(mUsbReceiver, filter);
  }

到目前为止,AccessoryDevice的连接已经建立,之后的数据传输就可以进行了。

Accessory这端的数据读写是在jni层中,可以参阅aoa-acc-ssl-client/jni/hu_usb.c这个文件。

发数据的流程是这样的:

hu_aap_usb_send() -> hu_usb_send() -> iusb_bulk_transfer(out)

接受数据的流程是这样的:

hu_aap_usb_recv() -> hu_usb_recv() -> iusb_bulk_transfer(in)

具体代码这里不贴了,有兴趣自己去看。

Device这端的数据读写是在java层,可以参阅aoa-dev-ssl-server/src/cn/sjtu/ipads/ual/UalTraActivity.java这个文件。

发数据就是调用了之前获得的UsbConnection类的这个接口:

1
mConnection.getOutputStream().write(buffer, 0, bufferLength);

收数据类似:

1
mConnection.getInputStream().read(buffer, bufferUsed, buffer.length - bufferUsed);

AOA协议基本就实现完成了。

OpenSSL握手协议

握手协议由aoa-acc-ssl-client发起,在文件aoa-acc-ssl-client/jni/hu_aap.c中:

1
2
3
4
5
int hu_aap_start (byte ep_in_addr, byte ep_out_addr) {
  ...
  ret = hu_ssl_handshake (); // Do SSL Client Handshake with AA SSL server
  ...
}

之后就会经历上面提到的整个握手过程。

这里需要注意的是,这个OpenSSL握手和加解密过程的实现,和我们平时通过socket传输数据时所涉及到的过程有点不一样。

我们在网络编程的时候,一般会调用下面两个API:

1
2
SSL *ssl = SSL_new(ctx);  /* get new SSL state with context */
SSL_set_fd(ssl, sockfd);  /* set connection to SSL state */

之后的网络数据读写直接通过SSL_write(ssl)SSL_read(ssl)来做就行了。因为SSL和这个负责读写数据的文件描述符sockfd已经绑定在一起了,在网络库的内部帮我们实现了网络buffer到SSL内部buffer的映射。

然而,当我们需要通过USB进行传输数据的时候就没有那么简单了。我们前面说过,我们同样可以通过对某个文件描述的读写操作来传送和接受USB数据,但是USB的库并没有帮我们实现其buffer到SSL内部buffer的映射。因此这步操作需要我们自己来实现。这里就用到了OpenSSL+Memory BIO的机制。

先提供一个参考资料:Using OpenSSL with Memory BIO

简单来说步骤是这样的:

  • 首先,我们需要配置OpenSSL的数据结构:
1
2
3
4
5
6
7
8
9
10
11
  ual_ssl_ctx = SSL_CTX_new(ual_ssl_method);

  ret = SSL_CTX_use_certificate(ual_ssl_ctx, x509_cert);
  ret = SSL_CTX_use_PrivateKey(ual_ssl_ctx, priv_key);

  ual_ssl_ssl = SSL_new(ual_ssl_ctx);

  ual_ssl_rm_bio = BIO_new(BIO_s_mem());
  ual_ssl_wm_bio = BIO_new(BIO_s_mem());

  SSL_set_bio(ual_ssl_ssl, ual_ssl_rm_bio, ual_ssl_wm_bio);

我中间跳过了很多步,不过那些都不重要(可以去看源码),这里最重要的就是这句话:

1
SSL_set_bio(ual_ssl_ssl, ual_ssl_rm_bio, ual_ssl_wm_bio)

这里将ual_ssl_ssl这个数据结构和两段内存联系在一起,这两段内存分别是read BIOwrite BIO

这有什么用呢?其实要解释清楚这个就需要先对OpenSSL的机制有一个初步的了解。

在SSL的所有操作中(比如证书验证,加密,解密等),说到底,就是从某段内存中读取数据,对其进行相应的操作,然后将结果写在另外一段内存中。因此这里的两段内存就分别对应了read BIOwrite BIO

似乎还是有点晕,那么我们来举个例子:

如果我们要进行数据加密,分解步骤是这样的:

  • 输入一段长度为len的明文数据plain_buf
  • 调用SSL_write(ual_ssl_ssl, plain_buf, len),这时OpenSSL内部的逻辑就会对这段数据进行加密,并且将结果保存在write BIO中;
  • 调用BIO_read(ual_ssl_wm_bio, cipher_buf, DEFBUF),就可以将这段加密好的数据读出来保存在cipher_buf中;
  • 最后,我们通过写USB对应的文件描述符就可以将这段加密的数据发送出去了。

因此,整个加密的逻辑就可以是这样的:

1
2
3
4
5
6
7
8
int ssl_encrypt_data(int len, char *plain_buf, char *cipher_buf) {
  bytes_written = SSL_write(ual_ssl_ssl, plain_buf, len);
  bytes_read = BIO_read(ual_ssl_wm_bio, cipher_buf, DEFBUF);
  return (bytes_read);
}

int length = ssl_encrypt_data(len, plain_buf, cipher_buf);
send_to_usb_fd(cipher_buf, length);

类似的,解密的分解步骤是这样的:

  • 通过读USB对应的文件描述符读取一段长度为len的密文数据cipher_buf
  • 调用BIO_write(ual_ssl_ssl, cipher_buf, len),将这段密文写入和SSL相关联的read BIO的内存中;
  • 调用SSL_read(ual_ssl_ssl, plain_buf, DEFBUF),将read BIO的数据进行解密,并将结果保存在plain_buf中;
  • 最后,我们就可以对这段明文数据进行处理了。

其相应的逻辑就变成这样了:

1
2
3
4
5
6
7
8
9
int ssl_decrypt_data(int len, char *cipher_buf, char *plain_buf) {
  bytes_written = BIO_write(ual_ssl_rm_bio, cipher_buf, len);
  bytes_read = SSL_read(ual_ssl_ssl, plain_buf, DEFBUF);
  return (bytes_read);
}

len = recv_from_usb_fd(cipher_buf);
ssl_decrypt_data(len, cipher_buf, plain_buf);
process(plain_buf);

和加解密过程相比,握手的过程会比较复杂一些,但是相关原理是一样的。

不管在Server端还是在Client端,都需要调用SSL_do_handshake(ual_ssl_ssl)这个API,OpenSSL内部的逻辑就会根据当前的状态对ual_ssl_rm_bio的数据进行处理,并将结果写到ual_ssl_wm_bio中。在调用SSL_do_handshake这个API前,需要将相关的数据写到read BIO中(比如在Server端,第一次调用SSL_do_handshake前需要将Client Hello的数据通过BIO_write写进ual_ssl_rm_bio中)。所以说,一般情况下需要手动调用大于一次的SSL_do_handshake接口。

整个逻辑大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int ssl_hs_data_enqueue(int len, char *buf) {
  ret = BIO_write(ual_ssl_rm_bio, &buf[2], len - 2);
  return ret;
}

int ssl_hs_data_dequeue(char *buf) {
  ret = BIO_read(ual_ssl_wm_bio, buf, DEFBUF - 6);
  return ret;
}

void ssl_handshake() {
  ret = SSL_do_handshake(ual_ssl_ssl);
}

while (handshake not finished) {
  len = recv_from_usb_fd(data);
  ssl_hs_data_enqueue(len, data);
  ssl_handshake();
  length = ssl_hs_data_dequeue(result);
  send_to_usb_fd(result, length);
}

讲到这里,OpenSSL的整个流程也基本介绍完了。最后需要说明的一点,在aoa-acc-ssl-client中,数据的传输和加密都是在JNI层完成的,所以代码比较简单。但是在aoa-dev-ssl-server中,数据的传输是在Java层完成的,而加密是在JNI层实现的,所以中间有一个JNI调用的过程,会显得比较复杂。不过整体的原理是一样的。

关于JNI如何调用,网上有很多教程,也可以直接参照源码,这里就不详述了。

最后,关于整个项目的编译和运行,可以参照github中的README.md

Comments