Add support for A71CH

- Add a71ch.py and a71ch_pubkey.py. These provide the same functionality
as the ecc608 variants.
- Modify core to check both security chips. If both are present, ecc608
would be used.
- Adjust debian dependencies to require either python3-cryptoauthlib, or
a71ch-crypto-support.
- Downgrade messages about not finding a crypto chip to debug, since it
is unlikely that you have both chips.

Change-Id: I09e04eb7025b19a0c8b5b4e608f4e7d4605c9d19
diff --git a/python/coral-cloudiot/coral/cloudiot/a71ch.py b/python/coral-cloudiot/coral/cloudiot/a71ch.py
new file mode 100644
index 0000000..9c616b1
--- /dev/null
+++ b/python/coral-cloudiot/coral/cloudiot/a71ch.py
@@ -0,0 +1,141 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import ctypes
+import jwt
+import logging
+import subprocess
+import tempfile
+from asn1crypto.core import Sequence
+from coral.cloudiot.utils import ascii_hex_string
+from cryptography.hazmat.primitives import hashes
+
+logger = logging.getLogger(__name__)
+library = None
+kA71ChOk = 0x9000
+
+
+def a71ch_serial():
+    uid_len = 18
+    ret_len = ctypes.c_uint16(uid_len)
+    uid = ctypes.create_string_buffer(uid_len)
+    get_unique_id = library.A71_GetUniqueID
+    get_unique_id.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint16)]
+    get_unique_id.restype = ctypes.c_uint16
+    assert get_unique_id(uid, ctypes.byref(ret_len)) == kA71ChOk
+    return ascii_hex_string(uid.raw, l=uid_len)
+
+
+def a71ch_public_key():
+    with tempfile.NamedTemporaryFile(mode='w+') as tempkey:
+        subprocess.check_call(['A71CHConfigTool', 'get', 'pub', '-c', '10', '-x',
+                               '0', '-k', tempkey.name],
+                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+        public_key = '\n'.join([x.strip() for x in tempkey.readlines()])
+
+    return public_key
+
+
+def a71ch_hw_sign(msg, key_id=0):
+    get_sha256 = library.A71_GetSha256
+    get_sha256.argtypes = [ctypes.c_char_p, ctypes.c_uint16,
+                           ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint16)]
+    get_sha256.restype = ctypes.c_uint16
+    hash = ctypes.create_string_buffer(32)
+    hash_len = ctypes.c_uint16(32)
+    assert get_sha256(msg, ctypes.c_uint16(len(msg)), hash,
+                      ctypes.byref(hash_len)) == kA71ChOk
+
+    ecc_sign = library.A71_EccSign
+    ecc_sign.argtypes = [ctypes.c_uint8, ctypes.c_char_p,
+                         ctypes.c_uint16, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint16)]
+    ecc_sign.restype = ctypes.c_uint16
+    sig = ctypes.create_string_buffer(256)
+    sig_len = ctypes.c_uint16(256)
+    assert ecc_sign(key_id, hash, hash_len, sig,
+                    ctypes.byref(sig_len)) == kA71ChOk
+
+    asn1 = Sequence.load(sig.raw)
+    signature = asn1[0].native.to_bytes(
+        32, 'big') + asn1[1].native.to_bytes(32, 'big')
+    return signature
+
+
+class HwEcAlgorithm(jwt.algorithms.Algorithm):
+    def __init__(self):
+        self.hash_alg = hashes.SHA256
+
+    def prepare_key(self, key):
+        return key
+
+    def sign(self, msg, key):
+        return a71ch_hw_sign(msg)
+
+    def verify(self, msg, key, sig):
+        try:
+            der_sig = jwt.utils.raw_to_der_signature(sig, key.curve)
+        except ValueError:
+            return False
+
+        try:
+            key.verify(der_sig, msg, ec.ECDSA(self.hash_alg()))
+            return True
+        except InvalidSignature:
+            return False
+
+
+class SmCommState_t(ctypes.Structure):
+    pass
+
+
+SmCommState_t.__slots__ = [
+    'connType',
+    'param1',
+    'param2',
+    'hostLibVersion',
+    'appletVersion',
+    'sbVersion',
+    'skip_select_applet',
+]
+SmCommState_t.__fields__ = [
+    ('connType', ctypes.c_uint16),
+    ('param1', ctypes.c_uint16),
+    ('param2', ctypes.c_uint16),
+    ('hostLibVersion', ctypes.c_uint16),
+    ('appletVersion', ctypes.c_uint32),
+    ('sbVersion', ctypes.c_uint16),
+    ('skip_select_applet', ctypes.c_uint8),
+]
+
+try:
+    a71ch_jwt_with_hw_alg = None
+
+    library = ctypes.cdll.LoadLibrary('libsss_engine.so')
+    sm_connect = library.SM_Connect
+    sm_connect.argtypes = [ctypes.POINTER(None),
+                           ctypes.POINTER(SmCommState_t), ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint16)]
+    sm_connect.restype = ctypes.c_uint16
+
+    comm_state = SmCommState_t()
+    atr_len = ctypes.c_uint16(64)
+    atr = ctypes.create_string_buffer(64)
+    assert sm_connect(None, ctypes.byref(comm_state), atr,
+                      ctypes.byref(atr_len)) == kA71ChOk
+
+    a71ch_jwt_with_hw_alg = jwt.PyJWT(algorithms=[])
+    a71ch_jwt_with_hw_alg.register_algorithm('ES256', HwEcAlgorithm())
+
+except Exception as e:
+    logger.debug('Unable to load A71CH')
diff --git a/python/coral-cloudiot/coral/cloudiot/a71ch_pubkey.py b/python/coral-cloudiot/coral/cloudiot/a71ch_pubkey.py
new file mode 100755
index 0000000..b0b39f4
--- /dev/null
+++ b/python/coral-cloudiot/coral/cloudiot/a71ch_pubkey.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python3
+#
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+
+from a71ch import a71ch_public_key, a71ch_serial
+
+
+def main():
+
+    print('Serial Number: %s\n\n' % a71ch_serial())
+
+    print(a71ch_public_key())
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/python/coral-cloudiot/coral/cloudiot/core.py b/python/coral-cloudiot/coral/cloudiot/core.py
index ef8f50c..e2e4bee 100644
--- a/python/coral-cloudiot/coral/cloudiot/core.py
+++ b/python/coral-cloudiot/coral/cloudiot/core.py
@@ -26,6 +26,7 @@
 import threading
 import time
 
