diff --git a/api/routes/auth/revoke.mjs b/api/routes/auth/revoke.mjs index 34b5e1f0..aac1e455 100644 --- a/api/routes/auth/revoke.mjs +++ b/api/routes/auth/revoke.mjs @@ -1,7 +1,6 @@ import express from 'express' import { verify_nano_community_revoke_key_signature, - is_nano_address_valid, decode_nano_address } from '#common' @@ -12,18 +11,14 @@ const SIGNATURE_RE = /^[0-9a-fA-F]{128}$/ router.post('/key/?', async (req, res) => { const { logger, db } = req.app.locals try { - const required = ['account', 'public_key', 'signature'] + const required = ['public_key', 'signature'] for (const prop of required) { if (!req.body[prop]) { return res.status(400).send({ error: `missing ${prop} param` }) } } - const { account, public_key, signature } = req.body - - if (!is_nano_address_valid(account)) { - return res.status(401).send({ error: 'invalid account param' }) - } + const { public_key, signature } = req.body if (typeof public_key !== 'string' || !PUBLIC_KEY_RE.test(public_key)) { return res.status(401).send({ error: 'invalid public_key param' }) @@ -33,27 +28,10 @@ router.post('/key/?', async (req, res) => { return res.status(401).send({ error: 'invalid signature param' }) } - const { public_key: account_public_key } = decode_nano_address({ - address: account - }) - const valid_signature = verify_nano_community_revoke_key_signature({ - linked_public_key: public_key, - nano_account: account, - nano_account_public_key: account_public_key, - signature - }) - if (!valid_signature) { - return res.status(401).send({ error: 'invalid signature' }) - } - - const linked_key = await db('account_keys') - .where({ account, public_key }) - .first() + const linked_key = await db('account_keys').where({ public_key }).first() if (!linked_key) { - return res - .status(401) - .send({ error: `key ${public_key} not linked to account ${account}` }) + return res.status(401).send({ error: `key ${public_key} not found` }) } if (linked_key.revoked_at) { @@ -62,13 +40,33 @@ router.post('/key/?', async (req, res) => { .send({ error: `key ${public_key} already revoked` }) } + const valid_signing_key_signature = + verify_nano_community_revoke_key_signature({ + linked_public_key: public_key, + either_public_key: public_key, + signature + }) + const { public_key: account_public_key } = decode_nano_address({ + address: linked_key.account + }) + const valid_account_key_signature = + verify_nano_community_revoke_key_signature({ + linked_public_key: public_key, + either_public_key: account_public_key, + signature + }) + + if (!valid_signing_key_signature && !valid_account_key_signature) { + return res.status(401).send({ error: 'invalid signature' }) + } + const revoked_at = Math.floor(Date.now() / 1000) await db('account_keys') .update({ revoked_at, revoke_signature: signature }) - .where({ account, public_key }) + .where({ account: linked_key.account, public_key }) res.status(200).send({ - account, + account: linked_key.account, public_key, signature, created_at: linked_key.created_at, diff --git a/cli/index.mjs b/cli/index.mjs index b851adc5..382607ad 100644 --- a/cli/index.mjs +++ b/cli/index.mjs @@ -18,12 +18,12 @@ const is_test = process.env.NODE_ENV === 'test' const base_url = is_test ? 'http://localhost:8080' : 'https://nano.community' const load_private_key = async () => { - let private_key = process.env.NANO_PRIVATE_KEY + let private_key = process.env.NC_CLI_NANO_PRIVATE_KEY if (private_key) { console.log('Private key found in environment variable.') } else { console.log( - 'No private key found in environment variable (NANO_PRIVATE_KEY).' + 'No private key found in environment variable (NC_CLI_NANO_PRIVATE_KEY).' ) // Restore stdin for inquirer const answers = await inquirer.prompt([ @@ -98,8 +98,7 @@ const revoke_signing_key = { type: 'string' }), handler: async ({ linked_public_key }) => { - const { private_key, public_key, nano_account_address } = - await load_private_key() + const { private_key, public_key } = await load_private_key() // Confirm revocation const answers = await inquirer.prompt([ @@ -115,13 +114,11 @@ const revoke_signing_key = { console.log('Revoking signing key...') const signature = sign_nano_community_revoke_key({ linked_public_key, - nano_account: nano_account_address, - nano_account_private_key: private_key, - nano_account_public_key: public_key + either_private_key: private_key, + either_public_key: public_key }) const payload = { - account: nano_account_address, public_key: linked_public_key.toString('hex'), signature: signature.toString('hex') } diff --git a/common/sign-nano-community-revoke-key.mjs b/common/sign-nano-community-revoke-key.mjs index 41614d4f..bc96d5c1 100644 --- a/common/sign-nano-community-revoke-key.mjs +++ b/common/sign-nano-community-revoke-key.mjs @@ -2,33 +2,24 @@ import ed25519 from '@trashman/ed25519-blake2b' export default function sign_nano_community_revoke_key({ linked_public_key, - nano_account, - nano_account_private_key, - nano_account_public_key + either_private_key, + either_public_key }) { if (!linked_public_key) { throw new Error('linked_public_key is required') } - if (!nano_account) { - throw new Error('nano_account is required') + if (!either_private_key) { + throw new Error('either_private_key is required') } - if (!nano_account_private_key) { - throw new Error('nano_account_private_key is required') + if (!either_public_key) { + throw new Error('either_public_key is required') } - if (!nano_account_public_key) { - throw new Error('nano_account_public_key is required') - } - - const data = Buffer.from(['REVOKE', nano_account, linked_public_key]) + const data = Buffer.from(['REVOKE', linked_public_key]) const message_hash = ed25519.hash(data) - return ed25519.sign( - message_hash, - nano_account_private_key, - nano_account_public_key - ) + return ed25519.sign(message_hash, either_private_key, either_public_key) } diff --git a/common/verify-nano-community-revoke-key-signature.mjs b/common/verify-nano-community-revoke-key-signature.mjs index 780cd743..30784bf8 100644 --- a/common/verify-nano-community-revoke-key-signature.mjs +++ b/common/verify-nano-community-revoke-key-signature.mjs @@ -2,28 +2,23 @@ import ed25519 from '@trashman/ed25519-blake2b' export default function verify_nano_community_revoke_key_signature({ linked_public_key, - nano_account, - nano_account_public_key, + either_public_key, signature }) { if (!linked_public_key) { throw new Error('linked_public_key is required') } - if (!nano_account) { - throw new Error('nano_account is required') - } - - if (!nano_account_public_key) { - throw new Error('nano_account_public_key is required') + if (!either_public_key) { + throw new Error('either_public_key is required') } if (!signature) { throw new Error('signature is required') } - const data = Buffer.from(['REVOKE', nano_account, linked_public_key]) + const data = Buffer.from(['REVOKE', linked_public_key]) const message_hash = ed25519.hash(data) - return ed25519.verify(signature, message_hash, nano_account_public_key) + return ed25519.verify(signature, message_hash, either_public_key) } diff --git a/test/auth.revoke.key.test.mjs b/test/auth.revoke.key.test.mjs index 576faf2b..de9e8e70 100644 --- a/test/auth.revoke.key.test.mjs +++ b/test/auth.revoke.key.test.mjs @@ -21,19 +21,19 @@ describe('API /auth/revoke/key', () => { before(mochaGlobalSetup) describe('POST /api/auth/revoke/key', () => { - it('should register and then revoke an existing linked public key', async () => { - const private_key = Buffer.from( + it('should register and then revoke an existing linked public key (using the account private key)', async () => { + const nano_account_private_key = Buffer.from( '00000000000000000000000000000000000000000000000000000000000000000', 'hex' ) - const public_key = ed25519.publicKey(private_key) + const nano_account_public_key = ed25519.publicKey(nano_account_private_key) - const nano_account_private_key = Buffer.from( + const new_signing_private_key = Buffer.from( '00000000000000000000000000000000000000000000000000000000000000001', 'hex' ) - const nano_account_public_key = ed25519.publicKey( - nano_account_private_key + const new_signing_public_key = ed25519.publicKey( + new_signing_private_key ) const nano_account = encode_nano_address({ public_key_buf: nano_account_public_key @@ -41,7 +41,7 @@ describe('API /auth/revoke/key', () => { // Register/Link the key const link_signature = sign_nano_community_link_key({ - linked_public_key: public_key.toString('hex'), + linked_public_key: new_signing_public_key.toString('hex'), nano_account, nano_account_private_key, nano_account_public_key @@ -51,24 +51,88 @@ describe('API /auth/revoke/key', () => { .request(server) .post('/api/auth/register/key') .send({ - public_key: public_key.toString('hex'), + public_key: new_signing_public_key.toString('hex'), signature: link_signature.toString('hex'), account: nano_account }) // Revoke the key const revoke_signature = sign_nano_community_revoke_key({ - linked_public_key: public_key.toString('hex'), + linked_public_key: new_signing_public_key.toString('hex'), + either_private_key: nano_account_private_key, + either_public_key: nano_account_public_key.toString('hex') + }) + + const response = await chai + .request(server) + .post('/api/auth/revoke/key') + .send({ + public_key: new_signing_public_key.toString('hex'), + signature: revoke_signature.toString('hex'), + account: nano_account + }) + + expect(response).to.have.status(200) + + const revoked_row = await knex('account_keys') + .where({ public_key: new_signing_public_key.toString('hex') }) + .first() + + // eslint-disable-next-line no-unused-expressions + expect(revoked_row).to.exist + expect(revoked_row.account).to.equal(nano_account) + expect(revoked_row.public_key).to.equal(new_signing_public_key.toString('hex')) + expect(revoked_row.revoke_signature).to.equal( + revoke_signature.toString('hex') + ) + expect(revoked_row.revoked_at).to.be.a('number') + }) + + it('should register and then revoke an existing linked public key (using the signing private key)', async () => { + const nano_account_private_key = Buffer.from( + '000000000000000000000000000000000000000000000000000000000000000FF', + 'hex' + ) + const nano_account_public_key = ed25519.publicKey(nano_account_private_key) + + const new_signing_private_key = Buffer.from( + '00000000000000000000000000000000000000000000000000000000000000FFF', + 'hex' + ) + const new_signing_public_key = ed25519.publicKey(new_signing_private_key) + const nano_account = encode_nano_address({ + public_key_buf: nano_account_public_key + }) + + // Register/Link the key + const link_signature = sign_nano_community_link_key({ + linked_public_key: new_signing_public_key.toString('hex'), nano_account, nano_account_private_key, nano_account_public_key }) + await chai + .request(server) + .post('/api/auth/register/key') + .send({ + public_key: new_signing_public_key.toString('hex'), + signature: link_signature.toString('hex'), + account: nano_account + }) + + // Revoke the key using the signing private key + const revoke_signature = sign_nano_community_revoke_key({ + linked_public_key: new_signing_public_key.toString('hex'), + either_private_key: new_signing_private_key, + either_public_key: new_signing_public_key.toString('hex') + }) + const response = await chai .request(server) .post('/api/auth/revoke/key') .send({ - public_key: public_key.toString('hex'), + public_key: new_signing_public_key.toString('hex'), signature: revoke_signature.toString('hex'), account: nano_account }) @@ -76,13 +140,13 @@ describe('API /auth/revoke/key', () => { expect(response).to.have.status(200) const revoked_row = await knex('account_keys') - .where({ public_key: public_key.toString('hex') }) + .where({ public_key: new_signing_public_key.toString('hex') }) .first() // eslint-disable-next-line no-unused-expressions expect(revoked_row).to.exist expect(revoked_row.account).to.equal(nano_account) - expect(revoked_row.public_key).to.equal(public_key.toString('hex')) + expect(revoked_row.public_key).to.equal(new_signing_public_key.toString('hex')) expect(revoked_row.revoke_signature).to.equal( revoke_signature.toString('hex') ) @@ -115,18 +179,6 @@ describe('API /auth/revoke/key', () => { expect(response.body.error).to.include('missing signature param') }) - it('should return 400 if account field is missing', async () => { - const response = await chai - .request(server) - .post('/api/auth/revoke/key') - .send({ - public_key: 'somepub', - signature: 'somesignature' - }) // missing account - expect(response).to.have.status(400) - expect(response.body.error).to.include('missing account param') - }) - it('should return 401 if public_key param is invalid', async () => { const nano_account = encode_nano_address({ public_key_buf: Buffer.from( @@ -146,59 +198,62 @@ describe('API /auth/revoke/key', () => { expect(response.body.error).to.equal('invalid public_key param') }) - it('should return 401 if account param is invalid', async () => { - const private_key = Buffer.from( - '00000000000000000000000000000000000000000000000000000000000000000', + it('should return 401 if signature is invalid', async () => { + // Generate a new account key pair + const new_account_private_key = Buffer.from( + '3000000000000000000000000000000000000000000000000000000000000000', 'hex' ) - const public_key = ed25519.publicKey(private_key) - const response = await chai - .request(server) - .post('/api/auth/revoke/key') - .send({ - public_key: public_key.toString('hex'), - signature: 'somesignature', - account: 'invalidaccount' - }) - expect(response).to.have.status(401) - expect(response.body.error).to.equal('invalid account param') - }) + const new_account_public_key = ed25519.publicKey(new_account_private_key) + const new_nano_account = encode_nano_address({ + public_key_buf: new_account_public_key + }) - it('should return 401 if signature is invalid', async () => { - const private_key = Buffer.from( - '00000000000000000000000000000000000000000000000000000000000000000', + // Generate a new signing key pair + const new_signing_private_key = Buffer.from( + '4000000000000000000000000000000000000000000000000000000000000000', 'hex' ) - const public_key = ed25519.publicKey(private_key) + const new_signing_public_key = ed25519.publicKey(new_signing_private_key) - const nano_account = encode_nano_address({ - public_key_buf: Buffer.from( - '0000000000000000000000000000000000000000000000000000000000000000', - 'hex' - ) + // Generate a link signature for the new signing key using the new account key + const link_signature = sign_nano_community_link_key({ + linked_public_key: new_signing_public_key.toString('hex'), + nano_account: new_nano_account, + nano_account_private_key: new_account_private_key, + nano_account_public_key: new_account_public_key }) - const nano_account_different_private_key = Buffer.from( - '00000000000000000000000000000000000000000000000000000000000000001', + // Register the new signing key with the link signature + await chai + .request(server) + .post('/api/auth/register/key') + .send({ + public_key: new_signing_public_key.toString('hex'), + signature: link_signature.toString('hex'), + account: new_nano_account + }) + + // Attempt to revoke with an invalid signature + const invalid_private_key = Buffer.from( + '2000000000000000000000000000000000000000000000000000000000000000', 'hex' ) - const nano_account_different_public_key = ed25519.publicKey( - nano_account_different_private_key - ) - const signature = sign_nano_community_revoke_key({ - linked_public_key: public_key.toString('hex'), - nano_account, - nano_account_private_key: nano_account_different_private_key, - nano_account_public_key: nano_account_different_public_key + const invalid_signature = sign_nano_community_revoke_key({ + linked_public_key: new_signing_public_key.toString('hex'), + either_private_key: invalid_private_key, + either_public_key: new_signing_public_key.toString('hex') }) + const response = await chai .request(server) .post('/api/auth/revoke/key') .send({ - public_key: public_key.toString('hex'), - signature: signature.toString('hex'), - account: nano_account + public_key: new_signing_public_key.toString('hex'), + signature: invalid_signature.toString('hex'), + account: new_nano_account }) + expect(response).to.have.status(401) expect(response.body.error).to.equal('invalid signature') }) diff --git a/test/cli.test.mjs b/test/cli.test.mjs index fbaaf5c6..8b581c92 100644 --- a/test/cli.test.mjs +++ b/test/cli.test.mjs @@ -32,7 +32,7 @@ describe('CLI', function () { let new_signing_key before(() => { - process.env.NANO_PRIVATE_KEY = + process.env.NC_CLI_NANO_PRIVATE_KEY = '1111111111111111111111111111111111111111111111111111111111111111' server.listen(port, () => console.log(`API listening on port ${port}`))