tesla-fleet-api 0.3.0 → 0.5.0
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.
- package/README.md +284 -1
- package/dist/_virtual/_rolldown/runtime.cjs +32 -0
- package/dist/_virtual/_rolldown/runtime.mjs +12 -0
- package/dist/bluetooth/adapters/shared.cjs +38 -0
- package/dist/bluetooth/adapters/shared.mjs +36 -0
- package/dist/bluetooth/async.cjs +121 -0
- package/dist/bluetooth/async.d.cts +10 -0
- package/dist/bluetooth/async.d.mts +10 -0
- package/dist/bluetooth/async.mjs +116 -0
- package/dist/bluetooth/broadcast.cjs +153 -0
- package/dist/bluetooth/broadcast.d.cts +59 -0
- package/dist/bluetooth/broadcast.d.mts +59 -0
- package/dist/bluetooth/broadcast.mjs +151 -0
- package/dist/bluetooth/constants.cjs +99 -0
- package/dist/bluetooth/constants.d.cts +70 -0
- package/dist/bluetooth/constants.d.mts +70 -0
- package/dist/bluetooth/constants.mjs +72 -0
- package/dist/bluetooth/esphome.cjs +292 -0
- package/dist/bluetooth/esphome.d.cts +83 -0
- package/dist/bluetooth/esphome.d.mts +83 -0
- package/dist/bluetooth/esphome.mjs +291 -0
- package/dist/bluetooth/framing.cjs +125 -0
- package/dist/bluetooth/framing.d.cts +45 -0
- package/dist/bluetooth/framing.d.mts +45 -0
- package/dist/bluetooth/framing.mjs +122 -0
- package/dist/bluetooth/homey.cjs +155 -0
- package/dist/bluetooth/homey.d.cts +75 -0
- package/dist/bluetooth/homey.d.mts +75 -0
- package/dist/bluetooth/homey.mjs +150 -0
- package/dist/bluetooth/names.cjs +24 -0
- package/dist/bluetooth/names.d.cts +10 -0
- package/dist/bluetooth/names.d.mts +10 -0
- package/dist/bluetooth/names.mjs +20 -0
- package/dist/bluetooth/noble.cjs +178 -0
- package/dist/bluetooth/noble.d.cts +58 -0
- package/dist/bluetooth/noble.d.mts +58 -0
- package/dist/bluetooth/noble.mjs +177 -0
- package/dist/bluetooth/streamglue.cjs +157 -0
- package/dist/bluetooth/streamglue.d.cts +37 -0
- package/dist/bluetooth/streamglue.d.mts +37 -0
- package/dist/bluetooth/streamglue.mjs +154 -0
- package/dist/bluetooth/teslabluetooth.cjs +67 -0
- package/dist/bluetooth/teslabluetooth.d.cts +41 -0
- package/dist/bluetooth/teslabluetooth.d.mts +41 -0
- package/dist/bluetooth/teslabluetooth.mjs +66 -0
- package/dist/bluetooth/transport.cjs +10 -0
- package/dist/bluetooth/transport.d.cts +95 -0
- package/dist/bluetooth/transport.d.mts +95 -0
- package/dist/bluetooth/transport.mjs +10 -0
- package/dist/bluetooth/vehiclebluetooth.cjs +1272 -0
- package/dist/bluetooth/vehiclebluetooth.d.cts +495 -0
- package/dist/bluetooth/vehiclebluetooth.d.mts +495 -0
- package/dist/bluetooth/vehiclebluetooth.mjs +1270 -0
- package/dist/bluetooth/verify.cjs +93 -0
- package/dist/bluetooth/verify.d.cts +35 -0
- package/dist/bluetooth/verify.d.mts +35 -0
- package/dist/bluetooth/verify.mjs +91 -0
- package/dist/bluetooth.cjs +53 -0
- package/dist/bluetooth.d.cts +10 -0
- package/dist/bluetooth.d.mts +10 -0
- package/dist/bluetooth.mjs +9 -0
- package/dist/charging.cjs +53 -0
- package/dist/charging.d.cts +31 -0
- package/dist/charging.d.mts +30 -0
- package/dist/charging.mjs +53 -0
- package/dist/commands.cjs +1797 -0
- package/dist/commands.d.cts +881 -0
- package/dist/commands.d.mts +881 -0
- package/dist/commands.mjs +1791 -0
- package/dist/const.cjs +322 -0
- package/dist/const.d.cts +308 -0
- package/dist/const.d.mts +308 -0
- package/dist/const.mjs +282 -0
- package/dist/energy.cjs +142 -0
- package/dist/energy.d.cts +101 -0
- package/dist/energy.d.mts +100 -0
- package/dist/energy.mjs +142 -0
- package/dist/energyisland.cjs +38 -0
- package/dist/energyisland.mjs +37 -0
- package/dist/energyparsers.cjs +118 -0
- package/dist/energyparsers.d.cts +39 -0
- package/dist/energyparsers.d.mts +39 -0
- package/dist/energyparsers.mjs +116 -0
- package/dist/energyspecific.cjs +457 -0
- package/dist/energyspecific.d.cts +258 -0
- package/dist/energyspecific.d.mts +258 -0
- package/dist/energyspecific.mjs +451 -0
- package/dist/exceptions.cjs +1488 -0
- package/dist/exceptions.d.cts +1143 -0
- package/dist/exceptions.d.mts +1143 -0
- package/dist/exceptions.mjs +1325 -0
- package/dist/funnel.cjs +297 -0
- package/dist/funnel.d.cts +118 -0
- package/dist/funnel.d.mts +118 -0
- package/dist/funnel.mjs +292 -0
- package/dist/index.cjs +287 -0
- package/dist/index.d.cts +28 -0
- package/dist/index.d.mts +28 -0
- package/dist/index.mjs +27 -0
- package/dist/log.cjs +75 -0
- package/dist/log.d.cts +4 -0
- package/dist/log.d.mts +4 -0
- package/dist/log.mjs +71 -0
- package/dist/oauth.cjs +143 -0
- package/dist/oauth.d.cts +69 -0
- package/dist/oauth.d.mts +69 -0
- package/dist/oauth.mjs +143 -0
- package/dist/partner.cjs +32 -0
- package/dist/partner.d.cts +24 -0
- package/dist/partner.d.mts +23 -0
- package/dist/partner.mjs +32 -0
- package/dist/powerwall/client.cjs +818 -0
- package/dist/powerwall/client.d.cts +297 -0
- package/dist/powerwall/client.d.mts +297 -0
- package/dist/powerwall/client.mjs +807 -0
- package/dist/powerwall/components.cjs +188 -0
- package/dist/powerwall/components.d.cts +45 -0
- package/dist/powerwall/components.d.mts +45 -0
- package/dist/powerwall/components.mjs +182 -0
- package/dist/powerwall/energysite.cjs +216 -0
- package/dist/powerwall/energysite.d.cts +104 -0
- package/dist/powerwall/energysite.d.mts +104 -0
- package/dist/powerwall/energysite.mjs +214 -0
- package/dist/powerwall/exceptions.cjs +59 -0
- package/dist/powerwall/exceptions.d.cts +38 -0
- package/dist/powerwall/exceptions.d.mts +38 -0
- package/dist/powerwall/exceptions.mjs +51 -0
- package/dist/powerwall/gateway.cjs +102 -0
- package/dist/powerwall/gateway.d.cts +57 -0
- package/dist/powerwall/gateway.d.mts +57 -0
- package/dist/powerwall/gateway.mjs +101 -0
- package/dist/powerwall/merge.cjs +163 -0
- package/dist/powerwall/merge.d.cts +63 -0
- package/dist/powerwall/merge.d.mts +63 -0
- package/dist/powerwall/merge.mjs +159 -0
- package/dist/powerwall/models.d.cts +95 -0
- package/dist/powerwall/models.d.mts +95 -0
- package/dist/powerwall/queries.cjs +17 -0
- package/dist/powerwall/queries.mjs +10 -0
- package/dist/powerwall/transport.cjs +232 -0
- package/dist/powerwall/transport.mjs +227 -0
- package/dist/powerwall.cjs +47 -0
- package/dist/powerwall.d.cts +9 -0
- package/dist/powerwall.d.mts +9 -0
- package/dist/powerwall.mjs +8 -0
- package/dist/router/base.cjs +203 -0
- package/dist/router/base.d.cts +112 -0
- package/dist/router/base.d.mts +112 -0
- package/dist/router/base.mjs +200 -0
- package/dist/router/energysite.cjs +65 -0
- package/dist/router/energysite.d.cts +47 -0
- package/dist/router/energysite.d.mts +47 -0
- package/dist/router/energysite.mjs +60 -0
- package/dist/router.cjs +13 -0
- package/dist/router.d.cts +3 -0
- package/dist/router.d.mts +3 -0
- package/dist/router.mjs +3 -0
- package/dist/signing/crypto.cjs +59 -0
- package/dist/signing/crypto.mjs +52 -0
- package/dist/signing/energy.cjs +123 -0
- package/dist/signing/energy.d.cts +45 -0
- package/dist/signing/energy.d.mts +45 -0
- package/dist/signing/energy.mjs +120 -0
- package/dist/signing/jws.cjs +229 -0
- package/dist/signing/jws.d.cts +45 -0
- package/dist/signing/jws.d.mts +45 -0
- package/dist/signing/jws.mjs +211 -0
- package/dist/signing/keys.cjs +191 -0
- package/dist/signing/keys.d.cts +21 -0
- package/dist/signing/keys.d.mts +21 -0
- package/dist/signing/keys.mjs +182 -0
- package/dist/signing/session.cjs +186 -0
- package/dist/signing/session.d.cts +106 -0
- package/dist/signing/session.d.mts +106 -0
- package/dist/signing/session.mjs +184 -0
- package/dist/tariff.cjs +319 -0
- package/dist/tariff.d.cts +40 -0
- package/dist/tariff.d.mts +40 -0
- package/dist/tariff.mjs +317 -0
- package/dist/teslafleetapi.cjs +326 -0
- package/dist/teslafleetapi.d.cts +160 -0
- package/dist/teslafleetapi.d.mts +160 -0
- package/dist/teslafleetapi.mjs +326 -0
- package/dist/teslemetry.cjs +183 -0
- package/dist/teslemetry.d.cts +113 -0
- package/dist/teslemetry.d.mts +113 -0
- package/dist/teslemetry.mjs +181 -0
- package/dist/teslemetryenergy.cjs +287 -0
- package/dist/teslemetryenergy.d.cts +154 -0
- package/dist/teslemetryenergy.d.mts +154 -0
- package/dist/teslemetryenergy.mjs +282 -0
- package/dist/teslemetryvehicle.cjs +407 -0
- package/dist/teslemetryvehicle.d.cts +238 -0
- package/dist/teslemetryvehicle.d.mts +238 -0
- package/dist/teslemetryvehicle.mjs +405 -0
- package/dist/tessie.cjs +71 -0
- package/dist/tessie.d.cts +69 -0
- package/dist/tessie.d.mts +69 -0
- package/dist/tessie.mjs +71 -0
- package/dist/tessievehicle.cjs +514 -0
- package/dist/tessievehicle.d.cts +333 -0
- package/dist/tessievehicle.d.mts +333 -0
- package/dist/tessievehicle.mjs +513 -0
- package/dist/types/commands.d.cts +28 -0
- package/dist/types/commands.d.mts +28 -0
- package/dist/types/live_status.d.cts +30 -0
- package/dist/types/live_status.d.mts +30 -0
- package/dist/types/products.d.cts +58 -0
- package/dist/types/products.d.mts +58 -0
- package/dist/types/responses.d.cts +13 -0
- package/dist/types/responses.d.mts +13 -0
- package/dist/types/site_info.d.cts +154 -0
- package/dist/types/site_info.d.mts +154 -0
- package/dist/types/vehicle.d.cts +33 -0
- package/dist/types/vehicle.d.mts +33 -0
- package/dist/types/vehicle_data.d.cts +278 -0
- package/dist/types/vehicle_data.d.mts +278 -0
- package/dist/types.cjs +0 -0
- package/dist/types.d.cts +9 -0
- package/dist/types.d.mts +9 -0
- package/dist/types.mjs +1 -0
- package/dist/user.cjs +28 -0
- package/dist/user.d.cts +17 -0
- package/dist/user.d.mts +16 -0
- package/dist/user.mjs +28 -0
- package/dist/util.cjs +41 -0
- package/dist/util.d.cts +18 -0
- package/dist/util.d.mts +18 -0
- package/dist/util.mjs +40 -0
- package/dist/vehicle.cjs +847 -0
- package/dist/vehicle.d.cts +566 -0
- package/dist/vehicle.d.mts +565 -0
- package/dist/vehicle.mjs +847 -0
- package/dist/vehiclesigned.cjs +35 -0
- package/dist/vehiclesigned.d.cts +23 -0
- package/dist/vehiclesigned.d.mts +22 -0
- package/dist/vehiclesigned.mjs +35 -0
- package/dist/vehiclespecific.cjs +715 -0
- package/dist/vehiclespecific.d.cts +497 -0
- package/dist/vehiclespecific.d.mts +496 -0
- package/dist/vehiclespecific.mjs +715 -0
- package/package.json +178 -38
- package/dist/charging.d.ts +0 -30
- package/dist/charging.js +0 -51
- package/dist/charging.js.map +0 -1
- package/dist/commands.d.ts +0 -352
- package/dist/commands.js +0 -875
- package/dist/commands.js.map +0 -1
- package/dist/energy.d.ts +0 -94
- package/dist/energy.js +0 -125
- package/dist/energy.js.map +0 -1
- package/dist/energyspecific.d.ts +0 -77
- package/dist/energyspecific.js +0 -97
- package/dist/energyspecific.js.map +0 -1
- package/dist/helpers.d.ts +0 -2
- package/dist/helpers.js +0 -10
- package/dist/helpers.js.map +0 -1
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -15
- package/dist/index.js.map +0 -1
- package/dist/partner.d.ts +0 -23
- package/dist/partner.js +0 -31
- package/dist/partner.js.map +0 -1
- package/dist/signing/crypto.d.ts +0 -10
- package/dist/signing/crypto.js +0 -26
- package/dist/signing/crypto.js.map +0 -1
- package/dist/signing/errors.d.ts +0 -29
- package/dist/signing/errors.js +0 -40
- package/dist/signing/errors.js.map +0 -1
- package/dist/signing/session.d.ts +0 -69
- package/dist/signing/session.js +0 -102
- package/dist/signing/session.js.map +0 -1
- package/dist/tariff.d.ts +0 -29
- package/dist/tariff.js +0 -302
- package/dist/tariff.js.map +0 -1
- package/dist/teslafleetapi.d.ts +0 -58
- package/dist/teslafleetapi.js +0 -120
- package/dist/teslafleetapi.js.map +0 -1
- package/dist/teslemetry.d.ts +0 -29
- package/dist/teslemetry.js +0 -39
- package/dist/teslemetry.js.map +0 -1
- package/dist/tessie.d.ts +0 -23
- package/dist/tessie.js +0 -28
- package/dist/tessie.js.map +0 -1
- package/dist/types/commands.d.ts +0 -44
- package/dist/types/commands.js +0 -11
- package/dist/types/commands.js.map +0 -1
- package/dist/types/live_status.d.ts +0 -26
- package/dist/types/live_status.js +0 -2
- package/dist/types/live_status.js.map +0 -1
- package/dist/types/products.d.ts +0 -54
- package/dist/types/products.js +0 -2
- package/dist/types/products.js.map +0 -1
- package/dist/types/responses.d.ts +0 -5
- package/dist/types/responses.js +0 -2
- package/dist/types/responses.js.map +0 -1
- package/dist/types/site_info.d.ts +0 -150
- package/dist/types/site_info.js +0 -2
- package/dist/types/site_info.js.map +0 -1
- package/dist/types/vehicle.d.ts +0 -23
- package/dist/types/vehicle.js +0 -10
- package/dist/types/vehicle.js.map +0 -1
- package/dist/types/vehicle_data.d.ts +0 -274
- package/dist/types/vehicle_data.js +0 -2
- package/dist/types/vehicle_data.js.map +0 -1
- package/dist/user.d.ts +0 -16
- package/dist/user.js +0 -26
- package/dist/user.js.map +0 -1
- package/dist/vehicle.d.ts +0 -548
- package/dist/vehicle.js +0 -774
- package/dist/vehicle.js.map +0 -1
- package/dist/vehiclesigned.d.ts +0 -13
- package/dist/vehiclesigned.js +0 -19
- package/dist/vehiclesigned.js.map +0 -1
- package/dist/vehiclespecific.d.ts +0 -432
- package/dist/vehiclespecific.js +0 -601
- package/dist/vehiclespecific.js.map +0 -1
- package/src/charging.ts +0 -66
- package/src/commands.ts +0 -1134
- package/src/energy.ts +0 -217
- package/src/energyspecific.ts +0 -116
- package/src/helpers.ts +0 -11
- package/src/index.ts +0 -21
- package/src/partner.ts +0 -37
- package/src/signing/crypto.ts +0 -29
- package/src/signing/errors.ts +0 -50
- package/src/signing/session.ts +0 -132
- package/src/tariff.ts +0 -375
- package/src/teslafleetapi.ts +0 -182
- package/src/teslemetry.ts +0 -54
- package/src/tessie.ts +0 -42
- package/src/types/commands.ts +0 -62
- package/src/types/live_status.ts +0 -27
- package/src/types/products.ts +0 -61
- package/src/types/responses.ts +0 -1
- package/src/types/site_info.ts +0 -159
- package/src/types/vehicle.ts +0 -33
- package/src/types/vehicle_data.ts +0 -286
- package/src/user.ts +0 -35
- package/src/vehicle.ts +0 -886
- package/src/vehiclesigned.ts +0 -20
- package/src/vehiclespecific.ts +0 -818
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { validate_island_mode } from "../energyisland.mjs";
|
|
2
|
+
import { uint32BE } from "./crypto.mjs";
|
|
3
|
+
import { rsaPublicDerPkcs1 } from "./keys.mjs";
|
|
4
|
+
import { MessageEnvelope } from "@teslemetry/tesla-protocol/energy_device/transport";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { AuthorizedClientType } from "@teslemetry/tesla-protocol/energy_device/authorization_types";
|
|
7
|
+
import { Domain, RoutableMessage, SignatureType, Tag } from "@teslemetry/tesla-protocol/energy_device/signed_message";
|
|
8
|
+
// src/signing/energy.ts
|
|
9
|
+
/** The cloud round trip needs more than the 12 s LAN TTL; 30 s worked live through `device_command`. */
|
|
10
|
+
const ENERGY_SIGNATURE_TTL_SECONDS = 30;
|
|
11
|
+
/** ts-proto's value for an enum input it does not recognise. */
|
|
12
|
+
const UNRECOGNIZED_ENUM = -1;
|
|
13
|
+
/** Encode one tag-length-value entry. */
|
|
14
|
+
function tlv(tag, value) {
|
|
15
|
+
return Buffer.concat([Buffer.from([tag, value.length]), value]);
|
|
16
|
+
}
|
|
17
|
+
function isPlainObject(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
/** A proto field name in the camelCase form ts-proto generates. */
|
|
21
|
+
function camelCase(name) {
|
|
22
|
+
return name.replace(/_([a-z0-9])/g, (_, character) => character.toUpperCase());
|
|
23
|
+
}
|
|
24
|
+
/** Convert every key from Python's proto field names; ts-proto's `fromJSON` reads camelCase only. */
|
|
25
|
+
function camelCaseKeys(value) {
|
|
26
|
+
if (Array.isArray(value)) return value.map(camelCaseKeys);
|
|
27
|
+
if (isPlainObject(value)) return Object.fromEntries(Object.entries(value).map(([key, item]) => [camelCase(key), camelCaseKeys(item)]));
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reject input `fromJSON` dropped or could not read, as Python's `ParseDict`
|
|
32
|
+
* does: otherwise a mistyped field would be signed and sent without it.
|
|
33
|
+
*/
|
|
34
|
+
function assertParsed(input, parsed, path) {
|
|
35
|
+
if (Array.isArray(input) && Array.isArray(parsed)) input.forEach((item, index) => assertParsed(item, parsed[index], `${path}[${index}]`));
|
|
36
|
+
else if (isPlainObject(input) && isPlainObject(parsed) && !(parsed instanceof Date) && !ArrayBuffer.isView(parsed)) for (const [key, item] of Object.entries(input)) {
|
|
37
|
+
if (!(key in parsed)) throw new TypeError(`Unknown energy device command field ${path}.${key}`);
|
|
38
|
+
assertParsed(item, parsed[key], `${path}.${key}`);
|
|
39
|
+
}
|
|
40
|
+
else if (parsed instanceof Date && Number.isNaN(parsed.getTime()) || parsed === UNRECOGNIZED_ENUM && input !== UNRECOGNIZED_ENUM) throw new TypeError(`Invalid value for energy device command field ${path}`);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Builds RSA-signed `RoutableMessage` bytes for one energy device key.
|
|
44
|
+
*
|
|
45
|
+
* The key must be an RSA key registered on the gateway with
|
|
46
|
+
* `add_authorized_client` and in state `VERIFIED`. An ECC key cannot sign this
|
|
47
|
+
* scheme: the `SignatureData` oneof has only `rsa_data`.
|
|
48
|
+
*/
|
|
49
|
+
var EnergyDeviceSigner = class {
|
|
50
|
+
/** The signer's public key, PKCS#1 DER. */
|
|
51
|
+
publicKeyDer;
|
|
52
|
+
/** Seconds a signed message stays valid. */
|
|
53
|
+
ttl;
|
|
54
|
+
#privateKey;
|
|
55
|
+
/**
|
|
56
|
+
* @param privateKey The RSA private key.
|
|
57
|
+
* @param ttl Seconds a signed message stays valid. `V1rTransport` passes 12.
|
|
58
|
+
* @throws TypeError for anything but an RSA private key.
|
|
59
|
+
*/
|
|
60
|
+
constructor(privateKey, ttl = 30) {
|
|
61
|
+
if (privateKey?.type !== "private" || privateKey.asymmetricKeyType !== "rsa") throw new TypeError("Energy device signing requires an RSA private key");
|
|
62
|
+
this.#privateKey = privateKey;
|
|
63
|
+
this.publicKeyDer = rsaPublicDerPkcs1(privateKey);
|
|
64
|
+
this.ttl = ttl;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Serialize a cloud `MessageEnvelope` addressed to `din`.
|
|
68
|
+
*
|
|
69
|
+
* `category`, `command` and `params` are the snake_case proto field names
|
|
70
|
+
* the unsigned `grpc_command` sends, so every command can be signed without
|
|
71
|
+
* per-command code. `delivery_channel` stays unset, as in the envelope
|
|
72
|
+
* proven on the cloud path.
|
|
73
|
+
* @throws TypeError for a field the message does not have, or an island mode that is not 1 or 6.
|
|
74
|
+
*/
|
|
75
|
+
static envelope(din, category, command, params) {
|
|
76
|
+
if (category === "teg" && command === "set_island_mode_request") validate_island_mode(params?.mode, params?.force);
|
|
77
|
+
const input = camelCaseKeys({ [category]: { [command]: params ?? {} } });
|
|
78
|
+
const envelope = MessageEnvelope.fromJSON(input);
|
|
79
|
+
assertParsed(input, envelope, "message");
|
|
80
|
+
envelope.sender = { authorizedClient: AuthorizedClientType.AUTHORIZED_CLIENT_TYPE_CUSTOMER_MOBILE_APP };
|
|
81
|
+
envelope.recipient = { din };
|
|
82
|
+
return MessageEnvelope.encode(envelope).finish();
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Wrap `envelope` in a signed `RoutableMessage` and serialize it.
|
|
86
|
+
*
|
|
87
|
+
* RSA-4096 blocks for milliseconds, so this signs on the thread pool.
|
|
88
|
+
* @param envelope The serialized `MessageEnvelope`.
|
|
89
|
+
* @param din The gateway DIN the envelope and the request target.
|
|
90
|
+
* @param options `now` (in seconds) and `messageUuid` exist for test vectors.
|
|
91
|
+
*/
|
|
92
|
+
async sign(envelope, din, options = {}) {
|
|
93
|
+
if (din.length > 255 || /\P{ASCII}/u.test(din)) throw new TypeError("A gateway DIN must be at most 255 ASCII characters");
|
|
94
|
+
const expiresAt = Math.ceil(options.now ?? Date.now() / 1e3) + this.ttl;
|
|
95
|
+
const signedData = Buffer.concat([
|
|
96
|
+
tlv(Tag.TAG_SIGNATURE_TYPE, Buffer.from([SignatureType.SIGNATURE_TYPE_RSA])),
|
|
97
|
+
tlv(Tag.TAG_DOMAIN, Buffer.from([Domain.DOMAIN_ENERGY_DEVICE])),
|
|
98
|
+
tlv(Tag.TAG_PERSONALIZATION, Buffer.from(din, "ascii")),
|
|
99
|
+
tlv(Tag.TAG_EXPIRES_AT, uint32BE(expiresAt)),
|
|
100
|
+
Buffer.from([Tag.TAG_END]),
|
|
101
|
+
envelope
|
|
102
|
+
]);
|
|
103
|
+
const signature = await new Promise((resolve, reject) => {
|
|
104
|
+
crypto.sign("sha512", signedData, this.#privateKey, (error, result) => error ? reject(error) : resolve(result));
|
|
105
|
+
});
|
|
106
|
+
return RoutableMessage.encode(RoutableMessage.create({
|
|
107
|
+
toDestination: { domain: Domain.DOMAIN_ENERGY_DEVICE },
|
|
108
|
+
protobufMessageAsBytes: envelope,
|
|
109
|
+
signatureData: {
|
|
110
|
+
signerIdentity: { publicKey: this.publicKeyDer },
|
|
111
|
+
rsaData: {
|
|
112
|
+
expiresAt,
|
|
113
|
+
signature
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
uuid: options.messageUuid ?? crypto.randomBytes(16)
|
|
117
|
+
})).finish();
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
export { ENERGY_SIGNATURE_TTL_SECONDS, EnergyDeviceSigner };
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_crypto = require("./crypto.cjs");
|
|
3
|
+
let crypto = require("crypto");
|
|
4
|
+
crypto = require_runtime.__toESM(crypto, 1);
|
|
5
|
+
// src/signing/jws.ts
|
|
6
|
+
/**
|
|
7
|
+
* Tesla.SS256: Schnorr signatures over P-256 and the compact JWS that carries
|
|
8
|
+
* them, ported from `python-tesla-fleet-api`'s `tesla/jws.py`.
|
|
9
|
+
*
|
|
10
|
+
* Tesla signs messages for fleet-wide or offline delivery, such as a Fleet
|
|
11
|
+
* Telemetry configuration, with Schnorr/P-256/SHA-256 (RFC 8235) using the EC
|
|
12
|
+
* key the partner registered for signed commands, wrapped in a compact JWS with
|
|
13
|
+
* `"alg": "Tesla.SS256"`. The scheme is Tesla's Go SDK's
|
|
14
|
+
* (`teslamotors/vehicle-command`, `internal/schnorr`,
|
|
15
|
+
* `internal/authentication/jwt.go`), and `test/jws.test.ts` checks it against
|
|
16
|
+
* vectors that code produced.
|
|
17
|
+
*
|
|
18
|
+
* A signature is 96 bytes `V_x || V_y || r`, where `V = vG` is the public nonce
|
|
19
|
+
* and `r = v - a*c (mod n)` for private key `a` and challenge `c`. The nonce `v`
|
|
20
|
+
* is deterministic (RFC 6979 over SHA-256 of the message), so signing the same
|
|
21
|
+
* message with the same key is reproducible. There is deliberately no low-`s`
|
|
22
|
+
* normalisation: that is an ECDSA malleability fix, not part of this scheme, and
|
|
23
|
+
* applying it would produce signatures Tesla rejects.
|
|
24
|
+
*/
|
|
25
|
+
var jws_exports = /* @__PURE__ */ require_runtime.__exportAll({
|
|
26
|
+
ALGORITHM: () => ALGORITHM,
|
|
27
|
+
G: () => G,
|
|
28
|
+
N: () => N,
|
|
29
|
+
P: () => P,
|
|
30
|
+
SCALAR_LENGTH: () => 32,
|
|
31
|
+
SIGNATURE_LENGTH: () => 96,
|
|
32
|
+
TELEMETRY_AUDIENCE: () => TELEMETRY_AUDIENCE,
|
|
33
|
+
deterministicNonce: () => deterministicNonce,
|
|
34
|
+
encodePoint: () => encodePoint,
|
|
35
|
+
public_key_bytes: () => public_key_bytes,
|
|
36
|
+
sign: () => sign,
|
|
37
|
+
sign_fleet_telemetry_config: () => sign_fleet_telemetry_config,
|
|
38
|
+
sign_jws: () => sign_jws,
|
|
39
|
+
verify: () => verify
|
|
40
|
+
});
|
|
41
|
+
const ALGORITHM = "Tesla.SS256";
|
|
42
|
+
const TELEMETRY_AUDIENCE = "com.tesla.fleet.TelemetryClient";
|
|
43
|
+
/** The 65-byte uncompressed public point for a raw P-256 private key. */
|
|
44
|
+
function public_key_bytes(privateKey) {
|
|
45
|
+
return require_crypto.publicKeyFor(Buffer.from(privateKey));
|
|
46
|
+
}
|
|
47
|
+
const P = 115792089210356248762697446949407573530086143415290314195533631308867097853951n;
|
|
48
|
+
const N = 115792089210356248762697446949407573529996955224135760342422259061068512044369n;
|
|
49
|
+
const G = [48439561293906451759052585252797914202762949526041747995844080717082404635286n, 36134250956749795798585127919587881956611106672985015071877198253568414405109n];
|
|
50
|
+
function toBigInt(bytes) {
|
|
51
|
+
return bytes.length === 0 ? 0n : BigInt(`0x${Buffer.from(bytes).toString("hex")}`);
|
|
52
|
+
}
|
|
53
|
+
function scalarBytes(value) {
|
|
54
|
+
return Buffer.from(value.toString(16).padStart(64, "0"), "hex");
|
|
55
|
+
}
|
|
56
|
+
/** `a mod m`, in `[0, m)` whatever the sign of `a`. */
|
|
57
|
+
function mod(a, m) {
|
|
58
|
+
const remainder = a % m;
|
|
59
|
+
return remainder < 0n ? remainder + m : remainder;
|
|
60
|
+
}
|
|
61
|
+
/** The inverse of `a` modulo the prime `m`, by the extended Euclidean algorithm. */
|
|
62
|
+
function inverse(a, m) {
|
|
63
|
+
let [r0, r1] = [mod(a, m), m];
|
|
64
|
+
let [s0, s1] = [1n, 0n];
|
|
65
|
+
while (r1 !== 0n) {
|
|
66
|
+
const quotient = r0 / r1;
|
|
67
|
+
[r0, r1] = [r1, r0 - quotient * r1];
|
|
68
|
+
[s0, s1] = [s1, s0 - quotient * s1];
|
|
69
|
+
}
|
|
70
|
+
return mod(s0, m);
|
|
71
|
+
}
|
|
72
|
+
/** Uncompressed SEC1 encoding: `0x04 || X || Y`. */
|
|
73
|
+
function encodePoint(point) {
|
|
74
|
+
return Buffer.concat([
|
|
75
|
+
Buffer.from([4]),
|
|
76
|
+
scalarBytes(point[0]),
|
|
77
|
+
scalarBytes(point[1])
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
function lengthValue(buffer) {
|
|
81
|
+
const length = Buffer.alloc(4);
|
|
82
|
+
length.writeUInt32BE(buffer.length, 0);
|
|
83
|
+
return Buffer.concat([length, buffer]);
|
|
84
|
+
}
|
|
85
|
+
/** SHA-256 over the length-prefixed generator, public nonce, signer key and message. */
|
|
86
|
+
function challenge(publicNonce, publicKey, message) {
|
|
87
|
+
return toBigInt(crypto.default.createHash("sha256").update(lengthValue(encodePoint(G))).update(lengthValue(publicNonce)).update(lengthValue(publicKey)).update(lengthValue(message)).digest());
|
|
88
|
+
}
|
|
89
|
+
function hmac(key, ...data) {
|
|
90
|
+
const mac = crypto.default.createHmac("sha256", key);
|
|
91
|
+
for (const part of data) mac.update(part);
|
|
92
|
+
return mac.digest();
|
|
93
|
+
}
|
|
94
|
+
/** RFC 6979 section 3.2 nonce, specialised to P-256 with SHA-256 (hlen = qlen). */
|
|
95
|
+
function deterministicNonce(scalar, messageHash) {
|
|
96
|
+
const h1 = scalarBytes(toBigInt(messageHash) % N);
|
|
97
|
+
const zero = Buffer.from([0]);
|
|
98
|
+
const one = Buffer.from([1]);
|
|
99
|
+
let k = Buffer.alloc(32, 0);
|
|
100
|
+
let v = Buffer.alloc(32, 1);
|
|
101
|
+
k = hmac(k, v, zero, scalar, h1);
|
|
102
|
+
v = hmac(k, v);
|
|
103
|
+
k = hmac(k, v, one, scalar, h1);
|
|
104
|
+
v = hmac(k, v);
|
|
105
|
+
while (true) {
|
|
106
|
+
v = hmac(k, v);
|
|
107
|
+
const candidate = toBigInt(v);
|
|
108
|
+
if (candidate > 0n && candidate < 115792089210356248762697446949407573529996955224135760342422259061068512044369n) return v;
|
|
109
|
+
k = hmac(k, v, zero);
|
|
110
|
+
v = hmac(k, v);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The 96-byte Tesla.SS256 Schnorr signature of `message`.
|
|
115
|
+
*
|
|
116
|
+
* The nonce point goes through OpenSSL, so only the public result of multiplying
|
|
117
|
+
* a secret scalar is ever visible; the arithmetic on `r` is the one place a
|
|
118
|
+
* secret meets `BigInt`, as it meets Python's `int`.
|
|
119
|
+
* @param privateKey Raw 32-byte P-256 scalar.
|
|
120
|
+
*/
|
|
121
|
+
function sign(privateKey, message) {
|
|
122
|
+
if (privateKey.length !== 32) throw new Error("Tesla.SS256 requires a P-256 (SECP256R1) key.");
|
|
123
|
+
const key = Buffer.from(privateKey);
|
|
124
|
+
const nonce = deterministicNonce(key, crypto.default.createHash("sha256").update(message).digest());
|
|
125
|
+
const publicNonce = require_crypto.publicKeyFor(nonce);
|
|
126
|
+
const c = challenge(publicNonce, require_crypto.publicKeyFor(key), message);
|
|
127
|
+
const r = mod(toBigInt(nonce) - toBigInt(key) * c, N);
|
|
128
|
+
return Buffer.concat([publicNonce.subarray(1), scalarBytes(r)]);
|
|
129
|
+
}
|
|
130
|
+
function add(p, q) {
|
|
131
|
+
if (p === null) return q;
|
|
132
|
+
if (q === null) return p;
|
|
133
|
+
let slope;
|
|
134
|
+
if (p[0] === q[0]) {
|
|
135
|
+
if (mod(p[1] + q[1], 115792089210356248762697446949407573530086143415290314195533631308867097853951n) === 0n) return null;
|
|
136
|
+
slope = mod((3n * p[0] * p[0] - 3n) * inverse(2n * p[1], P), P);
|
|
137
|
+
} else slope = mod((q[1] - p[1]) * inverse(q[0] - p[0], P), P);
|
|
138
|
+
const x = mod(slope * slope - p[0] - q[0], P);
|
|
139
|
+
return [x, mod(slope * (p[0] - x) - p[1], P)];
|
|
140
|
+
}
|
|
141
|
+
function multiply(k, point) {
|
|
142
|
+
let result = null;
|
|
143
|
+
let addend = point;
|
|
144
|
+
for (let remaining = k; remaining > 0n; remaining >>= 1n) {
|
|
145
|
+
if (remaining & 1n) result = add(result, addend);
|
|
146
|
+
addend = add(addend, addend);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
function decodePublicKey(publicKey) {
|
|
151
|
+
if (publicKey.length !== 65) return null;
|
|
152
|
+
try {
|
|
153
|
+
crypto.default.ECDH.convertKey(publicKey, "prime256v1");
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return [toBigInt(publicKey.subarray(1, 33)), toBigInt(publicKey.subarray(33))];
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check a Tesla.SS256 signature against a 65-byte uncompressed public key.
|
|
161
|
+
*
|
|
162
|
+
* Verification uses only public values, so the plain `BigInt` point arithmetic
|
|
163
|
+
* here is not a side-channel concern.
|
|
164
|
+
*/
|
|
165
|
+
function verify(publicKey, message, signature) {
|
|
166
|
+
if (signature.length !== 96) return false;
|
|
167
|
+
const point = decodePublicKey(publicKey);
|
|
168
|
+
if (point === null) return false;
|
|
169
|
+
const nonceBytes = Buffer.from(signature.subarray(0, 64));
|
|
170
|
+
const r = toBigInt(signature.subarray(64));
|
|
171
|
+
const expected = add(multiply(challenge(Buffer.concat([Buffer.from([4]), nonceBytes]), publicKey, message), point), multiply(r, G));
|
|
172
|
+
return expected !== null && encodePoint(expected).subarray(1).equals(nonceBytes);
|
|
173
|
+
}
|
|
174
|
+
function base64Url(data) {
|
|
175
|
+
return Buffer.from(data).toString("base64url");
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Sorted, compact, ASCII-only JSON: the bytes Python's
|
|
179
|
+
* `json.dumps(value, sort_keys=True, separators=(",", ":"))` produces. The sort
|
|
180
|
+
* happens here rather than on a new object, whose integer-like keys JavaScript
|
|
181
|
+
* would reorder.
|
|
182
|
+
*/
|
|
183
|
+
function canonicalJson(value) {
|
|
184
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item ?? null)).join(",")}]`;
|
|
185
|
+
if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${canonicalJson(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
186
|
+
return JSON.stringify(value).replace(/[\u007f-]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* A compact Tesla.SS256 JWS over `claims`.
|
|
190
|
+
*
|
|
191
|
+
* Like the Go SDK, `iss` is set to the standard (padded) base64 of the signer's
|
|
192
|
+
* uncompressed public key and `aud` to `audience`, overwriting any caller-supplied
|
|
193
|
+
* values.
|
|
194
|
+
* @param privateKey Raw 32-byte P-256 scalar.
|
|
195
|
+
*/
|
|
196
|
+
function sign_jws(privateKey, claims, audience) {
|
|
197
|
+
const payload = {
|
|
198
|
+
...claims,
|
|
199
|
+
iss: require_crypto.publicKeyFor(Buffer.from(privateKey)).toString("base64"),
|
|
200
|
+
aud: audience
|
|
201
|
+
};
|
|
202
|
+
const signingInput = `${base64Url(canonicalJson({
|
|
203
|
+
alg: ALGORITHM,
|
|
204
|
+
typ: "JWT"
|
|
205
|
+
}))}.${base64Url(canonicalJson(payload))}`;
|
|
206
|
+
return `${signingInput}.${base64Url(sign(privateKey, Buffer.from(signingInput)))}`;
|
|
207
|
+
}
|
|
208
|
+
/** The JWS token `fleet_telemetry_config_jws` expects for `config`. */
|
|
209
|
+
function sign_fleet_telemetry_config(privateKey, config) {
|
|
210
|
+
return sign_jws(privateKey, config, TELEMETRY_AUDIENCE);
|
|
211
|
+
}
|
|
212
|
+
exports.ALGORITHM = ALGORITHM;
|
|
213
|
+
exports.G = G;
|
|
214
|
+
exports.N = N;
|
|
215
|
+
exports.P = P;
|
|
216
|
+
exports.TELEMETRY_AUDIENCE = TELEMETRY_AUDIENCE;
|
|
217
|
+
exports.deterministicNonce = deterministicNonce;
|
|
218
|
+
exports.encodePoint = encodePoint;
|
|
219
|
+
Object.defineProperty(exports, "jws_exports", {
|
|
220
|
+
enumerable: true,
|
|
221
|
+
get: function() {
|
|
222
|
+
return jws_exports;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
exports.public_key_bytes = public_key_bytes;
|
|
226
|
+
exports.sign = sign;
|
|
227
|
+
exports.sign_fleet_telemetry_config = sign_fleet_telemetry_config;
|
|
228
|
+
exports.sign_jws = sign_jws;
|
|
229
|
+
exports.verify = verify;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
declare namespace jws_d_exports {
|
|
2
|
+
export { ALGORITHM, G, N, P, SCALAR_LENGTH, SIGNATURE_LENGTH, TELEMETRY_AUDIENCE, deterministicNonce, encodePoint, public_key_bytes, sign, sign_fleet_telemetry_config, sign_jws, verify };
|
|
3
|
+
}
|
|
4
|
+
export declare const ALGORITHM = "Tesla.SS256";
|
|
5
|
+
export declare const TELEMETRY_AUDIENCE = "com.tesla.fleet.TelemetryClient";
|
|
6
|
+
export declare const SCALAR_LENGTH = 32;
|
|
7
|
+
export declare const SIGNATURE_LENGTH: number;
|
|
8
|
+
/** The 65-byte uncompressed public point for a raw P-256 private key. */
|
|
9
|
+
export declare function public_key_bytes(privateKey: Uint8Array): Buffer;
|
|
10
|
+
type AffinePoint = readonly [x: bigint, y: bigint];
|
|
11
|
+
export declare const P = 115792089210356248762697446949407573530086143415290314195533631308867097853951n;
|
|
12
|
+
export declare const N = 115792089210356248762697446949407573529996955224135760342422259061068512044369n;
|
|
13
|
+
export declare const G: AffinePoint;
|
|
14
|
+
/** Uncompressed SEC1 encoding: `0x04 || X || Y`. */
|
|
15
|
+
export declare function encodePoint(point: AffinePoint): Buffer;
|
|
16
|
+
/** RFC 6979 section 3.2 nonce, specialised to P-256 with SHA-256 (hlen = qlen). */
|
|
17
|
+
export declare function deterministicNonce(scalar: Uint8Array, messageHash: Uint8Array): Buffer;
|
|
18
|
+
/**
|
|
19
|
+
* The 96-byte Tesla.SS256 Schnorr signature of `message`.
|
|
20
|
+
*
|
|
21
|
+
* The nonce point goes through OpenSSL, so only the public result of multiplying
|
|
22
|
+
* a secret scalar is ever visible; the arithmetic on `r` is the one place a
|
|
23
|
+
* secret meets `BigInt`, as it meets Python's `int`.
|
|
24
|
+
* @param privateKey Raw 32-byte P-256 scalar.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sign(privateKey: Uint8Array, message: Uint8Array): Buffer;
|
|
27
|
+
/**
|
|
28
|
+
* Check a Tesla.SS256 signature against a 65-byte uncompressed public key.
|
|
29
|
+
*
|
|
30
|
+
* Verification uses only public values, so the plain `BigInt` point arithmetic
|
|
31
|
+
* here is not a side-channel concern.
|
|
32
|
+
*/
|
|
33
|
+
export declare function verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* A compact Tesla.SS256 JWS over `claims`.
|
|
36
|
+
*
|
|
37
|
+
* Like the Go SDK, `iss` is set to the standard (padded) base64 of the signer's
|
|
38
|
+
* uncompressed public key and `aud` to `audience`, overwriting any caller-supplied
|
|
39
|
+
* values.
|
|
40
|
+
* @param privateKey Raw 32-byte P-256 scalar.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sign_jws(privateKey: Uint8Array, claims: Readonly<Record<string, unknown>>, audience: string): string;
|
|
43
|
+
/** The JWS token `fleet_telemetry_config_jws` expects for `config`. */
|
|
44
|
+
export declare function sign_fleet_telemetry_config(privateKey: Uint8Array, config: Readonly<Record<string, unknown>>): string;
|
|
45
|
+
export { jws_d_exports };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
declare namespace jws_d_exports {
|
|
2
|
+
export { ALGORITHM, G, N, P, SCALAR_LENGTH, SIGNATURE_LENGTH, TELEMETRY_AUDIENCE, deterministicNonce, encodePoint, public_key_bytes, sign, sign_fleet_telemetry_config, sign_jws, verify };
|
|
3
|
+
}
|
|
4
|
+
export declare const ALGORITHM = "Tesla.SS256";
|
|
5
|
+
export declare const TELEMETRY_AUDIENCE = "com.tesla.fleet.TelemetryClient";
|
|
6
|
+
export declare const SCALAR_LENGTH = 32;
|
|
7
|
+
export declare const SIGNATURE_LENGTH: number;
|
|
8
|
+
/** The 65-byte uncompressed public point for a raw P-256 private key. */
|
|
9
|
+
export declare function public_key_bytes(privateKey: Uint8Array): Buffer;
|
|
10
|
+
type AffinePoint = readonly [x: bigint, y: bigint];
|
|
11
|
+
export declare const P = 115792089210356248762697446949407573530086143415290314195533631308867097853951n;
|
|
12
|
+
export declare const N = 115792089210356248762697446949407573529996955224135760342422259061068512044369n;
|
|
13
|
+
export declare const G: AffinePoint;
|
|
14
|
+
/** Uncompressed SEC1 encoding: `0x04 || X || Y`. */
|
|
15
|
+
export declare function encodePoint(point: AffinePoint): Buffer;
|
|
16
|
+
/** RFC 6979 section 3.2 nonce, specialised to P-256 with SHA-256 (hlen = qlen). */
|
|
17
|
+
export declare function deterministicNonce(scalar: Uint8Array, messageHash: Uint8Array): Buffer;
|
|
18
|
+
/**
|
|
19
|
+
* The 96-byte Tesla.SS256 Schnorr signature of `message`.
|
|
20
|
+
*
|
|
21
|
+
* The nonce point goes through OpenSSL, so only the public result of multiplying
|
|
22
|
+
* a secret scalar is ever visible; the arithmetic on `r` is the one place a
|
|
23
|
+
* secret meets `BigInt`, as it meets Python's `int`.
|
|
24
|
+
* @param privateKey Raw 32-byte P-256 scalar.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sign(privateKey: Uint8Array, message: Uint8Array): Buffer;
|
|
27
|
+
/**
|
|
28
|
+
* Check a Tesla.SS256 signature against a 65-byte uncompressed public key.
|
|
29
|
+
*
|
|
30
|
+
* Verification uses only public values, so the plain `BigInt` point arithmetic
|
|
31
|
+
* here is not a side-channel concern.
|
|
32
|
+
*/
|
|
33
|
+
export declare function verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* A compact Tesla.SS256 JWS over `claims`.
|
|
36
|
+
*
|
|
37
|
+
* Like the Go SDK, `iss` is set to the standard (padded) base64 of the signer's
|
|
38
|
+
* uncompressed public key and `aud` to `audience`, overwriting any caller-supplied
|
|
39
|
+
* values.
|
|
40
|
+
* @param privateKey Raw 32-byte P-256 scalar.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sign_jws(privateKey: Uint8Array, claims: Readonly<Record<string, unknown>>, audience: string): string;
|
|
43
|
+
/** The JWS token `fleet_telemetry_config_jws` expects for `config`. */
|
|
44
|
+
export declare function sign_fleet_telemetry_config(privateKey: Uint8Array, config: Readonly<Record<string, unknown>>): string;
|
|
45
|
+
export { jws_d_exports };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { __exportAll } from "../_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
import { publicKeyFor } from "./crypto.mjs";
|
|
3
|
+
import crypto from "crypto";
|
|
4
|
+
// src/signing/jws.ts
|
|
5
|
+
/**
|
|
6
|
+
* Tesla.SS256: Schnorr signatures over P-256 and the compact JWS that carries
|
|
7
|
+
* them, ported from `python-tesla-fleet-api`'s `tesla/jws.py`.
|
|
8
|
+
*
|
|
9
|
+
* Tesla signs messages for fleet-wide or offline delivery, such as a Fleet
|
|
10
|
+
* Telemetry configuration, with Schnorr/P-256/SHA-256 (RFC 8235) using the EC
|
|
11
|
+
* key the partner registered for signed commands, wrapped in a compact JWS with
|
|
12
|
+
* `"alg": "Tesla.SS256"`. The scheme is Tesla's Go SDK's
|
|
13
|
+
* (`teslamotors/vehicle-command`, `internal/schnorr`,
|
|
14
|
+
* `internal/authentication/jwt.go`), and `test/jws.test.ts` checks it against
|
|
15
|
+
* vectors that code produced.
|
|
16
|
+
*
|
|
17
|
+
* A signature is 96 bytes `V_x || V_y || r`, where `V = vG` is the public nonce
|
|
18
|
+
* and `r = v - a*c (mod n)` for private key `a` and challenge `c`. The nonce `v`
|
|
19
|
+
* is deterministic (RFC 6979 over SHA-256 of the message), so signing the same
|
|
20
|
+
* message with the same key is reproducible. There is deliberately no low-`s`
|
|
21
|
+
* normalisation: that is an ECDSA malleability fix, not part of this scheme, and
|
|
22
|
+
* applying it would produce signatures Tesla rejects.
|
|
23
|
+
*/
|
|
24
|
+
var jws_exports = /* @__PURE__ */ __exportAll({
|
|
25
|
+
ALGORITHM: () => ALGORITHM,
|
|
26
|
+
G: () => G,
|
|
27
|
+
N: () => N,
|
|
28
|
+
P: () => P,
|
|
29
|
+
SCALAR_LENGTH: () => 32,
|
|
30
|
+
SIGNATURE_LENGTH: () => 96,
|
|
31
|
+
TELEMETRY_AUDIENCE: () => TELEMETRY_AUDIENCE,
|
|
32
|
+
deterministicNonce: () => deterministicNonce,
|
|
33
|
+
encodePoint: () => encodePoint,
|
|
34
|
+
public_key_bytes: () => public_key_bytes,
|
|
35
|
+
sign: () => sign,
|
|
36
|
+
sign_fleet_telemetry_config: () => sign_fleet_telemetry_config,
|
|
37
|
+
sign_jws: () => sign_jws,
|
|
38
|
+
verify: () => verify
|
|
39
|
+
});
|
|
40
|
+
const ALGORITHM = "Tesla.SS256";
|
|
41
|
+
const TELEMETRY_AUDIENCE = "com.tesla.fleet.TelemetryClient";
|
|
42
|
+
/** The 65-byte uncompressed public point for a raw P-256 private key. */
|
|
43
|
+
function public_key_bytes(privateKey) {
|
|
44
|
+
return publicKeyFor(Buffer.from(privateKey));
|
|
45
|
+
}
|
|
46
|
+
const P = 115792089210356248762697446949407573530086143415290314195533631308867097853951n;
|
|
47
|
+
const N = 115792089210356248762697446949407573529996955224135760342422259061068512044369n;
|
|
48
|
+
const G = [48439561293906451759052585252797914202762949526041747995844080717082404635286n, 36134250956749795798585127919587881956611106672985015071877198253568414405109n];
|
|
49
|
+
function toBigInt(bytes) {
|
|
50
|
+
return bytes.length === 0 ? 0n : BigInt(`0x${Buffer.from(bytes).toString("hex")}`);
|
|
51
|
+
}
|
|
52
|
+
function scalarBytes(value) {
|
|
53
|
+
return Buffer.from(value.toString(16).padStart(64, "0"), "hex");
|
|
54
|
+
}
|
|
55
|
+
/** `a mod m`, in `[0, m)` whatever the sign of `a`. */
|
|
56
|
+
function mod(a, m) {
|
|
57
|
+
const remainder = a % m;
|
|
58
|
+
return remainder < 0n ? remainder + m : remainder;
|
|
59
|
+
}
|
|
60
|
+
/** The inverse of `a` modulo the prime `m`, by the extended Euclidean algorithm. */
|
|
61
|
+
function inverse(a, m) {
|
|
62
|
+
let [r0, r1] = [mod(a, m), m];
|
|
63
|
+
let [s0, s1] = [1n, 0n];
|
|
64
|
+
while (r1 !== 0n) {
|
|
65
|
+
const quotient = r0 / r1;
|
|
66
|
+
[r0, r1] = [r1, r0 - quotient * r1];
|
|
67
|
+
[s0, s1] = [s1, s0 - quotient * s1];
|
|
68
|
+
}
|
|
69
|
+
return mod(s0, m);
|
|
70
|
+
}
|
|
71
|
+
/** Uncompressed SEC1 encoding: `0x04 || X || Y`. */
|
|
72
|
+
function encodePoint(point) {
|
|
73
|
+
return Buffer.concat([
|
|
74
|
+
Buffer.from([4]),
|
|
75
|
+
scalarBytes(point[0]),
|
|
76
|
+
scalarBytes(point[1])
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
function lengthValue(buffer) {
|
|
80
|
+
const length = Buffer.alloc(4);
|
|
81
|
+
length.writeUInt32BE(buffer.length, 0);
|
|
82
|
+
return Buffer.concat([length, buffer]);
|
|
83
|
+
}
|
|
84
|
+
/** SHA-256 over the length-prefixed generator, public nonce, signer key and message. */
|
|
85
|
+
function challenge(publicNonce, publicKey, message) {
|
|
86
|
+
return toBigInt(crypto.createHash("sha256").update(lengthValue(encodePoint(G))).update(lengthValue(publicNonce)).update(lengthValue(publicKey)).update(lengthValue(message)).digest());
|
|
87
|
+
}
|
|
88
|
+
function hmac(key, ...data) {
|
|
89
|
+
const mac = crypto.createHmac("sha256", key);
|
|
90
|
+
for (const part of data) mac.update(part);
|
|
91
|
+
return mac.digest();
|
|
92
|
+
}
|
|
93
|
+
/** RFC 6979 section 3.2 nonce, specialised to P-256 with SHA-256 (hlen = qlen). */
|
|
94
|
+
function deterministicNonce(scalar, messageHash) {
|
|
95
|
+
const h1 = scalarBytes(toBigInt(messageHash) % N);
|
|
96
|
+
const zero = Buffer.from([0]);
|
|
97
|
+
const one = Buffer.from([1]);
|
|
98
|
+
let k = Buffer.alloc(32, 0);
|
|
99
|
+
let v = Buffer.alloc(32, 1);
|
|
100
|
+
k = hmac(k, v, zero, scalar, h1);
|
|
101
|
+
v = hmac(k, v);
|
|
102
|
+
k = hmac(k, v, one, scalar, h1);
|
|
103
|
+
v = hmac(k, v);
|
|
104
|
+
while (true) {
|
|
105
|
+
v = hmac(k, v);
|
|
106
|
+
const candidate = toBigInt(v);
|
|
107
|
+
if (candidate > 0n && candidate < 115792089210356248762697446949407573529996955224135760342422259061068512044369n) return v;
|
|
108
|
+
k = hmac(k, v, zero);
|
|
109
|
+
v = hmac(k, v);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The 96-byte Tesla.SS256 Schnorr signature of `message`.
|
|
114
|
+
*
|
|
115
|
+
* The nonce point goes through OpenSSL, so only the public result of multiplying
|
|
116
|
+
* a secret scalar is ever visible; the arithmetic on `r` is the one place a
|
|
117
|
+
* secret meets `BigInt`, as it meets Python's `int`.
|
|
118
|
+
* @param privateKey Raw 32-byte P-256 scalar.
|
|
119
|
+
*/
|
|
120
|
+
function sign(privateKey, message) {
|
|
121
|
+
if (privateKey.length !== 32) throw new Error("Tesla.SS256 requires a P-256 (SECP256R1) key.");
|
|
122
|
+
const key = Buffer.from(privateKey);
|
|
123
|
+
const nonce = deterministicNonce(key, crypto.createHash("sha256").update(message).digest());
|
|
124
|
+
const publicNonce = publicKeyFor(nonce);
|
|
125
|
+
const c = challenge(publicNonce, publicKeyFor(key), message);
|
|
126
|
+
const r = mod(toBigInt(nonce) - toBigInt(key) * c, N);
|
|
127
|
+
return Buffer.concat([publicNonce.subarray(1), scalarBytes(r)]);
|
|
128
|
+
}
|
|
129
|
+
function add(p, q) {
|
|
130
|
+
if (p === null) return q;
|
|
131
|
+
if (q === null) return p;
|
|
132
|
+
let slope;
|
|
133
|
+
if (p[0] === q[0]) {
|
|
134
|
+
if (mod(p[1] + q[1], 115792089210356248762697446949407573530086143415290314195533631308867097853951n) === 0n) return null;
|
|
135
|
+
slope = mod((3n * p[0] * p[0] - 3n) * inverse(2n * p[1], P), P);
|
|
136
|
+
} else slope = mod((q[1] - p[1]) * inverse(q[0] - p[0], P), P);
|
|
137
|
+
const x = mod(slope * slope - p[0] - q[0], P);
|
|
138
|
+
return [x, mod(slope * (p[0] - x) - p[1], P)];
|
|
139
|
+
}
|
|
140
|
+
function multiply(k, point) {
|
|
141
|
+
let result = null;
|
|
142
|
+
let addend = point;
|
|
143
|
+
for (let remaining = k; remaining > 0n; remaining >>= 1n) {
|
|
144
|
+
if (remaining & 1n) result = add(result, addend);
|
|
145
|
+
addend = add(addend, addend);
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
function decodePublicKey(publicKey) {
|
|
150
|
+
if (publicKey.length !== 65) return null;
|
|
151
|
+
try {
|
|
152
|
+
crypto.ECDH.convertKey(publicKey, "prime256v1");
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return [toBigInt(publicKey.subarray(1, 33)), toBigInt(publicKey.subarray(33))];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Check a Tesla.SS256 signature against a 65-byte uncompressed public key.
|
|
160
|
+
*
|
|
161
|
+
* Verification uses only public values, so the plain `BigInt` point arithmetic
|
|
162
|
+
* here is not a side-channel concern.
|
|
163
|
+
*/
|
|
164
|
+
function verify(publicKey, message, signature) {
|
|
165
|
+
if (signature.length !== 96) return false;
|
|
166
|
+
const point = decodePublicKey(publicKey);
|
|
167
|
+
if (point === null) return false;
|
|
168
|
+
const nonceBytes = Buffer.from(signature.subarray(0, 64));
|
|
169
|
+
const r = toBigInt(signature.subarray(64));
|
|
170
|
+
const expected = add(multiply(challenge(Buffer.concat([Buffer.from([4]), nonceBytes]), publicKey, message), point), multiply(r, G));
|
|
171
|
+
return expected !== null && encodePoint(expected).subarray(1).equals(nonceBytes);
|
|
172
|
+
}
|
|
173
|
+
function base64Url(data) {
|
|
174
|
+
return Buffer.from(data).toString("base64url");
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Sorted, compact, ASCII-only JSON: the bytes Python's
|
|
178
|
+
* `json.dumps(value, sort_keys=True, separators=(",", ":"))` produces. The sort
|
|
179
|
+
* happens here rather than on a new object, whose integer-like keys JavaScript
|
|
180
|
+
* would reorder.
|
|
181
|
+
*/
|
|
182
|
+
function canonicalJson(value) {
|
|
183
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item ?? null)).join(",")}]`;
|
|
184
|
+
if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${canonicalJson(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
185
|
+
return JSON.stringify(value).replace(/[\u007f-]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A compact Tesla.SS256 JWS over `claims`.
|
|
189
|
+
*
|
|
190
|
+
* Like the Go SDK, `iss` is set to the standard (padded) base64 of the signer's
|
|
191
|
+
* uncompressed public key and `aud` to `audience`, overwriting any caller-supplied
|
|
192
|
+
* values.
|
|
193
|
+
* @param privateKey Raw 32-byte P-256 scalar.
|
|
194
|
+
*/
|
|
195
|
+
function sign_jws(privateKey, claims, audience) {
|
|
196
|
+
const payload = {
|
|
197
|
+
...claims,
|
|
198
|
+
iss: publicKeyFor(Buffer.from(privateKey)).toString("base64"),
|
|
199
|
+
aud: audience
|
|
200
|
+
};
|
|
201
|
+
const signingInput = `${base64Url(canonicalJson({
|
|
202
|
+
alg: ALGORITHM,
|
|
203
|
+
typ: "JWT"
|
|
204
|
+
}))}.${base64Url(canonicalJson(payload))}`;
|
|
205
|
+
return `${signingInput}.${base64Url(sign(privateKey, Buffer.from(signingInput)))}`;
|
|
206
|
+
}
|
|
207
|
+
/** The JWS token `fleet_telemetry_config_jws` expects for `config`. */
|
|
208
|
+
function sign_fleet_telemetry_config(privateKey, config) {
|
|
209
|
+
return sign_jws(privateKey, config, TELEMETRY_AUDIENCE);
|
|
210
|
+
}
|
|
211
|
+
export { ALGORITHM, G, N, P, TELEMETRY_AUDIENCE, deterministicNonce, encodePoint, jws_exports, public_key_bytes, sign, sign_fleet_telemetry_config, sign_jws, verify };
|