特斯拉数字车钥匙卡评价

另辟蹊径的特斯拉卡片形式的数字车钥匙



前言

第一篇文章我们大概评价了数字车钥匙CCC3.0,里面经典的PKI体系的数字证书体系让人眼花缭乱,然后好奇当下火爆到极致的特斯拉数字车钥匙是怎么玩的呢,虽然没有实测,但通过网上两个核心的代码和中继攻击数据Log的互相印证,基本应该可以理解为是真实的情况。


一、特斯拉数字车钥匙卡的核心是什么?

卡的核心安全理论依然是椭圆曲线密码ECC,但特斯拉卡区别与CCC3.0的最大特点,直接干掉CA,自成一派,不考虑其他车钥匙,车中苹果特斯拉确实有这个实力,如果干掉CA那么车钥匙的实现就简单太多了,直接注册卡的公钥,干脆简洁,好比PGP直接给公钥模式,只要保障注册卡公钥是安全就可以了,还有车里面系统安全的,其实这样也是合理的,如果黑客可以修改车里面的公钥,即使用CA体系,黑客也可以修改CA公钥。

二、特斯拉卡片数字车钥匙的核心认证APDU

1.关键TeslaAuth_APDU

80 11 00 51 04 64字节车随机ECDH挑战 16字节挑战
16字节应答 90 00

2.认证内部原理分析

因为没有开源协议,只能根据代码分析,
准备阶段:卡内部生成随机密钥对d,dG是公钥,注册公钥dG到车内。
认证流程:

  1. 车NFC读头连接卡片选择特斯拉应用,然后获取卡片公钥dG
  2. 车生成随机数K,计算KG做为挑战ECDH,同时产生16字节随机数Random一起发送给卡片
  3. 卡片用自己的私钥d和挑战KG一起计算dKG得64字节共享密钥,取SHA1(dKG)前16字节做AES密钥
  4. 卡片用AESKey加密Random得16字节Response
  5. 车可以并行用随机数K和卡片公钥dG计算会话密钥KdG,然后同样取SHA1(KdG)前16字节做AES密钥加密Random得到CarCipher
  6. 比对CarChipher?=Response 完成认证流程
    核心认证交互就3条APDU就完成了。

3.设计理念

极致的简洁和安全,相比于CCC3.0蓝牙独立签名开锁模式,卡用了AES转化证明模式,安全性一样,但空中NFC的传输字节丛64到16字节会大大提高NFC通讯的稳定性,签名是一种证明和不可否认性,开锁只要证明有私钥就行,所以特斯拉采用对称转化在NFC场景是非常合理和高效的,而且如果车端充分利用多线程,可以并行计算会话密钥KdG,这样认证时间就是1ms内可以完成的aes解密对比后12字节,如果采用签名验证机制,必须等到签名才能做ECC运算,运算时间要做到1ms还是蛮难的,但这样巧妙一转化就轻松完成1ms验证任务,根据京东购买的实际特斯拉卡片数据验证发现卡面内部没有加随机数,这种情况下车端可以提前计算Response,这种情况下车端验证时间几乎为0。

三、Java编写的模拟车端与真实特斯拉卡片车钥匙的Demo程序

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import javax.smartcardio.*;
import java.math.BigInteger;
import java.security.*;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.*;
import java.util.Arrays;
import java.util.List;

import apdu4j.pcsc.TerminalManager;
import apdu4j.pcsc.terminals.LoggingCardTerminal;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.util.encoders.Hex;

public class PCSCTesla {
    static TerminalFactory terminalFactory = null;
    static List<CardTerminal> terminals = null;
    static CardTerminal cardTerminal = null;
    static LoggingCardTerminal loggingCardTerminal = null;
    static Card card = null;
    static CardChannel cardChannel = null;

    static KeyPairGenerator keyGen = null;
    static ECFieldFp ecFp = null;
    static EllipticCurve Curve = null;
    static ECPoint G = null;
    static ECParameterSpec ecSpec = null;

    static KeyFactory keyFactory = null;
    static KeyAgreement carKeyAgree = null;


    public static byte[] SendAPDU(byte[] sendAPDU) throws CardException {
        CommandAPDU commandAPDU = new CommandAPDU(sendAPDU);
        ResponseAPDU responseAPDU = cardChannel.transmit(commandAPDU);
        return responseAPDU.getBytes();
    }