+from coral.cloudiot.a71ch import a71ch_jwt_with_hw_alg
 from coral.cloudiot.ecc608 import ecc608_jwt_with_hw_alg
 
 logger = logging.getLogger(__name__)
@@ -50,13 +51,13 @@
         """
         self._config = configparser.ConfigParser()
         if not self._config.read(config_file):
-            logger.warn('No valid config provided (reading %s).\nCloud IoT is disabled.' % config_file)
+            logger.warning('No valid config provided (reading %s).\nCloud IoT is disabled.' % config_file)
             self._enabled = False
             return
 
 
         if not self._config.getboolean(config_section, 'Enabled'):
-            logger.warn('Cloud IoT is disabled per configuration.')
+            logger.warning('Cloud IoT is disabled per configuration.')
             self._enabled = False
             return
 
@@ -77,8 +78,13 @@
             self._algorithm = 'ES256'
             self._private_key = None
             self._jwt_inst = ecc608_jwt_with_hw_alg
+        elif a71ch_jwt_with_hw_alg:
+            self._algorithm = 'ES256'
+            self._private_key = None
+            self._jwt_inst = a71ch_jwt_with_hw_alg
         else:
             # For SW, use RS256 on a key file provided in the configuration.
+            logger.warning('Using SW crypto')
             self._algorithm = 'RS256'
             rsa_cert = config['RSACertFile']
             with open(rsa_cert, 'r') as f:
diff --git a/python/coral-cloudiot/coral/cloudiot/ecc608.py b/python/coral-cloudiot/coral/cloudiot/ecc608.py
index e3c52e8..f4b89dc 100644
--- a/python/coral-cloudiot/coral/cloudiot/ecc608.py
+++ b/python/coral-cloudiot/coral/cloudiot/ecc608.py
@@ -19,25 +19,19 @@
 import os
 import sys
 import jwt
+from coral.cloudiot.utils import ascii_hex_string
+from coral.cloudiot.utils import split_equal_parts
 from cryptography.exceptions import InvalidSignature
 from cryptography.hazmat.primitives import hashes
 from cryptography.hazmat.primitives.asymmetric import ec
-from cryptoauthlib import *
+try:
+    from cryptoauthlib import *
+except:
+    pass
 
 logger = logging.getLogger(__name__)
 
 
-def _split_equal_parts(line, n):
-    return [line[i:i+n] for i in range(0, len(line), n)]
-
-
-def _ascii_hex_string(a, l=16):
-    """
-    Format a bytearray object into a formatted ascii hex string
-    """
-    return '\n'.join(x.hex().upper() for x in _split_equal_parts(a, l))
-
-
 def _convert_ec_pub_to_pem(raw_pub_key):
     """
     Convert to the key to PEM format. Expects bytes
