Implement gen_key and gen_key_confirm commands (#11)

This commit is contained in:
Michał Leszczyński
2023-03-02 02:30:48 +01:00
committed by GitHub
parent 2dcf101004
commit ef0924c62c
7 changed files with 198 additions and 42 deletions

View File

@@ -55,5 +55,7 @@ This library supports the following HaLo tag commands:
* `sign_random` - sign a sequential counter with random pad using ECDSA private key on the NFC tag;
* `write_latch` - write one-time programmable memory slot on the NFC tag;
* `cfg_ndef` - configure the parameters returned in the dynamic URL when the NFC tag is scanned;
* `gen_key` - request generation of the key #3 on the NFC tag;
* `gen_key_confirm` - confirm the generated public key of key slot #3;
Full article: [Documentation of the available commands (HaLo Command Set)](/docs/halo-command-set.md)

View File

@@ -3,7 +3,7 @@ const {ArgumentParser} = require("argparse");
const parser = new ArgumentParser({
description: 'HaLo - Command Line Tool for PC/SC'
});
const subparsers = parser.add_subparsers({help: 'command', dest: 'command'});
const subparsers = parser.add_subparsers({help: 'command', dest: 'name'});
subparsers.add_parser("version", {help: "Get tag version."});
@@ -111,10 +111,16 @@ setNDEFCfgParser.add_argument("--flag-legacy-static", {
required: false
});
let genKeyParser = subparsers.add_parser("gen_key", {help: "Generate key in slot #3."});
genKeyParser.add_argument("--entropy", {dest: 'entropy', help: "Additional entropy (32 bytes, hex encoded). Optional."});
let genKeyConfirmParser = subparsers.add_parser("gen_key_confirm", {help: "Confirm public key in slot #3 (only if additional entropy was provided)."});
genKeyConfirmParser.add_argument("--public-key", {dest: 'publicKey', help: "Key slot #3 public key", required: true});
function parseArgs() {
let args = parser.parse_args();
if (!args.command) {
if (!args.name) {
parser.print_help();
return null;
}

View File

@@ -61,7 +61,7 @@ Response:
Command:
```json
{
"command": "sign_raw",
"name": "sign_raw",
"keyNo": 1,
"digest": "0102030401020304010203040102030401020304010203040102030401020304"
}
@@ -130,7 +130,7 @@ of the digest that will be signed.
Command:
```json
{
"command": "sign_random",
"name": "sign_random",
"keyNo": 2
}
```
@@ -166,7 +166,7 @@ An object with `status: "ok"` key if the operation was successful.
Command:
```json
{
"command": "write_latch",
"name": "write_latch",
"latchNo": 1,
"data": "0102030401020304010203040102030401020304010203040102030401020304"
}
@@ -213,7 +213,7 @@ An object with `status: "ok"` key if the operation was successful.
Command:
```json
{
"command": "cfg_ndef",
"name": "cfg_ndef",
"flagUseText": false,
"flagHidePk1": false,
"flagHidePk2": false,
@@ -235,3 +235,73 @@ Response:
"status": "ok"
}
```
## Command: gen_key
Request the card to generate the key in #3 slot, which is uninitialized by default. You can optionally provide extra entropy for the key generation process using `command.entropy` key.
**Note:** If you provide additional entropy, this command will return the derived public key. You will need to confirm the public key using `gen_key_confirm` command.
This step is not necessary if you don't provide additional entropy.
### Arguments
* `entropy` (str) - optional, additional entropy (32 bytes, hex encoded);
### Return value
* `status` (str) - `ok` when the key is generated successfully;
* `publicKey` (str) - the public key that was generated;
* `needsConfirm` (bool) - whether you need to call `gen_key_confrm` after running this command;
### Examples
Command:
```json
{
"name": "gen_key",
"entropy": "3c825af7d2e1b02b6a00c257ebe883260b4aa6302c9878d412046d10141b261d"
}
```
Response:
```json
{
"status": "ok",
"publicKey": "04f670a3d30e2b98b2e3691908722e643791be4a58eaf63e02026df7d67ae456cdece3e27671a96a104c50f6184cdc548b13a9fa3cc7a5c96956339256681a426d",
"needsConfirm": true
}
```
### Errors
* `ERROR_CODE_KEY_ALREADY_EXISTS` - the key in slot #3 already exists;
## Command: gen_key_confirm
Confirm the generated public key in slot #3. This call is only necessary if you have called `gen_key` with additional entropy.
### Arguments
* `publicKey` (str) - the public key returned from `gen_key` command;
### Return value
* `status` (str) - `ok` when the key generation is confirmed successfully;
### Examples
Command:
```json
{
"name": "gen_key_confirm",
"publicKey": "04f670a3d30e2b98b2e3691908722e643791be4a58eaf63e02026df7d67ae456cdece3e27671a96a104c50f6184cdc548b13a9fa3cc7a5c96956339256681a426d"
}
```
Response:
```json
{
"status": "ok"
}
```
### Errors
* `ERROR_CODE_KEY_ALREADY_EXISTS` - the key in slot #3 already exists;
* `ERROR_CODE_CRYPTO_ERROR` - the `command.publicKey` that you have provided doesn't match the public key from the previous step or there was an internal failure with the key generator;

View File

@@ -5,7 +5,7 @@ const {
NFCMethodNotSupported,
HaloLogicError
} = require("../halo/exceptions");
const {cmdSign, cmdCfgNDEF, cmdWriteLatch, cmdSignRandom} = require("../halo/commands");
const {cmdSign, cmdCfgNDEF, cmdWriteLatch, cmdSignRandom, cmdGenKey, cmdGenKeyConfirm} = require("../halo/commands");
let isCallRunning = null;
@@ -35,6 +35,10 @@ async function execHaloCmd(command, options) {
return await cmdWriteLatch(options, command);
case 'cfg_ndef':
return await cmdCfgNDEF(options, command);
case 'gen_key':
return await cmdGenKey(options, command);
case 'gen_key_confirm':
return await cmdGenKeyConfirm(options, command);
default:
throw new HaloLogicError("Unsupported command.name parameter specified.");
}

View File

@@ -1,7 +1,7 @@
const {ERROR_CODES} = require("../halo/errors");
const {readNDEF} = require("./pcsc_ndef");
const {cmdSign, cmdWriteLatch, cmdCfgNDEF, cmdSignRandom} = require("../halo/commands");
const {HaloLogicError, HaloTagError} = require("../halo/exceptions");
const {execHaloCmd} = require("./common");
async function transceive(reader, command, options) {
options = options || {};
@@ -80,7 +80,7 @@ function makeOptions(reader) {
}
}
async function execHaloCmdPCSC(args, reader) {
async function execHaloCmdPCSC(command, reader) {
let version = await getVersion(reader);
let [verMajor, verMinor, verSeq, verShortId] = version.split('.');
@@ -93,24 +93,10 @@ async function execHaloCmdPCSC(args, reader) {
await selectCore(reader);
let options = makeOptions(reader);
command = {...command};
try {
switch (args.command) {
case 'version':
return version;
case 'sign':
return await cmdSign(options, args);
case 'sign_raw':
return await cmdSign(options, args);
case 'sign_random':
return await cmdSignRandom(options, args);
case 'write_latch':
return await cmdWriteLatch(options, args);
case 'cfg_ndef':
return await cmdCfgNDEF(options, args);
default:
throw new Error("Unsupported command");
}
return await execHaloCmd(command, options);
} catch (e) {
console.error(e);
return null;

View File

@@ -1,8 +1,12 @@
const Buffer = require('buffer/').Buffer;
const ethers = require('ethers');
const {HaloLogicError, HaloTagError} = require("./exceptions");
const {parseStatic, reformatSignature} = require("./utils");
const {parseStatic, reformatSignature, mode, parseSig} = require("./utils");
const {FLAGS} = require("./flags");
const {sha256} = require("js-sha256");
const EC = require("elliptic").ec;
const ec = new EC('secp256k1');
function extractPublicKeyWebNFC(keyNo, resp) {
let publicKey = null;
@@ -164,4 +168,75 @@ async function cmdCfgNDEF(options, args) {
return {"status": "ok"};
}
module.exports = {cmdSign, cmdSignRandom, cmdWriteLatch, cmdCfgNDEF};
async function cmdGenKey(options, args) {
if (!args.entropy) {
let payload = Buffer.concat([
Buffer.from("03", "hex")
]);
let resp = await options.exec(payload);
let res = Buffer.from(resp.result, "hex");
return {"status": "ok", "publicKey": res.toString('hex'), "needsConfirm": false};
} else {
let entropyBuf = Buffer.from(args.entropy, "hex");
if (entropyBuf.length !== 32) {
throw new HaloLogicError("The command.entropy should be exactly 32 bytes, hex encoded.");
}
let payload = Buffer.concat([
Buffer.from("03", "hex"),
entropyBuf
]);
let resp;
try {
resp = await options.exec(payload);
} catch (e) {
if (e instanceof HaloTagError) {
if (e.name === "ERROR_CODE_INVALID_LENGTH") {
throw new HaloLogicError("The hardened key generation algorithm is not supported with this tag version.");
}
}
throw e;
}
let res = Buffer.from(resp.result, "hex");
if (res.length === 65) {
throw new HaloLogicError("The hardened key generation algorithm is not supported with this tag version.");
}
let msg1 = Buffer.from(sha256(res.slice(0, 32)), 'hex');
let msg2 = Buffer.from(sha256(res.slice(32, 64)), 'hex');
let sig = res.slice(64);
let sig1Length = sig[1];
let sig1 = sig.slice(0, 2 + sig1Length);
let sig2 = sig.slice(2 + sig1Length);
let candidates = [];
for (let i = 0; i < 2; i++) {
candidates.push(ec.recoverPubKey(msg1, parseSig(sig1), i).encode('hex'));
candidates.push(ec.recoverPubKey(msg2, parseSig(sig2), i).encode('hex'));
}
let bestPk = Buffer.from(mode(candidates), 'hex');
return {"status": "ok", "publicKey": bestPk.toString('hex'), "needsConfirm": true};
}
}
async function cmdGenKeyConfirm(options, args) {
let payload = Buffer.concat([
Buffer.from("09", "hex"),
Buffer.from(args.publicKey, "hex")
]);
await options.exec(payload);
return {"status": "ok"};
}
module.exports = {cmdSign, cmdSignRandom, cmdWriteLatch, cmdCfgNDEF, cmdGenKey, cmdGenKeyConfirm};

View File

@@ -41,27 +41,25 @@ function parseStatic(buffer) {
return out;
}
function reformatSignature(digest, signature, publicKey) {
signature = Buffer.from(signature, "hex");
if (signature[0] !== 0x30 || signature[2] !== 0x02) {
throw new HaloLogicError("Invalid signature returned by the tag (2).");
function parseSig(res) {
if (res[0] !== 0x30 || res[2] !== 0x02) {
throw new HaloLogicError("Unable to parse signature, unexpected header (1).");
}
let rLen = signature[3];
let rLen = res[3];
if (signature[rLen+4] !== 0x02) {
throw new HaloLogicError("Invalid signature returned by the tag (3).");
if (res[rLen + 4] !== 0x02) {
throw new HaloLogicError("Unable to parse signature, unexpected header (2).");
}
let sLen = signature[rLen+5];
let sLen = res[rLen + 5];
if (signature.length !== rLen+4+2+sLen) {
throw new HaloLogicError("Invalid signature returned by the tag (4).");
if (res.length !== rLen + 4 + 2 + sLen) {
throw new HaloLogicError("Unable to parse signature, unexpected length.");
}
let r = signature.slice(4, rLen+4);
let s = signature.slice(rLen+4+2, rLen+4+2+sLen);
let r = res.slice(4, rLen + 4);
let s = res.slice(rLen + 4 + 2, rLen + 4 + 2 + sLen);
let rn = BigInt('0x' + r.toString('hex'));
let sn = BigInt('0x' + s.toString('hex'));
@@ -74,7 +72,13 @@ function reformatSignature(digest, signature, publicKey) {
sn = -sn + curveOrder;
}
let fixedSig = {r: rn.toString(16), s: sn.toString(16)};
return {r: rn.toString(16), s: sn.toString(16)};
}
function reformatSignature(digest, signature, publicKey) {
signature = Buffer.from(signature, "hex");
let fixedSig = parseSig(signature);
let recoveryParam = null;
for (let i = 0; i < 2; i++) {
@@ -111,9 +115,18 @@ function reformatSignature(digest, signature, publicKey) {
};
}
function mode(arr) {
return arr.sort((a, b) =>
arr.filter(v => v === a).length
- arr.filter(v => v === b).length
).pop();
}
module.exports = {
hex2arr,
arr2hex,
parseStatic,
reformatSignature
parseSig,
reformatSignature,
mode
};