    public static void ConnectCard() throws CardException {
        terminalFactory = TerminalFactory.getDefault();
        terminals = terminalFactory.terminals().list();
        System.out.println("Terminals: " + terminals);
        // get the 0 or 1 or 2 or 3 or 4 or 5? terminal
        cardTerminal = terminals.get(4);
        loggingCardTerminal = LoggingCardTerminal.getInstance(cardTerminal);
        try {
            card = loggingCardTerminal.connect("T=1");
            System.out.println("Terminal connected");
            cardChannel = card.getBasicChannel();
        } catch (Exception e) {
            System.out.println("Terminal NOT connected: ");
        }
    }

    public static void CarECCInit() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
        BigInteger p = new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16);
        BigInteger a = new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16);
        BigInteger b = new BigInteger("5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b", 16);
        BigInteger Gx = new BigInteger("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296", 16);
        BigInteger Gy = new BigInteger("4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5", 16);
        BigInteger n = new BigInteger("ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551", 16);
        keyGen = KeyPairGenerator.getInstance("ECDH", "BC");
        ecFp = new ECFieldFp(p);
        Curve = new EllipticCurve(ecFp, a, b);
        G = new ECPoint(Gx, Gy);
        ecSpec = new ECParameterSpec(Curve, G, n, 1);
        keyGen.initialize(ecSpec, new SecureRandom());
        keyFactory = java.security.KeyFactory.getInstance("EC");
        carKeyAgree = KeyAgreement.getInstance("ECDH");
    }

    public static byte[] CarGenECDHKey() throws InvalidKeyException {
        KeyPair CarPair = keyGen.generateKeyPair();
        ECPrivateKey privateKey = (ECPrivateKey) CarPair.getPrivate();
        carKeyAgree.init(CarPair.getPrivate());
        byte[] privateKeyS = privateKey.getS().toByteArray();
        System.out.println("Car Private Key is");
        System.out.println(new String(Hex.encode(privateKeyS)));
        ECPublicKey CarPubKey = (ECPublicKey) CarPair.getPublic();
        BCECPublicKey BCCarPubKey = (BCECPublicKey) CarPubKey;
        return BCCarPubKey.getQ().getEncoded(false);
    }


    public static byte[] CarGetCipherUsingCardPubKey(byte[] carChallenge, byte[] cardPubKey) throws InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {
        byte[] CardPubKeyX = new byte[32];
        byte[] CardPubKeyY = new byte[32];
        System.arraycopy(cardPubKey, 1, CardPubKeyX, 0, 32);
        System.arraycopy(cardPubKey, 1 + 32, CardPubKeyY, 0, 32);
        BigInteger bigIntegerCardPubKeyX = new BigInteger(1, CardPubKeyX);
        BigInteger bigIntegerCardPubKeyY = new BigInteger(1, CardPubKeyY);
        ECPoint publicPoint = new ECPoint(bigIntegerCardPubKeyX, bigIntegerCardPubKeyY);
        ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(publicPoint, ecSpec);
        ECPublicKey Card_PublicKey = (ECPublicKey) keyFactory.generatePublic(pubKeySpec);
        carKeyAgree.doPhase(Card_PublicKey, true);
        byte[] sharedSecret = carKeyAgree.generateSecret();
        System.out.println("Shared secret is");
        System.out.println(new String(Hex.encode(sharedSecret)));
        // Derive aes key from the shared secret and both public keys
        MessageDigest hash = MessageDigest.getInstance("SHA1");
        byte[] hashValue = hash.digest(sharedSecret);
        System.out.println("aKeyAgree digest is");
        System.out.println(new String(Hex.encode(hashValue)));
        KeyGenerator keyGenHandle = KeyGenerator.getInstance("AES");
        int keySize = 128;
        keyGenHandle.init(keySize);
        byte[] aesKey = new byte[16];
        System.arraycopy(hashValue, 0, aesKey, 0, 16);
        System.out.println("AES Key is");
        System.out.println(new String(Hex.encode(aesKey)));
        SecretKeySpec aesKeySpec = new SecretKeySpec(aesKey, "AES");
        Cipher aesCipher = Cipher.getInstance("AES/ECB/NoPadding");
        aesCipher.init(Cipher.ENCRYPT_MODE, aesKeySpec);
        return aesCipher.doFinal(carChallenge);
    }

    public static boolean CarAuthCard() throws CardException, InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException, InvalidKeySpecException, NoSuchAlgorithmException, BadPaddingException {
        byte[] challenge = new byte[16];
        SecureRandom secRand = new SecureRandom();
        secRand.nextBytes(challenge);
        String strChallenge = new String(Hex.encode(challenge));
        System.out.println("challenge is");
        System.out.println(strChallenge);


        byte[] apduSelectAID = Hex.decode("00a404000a7465736c614c6f67696301");
        //byte[] apduSelectAID = Hex.decode("00a4040006010203040501");

        SendAPDU(apduSelectAID);
        byte[] apduGetPubKey = Hex.decode("8004000000");
        byte[] responseGetPubKey = SendAPDU(apduGetPubKey);
        System.out.println("Card PubKey is");
        System.out.println(new String(Hex.encode(responseGetPubKey)));

        byte[] carECDHKey = CarGenECDHKey();
        byte[] carCipher = CarGetCipherUsingCardPubKey(challenge, responseGetPubKey);
        System.out.println(" Car Compute Cipher is");
        System.out.println(new String(Hex.encode(carCipher)));
        String strCarECDHKey = new String(Hex.encode(carECDHKey));
        String strApduAuth = "8011000051" + strCarECDHKey + strChallenge;
        byte[] apduAuth = Hex.decode(strApduAuth);
        byte[] responseAuth = SendAPDU(apduAuth);
        byte[] responseData = new byte[16];
        System.arraycopy(responseAuth, 0, responseData, 0, 16);
        System.out.println(" Card Response is");
        System.out.println(new String(Hex.encode(responseData)));
        return Arrays.equals(carCipher, responseData);


    }

    public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException, InvalidKeyException, InvalidKeySpecException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, CardException {
        System.out.println("Hello Paul!");
        TerminalManager.fixPlatformPaths();
        ConnectCard();
        CarECCInit();
        while (true) {
            if (CarAuthCard()) {
                System.out.println(" Now you can open the Car");
            } else {
                System.out.println(" Auth Failed");
                break;
            }
        }


    }
}

