diff --git a/packages/js-dpp/lib/identity/IdentityPublicKey.js b/packages/js-dpp/lib/identity/IdentityPublicKey.js index 1ef68b8e236..b3460b979e1 100644 --- a/packages/js-dpp/lib/identity/IdentityPublicKey.js +++ b/packages/js-dpp/lib/identity/IdentityPublicKey.js @@ -143,6 +143,10 @@ class IdentityPublicKey { throw new EmptyPublicKeyDataError(); } + if (this.getType() === IdentityPublicKey.TYPES.ECDSA_HASH160) { + return this.getData(); + } + const originalPublicKey = new PublicKey( this.getData(), ); @@ -199,6 +203,7 @@ class IdentityPublicKey { IdentityPublicKey.TYPES = { ECDSA_SECP256K1: 0, BLS12_381: 1, + ECDSA_HASH160: 2, }; IdentityPublicKey.PURPOSES = { diff --git a/packages/js-dpp/lib/stateTransition/AbstractStateTransition.js b/packages/js-dpp/lib/stateTransition/AbstractStateTransition.js index 369c9436e95..208c0695edc 100644 --- a/packages/js-dpp/lib/stateTransition/AbstractStateTransition.js +++ b/packages/js-dpp/lib/stateTransition/AbstractStateTransition.js @@ -1,7 +1,7 @@ const { PublicKey, PrivateKey, - Signer: { sign, verifySignature }, + Signer: { sign, verifySignature, verifyHashSignature }, } = require('@dashevo/dashcore-lib'); const StateTransitionIsNotSignedError = require( @@ -165,6 +165,29 @@ class AbstractStateTransition { return this; } + /** + * + * @param {Buffer} publicKeyHash + * @return {boolean} + */ + verifySignatureByPublicKeyHash(publicKeyHash) { + const signature = this.getSignature(); + if (!signature) { + throw new StateTransitionIsNotSignedError(this); + } + + const hash = this.hash({ skipSignature: true }); + + let isSignatureVerified; + try { + isSignatureVerified = verifyHashSignature(hash, signature, publicKeyHash); + } catch (e) { + isSignatureVerified = false; + } + + return isSignatureVerified; + } + /** * Verify signature with public key * @param {string|Buffer|Uint8Array|PublicKey} publicKey string must be hex or base58 diff --git a/packages/js-dpp/lib/stateTransition/AbstractStateTransitionIdentitySigned.js b/packages/js-dpp/lib/stateTransition/AbstractStateTransitionIdentitySigned.js index 7dde4bd5ded..5046a3093b9 100644 --- a/packages/js-dpp/lib/stateTransition/AbstractStateTransitionIdentitySigned.js +++ b/packages/js-dpp/lib/stateTransition/AbstractStateTransitionIdentitySigned.js @@ -1,6 +1,7 @@ const { PublicKey, PrivateKey, + crypto: { Hash }, } = require('@dashevo/dashcore-lib'); const AbstractStateTransition = require('./AbstractStateTransition'); @@ -72,6 +73,23 @@ class AbstractStateTransitionIdentitySigned extends AbstractStateTransition { this.signByPrivateKey(privateKeyModel); break; + case IdentityPublicKey.TYPES.ECDSA_HASH160: { + privateKeyModel = new PrivateKey(privateKey); + pubKeyBase = new PublicKey({ + ...privateKeyModel.toPublicKey().toObject(), + compressed: true, + }) + .toBuffer(); + + pubKeyBase = Hash.sha256ripemd160(pubKeyBase); + + if (!pubKeyBase.equals(identityPublicKey.getData())) { + throw new InvalidSignaturePublicKeyError(identityPublicKey.getData()); + } + + this.signByPrivateKey(privateKeyModel); + break; + } case IdentityPublicKey.TYPES.BLS12_381: default: throw new InvalidIdentityPublicKeyTypeError(identityPublicKey.getType()); @@ -123,6 +141,11 @@ class AbstractStateTransitionIdentitySigned extends AbstractStateTransition { } const publicKeyBuffer = publicKey.getData(); + + if (publicKey.getType() === IdentityPublicKey.TYPES.ECDSA_HASH160) { + return this.verifySignatureByPublicKeyHash(publicKeyBuffer); + } + const publicKeyModel = PublicKey.fromBuffer(publicKeyBuffer); return this.verifySignatureByPublicKey(publicKeyModel); diff --git a/packages/js-dpp/lib/stateTransition/validation/validateStateTransitionIdentitySignatureFactory.js b/packages/js-dpp/lib/stateTransition/validation/validateStateTransitionIdentitySignatureFactory.js index a448e01a47c..b36741a2429 100644 --- a/packages/js-dpp/lib/stateTransition/validation/validateStateTransitionIdentitySignatureFactory.js +++ b/packages/js-dpp/lib/stateTransition/validation/validateStateTransitionIdentitySignatureFactory.js @@ -41,7 +41,10 @@ function validateStateTransitionIdentitySignatureFactory( return result; } - if (publicKey.getType() !== IdentityPublicKey.TYPES.ECDSA_SECP256K1) { + if ( + publicKey.getType() !== IdentityPublicKey.TYPES.ECDSA_SECP256K1 + && publicKey.getType() !== IdentityPublicKey.TYPES.ECDSA_HASH160 + ) { result.addError( new InvalidIdentityPublicKeyTypeError(publicKey.getType()), ); diff --git a/packages/js-dpp/schema/identity/publicKey.json b/packages/js-dpp/schema/identity/publicKey.json index 7492cc88fb2..891ac676cde 100644 --- a/packages/js-dpp/schema/identity/publicKey.json +++ b/packages/js-dpp/schema/identity/publicKey.json @@ -12,9 +12,10 @@ "type": "integer", "enum": [ 0, - 1 + 1, + 2 ], - "description": "Public key type. 0 - ECDSA Secp256k1, 1 - BLS 12-381", + "description": "Public key type. 0 - ECDSA Secp256k1, 1 - BLS 12-381, 2 - ECDSA Secp256k1 Hash160", "$comment": "It can't be changed after adding a key" }, "purpose": { @@ -82,6 +83,27 @@ } } } + }, + { + "if": { + "properties": { + "type": { + "const": 2 + } + } + }, + "then": { + "properties": { + "data": { + "type": "array", + "byteArray": true, + "minItems": 20, + "maxItems": 20, + "description": "ECDSA Secp256k1 public key Hash160", + "$comment": "It must be a valid key hash of the specified type and unique for the identity. It can’t be changed after adding a key" + } + } + } } ], "required": [ diff --git a/packages/js-dpp/test/integration/stateTransition/AbstractStateTransitionIdentitySigned.spec.js b/packages/js-dpp/test/integration/stateTransition/AbstractStateTransitionIdentitySigned.spec.js index 665638fb883..4222f6c1045 100644 --- a/packages/js-dpp/test/integration/stateTransition/AbstractStateTransitionIdentitySigned.spec.js +++ b/packages/js-dpp/test/integration/stateTransition/AbstractStateTransitionIdentitySigned.spec.js @@ -1,4 +1,4 @@ -const { PrivateKey } = require('@dashevo/dashcore-lib'); +const { PrivateKey, crypto: { Hash } } = require('@dashevo/dashcore-lib'); const calculateStateTransitionFee = require('../../../lib/stateTransition/calculateStateTransitionFee'); @@ -127,6 +127,21 @@ describe('AbstractStateTransitionIdentitySigned', () => { expect(isValid).to.be.true(); }); + it('should sign data and validate signature with ECDSA_HASH160 identityPublicKey', () => { + identityPublicKey.setType(IdentityPublicKey.TYPES.ECDSA_HASH160); + identityPublicKey.setData( + Hash.sha256ripemd160(identityPublicKey.getData()), + ); + + stateTransition.sign(identityPublicKey, privateKeyHex); + + expect(stateTransition.signature).to.be.an.instanceOf(Buffer); + + const isValid = stateTransition.verifySignature(identityPublicKey); + + expect(isValid).to.be.true(); + }); + it('should throw an error if we try to sign with wrong public key', () => { const publicKey = new PrivateKey() .toPublicKey() @@ -291,6 +306,31 @@ describe('AbstractStateTransitionIdentitySigned', () => { }); }); + describe('#verifySignatureByPublicKeyHash', () => { + it('should validate sign by public key hash', () => { + privateKeyHex = 'fdfa0d878967ac17ca3e6fa6ca7f647fea51cffac85e41424c6954fcbe97721c'; + const publicKey = 'dLfavDCp+ARA3O0AXsOFJ0W//mg='; + + stateTransition.signByPrivateKey(privateKeyHex); + + const isValid = stateTransition.verifySignatureByPublicKeyHash(Buffer.from(publicKey, 'base64')); + + expect(isValid).to.be.true(); + }); + + it('should throw an StateTransitionIsNotSignedError error if transition is not signed', () => { + const publicKey = 'dLfavDCp+ARA3O0AXsOFJ0W//mg='; + try { + stateTransition.verifySignatureByPublicKeyHash(Buffer.from(publicKey, 'base64')); + + expect.fail('should throw StateTransitionIsNotSignedError'); + } catch (e) { + expect(e).to.be.instanceOf(StateTransitionIsNotSignedError); + expect(e.getStateTransition()).to.equal(stateTransition); + } + }); + }); + describe('#verifySignatureByPublicKey', () => { it('should validate sign by public key', () => { privateKeyHex = '9b67f852093bc61cea0eeca38599dbfba0de28574d2ed9b99d10d33dc1bde7b2'; diff --git a/packages/js-dpp/test/unit/identity/IdentityPublicKey.spec.js b/packages/js-dpp/test/unit/identity/IdentityPublicKey.spec.js index 33dd92ee7d7..eb7749daa24 100644 --- a/packages/js-dpp/test/unit/identity/IdentityPublicKey.spec.js +++ b/packages/js-dpp/test/unit/identity/IdentityPublicKey.spec.js @@ -118,6 +118,24 @@ describe('IdentityPublicKey', () => { expect(result).to.deep.equal(expectedHash); }); + it('should return data in case ECDSA_HASH160', () => { + rawPublicKey = { + id: 0, + type: IdentityPublicKey.TYPES.ECDSA_HASH160, + data: Buffer.from('AkVuTKyF3YgKLAQlLEtaUL2HTditwGILfWUVqjzYnIgH', 'base64'), + purpose: IdentityPublicKey.PURPOSES.AUTHENTICATION, + securityLevel: IdentityPublicKey.SECURITY_LEVELS.MASTER, + }; + + publicKey = new IdentityPublicKey(rawPublicKey); + + const result = publicKey.hash(); + + const expectedHash = Buffer.from('AkVuTKyF3YgKLAQlLEtaUL2HTditwGILfWUVqjzYnIgH', 'base64'); + + expect(result).to.deep.equal(expectedHash); + }); + it('should throw invalid argument error if data was not originally provided', () => { publicKey = new IdentityPublicKey({ id: 0, diff --git a/packages/js-dpp/test/unit/stateTransition/validation/validateStateTransitionIdentitySignatureFactory.spec.js b/packages/js-dpp/test/unit/stateTransition/validation/validateStateTransitionIdentitySignatureFactory.spec.js index 3ed9df449de..1d1221e5507 100644 --- a/packages/js-dpp/test/unit/stateTransition/validation/validateStateTransitionIdentitySignatureFactory.spec.js +++ b/packages/js-dpp/test/unit/stateTransition/validation/validateStateTransitionIdentitySignatureFactory.spec.js @@ -118,8 +118,8 @@ describe('validateStateTransitionIdentitySignatureFactory', () => { expect(error.getPublicKeyId()).to.equal(publicKeyId); }); - it('should return InvalidIdentityPublicKeyTypeError if type is not ECDSA_SECP256K1', async () => { - const type = IdentityPublicKey.TYPES.ECDSA_SECP256K1 + 1; + it('should return InvalidIdentityPublicKeyTypeError if type is not ECDSA_SECP256K1 and not ECDSA_HASH160', async () => { + const type = IdentityPublicKey.TYPES.ECDSA_HASH160 + 1; identityPublicKey.getType.returns(type); const result = await validateStateTransitionIdentitySignature( @@ -130,7 +130,7 @@ describe('validateStateTransitionIdentitySignatureFactory', () => { expect(result.isValid()).to.be.false(); expect(validateIdentityExistenceMock).to.be.calledOnceWithExactly(ownerId); expect(identity.getPublicKeyById).to.be.calledOnceWithExactly(publicKeyId); - expect(identityPublicKey.getType).to.be.calledTwice(); + expect(identityPublicKey.getType).to.be.calledThrice(); expect(stateTransition.getSignaturePublicKeyId).to.be.calledOnce(); expect(stateTransition.verifySignature).to.not.be.called();