tescmd 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tescmd/__init__.py +3 -0
- tescmd/__main__.py +5 -0
- tescmd/_internal/__init__.py +0 -0
- tescmd/_internal/async_utils.py +25 -0
- tescmd/_internal/permissions.py +43 -0
- tescmd/_internal/vin.py +44 -0
- tescmd/api/__init__.py +1 -0
- tescmd/api/charging.py +102 -0
- tescmd/api/client.py +189 -0
- tescmd/api/command.py +540 -0
- tescmd/api/energy.py +146 -0
- tescmd/api/errors.py +76 -0
- tescmd/api/partner.py +40 -0
- tescmd/api/sharing.py +65 -0
- tescmd/api/signed_command.py +277 -0
- tescmd/api/user.py +38 -0
- tescmd/api/vehicle.py +150 -0
- tescmd/auth/__init__.py +1 -0
- tescmd/auth/oauth.py +312 -0
- tescmd/auth/server.py +108 -0
- tescmd/auth/token_store.py +273 -0
- tescmd/ble/__init__.py +0 -0
- tescmd/cache/__init__.py +6 -0
- tescmd/cache/keys.py +51 -0
- tescmd/cache/response_cache.py +213 -0
- tescmd/cli/__init__.py +0 -0
- tescmd/cli/_client.py +603 -0
- tescmd/cli/_options.py +126 -0
- tescmd/cli/auth.py +682 -0
- tescmd/cli/billing.py +240 -0
- tescmd/cli/cache.py +85 -0
- tescmd/cli/charge.py +610 -0
- tescmd/cli/climate.py +501 -0
- tescmd/cli/energy.py +385 -0
- tescmd/cli/key.py +611 -0
- tescmd/cli/main.py +601 -0
- tescmd/cli/media.py +146 -0
- tescmd/cli/nav.py +242 -0
- tescmd/cli/partner.py +112 -0
- tescmd/cli/raw.py +75 -0
- tescmd/cli/security.py +495 -0
- tescmd/cli/setup.py +786 -0
- tescmd/cli/sharing.py +188 -0
- tescmd/cli/software.py +81 -0
- tescmd/cli/status.py +106 -0
- tescmd/cli/trunk.py +240 -0
- tescmd/cli/user.py +145 -0
- tescmd/cli/vehicle.py +837 -0
- tescmd/config/__init__.py +0 -0
- tescmd/crypto/__init__.py +19 -0
- tescmd/crypto/ecdh.py +46 -0
- tescmd/crypto/keys.py +122 -0
- tescmd/deploy/__init__.py +0 -0
- tescmd/deploy/github_pages.py +268 -0
- tescmd/models/__init__.py +85 -0
- tescmd/models/auth.py +108 -0
- tescmd/models/command.py +18 -0
- tescmd/models/config.py +63 -0
- tescmd/models/energy.py +56 -0
- tescmd/models/sharing.py +26 -0
- tescmd/models/user.py +37 -0
- tescmd/models/vehicle.py +185 -0
- tescmd/output/__init__.py +5 -0
- tescmd/output/formatter.py +132 -0
- tescmd/output/json_output.py +83 -0
- tescmd/output/rich_output.py +809 -0
- tescmd/protocol/__init__.py +23 -0
- tescmd/protocol/commands.py +175 -0
- tescmd/protocol/encoder.py +122 -0
- tescmd/protocol/metadata.py +116 -0
- tescmd/protocol/payloads.py +621 -0
- tescmd/protocol/protobuf/__init__.py +6 -0
- tescmd/protocol/protobuf/messages.py +564 -0
- tescmd/protocol/session.py +318 -0
- tescmd/protocol/signer.py +84 -0
- tescmd/py.typed +0 -0
- tescmd-0.1.2.dist-info/METADATA +458 -0
- tescmd-0.1.2.dist-info/RECORD +81 -0
- tescmd-0.1.2.dist-info/WHEEL +4 -0
- tescmd-0.1.2.dist-info/entry_points.txt +2 -0
- tescmd-0.1.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""ECDH session management for the Vehicle Command Protocol.
|
|
2
|
+
|
|
3
|
+
Manages per-(VIN, domain) ECDH sessions with in-memory caching,
|
|
4
|
+
counter management, and automatic re-handshake on expiry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import base64
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from tescmd.api.errors import (
|
|
17
|
+
KeyNotEnrolledError,
|
|
18
|
+
SessionError,
|
|
19
|
+
TeslaAPIError,
|
|
20
|
+
VehicleAsleepError,
|
|
21
|
+
)
|
|
22
|
+
from tescmd.crypto.ecdh import derive_session_key, get_uncompressed_public_key
|
|
23
|
+
from tescmd.protocol.encoder import build_session_info_request, encode_routable_message
|
|
24
|
+
from tescmd.protocol.protobuf.messages import (
|
|
25
|
+
FAULT_DESCRIPTIONS,
|
|
26
|
+
KEY_FAULTS,
|
|
27
|
+
TRANSIENT_FAULTS,
|
|
28
|
+
Domain,
|
|
29
|
+
MessageFault,
|
|
30
|
+
RoutableMessage,
|
|
31
|
+
SessionInfo,
|
|
32
|
+
)
|
|
33
|
+
from tescmd.protocol.signer import (
|
|
34
|
+
derive_session_info_key,
|
|
35
|
+
derive_signing_key,
|
|
36
|
+
verify_session_info_tag,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
43
|
+
|
|
44
|
+
from tescmd.api.client import TeslaFleetClient
|
|
45
|
+
|
|
46
|
+
# Default session TTL (5 minutes)
|
|
47
|
+
_SESSION_TTL = 300
|
|
48
|
+
|
|
49
|
+
# Retry config for transient handshake faults (BUSY, TIMEOUT, INTERNAL).
|
|
50
|
+
# The vehicle's VCSEC subsystem needs time to boot after waking.
|
|
51
|
+
_HANDSHAKE_MAX_RETRIES = 3
|
|
52
|
+
_HANDSHAKE_RETRY_DELAY = 3.0 # seconds between retries
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Session:
|
|
57
|
+
"""An active ECDH session with a vehicle for a specific domain."""
|
|
58
|
+
|
|
59
|
+
vin: str
|
|
60
|
+
domain: Domain
|
|
61
|
+
shared_key: bytes
|
|
62
|
+
signing_key: bytes
|
|
63
|
+
session_info_key: bytes
|
|
64
|
+
epoch: bytes
|
|
65
|
+
counter: int
|
|
66
|
+
clock_offset: int # vehicle_clock - local_clock (seconds)
|
|
67
|
+
created_at: float
|
|
68
|
+
ttl: float = _SESSION_TTL
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_expired(self) -> bool:
|
|
72
|
+
return time.monotonic() - self.created_at > self.ttl
|
|
73
|
+
|
|
74
|
+
def next_counter(self) -> int:
|
|
75
|
+
"""Return and increment the anti-replay counter."""
|
|
76
|
+
self.counter += 1
|
|
77
|
+
return self.counter
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SessionManager:
|
|
81
|
+
"""Manages ECDH sessions per (VIN, domain).
|
|
82
|
+
|
|
83
|
+
Usage::
|
|
84
|
+
|
|
85
|
+
mgr = SessionManager(private_key, client)
|
|
86
|
+
session = await mgr.get_session(vin, Domain.DOMAIN_INFOTAINMENT)
|
|
87
|
+
# session.signing_key, session.epoch, session.counter, ...
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
private_key: ec.EllipticCurvePrivateKey,
|
|
93
|
+
client: TeslaFleetClient,
|
|
94
|
+
) -> None:
|
|
95
|
+
self._private_key = private_key
|
|
96
|
+
self._client = client
|
|
97
|
+
self._client_public_key = get_uncompressed_public_key(private_key)
|
|
98
|
+
self._sessions: dict[tuple[str, Domain], Session] = {}
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def client_public_key(self) -> bytes:
|
|
102
|
+
return self._client_public_key
|
|
103
|
+
|
|
104
|
+
async def get_session(self, vin: str, domain: Domain) -> Session:
|
|
105
|
+
"""Return a valid session, performing a handshake if needed."""
|
|
106
|
+
key = (vin, domain)
|
|
107
|
+
session = self._sessions.get(key)
|
|
108
|
+
if session is not None and not session.is_expired:
|
|
109
|
+
return session
|
|
110
|
+
|
|
111
|
+
session = await self._handshake(vin, domain)
|
|
112
|
+
self._sessions[key] = session
|
|
113
|
+
return session
|
|
114
|
+
|
|
115
|
+
def invalidate(self, vin: str, domain: Domain | None = None) -> None:
|
|
116
|
+
"""Invalidate cached sessions for a VIN (optionally for a specific domain)."""
|
|
117
|
+
if domain is not None:
|
|
118
|
+
self._sessions.pop((vin, domain), None)
|
|
119
|
+
else:
|
|
120
|
+
for key in list(self._sessions):
|
|
121
|
+
if key[0] == vin:
|
|
122
|
+
del self._sessions[key]
|
|
123
|
+
|
|
124
|
+
async def _handshake(self, vin: str, domain: Domain) -> Session:
|
|
125
|
+
"""Perform ECDH handshake with the vehicle.
|
|
126
|
+
|
|
127
|
+
1. Build RoutableMessage with session_info_request
|
|
128
|
+
2. POST to /api/1/vehicles/{vin}/signed_command
|
|
129
|
+
3. Check for fault codes — retry transient faults (BUSY/TIMEOUT/INTERNAL)
|
|
130
|
+
4. Parse vehicle's SessionInfo response
|
|
131
|
+
5. ECDH → derive shared key
|
|
132
|
+
6. Validate response HMAC
|
|
133
|
+
7. Store session
|
|
134
|
+
"""
|
|
135
|
+
last_fault: MessageFault | None = None
|
|
136
|
+
|
|
137
|
+
for attempt in range(1, _HANDSHAKE_MAX_RETRIES + 1):
|
|
138
|
+
# HTTP 408 from the signed_command endpoint means the vehicle's
|
|
139
|
+
# subsystem (VCSEC/Infotainment) didn't respond within the API
|
|
140
|
+
# server's timeout window — NOT necessarily that the vehicle is
|
|
141
|
+
# asleep. Retry it like a transient protobuf fault.
|
|
142
|
+
try:
|
|
143
|
+
response_msg = await self._send_handshake(vin, domain)
|
|
144
|
+
except VehicleAsleepError:
|
|
145
|
+
if attempt < _HANDSHAKE_MAX_RETRIES:
|
|
146
|
+
logger.debug(
|
|
147
|
+
"Handshake HTTP 408 for %s (%s), retry %d/%d",
|
|
148
|
+
vin,
|
|
149
|
+
domain.name,
|
|
150
|
+
attempt,
|
|
151
|
+
_HANDSHAKE_MAX_RETRIES,
|
|
152
|
+
)
|
|
153
|
+
await asyncio.sleep(_HANDSHAKE_RETRY_DELAY)
|
|
154
|
+
continue
|
|
155
|
+
# Exhausted retries — re-raise so auto_wake() can handle
|
|
156
|
+
# genuinely asleep vehicles.
|
|
157
|
+
raise
|
|
158
|
+
|
|
159
|
+
# Check for vehicle-side fault before looking at session_info.
|
|
160
|
+
fault = response_msg.signed_message_fault
|
|
161
|
+
logger.debug(
|
|
162
|
+
"Handshake parsed for %s (%s): fault=%s, has_session_info=%s, "
|
|
163
|
+
"has_protobuf_msg=%s, has_signature=%s",
|
|
164
|
+
vin,
|
|
165
|
+
domain.name,
|
|
166
|
+
fault.name,
|
|
167
|
+
bool(response_msg.session_info),
|
|
168
|
+
bool(response_msg.protobuf_message_as_bytes),
|
|
169
|
+
response_msg.signature_data is not None,
|
|
170
|
+
)
|
|
171
|
+
if fault != MessageFault.ERROR_NONE:
|
|
172
|
+
desc = FAULT_DESCRIPTIONS.get(fault, fault.name)
|
|
173
|
+
last_fault = fault
|
|
174
|
+
|
|
175
|
+
# Key not recognized → permanent error, don't retry.
|
|
176
|
+
if fault in KEY_FAULTS:
|
|
177
|
+
raise KeyNotEnrolledError(
|
|
178
|
+
f"Vehicle rejected handshake: {desc}",
|
|
179
|
+
status_code=422,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Transient fault → retry after delay.
|
|
183
|
+
if fault in TRANSIENT_FAULTS and attempt < _HANDSHAKE_MAX_RETRIES:
|
|
184
|
+
logger.debug(
|
|
185
|
+
"Handshake fault %s for %s (%s), retry %d/%d",
|
|
186
|
+
fault.name,
|
|
187
|
+
vin,
|
|
188
|
+
domain.name,
|
|
189
|
+
attempt,
|
|
190
|
+
_HANDSHAKE_MAX_RETRIES,
|
|
191
|
+
)
|
|
192
|
+
await asyncio.sleep(_HANDSHAKE_RETRY_DELAY)
|
|
193
|
+
continue
|
|
194
|
+
|
|
195
|
+
# Non-transient or exhausted retries.
|
|
196
|
+
raise SessionError(f"Vehicle rejected handshake for {vin} ({domain.name}): {desc}")
|
|
197
|
+
|
|
198
|
+
if not response_msg.session_info:
|
|
199
|
+
raise SessionError(
|
|
200
|
+
f"No session_info in handshake response for {vin} ({domain.name})"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
return self._build_session(vin, domain, response_msg)
|
|
204
|
+
|
|
205
|
+
# Should not be reached, but handle exhausted retries with last fault.
|
|
206
|
+
desc = FAULT_DESCRIPTIONS.get(last_fault, str(last_fault)) if last_fault else "unknown"
|
|
207
|
+
raise SessionError(
|
|
208
|
+
f"Handshake failed after {_HANDSHAKE_MAX_RETRIES} attempts "
|
|
209
|
+
f"for {vin} ({domain.name}): {desc}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
async def _send_handshake(self, vin: str, domain: Domain) -> RoutableMessage:
|
|
213
|
+
"""Send a session_info_request and return the parsed response."""
|
|
214
|
+
request_msg = build_session_info_request(
|
|
215
|
+
domain=domain,
|
|
216
|
+
client_public_key=self._client_public_key,
|
|
217
|
+
)
|
|
218
|
+
encoded = encode_routable_message(request_msg)
|
|
219
|
+
logger.debug(
|
|
220
|
+
"Sending handshake for %s (%s): %d bytes encoded, pub_key=%d bytes",
|
|
221
|
+
vin,
|
|
222
|
+
domain.name,
|
|
223
|
+
len(encoded),
|
|
224
|
+
len(self._client_public_key),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
response_data = await self._client.post(
|
|
229
|
+
f"/api/1/vehicles/{vin}/signed_command",
|
|
230
|
+
json={"routable_message": encoded},
|
|
231
|
+
)
|
|
232
|
+
except TeslaAPIError:
|
|
233
|
+
# Let domain-specific errors (VehicleAsleepError, RateLimitError,
|
|
234
|
+
# etc.) propagate so callers like auto_wake() can handle them.
|
|
235
|
+
raise
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
raise SessionError(
|
|
238
|
+
f"Session handshake failed for {vin} ({domain.name}): {exc}"
|
|
239
|
+
) from exc
|
|
240
|
+
|
|
241
|
+
response_b64: str = response_data.get("response", "")
|
|
242
|
+
if not response_b64:
|
|
243
|
+
logger.debug(
|
|
244
|
+
"Empty handshake response for %s (%s): %r",
|
|
245
|
+
vin,
|
|
246
|
+
domain.name,
|
|
247
|
+
response_data,
|
|
248
|
+
)
|
|
249
|
+
raise SessionError(f"Empty session handshake response for {vin} ({domain.name})")
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
response_bytes = base64.b64decode(response_b64)
|
|
253
|
+
except Exception as exc:
|
|
254
|
+
raise SessionError(f"Failed to decode handshake response: {exc}") from exc
|
|
255
|
+
|
|
256
|
+
logger.debug(
|
|
257
|
+
"Handshake response for %s (%s): %d bytes, hex=%s",
|
|
258
|
+
vin,
|
|
259
|
+
domain.name,
|
|
260
|
+
len(response_bytes),
|
|
261
|
+
response_bytes.hex(),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
return RoutableMessage.parse(response_bytes)
|
|
266
|
+
except Exception as exc:
|
|
267
|
+
raise SessionError(f"Failed to parse handshake response: {exc}") from exc
|
|
268
|
+
|
|
269
|
+
def _build_session(self, vin: str, domain: Domain, response_msg: RoutableMessage) -> Session:
|
|
270
|
+
"""Derive keys and build a Session from a successful handshake response."""
|
|
271
|
+
session_info = SessionInfo.parse(response_msg.session_info)
|
|
272
|
+
if not session_info.public_key:
|
|
273
|
+
raise SessionError(f"No vehicle public key in session info for {vin} ({domain.name})")
|
|
274
|
+
|
|
275
|
+
# ECDH key derivation
|
|
276
|
+
try:
|
|
277
|
+
shared_key = derive_session_key(self._private_key, session_info.public_key)
|
|
278
|
+
except Exception as exc:
|
|
279
|
+
raise SessionError(
|
|
280
|
+
f"ECDH key derivation failed for {vin} ({domain.name}): {exc}"
|
|
281
|
+
) from exc
|
|
282
|
+
|
|
283
|
+
# Derive sub-keys
|
|
284
|
+
signing_key = derive_signing_key(shared_key)
|
|
285
|
+
session_info_key = derive_session_info_key(shared_key)
|
|
286
|
+
|
|
287
|
+
# Verify session info HMAC tag (if present in response)
|
|
288
|
+
sig = response_msg.signature_data
|
|
289
|
+
si_tag = sig.session_info_tag if sig else None
|
|
290
|
+
if (
|
|
291
|
+
si_tag
|
|
292
|
+
and si_tag.tag
|
|
293
|
+
and not verify_session_info_tag(
|
|
294
|
+
session_info_key,
|
|
295
|
+
response_msg.session_info,
|
|
296
|
+
si_tag.tag,
|
|
297
|
+
)
|
|
298
|
+
):
|
|
299
|
+
raise SessionError(
|
|
300
|
+
f"Session info HMAC verification failed for {vin} ({domain.name}). "
|
|
301
|
+
"The vehicle response may have been tampered with."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Calculate clock offset
|
|
305
|
+
local_time = int(time.time())
|
|
306
|
+
clock_offset = session_info.clock_time - local_time if session_info.clock_time else 0
|
|
307
|
+
|
|
308
|
+
return Session(
|
|
309
|
+
vin=vin,
|
|
310
|
+
domain=domain,
|
|
311
|
+
shared_key=shared_key,
|
|
312
|
+
signing_key=signing_key,
|
|
313
|
+
session_info_key=session_info_key,
|
|
314
|
+
epoch=session_info.epoch,
|
|
315
|
+
counter=session_info.counter,
|
|
316
|
+
clock_offset=clock_offset,
|
|
317
|
+
created_at=time.monotonic(),
|
|
318
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""HMAC-SHA256 command signing for the Vehicle Command Protocol.
|
|
2
|
+
|
|
3
|
+
Signing flow (for the REST/Fleet API path):
|
|
4
|
+
|
|
5
|
+
1. Serialize metadata as TLV: ``encode_metadata(epoch, expires_at, counter)``
|
|
6
|
+
2. Derive signing key: ``K' = HMAC-SHA256(K, b"authenticated command")``
|
|
7
|
+
3. Compute tag: ``HMAC-SHA256(K', metadata_bytes || 0xFF || payload_bytes)``
|
|
8
|
+
4. For VCSEC domain: truncate tag to 17 bytes.
|
|
9
|
+
5. Attach to ``RoutableMessage.signature_data.HMAC_PersonalizedData.tag``
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import hmac
|
|
16
|
+
|
|
17
|
+
from tescmd.protocol.protobuf.messages import Domain
|
|
18
|
+
|
|
19
|
+
# Derivation labels (from Tesla vehicle-command specification)
|
|
20
|
+
_LABEL_AUTHENTICATED_COMMAND = b"authenticated command"
|
|
21
|
+
_LABEL_SESSION_INFO = b"session info"
|
|
22
|
+
|
|
23
|
+
# VCSEC domain uses a truncated 17-byte tag
|
|
24
|
+
_VCSEC_TAG_LENGTH = 17
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def derive_signing_key(session_key: bytes) -> bytes:
|
|
28
|
+
"""Derive the command signing key: ``HMAC-SHA256(K, "authenticated command")``."""
|
|
29
|
+
return hmac.new(session_key, _LABEL_AUTHENTICATED_COMMAND, hashlib.sha256).digest()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def derive_session_info_key(session_key: bytes) -> bytes:
|
|
33
|
+
"""Derive the session info verification key: ``HMAC-SHA256(K, "session info")``."""
|
|
34
|
+
return hmac.new(session_key, _LABEL_SESSION_INFO, hashlib.sha256).digest()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def compute_hmac_tag(
|
|
38
|
+
signing_key: bytes,
|
|
39
|
+
metadata_bytes: bytes,
|
|
40
|
+
payload_bytes: bytes,
|
|
41
|
+
*,
|
|
42
|
+
domain: Domain = Domain.DOMAIN_INFOTAINMENT,
|
|
43
|
+
) -> bytes:
|
|
44
|
+
"""Compute the HMAC-SHA256 authentication tag.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
signing_key:
|
|
49
|
+
The derived signing key from :func:`derive_signing_key`.
|
|
50
|
+
metadata_bytes:
|
|
51
|
+
TLV-encoded metadata (epoch, expires_at, counter, flags).
|
|
52
|
+
payload_bytes:
|
|
53
|
+
The serialized protobuf command payload.
|
|
54
|
+
domain:
|
|
55
|
+
The routing domain — VCSEC tags are truncated to 17 bytes.
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
bytes
|
|
60
|
+
The HMAC tag (32 bytes for Infotainment, 17 bytes for VCSEC).
|
|
61
|
+
"""
|
|
62
|
+
# The Go SDK streams metadata entries into the hash, then Checksum()
|
|
63
|
+
# writes a bare TAG_END byte (0xFF) before the payload — no length byte.
|
|
64
|
+
# m.Context.Write([]byte{byte(signatures.Tag_TAG_END)}) // just 0xFF
|
|
65
|
+
# m.Context.Write(message)
|
|
66
|
+
msg = metadata_bytes + b"\xff" + payload_bytes
|
|
67
|
+
tag = hmac.new(signing_key, msg, hashlib.sha256).digest()
|
|
68
|
+
|
|
69
|
+
if domain == Domain.DOMAIN_VEHICLE_SECURITY:
|
|
70
|
+
return tag[:_VCSEC_TAG_LENGTH]
|
|
71
|
+
return tag
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def verify_session_info_tag(
|
|
75
|
+
session_info_key: bytes,
|
|
76
|
+
session_info_bytes: bytes,
|
|
77
|
+
expected_tag: bytes,
|
|
78
|
+
) -> bool:
|
|
79
|
+
"""Verify the HMAC tag on a SessionInfo response.
|
|
80
|
+
|
|
81
|
+
Returns True if the tag is valid.
|
|
82
|
+
"""
|
|
83
|
+
computed = hmac.new(session_info_key, session_info_bytes, hashlib.sha256).digest()
|
|
84
|
+
return hmac.compare_digest(computed, expected_tag)
|
tescmd/py.typed
ADDED
|
File without changes
|