@@ -46,7 +40,7 @@
         '3059301306072A8648CE3D020106082A8648CE3D03010703420004') + raw_pub_key
     public_key_b64 = base64.b64encode(public_key_der).decode('ascii')
     public_key_pem = '-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----' % '\n'.join(
-        _split_equal_parts(public_key_b64, 64))
+        split_equal_parts(public_key_b64, 64))
     return public_key_pem
 
 
@@ -71,7 +65,7 @@
             if _ecc608_check_address(bus, addr):
                 logger.info('Found crypto chip at 0x%x', addr)
                 return addr
-    logger.warning('No crypto detected, using SW.')
+    logger.debug('ECC608 not detected')
     return None
 
 
@@ -111,7 +105,7 @@
 def ecc608_serial():
     serial = bytearray(9)
     assert atcab_read_serial_number(serial) == 0
-    return _ascii_hex_string(serial)
+    return ascii_hex_string(serial)
 
 
 def ecc608_public_key(key_id=0):
@@ -156,4 +150,4 @@
         ecc608_jwt_with_hw_alg = jwt.PyJWT(algorithms=[])
         ecc608_jwt_with_hw_alg.register_algorithm('ES256', HwEcAlgorithm())
 except Exception:
-    logger.warning('Unable to load HW crypto library, using SW.')
+    logger.debug('Unable to load ECC608')
diff --git a/python/coral-cloudiot/coral/cloudiot/utils.py b/python/coral-cloudiot/coral/cloudiot/utils.py
new file mode 100644
index 0000000..da302cb
--- /dev/null
+++ b/python/coral-cloudiot/coral/cloudiot/utils.py
@@ -0,0 +1,23 @@
+# Copyright 2020 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+def split_equal_parts(line, n):
+    return [line[i:i+n] for i in range(0, len(line), n)]
+
+
+def ascii_hex_string(a, l=16):
+    """
+    Format a bytearray object into a formatted ascii hex string
+    """
+    return '\n'.join(x.hex().upper() for x in split_equal_parts(a, l))
diff --git a/python/coral-cloudiot/debian/changelog b/python/coral-cloudiot/debian/changelog
index da34dd8..a877311 100644
--- a/python/coral-cloudiot/debian/changelog
+++ b/python/coral-cloudiot/debian/changelog
@@ -1,3 +1,9 @@
+coral-cloudiot (1.3) stable; urgency=medium
+
+  * Add support for A71CH.
+
+ -- Coral <coral-support@google.com>  Tue, 10 Nov 2020 12:04:06 -0800
+
 coral-cloudiot (1.2) stable; urgency=low
 
   * Bug fix for multiple i2c busses.
diff --git a/python/coral-cloudiot/debian/control b/python/coral-cloudiot/debian/control
index 3baa549..d159ef6 100644
--- a/python/coral-cloudiot/debian/control
+++ b/python/coral-cloudiot/debian/control
@@ -10,10 +10,10 @@
 Architecture: all
 Depends: ${misc:Depends},
          ${python3:Depends},
-         python3-cryptoauthlib,
          python3-jwt,
          python3-paho-mqtt,
-         python3-cryptography
+         python3-cryptography,
+         a71ch-crypto-support | python3-cryptoauthlib
 Description: Coral Cloud IoT API
  API for connected Coral devices to Google Cloud Platform.