另辟蹊径的特斯拉卡片形式的数字车钥匙
文章目录
前言
第一篇文章我们大概评价了数字车钥匙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到车内。
认证流程:
- 车NFC读头连接卡片选择特斯拉应用,然后获取卡片公钥dG
- 车生成随机数K,计算KG做为挑战ECDH,同时产生16字节随机数Random一起发送给卡片
- 卡片用自己的私钥d和挑战KG一起计算dKG得64字节共享密钥,取SHA1(dKG)前16字节做AES密钥
- 卡片用AESKey加密Random得16字节Response
- 车可以并行用随机数K和卡片公钥dG计算会话密钥KdG,然后同样取SHA1(KdG)前16字节做AES密钥加密Random得到CarCipher
- 比对CarChipher?=Response 完成认证流程
核心认证交互就3条APDU就完成了。
3.设计理念
极致的简洁和安全,相比于CCC3.0蓝牙独立签名开锁模式,卡用了AES转化证明模式,安全性一样,但空中NFC的传输字节丛64到16字节会大大提高NFC通讯的稳定性,签名是一种证明和不可否认性,开锁只要证明有私钥就行,所以特斯拉采用对称转化在NFC场景是非常合理和高效的,而且如果车端充分利用多线程,可以并行计算会话密钥KdG,这样认证时间就是1ms内可以完成的aes解密对比后12字节,如果采用签名验证机制,必须等到签名才能做ECC运算,运算时间要做到1ms还是蛮难的,但这样巧妙一转化就轻松完成1ms验证任务,根据京东购买的实际特斯拉卡片数据验证发现卡面内部没有加随机数,这种情况下车端可以提前计算Response,这种情况下车端验证时间几乎为0。
3.AI生成的流程图
三、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
五、AI编写的Python版本的模拟车端和卡的双向参考代码
import os
import hashlib
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import serialization
class TeslaCard:
"""模拟Tesla智能卡"""
def __init__(self):
# 初始化卡片,生成密钥对
self.private_key = ec.generate_private_key(ec.SECP256R1())
self.public_key = self.private_key.public_key()
# 提取公钥为未压缩格式 (04 + X + Y)
self.public_key_bytes = self.public_key.public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint
)
print(f"卡片初始化完成,公钥长度: {len(self.public_key_bytes)} 字节")
def process_apdu(self, apdu):
"""处理接收到的APDU命令"""
# 解析APDU命令
cla = apdu[0]
ins = apdu[1]
# 选择应用
if cla == 0x00 and ins == 0xA4:
aid = bytes(apdu[5:15])
if aid == b'teslaLogic':
return [], 0x90, 0x00 # 成功状态
else:
return [], 0x6A, 0x82 # 应用未找到
# 获取公钥
elif cla == 0x80 and ins == 0x04:
return list(self.public_key_bytes), 0x90, 0x00
# 认证请求
elif cla == 0x80 and ins == 0x11:
# 提取车辆公钥和挑战
car_pubkey = bytes(apdu[5:70]) # 65字节公钥
challenge = bytes(apdu[70:86]) # 16字节挑战
# 从车辆公钥中提取EC点
car_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(),
car_pubkey
)
# 计算共享密钥
shared_secret = self.private_key.exchange(ec.ECDH(), car_public_key)
# 计算SHA1哈希
hash_value = hashlib.sha1(shared_secret).digest()
# 取前16字节作为AES密钥
aes_key = hash_value[:16]
# 使用AES-ECB加密挑战
cipher = Cipher(algorithms.AES(aes_key), modes.ECB())
encryptor = cipher.encryptor()
encrypted_challenge = encryptor.update(challenge) + encryptor.finalize()
return list(encrypted_challenge), 0x90, 0x00
else:
return [], 0x6D, 0x00 # 指令不支持
class TeslaCarSimulator:
"""模拟Tesla车辆终端"""
def __init__(self):
# 初始化车辆终端
self.card = None # 将在连接时设置
def connect_card(self, card):
"""连接到卡片"""
self.card = card
print("车辆终端已连接到卡片")
return True
def send_apdu(self, apdu_bytes):
"""向卡片发送APDU并返回响应"""
if not self.card:
raise Exception("未连接到卡片")
response, sw1, sw2 = self.card.process_apdu(apdu_bytes)
print(f"APDU: {bytes(apdu_bytes).hex()}")
print(f"响应: {bytes(response).hex()}, 状态: {sw1:02X} {sw2:02X}")
return response, sw1, sw2
def select_tesla_application(self):
"""选择Tesla应用"""
select_apdu = [0x00, 0xA4, 0x04, 0x00, 0x0A] + list(b'teslaLogic') + [0x01]
response, sw1, sw2 = self.send_apdu(select_apdu)
if (sw1, sw2) == (0x90, 0x00):
print("Tesla应用选择成功")
return True
else:
print(f"Tesla应用选择失败: {sw1:02X} {sw2:02X}")
return False
def get_card_public_key(self):
"""获取卡片公钥"""
get_pubkey_apdu = [0x80, 0x04, 0x00, 0x00, 0x00]
card_pubkey, sw1, sw2 = self.send_apdu(get_pubkey_apdu)
if (sw1, sw2) == (0x90, 0x00):
print(f"获取卡片公钥成功: {bytes(card_pubkey).hex()}")
return bytes(card_pubkey)
else:
print(f"获取卡片公钥失败: {sw1:02X} {sw2:02X}")
return None
def authenticate_card(self):
"""进行卡片认证"""
# 获取卡片公钥
card_pubkey = self.get_card_public_key()
if not card_pubkey:
return False
# 生成车辆ECDH密钥对
car_private_key = ec.generate_private_key(ec.SECP256R1())
car_public_key = car_private_key.public_key()
# 导出车辆公钥为未压缩格式
car_pubkey = car_public_key.public_bytes(
encoding=serialization.Encoding.X962,
format=serialization.PublicFormat.UncompressedPoint
)
print(f"车辆公钥生成: {car_pubkey.hex()}")
# 从卡片公钥中提取EC点 (去掉0x04前缀)
card_public_key = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(),
card_pubkey
)
# 计算共享密钥
shared_secret = car_private_key.exchange(ec.ECDH(), card_public_key)
print(f"共享密钥计算完成: {shared_secret.hex()}")
# 计算SHA1哈希
hash_value = hashlib.sha1(shared_secret).digest()
print(f"哈希值: {hash_value.hex()}")
# 取前16字节作为AES密钥
aes_key = hash_value[:16]
print(f"AES密钥: {aes_key.hex()}")
# 生成16字节随机挑战
challenge = os.urandom(16)
print(f"随机挑战: {challenge.hex()}")
# 使用AES-ECB加密挑战,得到预期密文
cipher = Cipher(algorithms.AES(aes_key), modes.ECB())
encryptor = cipher.encryptor()
expected_response = encryptor.update(challenge) + encryptor.finalize()
print(f"预期密文: {expected_response.hex()}")
# 发送认证APDU
auth_apdu = [0x80, 0x11, 0x00, 0x00, 0x51] + list(car_pubkey) + list(challenge)
response, sw1, sw2 = self.send_apdu(auth_apdu)
if (sw1, sw2) != (0x90, 0x00):
print(f"认证命令失败: {sw1:02X} {sw2:02X}")
return False
# 比较卡片返回的密文与预期密文
card_response = bytes(response)
print(f"卡片响应: {card_response.hex()}")
if card_response == expected_response:
print("认证成功!密文匹配")
return True
else:
print("认证失败!密文不匹配")
return False
def main():
print("===== Tesla车辆卡认证模拟开始 =====")
# 创建模拟卡片
card = TeslaCard()
# 创建车辆模拟器
car = TeslaCarSimulator()
# 建立连接
if car.connect_card(card):
# 选择应用
if car.select_tesla_application():
# 执行认证
if car.authenticate_card():
print("\n车辆解锁成功!欢迎使用Tesla")
else:
print("\n车辆认证失败,拒绝解锁")
print("===== 模拟结束 =====")
if __name__ == "__main__":
main()
六、特斯拉手机车钥匙未来展望
谷歌大力推广的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这个指标,第一次写这个帖子还不知道这个指标,后来CCC3.0 的苹果要求快速交易时间和标准交易时间基本都是关键密码应用级性能要求,跟我预期的一样
AI时代借助AI画了流程图和参考理解代码,AI还是比较厉害的。