四、程序实际运行的特斯拉车端与卡端交互数据

challenge is
a9e1dfc8f594a6ae80e693021bef5fcc
A>> T=1 (4+0010) 00A40400 0A 7465736C614C6F676963 01
A<< (0000+2) (17ms) 9000
A>> T=1 (4+0000) 80040000 00
A<< (0065+2) (16ms) 043CE0B8EB5A2195100A4AE7D716DC57530F4F6CCD51217CB116AD887A0E028F8233411EAD26EAFCEA9B038A7E801EBFCE7F5E0F9437A5D01BF5B18451FEA15D25 9000
Card PubKey is
043ce0b8eb5a2195100a4ae7d716dc57530f4f6ccd51217cb116ad887a0e028f8233411ead26eafcea9b038a7e801ebfce7f5e0f9437a5d01bf5b18451fea15d259000
Car Private Key is
22e5c21a0c84190521b0106ba418b89dabc1e537a1708591e8db6211ed2fbb8b
Shared secret is
ad935496b0d1d7484c27b47708eb13a20e0ca411602689f61adfe4b0cb3afe11
aKeyAgree digest is
9da293a7c4273899110c44d1f12fd1588e8f6a68
AES Key is
9da293a7c4273899110c44d1f12fd158
Car Compute Cipher is
6f21d8e62406aa92608fecb25e1aefc3
A>> T=1 (4+0081) 80110000 51 04F99FB6870833B4AF5D19E23A48D6C091A65D94C82D4B524D5B9AACF4F0C9D9B08930151421A4E8F00BAECB417707F6AB63F44B740787E203AEB7070AAA57761FA9E1DFC8F594A6AE80E693021BEF5FCC
A<< (0016+2) (45ms) 6F21D8E62406AA92608FECB25E1AEFC3 9000
Card Response is
6f21d8e62406aa92608fecb25e1aefc3
Now you can open the Car

五、Python版本的模拟车端参考代码

要安装pyscard和mbedtls包才能运行

from smartcard.Exceptions import NoCardException
from smartcard.pcsc.PCSCReader import PCSCReader
from smartcard.util import *
from mbedtls import hashlib
from mbedtls import cipher
from mbedtls import pk
import binascii

