diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..090a1f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..31949b5 --- /dev/null +++ b/README.md @@ -0,0 +1,268 @@ +# Tool to sign data with a Cardano-Secret-Key // verify data with a Cardano-Public-Key // generate CIP-8 & CIP-36 data + + + +### What can cardano-signer sign? +* **Sign** any hexdata, textdata or binaryfile with a provided normal or extended secret key. The key can be provided in hex, bech or file format. The signing output is a signature in hex format and also the public key of the provided secret key for verification. +* Sign payloads in **CIP-8** mode. The signing output is a signature in hex format and also the public key of the provided secret key for verification. +* Generate and sign **Catalyst registration/delegation** metadata cbor in **CIP-36** mode. This also includes relatively weighted voting power delegation. The output is the registration/delegation data in cbor hex format or a binary cbor file, which can be transmitted on chain as it is. + +### What can cardano-signer verify? +* **Verify** a signature for any hexdata, textdata or binaryfile together with a provided public key. The key can be provided in hex, bech or file format. The verification output is true(exitcode=0) or false(exitcode=1). + +
+
+ +## Usage + +``` console + +$ ./cardano-signer help + +cardano-signer 1.6.0 + +Signing a hex/text-string or a binary-file: + + Syntax: cardano-signer sign + Params: --data-hex "" | --data "" | --data-file "" + data/payload/file to sign in hexformat or textformat + --secret-key "||" path to a signing-key-file or a direct signing hex/bech-key string + [--out-file ""] path to an output file, default: standard-output + Output: signature_hex + publicKey_hex + + +Signing a payload in CIP-8 mode: + + Syntax: cardano-signer sign --cip8 + Params: --data-hex "" | --data "" | --data-file "" + data/payload/file to sign in hexformat or textformat + --secret-key "||" path to a signing-key-file or a direct signing hex/bech-key string + --address "" signing address (bech format like 'stake1_...') + [--out-file ""] path to an output file, default: standard-output + Output: signature_hex + publicKey_hex + + +Signing a catalyst registration/delegation in CIP-36 mode: + + Syntax: cardano-signer sign --cip36 + Params: --vote-public-key "||" public-key-file or public hex/bech-key string to delegate the votingpower to + --vote-weight relative weight of the delegated votingpower, default: 1 (=100% for single delegation) + --secret-key "||" signing-key-file or a direct signing hex/bech-key string of the stake key (votingpower) + --rewards-address "" rewards stake address (bech format like 'stake1_...') + --nonce nonce value, this is typically the slotheight(tip) of the chain + [--vote-purpose ] optional parameter (unsigned int), default: 0 (catalyst) + [--out-file ""] path to write a binary metadata.cbor file to + Output: registration_data_cbor_hex + + +Verifying a hex/text-string or a binary-file(data) via signature + publicKey: + + Syntax: cardano-signer verify + Params: --data-hex "" | --data "" | --data-file "" + data/payload/file to verify in hexformat or textformat + --signature "" signature in hexformat + --public-key "||" path to a public-key-file or a direct public hex/bech-key string + Output: true(exitcode 0) or false(exitcode 1) + +``` + +
+
+ +## Examples + +### Signing (defaultmode) + +``` console +### SIGN HEXDATA OR TEXTDATA WITH A KEY-HEXSTRING + +$ cardano-signer sign \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --secret-key "c14ef0cc5e352446d6243976f51e8ffb2ae257f2a547c4fba170964a76501e7a" + +ca3ddc10f845dbe0c22875aaf91f66323d3f28e265696dcd3c56b91a8e675c9e30fd86ba69b9d1cf271a12f7710c9f3385c78cbf016e17e1df339bea8bd2db03 9be513df12b3fabe7c1b8c3f9fab0968eb2168d5689bf981c2f7c35b11718b27 + +$ cardano-signer sign \ + --out-file mySignature.txt \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --secret-key "c14ef0cc5e352446d6243976f51e8ffb2ae257f2a547c4fba170964a76501e7a" +#Signature+publicKey was written to the file mySignature.txt + +$ cardano-signer sign \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --secret-key "c14ef0cc5e352446d6243976f51e8ffb2ae257f2a547c4fba170964a" +Error: Invalid normal secret key + +$ cardano-signer sign \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --secret-key "c14ef0cc5e352446d6243976f51e8ffb2ae257f2a547c4fba170964a76501e7a88afe88fa8f888544e6f5a5f555e5faf6f6f" +Error: Invalid extended secret key + +### SIGN HEXDATA OR TEXTDATA WITH A KEY-FILE + +$ cardano-signer sign \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --secret-key owner.staking.skey + +ca3ddc10f845dbe0c22875aaf91f66323d3f28e265696dcd3c56b91a8e675c9e30fd86ba69b9d1cf271a12f7710c9f3385c78cbf016e17e1df339bea8bd2db03 9be513df12b3fabe7c1b8c3f9fab0968eb2168d5689bf981c2f7c35b11718b27 + +$ cardano-signer sign \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --secret-key owner.staking.vkey +Error: The file 'owner.staking.vkey' is not a signing/secret key json + +### SIGN A FILE WITH A KEY-FILE + +$ cardano-signer sign --data-file test.txt --secret-key test.skey + +caacb18c46319f55b932efa77357f14b66b27aa908750df2c91800dc59711015ea2e568974ac0bcabf9b1c4708b877c2b94a7658c2dcad78b108049062572e09 57758911253f6b31df2a87c10eb08a2c9b8450768cb8dd0d378d93f7c2e220f0 + +``` + +
+ +### Signing (CIP-8 mode) + +``` console +### SIGN TEXTDATA IN CIP-8 MODE + +$ cardano-signer sign --cip8 \ + --address "stake_test1uqt3nqapz799tvp2lt8adttt29k6xa2xnltahn655tu4sgc6asaqg" \ + --data '{"choice":"Yes","comment":"","network":"preview","proposal":"2038c417d112e005ef61c95d710ee62184a6c177d18b2da891f97cefae4f8535","protocol":"SundaeSwap","title":"Test Proposal - Tampered","version":"1","votedAt":"3137227","voter":"stake_test1uqt3nqapz799tvp2lt8adttt29k6xa2xnltahn655tu4sgc6asaqg"}' \ + --secret-key myStakeKey.skey + +5b2e7ac3fbe3cec1540f98fcc29c1ab63778e14a653a2328b2e56af6fd2a714540708e5f3e19670b9b867151c7dfb75061c6b94508d88f43ad3b3893ca213506 57758911253f6b31df2a87c10eb08a2c9b8450768cb8dd0d378d93f7c2e220f0 + +### SIGN HEXDATA IN CIP-8 MODE + +$ cardano-signer sign --cip8 \ + --address "stake_test1uqt3nqapz799tvp2lt8adttt29k6xa2xnltahn655tu4sgc6asaqg" \ + --data-hex "7b2263686f696365223a22596573222c22636f6d6d656e74223a22222c226e6574776f726b223a2270726576696577222c2270726f706f73616c223a2232303338633431376431313265303035656636316339356437313065653632313834613663313737643138623264613839316639376365666165346638353335222c2270726f746f636f6c223a2253756e64616553776170222c227469746c65223a22546573742050726f706f73616c202d2054616d7065726564222c2276657273696f6e223a2231222c22766f7465644174223a2233313337323237222c22766f746572223a227374616b655f7465737431757174336e7161707a373939747670326c7438616474747432396b36786132786e6c7461686e363535747534736763366173617167227d" \ + --secret-key myStakeKey.skey + +5b2e7ac3fbe3cec1540f98fcc29c1ab63778e14a653a2328b2e56af6fd2a714540708e5f3e19670b9b867151c7dfb75061c6b94508d88f43ad3b3893ca213506 57758911253f6b31df2a87c10eb08a2c9b8450768cb8dd0d378d93f7c2e220f0 +``` + +
+ +### Signing (CIP-36 mode) - Catalyst Voting Registration / VotingPower Delegation + +``` console +### REGISTER/DELEGATE TO A SINGLE VOTING-KEY + +$ cardano-signer sign --cip36 \ + --rewards-address "stake_test1urqntq4wexjylnrdnp97qq79qkxxvrsa9lcnwr7ckjd6w0cr04y4p" \ + --secret-key ../owner.staking.skey \ + --vote-public-key somevote.vkey \ + --nonce 71948552 \ + --out-file catalyst-delegation.cbor + +a219ef64a5018182582057758911253f6b31df2a87c10eb08a2c9b8450768cb8dd0d378d93f7c2e220f0010258209be513df12b3fabe7c1b8c3f9fab0968eb2168d5689bf981c2f7c35b11718b2703581de0c13582aec9a44fcc6d984be003c5058c660e1d2ff1370fd8b49ba73f041a0449d908050019ef65a1015840c839244556db17a2df914c7291c891e5abd1bd580de7786d640da9e27983efe86495cbee900eb685c08e367e778bb0860c6e366b9ec715d8fba824ef55c8aa0f + +### REGISTER/DELEGATE TO MULTIPLE VOTING-KEYS WITH VOTINGPOWER 10%,20%,70% + +$ cardano-signer sign --cip36 \ + --rewards-address "stake_test1urqntq4wexjylnrdnp97qq79qkxxvrsa9lcnwr7ckjd6w0cr04y4p" \ + --secret-key ../owner.staking.skey \ + --vote-public-key ../somevote.vkey \ + --vote-weight 10 \ + --vote-public-key "C2CD50D8A231FBC1444D65ABAB4F6BF74178E6DE64722558EEEF0B73DE293A8A" \ + --vote-weight 20 \ + --vote-public-key "ed25519_pk128c305nw9xh20kearuhcwj447kzlvxdfttkk6uwnrf6qfjm9276svd678w" \ + --vote-weight 70 \ + --nonce 71948552 \ + --out-file catalyst-multidelegation.cbor + +a219ef64a5018382582099d1d0c4cdc8a4b206066e9606c6c3729678bd7338a8eab9bffdffa39d3df9580a825820c2cd50d8a231fbc1444d65abab4f6bf74178e6de64722558eeef0b73de293a8a1482582051f117d26e29aea7db3d1f2f874ab5f585f619a95aed6d71d31a7404cb6557b518460258209be513df12b3fabe7c1b8c3f9fab0968eb2168d5689bf981c2f7c35b11718b2703581de0c13582aec9a44fcc6d984be003c5058c660e1d2ff1370fd8b49ba73f041a0449d908050019ef65a1015840ecce4b2e10146857b9f583ce01b10a26726022963d47fd61d0fbb67b543428fa46315d4e35b2ab73e7e15f620883176422a19e780a751d71ac488053365e6402 + +``` + +
+ +### Verification (defaultmode) + +``` console +### VERIFY HEXDATA or TEXTDATA WITH A SIGNATURE AND A KEY-HEXSTRING + +$ cardano-signer verify \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --signature "ca3ddc10f845dbe0c22875aaf91f66323d3f28e265696dcd3c56b91a8e675c9e30fd86ba69b9d1cf271a12f7710c9f3385c78cbf016e17e1df339bea8bd2db03" \ + --public-key "9be513df12b3fabe7c1b8c3f9fab0968eb2168d5689bf981c2f7c35b11718b27" + +true + +$ cardano-signer verify \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --signature "ca3ddc10f845dbe0c22875aaf91f66323d3f28e265696dcd3c56b91a8e675c9e30fd86ba69b9d1cf271a12f7710c9f3385c78cbf016e17e1df339bea8bd2db03" \ + --public-key "aaaaaaaaaab3fabe7c1b8c3f9fab0968eb2168d5689bf981c2f7c35b11718b27" + +false + +$ cardano-signer verify \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --signature "aaaaaaaaaa45dbe0c22875aaf91f66323d3f28e265696dcd3c56b91a8e675c9e30fd86ba69b9d1cf271a12f7710c9f3385c78cbf016e17e1df339bea8bd2db03" \ + --public-key "9be513df12b3fabe7c1b8c3f9fab0968eb2168d5689bf981c2f7c35b11718b27" + +false + +### VERIFY HEXDATA WITH A SIGNATURE AND A KEY-FILE + +$ cardano-signer verify \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --signature "ca3ddc10f845dbe0c22875aaf91f66323d3f28e265696dcd3c56b91a8e675c9e30fd86ba69b9d1cf271a12f7710c9f3385c78cbf016e17e1df339bea8bd2db03" \ + --public-key owner.staking.vkey + +true + +$ cardano-signer verify \ + --data-hex "8f21b675423a65244483506122492f5720d7bd35d70616348089678ed4eb07a9" \ + --signature "ca3ddc10f845dbe0c22875aaf91f66323d3f28e265696dcd3c56b91a8e675c9e30fd86ba69b9d1cf271a12f7710c9f3385c78cbf016e17e1df339bea8bd2db03" \ + --public-key owner.staking.skey +Error: The file 'owner.staking.skey' is not a verification/public key json + +### VERIFY A FILE WITH A SIGNATURE AND A KEY-FILE + +$ cardano-signer verify --data-file test.txt --public-key test.vkey --signature "caacb18c46319f55b932efa77357f14b66b27aa908750df2c91800dc59711015ea2e568974ac0bcabf9b1c4708b877c2b94a7658c2dcad78b108049062572e09" + +true +``` + +
+
+ +## Release Notes + +* **1.6.0** + - New Syntax - Now you can use the parameter `--data-file` to use any binary file as the data source to sign. + - Added the function to directly use bech encoded secret and public keys for the signing/verification. You can mix the formats. + +* **1.5.0** + - New CIP-36 mode via parameter `--cip36`. This enables the new catalyst/governance registration and votingpower (multi-)delegation mode. Output generates a signed cbor file or hex_string. + +* **1.4.0** + - New CIP-8 mode via parameter `--cip8`. This enables CIP-8 conform payload signing. + - New Syntax - Now you can use the parameter `--data` for pure text payloads, and `--data-hex` for hex-encoded payloads. + +* **1.3.0** + - Now supporting true parameter/flag names. + - Added new optional `--out-file` option, which would write the signature+publicKey to a file and not to the standard output. + +* **1.2.0** + - Added support to use Cardano-Key-Files in addition to a direct Key-Hexstring. Supports standard sKey/vKey JSON files and also files with a Bech32-Key in it, like the ones generated via jcli + +* **1.1.0** + - Added functionality to do also a Verification of the Signature together with the data and the Public Key. + +* **1.0.0** + - Initial version, supports signing of a Data-Hexstring string with a Key-Hexstring. + +
+
+ +## Contacts + +* Telegram - @atada_stakepool
+* Twitter - [@ATADA_Stakepool](https://twitter.com/ATADA_Stakepool)
+* Discord - MartinLang \[ATADA, SPO Scripts\]#5306 +* Email - stakepool@stakepool.at
+* Homepage - https://stakepool.at diff --git a/cardano-signer-1.6.0_linux-x64.tar.gz b/cardano-signer-1.6.0_linux-x64.tar.gz new file mode 100644 index 0000000..6d8ac2b Binary files /dev/null and b/cardano-signer-1.6.0_linux-x64.tar.gz differ diff --git a/cardano-signer-1.6.0_mac-x64.tar.gz b/cardano-signer-1.6.0_mac-x64.tar.gz new file mode 100644 index 0000000..fc5f252 Binary files /dev/null and b/cardano-signer-1.6.0_mac-x64.tar.gz differ diff --git a/cardano-signer-1.6.0_windows-x64.tar.gz b/cardano-signer-1.6.0_windows-x64.tar.gz new file mode 100644 index 0000000..d30aae2 Binary files /dev/null and b/cardano-signer-1.6.0_windows-x64.tar.gz differ diff --git a/src/cardano-signer.js b/src/cardano-signer.js new file mode 100644 index 0000000..097f7de --- /dev/null +++ b/src/cardano-signer.js @@ -0,0 +1,572 @@ +const appname = "cardano-signer" +const version = "1.6.0" + +const CardanoWasm = require("@emurgo/cardano-serialization-lib-nodejs") +const cbor = require("cbor"); +const fs = require("fs"); +const blake2 = require('blake2'); +const args = require('minimist')(process.argv.slice(2)); + +const regExp = /^[0-9a-fA-F]+$/; + +//catch all exceptions that are not catched via try +process.on('uncaughtException', function (error) { + console.error(`${error}`); process.exit(1); +}); + + + +function showUsage(){ + console.log(``) + console.log(`Signing a hex/text-string or a binary-file:`) + console.log(``) + console.log(` Syntax: ${appname} sign`); + console.log(` Params: --data-hex "" | --data "" | --data-file ""`); + console.log(` data/payload/file to sign in hexformat or textformat`); + console.log(` --secret-key "||" path to a signing-key-file or a direct signing hex/bech-key string`); + console.log(` [--out-file ""] path to an output file, default: standard-output`); + console.log(` Output: signature_hex + publicKey_hex`); + console.log(``) + console.log(``) + console.log(`Signing a payload in CIP-8 mode:`) + console.log(``) + console.log(` Syntax: ${appname} sign --cip8`); + console.log(` Params: --data-hex "" | --data "" | --data-file ""`); + console.log(` data/payload/file to sign in hexformat or textformat`); + console.log(` --secret-key "||" path to a signing-key-file or a direct signing hex/bech-key string`); + console.log(` --address "" signing address (bech format like 'stake1_...')`); + console.log(` [--out-file ""] path to an output file, default: standard-output`); + console.log(` Output: signature_hex + publicKey_hex`); + console.log(``) + console.log(``) + console.log(`Signing a catalyst registration/delegation in CIP-36 mode:`) + console.log(``) + console.log(` Syntax: ${appname} sign --cip36`); + console.log(` Params: --vote-public-key "||" public-key-file or public hex/bech-key string to delegate the votingpower to`); + console.log(` --vote-weight relative weight of the delegated votingpower, default: 1 (=100% for single delegation)`); + console.log(` --secret-key "||" signing-key-file or a direct signing hex/bech-key string of the stake key (votingpower)`); + console.log(` --rewards-address "" rewards stake address (bech format like 'stake1_...')`); + console.log(` --nonce nonce value, this is typically the slotheight(tip) of the chain`); + console.log(` [--vote-purpose ] optional parameter (unsigned int), default: 0 (catalyst)`); + console.log(` [--out-file ""] path to write a binary metadata.cbor file to`); + console.log(` Output: registration_data_cbor_hex`); + console.log(``) + console.log(``) + console.log(`Verifying a hex/text-string or a binary-file(data) via signature + publicKey:`) + console.log(``) + console.log(` Syntax: ${appname} verify`); + console.log(` Params: --data-hex "" | --data "" | --data-file ""`); + console.log(` data/payload/file to verify in hexformat or textformat`); + console.log(` --signature "" signature in hexformat`); + console.log(` --public-key "||" path to a public-key-file or a direct public hex/bech-key string`); + console.log(` Output: true(exitcode 0) or false(exitcode 1)`) + console.log(``) + console.log(``) + console.log(`Info:`); + console.log(` https://github.com/gitmachtl (Cardano SPO Scripts \/\/ ATADA Stakepools Austria)`) + console.log(``) + process.exit(1); +} + + +function trimString(s){ + s = s.replace(/(^\s*)|(\s*$)/gi,""); //exclude start and end white-space + s = s.replace(/\n /,"\n"); // exclude newline with a start spacing + return s; +} + + +function readKey2hex(key,type) { //reads a standard-cardano-skey/vkey-file-json or a direct hex entry // returns a hexstring of the key + + var key_hex = ""; + + switch (type) { + + case "secret": //convert a secret key into a hex string + + // try to use the parameter as a filename for a cardano skey json with a cborHex entry + try { + const key_json = JSON.parse(fs.readFileSync(key,'utf8')); //parse the given key as a json file + const is_singing_key = key_json.type.toLowerCase().includes('signing') //boolean if the json contains the keyword 'signing' in the type field + if ( ! is_singing_key ) { console.error(`Error: The file '${key}' is not a signing/secret key json`); process.exit(1); } + key_hex = key_json.cborHex.substring(4).toLowerCase(); //cut off the leading "5820/5840" from the cborHex + //check that the given key is a hex string + if ( ! regExp.test(key_hex) ) { console.error(`Error: The secret key in file '${key}' entry 'cborHex' is not a valid hex string`); process.exit(1); } + return key_hex; + } catch (error) {} + + // try to use the parameter as a filename for a bech encoded string in it (typical keyfiles generated via jcli) + try { + const content = trimString(fs.readFileSync(key,'utf8')); //read the content of the given key from a file + try { //try to load it as a bech secret key + const tmp_key = CardanoWasm.PrivateKey.from_bech32(content); //temporary key to check about bech32 format + key_hex = Buffer.from(tmp_key.as_bytes()).toString('hex'); + } catch (error) { console.error(`Error: The content in file '${key}' is not a valid bech secret key`); process.exit(1); } + return key_hex; + } catch (error) {} + + // try to use the parameter as a bech encoded string + try { + const tmp_key = CardanoWasm.PrivateKey.from_bech32(key); //temporary key to check about bech32 format + key_hex = Buffer.from(tmp_key.as_bytes()).toString('hex'); + return key_hex; + } catch (error) {} + + // try to use the parameter as a direct hex string + key_hex = trimString(key.toLowerCase()); + //check that the given key is a hex string + if ( ! regExp.test(key) ) { console.error(`Error: Provided secret key '${key}' is not a valid secret key. Or not a hex string, bech encoded key, or the file is missing`); process.exit(1); } + return key_hex; + break; + + + case "public": //convert a public key into a hex string + + // try to use the parameter as a filename for a cardano vkey json with a cborHex entry + try { + const key_json = JSON.parse(fs.readFileSync(key,'utf8')); //parse the given key as a json file + const is_verification_key = key_json.type.toLowerCase().includes('verification') //boolean if the json contains the keyword 'verification' in the type field + if ( ! is_verification_key ) { console.error(`Error: The file '${key}' is not a verification/public key json`); process.exit(1); } + key_hex = key_json.cborHex.substring(4).toLowerCase(); //cut off the leading "5820/5840" from the cborHex + //check that the given key is a hex string + if ( ! regExp.test(key_hex) ) { console.error(`Error: The public key in file '${key}' entry 'cborHex' is not a valid hex string`); process.exit(1); } + return key_hex; + } catch (error) {} + + // try to use the parameter as a filename for a bech encoded string in it (typical keyfiles generated via jcli) + try { + const content = trimString(fs.readFileSync(key,'utf8')); //read the content of the given key from a file + try { //try to load it as a bech public key + const tmp_key = CardanoWasm.PublicKey.from_bech32(content); //temporary key to check about bech32 format + key_hex = Buffer.from(tmp_key.as_bytes()).toString('hex'); + } catch (error) { console.error(`Error: The content in file '${key}' is not a valid bech public key`); process.exit(1); } + return key_hex; + } catch (error) {} + + // try to use the parameter as a bech encoded string + try { + const tmp_key = CardanoWasm.PublicKey.from_bech32(key); //temporary key to check about bech32 format + key_hex = Buffer.from(tmp_key.as_bytes()).toString('hex'); + return key_hex; + } catch (error) {} + + // try to use the parameter as a direct hex string + key_hex = trimString(key.toLowerCase()); + //check that the given key is a hex string + if ( ! regExp.test(key) ) { console.error(`Error: Provided public key '${key}' is not a valid public key. Or not a hex string, bech encoded key, or the file is missing`); process.exit(1); } + return key_hex; + break; + + } //switch (type) + +} + +function getHash(content) { //hashes a given hex-string content with blake2b_256 (digestLength 32) + const h = blake2.createHash("blake2b", { digestLength: 32 }); + h.update(Buffer.from(content, 'hex')); + return h.digest("hex") +} + +// MAIN +// +// first parameter -> workMode: sign or verify +// +// workMode: sign (defaultmode without flags) +// --data / --data-hex -> textdata / hexdata that should be signed +// --secret-key -> signing key in hex/bech/file format +// --out-file -> signed data in hex format + public key in hex format +// +// workMode: sign --cip8 FLAG +// --data / --data-hex -> textdata / hexdata that should be signed +// --secret-key -> signing key in hex/bech/file format +// --address -> signing address +// --out-file -> signed data in hex format + public key in hex format +// +// workMode: sign --cip36 FLAG +// --vote-public-key -> public key in hex/bech/file format of the voting public key (one or multiple) +// --vote-weight -> relative voting weight (one or multiple) +// --secret-key -> signing key in hex/bech/file format +// --rewards-address -> rewards stake address +// --nonce -> nonce, typically the slotheight(tip) of the chain +// --vote-purpose -> optional unsigned_int parameter, default: 0 (catalyst) +// --out-file -> binary metadata.cbor file +// +// workMode: verify (defaultmode without flags) +// --data / --data-hex -> textdata / hexdata that should be verified +// --signature -> signed data(signature) in hex format for verification +// --public-key -> public key for verification in hex/bech/file format +// output -> true (exitcode 0) or false (exitcode 1) +// + +async function main() { + + //show help or usage if no parameter is provided + if ( ! process.argv[2] || process.argv[2].toLowerCase().includes('help') ) { console.log(`${appname} ${version}`); showUsage(); } + + //show version + if ( process.argv[2].toLowerCase().includes('version') ) { console.log(`${appname} ${version}`); process.exit(0); } + + //first paramter - workMode: "sign or verify" + var workMode = process.argv[2]; + if ( ! workMode ) { showUsage(); } + workMode = trimString(workMode.toLowerCase()); + + //CIP8-Flag-Check + const cip8_flag = args['cip8']; + if ( cip8_flag === true ) {workMode = workMode + '-cip8'} + + //CIP36-Flag-Check + const cip36_flag = args['cip36']; + if ( cip36_flag === true ) {workMode = workMode + '-cip36'} + + //choose the workmode + switch (workMode) { + + case "sign": //SIGN DATA IN DEFAULT MODE + + //get data-hex to sign -> store it in sign_data_hex + var sign_data_hex = args['data-hex']; + if ( typeof sign_data_hex === 'undefined' || sign_data_hex === true ) { + + //no data-hex parameter present, lets try the data parameter + var sign_data = args['data']; + if ( typeof sign_data === 'undefined' || sign_data === true ) { + + //no data parameter present, lets try the data-file parameter + var sign_data_file = args['data-file']; + if ( typeof sign_data_file === 'undefined' || sign_data_file === true ) {console.error(`Error: Missing data / data-hex / data-file to sign`); showUsage();} + + //data-file present lets read the file and store it hex encoded in sign_data_hex + try { + sign_data_hex = fs.readFileSync(sign_data_file,null).toString('hex'); //reads the file as binary + } catch (error) { console.log(`Error: Can't read data-file '${sign_data_file}'`); process.exit(1); } + + } else { + //data parameter present, lets convert it to hex and store it in the sign_data_hex variable + sign_data_hex = Buffer.from(sign_data).toString('hex'); + } + + } + sign_data_hex = trimString(sign_data_hex.toLowerCase()); + + //check that the given data is a hex string + if ( ! regExp.test(sign_data_hex) ) { console.error(`Error: Data to sign is not a valid hex string`); showUsage(); } + + //get signing key -> store it in sign_key + var key_file_hex = args['secret-key']; + if ( typeof key_file_hex === 'undefined' || key_file_hex === true ) { console.error(`Error: Missing secret key parameter`); showUsage(); } + + //read in the key from a file or direct hex + sign_key = readKey2hex(key_file_hex, 'secret'); + + //load the private key (normal or extended) + try { + if ( sign_key.length <= 64 ) { var prvKey = CardanoWasm.PrivateKey.from_normal_bytes(Buffer.from(sign_key, "hex")); } + else { var prvKey = CardanoWasm.PrivateKey.from_extended_bytes(Buffer.from(sign_key.substring(0,128), "hex")); } //use only the first 64 bytes (128 chars) + } catch (error) { console.log(`Error: ${error}`); process.exit(1); } + + //generate the public key from the secret key for external verification + var pubKey = Buffer.from(prvKey.to_public().as_bytes()).toString('hex') + + //sign the data + try { + var signedBytes = prvKey.sign(Buffer.from(sign_data_hex, 'hex')).to_bytes(); + var signature = Buffer.from(signedBytes).toString('hex'); + } catch (error) { console.error(`Error: ${error}`); process.exit(1); } + + //output the signature data and the public key + var content = signature + " " + pubKey; + var out_file = args['out-file']; + //if there is no --out-file parameter specified or the parameter alone (true) then output to the console + if ( typeof out_file === 'undefined' || out_file === true ) { console.log(content); } + else { //else try to write the content out to the given file + try { + const outFile = fs.createWriteStream(out_file); + outFile.write(content, 'utf8'); + outFile.end(); + // file written successfully + } catch (error) { console.error(`${error}`); process.exit(1); } + } + break; + + + case "sign-cip8": //SIGN DATA IN CIP-8 MODE + + //get data-hex to sign -> store it in sign_data_hex + var sign_data_hex = args['data-hex']; + if ( typeof sign_data_hex === 'undefined' || sign_data_hex === true ) { + + //no data-hex parameter present, lets try the data parameter + var sign_data = args['data']; + if ( typeof sign_data === 'undefined' || sign_data === true ) { + + //no data parameter present, lets try the data-file parameter + var sign_data_file = args['data-file']; + if ( typeof sign_data_file === 'undefined' || sign_data_file === true ) {console.error(`Error: Missing data / data-hex / data-file to sign`); showUsage();} + + //data-file present lets read the file and store it hex encoded in sign_data_hex + try { + sign_data_hex = fs.readFileSync(sign_data_file,null).toString('hex'); //reads the file as binary + } catch (error) { console.log(`Error: Can't read data-file '${sign_data_file}'`); process.exit(1); } + + } else { + //data parameter present, lets convert it to hex and store it in the sign_data_hex variable + sign_data_hex = Buffer.from(sign_data).toString('hex'); + } + + } + sign_data_hex = trimString(sign_data_hex.toLowerCase()); + + //check that the given data is a hex string + if ( ! regExp.test(sign_data_hex) ) { console.error(`Error: Data to sign is not a valid hex string`); showUsage(); } + + //get signing address (stake or paymentaddress in bech format) + var sign_addr = args['address']; + if ( typeof sign_addr === 'undefined' || sign_addr === true ) { console.error(`Error: Missing CIP-8 signing address (bech-format)`); showUsage(); } + sign_addr = trimString(sign_addr.toLowerCase()); + try { + var sign_addr_hex = CardanoWasm.Address.from_bech32(sign_addr).to_hex(); + } catch (error) { console.error(`Error: The CIP-8 signing address '${sign_addr}' is not a valid bech address`); process.exit(1); } + + //generate the Signature1 inner cbor (single signing key) + const signature1_cbor = Buffer.from(cbor.encode(new Map().set(1,-8).set('address',Buffer.from(sign_addr_hex,'hex')))).toString('hex') + + //generate the data to sign cbor -> overwrites the current sign_data_hex variable at the end + const sign_data_array = [ "Signature1", Buffer.from(signature1_cbor,'hex'),Buffer.from(''), Buffer.from(sign_data_hex,'hex') ] + + //overwrite the sign_data_hex with the cbor encoded sign_data_array + sign_data_hex = cbor.encode(sign_data_array).toString('hex'); + + //get signing key -> store it in sign_key + var key_file_hex = args['secret-key']; + if ( typeof key_file_hex === 'undefined' || key_file_hex === true ) { console.error(`Error: Missing secret key parameter`); showUsage(); } + + //read in the key from a file or direct hex + sign_key = readKey2hex(key_file_hex, 'secret'); + + //load the private key (normal or extended) + try { + if ( sign_key.length <= 64 ) { var prvKey = CardanoWasm.PrivateKey.from_normal_bytes(Buffer.from(sign_key, "hex")); } + else { var prvKey = CardanoWasm.PrivateKey.from_extended_bytes(Buffer.from(sign_key.substring(0,128), "hex")); } //use only the first 64 bytes (128 chars) + } catch (error) { console.log(`Error: ${error}`); process.exit(1); } + + //generate the public key from the secret key for external verification + var pubKey = Buffer.from(prvKey.to_public().as_bytes()).toString('hex') + + //sign the data + try { + var signedBytes = prvKey.sign(Buffer.from(sign_data_hex, 'hex')).to_bytes(); + var signature = Buffer.from(signedBytes).toString('hex'); + } catch (error) { console.error(`Error: ${error}`); process.exit(1); } + + //output the signature data and the public key + var content = signature + " " + pubKey; + var out_file = args['out-file']; + //if there is no --out-file parameter specified or the parameter alone (true) then output to the console + if ( typeof out_file === 'undefined' || out_file === true ) { console.log(content); } + else { //else try to write the content out to the given file + try { + const outFile = fs.createWriteStream(out_file); + outFile.write(content, 'utf8'); + outFile.end(); + // file written successfully + } catch (error) { console.error(`${error}`); process.exit(1); } + } + break; + + + case "sign-cip36": //SIGN DATA IN CIP-36 MODE (Catalyst) + + //get rewards stakeaddress in bech format + var rewards_addr = args['rewards-address']; + if ( typeof rewards_addr === 'undefined' || rewards_addr === true ) { console.error(`Error: Missing rewards stake address (bech-format)`); process.exit(1); } + rewards_addr = trimString(rewards_addr.toLowerCase()); + if ( rewards_addr.substring(0,5) != 'stake' ) { console.error(`Error: The rewards stake address '${rewards_addr}' is not a stake address`); process.exit(1); } + try { + var rewards_addr_hex = CardanoWasm.Address.from_bech32(rewards_addr).to_hex(); + } catch (error) { console.error(`Error: The rewards stake address '${rewards_addr}' is not a valid bech address`); process.exit(1); } + + //get signing key -> store it in sign_key + var key_file_hex = args['secret-key']; + if ( typeof key_file_hex === 'undefined' || key_file_hex === true ) { console.error(`Error: Missing secret key parameter`); process.exit(1); } + + //read in the key from a file or direct hex + sign_key = readKey2hex(key_file_hex, 'secret'); + + //load the private key (normal or extended) + try { + if ( sign_key.length <= 64 ) { var prvKey = CardanoWasm.PrivateKey.from_normal_bytes(Buffer.from(sign_key, "hex")); } + else { var prvKey = CardanoWasm.PrivateKey.from_extended_bytes(Buffer.from(sign_key.substring(0,128), "hex")); } //use only the first 64 bytes (128 chars) + } catch (error) { console.log(`Error: ${error}`); process.exit(1); } + + //generate the public key from the secret key for external verification + var pubKey = Buffer.from(prvKey.to_public().as_bytes()).toString('hex') + + //get deleg vote public key(s) -> store it in vote_public_key + var vote_public_key = args['vote-public-key']; + if ( typeof vote_public_key === 'undefined' || vote_public_key === true ) { console.error(`Error: Missing vote public key(s) parameter`); process.exit(1); } + + //if there is only one --vote-public-key parameter present, convert it to an array + if ( typeof vote_public_key === 'string' ) { vote_public_key = [ vote_public_key ]; } + if ( typeof vote_public_key === 'number' || vote_public_key === true ) { console.error(`Error: You've provided a number as a public key`); process.exit(1); } + + //get deleg voting weight -> store it in vote_weight + var vote_weight = args['vote-weight']; + if ( typeof vote_weight === 'undefined' ) { vote_weight = 1 } + if ( vote_weight === true ) { console.error(`Error: Please specify a --vote-weight parameter with an unsigned integer value > 0`); process.exit(1); } + + //if there is only one --vote-weight parameter present, convert it to an array + if ( typeof vote_weight === 'number' ) { vote_weight = [ vote_weight ]; } + + //if not the same amounts of vote_public_keys and vote_weights provided, show an error + if ( vote_public_key.length != vote_weight.length ) { console.error(`Error: Not the same count of --vote-public-key(` + vote_public_key.length + `) and --vote-weight(` + vote_weight.length + `) parameters`); process.exit(1); } + + //build the vote_delegation array + const vote_delegation_array = []; + for (let cnt = 0; cnt < vote_public_key.length; cnt++) { + entry_vote_public_key = vote_public_key[cnt] + if ( typeof entry_vote_public_key === 'number' || entry_vote_public_key === true ) { console.error(`Error: Invalid public key parameter found, please use a filename or a hex string`); process.exit(1); } + entry_vote_public_key_hex = readKey2hex(entry_vote_public_key, 'public'); + entry_vote_weight = vote_weight[cnt] + 0; + if (typeof entry_vote_weight !== 'number' || entry_vote_weight <= 0) { console.error(`Error: Please specify a --vote-weight parameter with an unsigned integer value > 0`); process.exit(1); } + vote_delegation_array.push([Buffer.from(entry_vote_public_key_hex.substring(0,64),'hex'),entry_vote_weight]) //during the push, only use the first 32bytes (64chars) of the public_key_hex + } + + //get the --nonce parameter + var nonce = args['nonce']; + if ( typeof nonce !== 'number' || nonce === true ) { console.error(`Error: Please specify a --nonce parameter with an unsigned integer value`); process.exit(1); } + + //get the --vote-purpose parameter, set default = 0 + var vote_purpose_param = args['vote-purpose']; + + if ( typeof vote_purpose_param === 'undefined' ) { vote_purpose = 0 } //if not defined, set it to default=0 + else if ( typeof vote_purpose_param === 'number' && vote_purpose_param >= 0 ) { vote_purpose = vote_purpose_param } + else { console.error(`Error: Please specify a --vote-purpose parameter with an unsigned integer value`); process.exit(1); } + + /* + build the delegation map + 61284: { + // delegations - CBOR byte array(s) of the voting_public_keys and the relative voting_weight + 1: [["0xa6a3c0447aeb9cc54cf6422ba32b294e5e1c3ef6d782f2acff4a70694c4d1663", 1], ["0x00588e8e1d18cba576a4d35758069fe94e53f638b6faf7c07b8abd2bc5c5cdee", 3]], + // stake_pub - CBOR byte array + 2: "0xad4b948699193634a39dd56f779a2951a24779ad52aa7916f6912b8ec4702cee", + // reward_address - CBOR byte array + 3: "0x00588e8e1d18cba576a4d35758069fe94e53f638b6faf7c07b8abd2bc5c5cdee47b60edc7772855324c85033c638364214cbfc6627889f81c4", + // nonce + 4: 5479467 + // voting_purpose: 0 = Catalyst + 5: 0 + } + */ + const delegationMap = new Map().set(61284,new Map().set(1,vote_delegation_array).set(2,Buffer.from(pubKey,'hex')).set(3,Buffer.from(rewards_addr_hex,'hex')).set(4,nonce).set(5,vote_purpose)); + + //convert it to a cbor hex string + const delegationCBOR = Buffer.from(cbor.encode(delegationMap)).toString('hex'); + + //hash the delegationCBOR hex string + sign_data_hex = getHash(delegationCBOR); + + //sign the data + try { + var signedBytes = prvKey.sign(Buffer.from(sign_data_hex, 'hex')).to_bytes(); + var signature = Buffer.from(signedBytes).toString('hex'); + } catch (error) { console.error(`Error: ${error}`); process.exit(1); } + + //build the full registration map by adding the root key 61285 and the signature in key 1 below that + const registrationMap = delegationMap.set(61285,new Map().set(1,Buffer.from(signature,'hex'))) + + //convert it to a cbor hex string + const registrationCBOR = Buffer.from(cbor.encode(registrationMap)).toString('hex'); + + //output the registrationCBOR or write it to a binary file + var out_file = args['out-file']; + //if there is no --out-file parameter specified or the parameter alone (true) then output to the console + if ( typeof out_file === 'undefined' || out_file === true ) { console.log(registrationCBOR); } + else { //else try to write the content out to the given file + try { + var writeBuf = Buffer.from(registrationCBOR,'hex') + fs.writeFileSync(out_file, writeBuf, 'binary') + } catch (error) { console.error(`${error}`); process.exit(1); } + } + break; + + + case "verify": //VERIFY DATA IN DEFAULT MODE + + //get data-hex to verify -> store it in verify_data_hex + var verify_data_hex = args['data-hex']; + if ( typeof verify_data_hex === 'undefined' || verify_data_hex === true ) { + + //no data-hex parameter present, lets try the data parameter + var verify_data = args['data']; + if ( typeof verify_data === 'undefined' || verify_data === true ) { + + //no data parameter present, lets try the data-file parameter + var verify_data_file = args['data-file']; + if ( typeof verify_data_file === 'undefined' || verify_data_file === true ) {console.error(`Error: Missing data / data-hex / data-file to verify`); showUsage();} + + //data-file present lets read the file and store it hex encoded in verify_data_hex + try { + verify_data_hex = fs.readFileSync(verify_data_file,null).toString('hex'); //reads the file as binary + } catch (error) { console.log(`Error: Can't read data-file '${verify_data_file}'`); process.exit(1); } + + } else { + //data parameter present, lets convert it to hex and store it in the verify_data_hex variable + verify_data_hex = Buffer.from(verify_data).toString('hex'); + } + + } + verify_data_hex = trimString(verify_data_hex.toLowerCase()); + + //check that the given data is a hex string + if ( ! regExp.test(verify_data_hex) ) { console.error(`Error: Data to verify is not a valid hex string`); process.exit(1); } + + //get the signature to verify -> store it in signature + var signature = args['signature']; + if ( typeof signature === 'undefined' || signature === true ) { console.error(`Error: Missing signature`); showUsage(); } + signature = trimString(signature.toLowerCase()); + + //check that the given signature is a hex string + if ( ! regExp.test(signature) ) { console.error(`Error: Signature is not a valid hex string`); process.exit(1); } + + //get public key -> store it in public_key + var key_file_hex = args['public-key']; + if ( typeof key_file_hex === 'undefined' || key_file_hex === true ) { console.error(`Error: Missing public key parameter`); showUsage(); } + + //read in the key from a file or direct hex + public_key = readKey2hex(key_file_hex, 'public'); + + //load the public key + try { + var publicKey = CardanoWasm.PublicKey.from_bytes(Buffer.from(public_key.substring(0,64),'hex')); //only use the first 32 bytes (64 chars) + } catch (error) { console.error(`Error: ${error}`); process.exit(1); } + + //load the Ed25519Signature + try { + var ed25519signature = CardanoWasm.Ed25519Signature.from_hex(signature); + } catch (error) { console.error(`Error: ${error}`); process.exit(1); } + + //do the verification + const verified = publicKey.verify(Buffer.from(verify_data_hex,'hex'),ed25519signature); + + //output the result and exit with the right exitcode + if ( verified ) { console.log(`true`); process.exit(0); } + else { console.log(`false`); process.exit(1); } + + break; + + + default: + //if workMode is not found, exit with and errormessage and showUsage + console.error(`Error: Unsupported command '${workMode}'`); + showUsage(); + + } //switch + +} + +main(); + +process.exit(0); //we're finished, exit with errorcode 0 (all good) + + diff --git a/src/package.json b/src/package.json new file mode 100644 index 0000000..de802ae --- /dev/null +++ b/src/package.json @@ -0,0 +1,21 @@ +{ + "name": "cardano-signer", + "version": "1.6.0", + "description": "cardano-signer signs a given data(hex/text/file) with a signing key(hex/bech/file) or verify the signature via a public key(hex/bech/file). also it can produce a cip-8 and cip-36 conform payload signing.", + "main": "cardano-signer.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "@gitmachtl - ATADA Stakepools Austria", + "license": "MIT", + "dependencies": { + "@emurgo/cardano-serialization-lib-nodejs": "^11.0.5", + "blake2": "^4.1.1", + "cbor": "^8.1.0", + "minimist": "^1.2.6" + }, + "repository": { + "type": "git", + "url": "https://github.com/gitmachtl/cardano-related-stuff.git" + } +}