reader_list = PCSCReader.readers()
for reader in reader_list:
    try:
        print(reader.name)
        connection = reader.createConnection()
        connection.connect()
        print(toHexString(connection.getATR()))
        apdu_select = toBytes("00a404000a7465736c614c6f676963")
        data, sw1, sw2 = connection.transmit(apdu_select)
        print("{:02X} {:02X}".format(sw1, sw2))
        while True:
            apduGetKey = toBytes("80 04 00 00 00")
            data, sw1, sw2 = connection.transmit(apduGetKey)
            cardPubkey = toHexString(data, PACK)
            print("{:02X} {:02X}".format(sw1, sw2))
            print(cardPubkey)
            ecdh_key = pk.ECC(pk.Curve.SECP256R1)
            ecdh_key.generate()
            ecdh_srv = pk.ECDHServer(ecdh_key)
            ske = ecdh_srv.generate()
            carECDH = ske.hex()[8:]
            cke = "41" + cardPubkey
            ecdh_srv.import_CKE(binascii.unhexlify(cke.encode(encoding='utf_8')))
            secretA = ecdh_srv.generate_secret()
            shareKey = secretA.to_bytes(32, byteorder="big")
            hashEngine = hashlib.sha1()
            hashEngine.update(shareKey)
            digest = hashEngine.digest()
            aesKey = digest[:-4]
            aesEngine = cipher.AES.new(aesKey, cipher.MODE_ECB, b"ECB needs an IV.")
            strRand = "adfd9e9d09a7575968dc59a071363779"
            print("the Challenge is " + strRand)
            plain = binascii.unhexlify(strRand.encode(encoding='utf_8'))
            outBuf = aesEngine.encrypt(plain)
            strOut = binascii.hexlify(outBuf).decode()
            print("Server Compute Cipher is " + strOut.upper())
            apduAuth = toBytes("80 11 00 00 51 " + carECDH + strRand)
            data, sw1, sw2 = connection.transmit(apduAuth)
            response = toHexString(data, PACK)
            print("Card Compute Response is " + response)
            print("{:02X} {:02X}".format(sw1, sw2))
            if strOut.upper() == response:
                print("you can open the car!\n")
    except NoCardException:
        print('no card in reader')
      


六、特斯拉手机车钥匙未来展望

谷歌大力推广的Strongbox的愿景是手机做车钥匙,数字货币,身份证,Strongbox确实设计前沿,颠覆了安全芯片通常的思想,一直以来密钥一定要安全芯片存储而且不出安全芯片,这样很多应用都会被SE应用空间所局限,Strongbox开创性的将密钥加密出安全芯片,然后需要用再进来,由安全芯片内部对称密钥保障导出密钥只能回安全芯片使用,这样一下子就把安全芯片的空间无限扩大,而且不损害安全性,而且避免TSM的应用下载问题,避免应用兼容问题,安全芯片就提供安全功能,其他TEE或者REE实现,这个思想确实超前和实用,期待Strongbox未来大放异彩,回头来聊Strongbox如何实现特斯拉数字车钥匙呢,只要手机支持NFC,支持Strongbox,我们可以先生成公私钥对,然后能启用HCE模式的APK,用户点击模拟卡,将密钥Blob导入Strongbox,HCE接收的NFC公钥命令和Select命令正常会,关键认证命令过来,按照Strongbox实现ECDH密钥协商,然后SHA1,然后AES,全部调用Strongbox实现,然后NFC返回信息,这样不用下载特斯拉应用就可以实现特斯拉车卡,而且可以装海量特斯拉卡,等回头有空了,分享APK源代码。

总结

感谢国外大佬的
https://github.com/darconeous/gauss-key-card/blob/master/src/io/github/darconeous/gausskeycard/GaussKeyCard.java的分享
感谢国内NFC中继攻击提供的佐证数据
https://www.anquanke.com/post/id/213885
国内中继帖子标出的车公钥是错误的,这是ECDH挑战,每次都变的。
笔者购买的是正版199元的京东特斯拉卡,Log数据是实测数据,实际数据证明认证逻辑符合文章分析。
笔者同时用NXP的P71卡片测试Applet的AuthAPDU实际性能45ms,用正品卡实际测试性能是113ms,加上TagInfo信息推断,特斯拉卡片采用的应该是NXP的P6系列产品,以前大家谈性能总提PBOC200ms,其实PBOC被微信支付宝打的无还手之力,以后大家讨论性能就比拼TeslaAuth性能,这个指令包含了非对称,摘要,对称,衡量算法的黄金APDU指令,如果有一天大家比拼密码性能用TeslaAuth这个指标,这个帖子就是发源地。

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值