diff --git a/sigmastate/src/main/scala/sigmastate/SigSerializer.scala b/sigmastate/src/main/scala/sigmastate/SigSerializer.scala index c17178e9c3..40fa3d8ad6 100644 --- a/sigmastate/src/main/scala/sigmastate/SigSerializer.scala +++ b/sigmastate/src/main/scala/sigmastate/SigSerializer.scala @@ -10,7 +10,6 @@ import Helpers.xor import gf2t.GF2_192_Poly import sigmastate.basics.{SecondDiffieHellmanTupleProverMessage, ProveDHTuple} - object SigSerializer { val hashSize = CryptoConstants.soundnessBits / 8 diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala index 5aee794a74..543dd6bbfb 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala @@ -265,23 +265,7 @@ trait Interpreter extends ScorexLogging { val checkingResult = cProp match { case TrivialProp.TrueProp => true case TrivialProp.FalseProp => false - case _ => - // Perform Verifier Steps 1-3 - SigSerializer.parseAndComputeChallenges(cProp, proof) match { - case NoProof => false - case sp: UncheckedSigmaTree => - // Perform Verifier Step 4 - val newRoot = computeCommitments(sp).get.asInstanceOf[UncheckedSigmaTree] - - /** - * Verifier Steps 5-6: Convert the tree to a string `s` for input to the Fiat-Shamir hash function, - * using the same conversion as the prover in 7 - * Accept the proof if the challenge at the root of the tree is equal to the Fiat-Shamir hash of `s` - * (and, if applicable, the associated data). Reject otherwise. - */ - val expectedChallenge = CryptoFunctions.hashFn(FiatShamirTree.toBytes(newRoot) ++ message) - util.Arrays.equals(newRoot.challenge, expectedChallenge) - } + case _ => verifySignature(cProp, message, proof) } checkingResult -> cost }) @@ -303,6 +287,21 @@ trait Interpreter extends ScorexLogging { res } + // Perform Verifier Steps 4-6 + private def checkCommitments(sp: UncheckedSigmaTree, message: Array[Byte]): Boolean = { + // Perform Verifier Step 4 + val newRoot = computeCommitments(sp).get.asInstanceOf[UncheckedSigmaTree] + + /** + * Verifier Steps 5-6: Convert the tree to a string `s` for input to the Fiat-Shamir hash function, + * using the same conversion as the prover in 7 + * Accept the proof if the challenge at the root of the tree is equal to the Fiat-Shamir hash of `s` + * (and, if applicable, the associated data). Reject otherwise. + */ + val expectedChallenge = CryptoFunctions.hashFn(FiatShamirTree.toBytes(newRoot) ++ message) + util.Arrays.equals(newRoot.challenge, expectedChallenge) + } + /** * Verifier Step 4: For every leaf node, compute the commitment a from the challenge e and response $z$, * per the verifier algorithm of the leaf's Sigma-protocol. @@ -347,6 +346,30 @@ trait Interpreter extends ScorexLogging { verify(Interpreter.emptyEnv, ergoTree, context, SigSerializer.toBytes(proof), message) } + /** + * Verify a signature on given (arbitrary) message for a given public key. + * + * @param sigmaTree - public key (represented as a tree) + * @param message - message + * @param signature - signature for the message + * @return - whether signature is valid or not + */ + def verifySignature(sigmaTree: SigmaBoolean, + message: Array[Byte], + signature: Array[Byte]): Boolean = { + // Perform Verifier Steps 1-3 + try { + SigSerializer.parseAndComputeChallenges(sigmaTree, signature) match { + case NoProof => false + case sp: UncheckedSigmaTree => + // Perform Verifier Steps 4-6 + checkCommitments(sp, message) + } + } catch { + case e: Exception => log.warn("Improper signature: ", e); false + } + } + } object Interpreter { @@ -379,4 +402,4 @@ object Interpreter { def error(msg: String) = throw new InterpreterException(msg) -} +} \ No newline at end of file diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala b/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala index 1c7cc0082c..da0f26dac6 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/ProverInterpreter.scala @@ -111,7 +111,6 @@ trait ProverInterpreter extends Interpreter with ProverUtils with AttributionCor message: Array[Byte]): Try[CostedProverResult] = prove(emptyEnv, ergoTree, context, message, HintsBag.empty) - def prove(env: ScriptEnv, ergoTree: ErgoTree, context: CTX, @@ -580,4 +579,21 @@ trait ProverInterpreter extends Interpreter with ProverUtils with AttributionCor error(s"Cannot convertToUnproven($a)") } + /** + * + * Sign arbitrary message under a key representing a statement provable via a sigma-protocol. + * + * @param sigmaTree - public key + * @param message - message to sign + * @param hintsBag - additional hints for a signer (useful for distributed signing) + * @return - signature or error + */ + def signMessage(sigmaTree: SigmaBoolean, + message: Array[Byte], + hintsBag: HintsBag): Try[Array[Byte]] = Try { + val unprovenTree = convertToUnproven(sigmaTree) + val proofTree = prove(unprovenTree, message, hintsBag) + SigSerializer.toBytes(proofTree) + } + } diff --git a/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala b/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala index ff19d2cc0c..25f8be0a65 100644 --- a/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala +++ b/sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala @@ -68,7 +68,7 @@ trait ProverUtils extends Interpreter { * See DistributedSigSpecification for examples of usage. * * @param context - context used to reduce the proposition - * @param ergoTree - proposition to reduce + * @param ergoTree - proposition to reduce * @param proof - proof for reduced proposition * @param realSecretsToExtract - public keys of secrets with real proofs * @param simulatedSecretsToExtract - public keys of secrets with simulated proofs @@ -79,10 +79,30 @@ trait ProverUtils extends Interpreter { proof: Array[Byte], realSecretsToExtract: Seq[SigmaBoolean], simulatedSecretsToExtract: Seq[SigmaBoolean] = Seq.empty): HintsBag = { - val reducedTree = fullReduction(ergoTree, context, Interpreter.emptyEnv)._1 + bagForMultisig(context, reducedTree, proof, realSecretsToExtract, simulatedSecretsToExtract) + } + + /** + * A method which is extracting partial proofs of secret knowledge for particular secrets with their + * respective public images given. Useful for distributed signature applications. + * + * See DistributedSigSpecification for examples of usage. + * + * @param context - context used to reduce the proposition + * @param sigmaTree - public key (in form of a sigma-tree) + * @param proof - signature for the key + * @param realSecretsToExtract - public keys of secrets with real proofs + * @param simulatedSecretsToExtract - public keys of secrets with simulated proofs + * @return - bag of OtherSecretProven and OtherCommitment hints + */ + def bagForMultisig(context: CTX, + sigmaTree: SigmaBoolean, + proof: Array[Byte], + realSecretsToExtract: Seq[SigmaBoolean], + simulatedSecretsToExtract: Seq[SigmaBoolean]): HintsBag = { - val ut = SigSerializer.parseAndComputeChallenges(reducedTree, proof) + val ut = SigSerializer.parseAndComputeChallenges(sigmaTree, proof) val proofTree = computeCommitments(ut).get.asInstanceOf[UncheckedSigmaTree] def traverseNode(tree: ProofTree, diff --git a/sigmastate/src/test/scala/sigmastate/crypto/SigningSpecification.scala b/sigmastate/src/test/scala/sigmastate/crypto/SigningSpecification.scala index 9a173d7825..5a81fcdfa4 100644 --- a/sigmastate/src/test/scala/sigmastate/crypto/SigningSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/crypto/SigningSpecification.scala @@ -1,13 +1,15 @@ package sigmastate.crypto +import org.scalacheck.Gen import scorex.util.encode.Base16 -import sigmastate.AtLeast +import sigmastate.{AtLeast, CAND} +import sigmastate.Values.SigmaBoolean import sigmastate.basics.DLogProtocol.DLogProverInput import sigmastate.helpers.{ErgoLikeTestInterpreter, ErgoLikeTestProvingInterpreter, SigmaTestingCommons} -import sigmastate.interpreter.{ContextExtension, ProverResult} +import sigmastate.interpreter.{ContextExtension, HintsBag, ProverResult} class SigningSpecification extends SigmaTestingCommons { - implicit lazy val IR = new TestingIRContext + implicit lazy val IR: TestingIRContext = new TestingIRContext property("simple signature test vector") { @@ -45,6 +47,48 @@ class SigningSpecification extends SigmaTestingCommons { printThresholdSignature(msg) } + property("signMessage / verifyMessage roundtrip - simple dlog") { + forAll(Gen.alphaNumStr){str => + val msg = str.getBytes("UTF-8") + val pi = new ErgoLikeTestProvingInterpreter() + val sigmaTree: SigmaBoolean = pi.publicKeys.head + val sig = pi.signMessage(sigmaTree, msg, HintsBag.empty).get + pi.verifySignature(sigmaTree, msg, sig) shouldBe true + pi.verifySignature(sigmaTree, (str + "1").getBytes("UTF-8"), sig) shouldBe false + pi.verifySignature(sigmaTree, msg, sig :+ (1: Byte)) shouldBe true //possible to append bytes + val wrongTree = pi.publicKeys(1) + pi.verifySignature(wrongTree, msg, sig) shouldBe false + } + } + + property("signMessage / verifyMessage roundtrip - complex key") { + forAll(Gen.alphaNumStr){str => + val msg = str.getBytes("UTF-8") + val pi = new ErgoLikeTestProvingInterpreter() + val sigmaTree: SigmaBoolean = CAND(Seq(pi.dlogSecrets.head.publicImage, pi.dhSecrets.head.publicImage)) + val sig = pi.signMessage(sigmaTree, msg, HintsBag.empty).get + pi.verifySignature(sigmaTree, msg, sig) shouldBe true + pi.verifySignature(sigmaTree, (str + "1").getBytes("UTF-8"), sig) shouldBe false + pi.verifySignature(sigmaTree, msg, sig :+ (1: Byte)) shouldBe true //possible to append bytes + val wrongTree = CAND(Seq(pi.dlogSecrets.head.publicImage, pi.dhSecrets(1).publicImage)) + pi.verifySignature(wrongTree, msg, sig) shouldBe false + } + } + + property("verifySignature w. simple signature test vector") { + + val msg = Base16.decode("1dc01772ee0171f5f614c673e3c7fa1107a8cf727bdf5a6dadb379e93c0d1d00").get + val sk = DLogProverInput(BigInt("109749205800194830127901595352600384558037183218698112947062497909408298157746").bigInteger) + val signature = Base16.decode("bcb866ba434d5c77869ddcbc3f09ddd62dd2d2539bf99076674d1ae0c32338ea95581fdc18a3b66789904938ac641eba1a66d234070207a2").get + + // check that signature is correct + val verifier = new ErgoLikeTestInterpreter + verifier.verifySignature(sk.publicImage, msg, signature) shouldBe true + + // print one more random vector for debug purposes + printSimpleSignature(msg: Array[Byte]) + } + private def printSimpleSignature(msg: Array[Byte]) { val proverA = new ErgoLikeTestProvingInterpreter diff --git a/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala b/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala index 5c17003424..6b9d79e4d6 100644 --- a/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala +++ b/sigmastate/src/test/scala/sigmastate/utxo/DistributedSigSpecification.scala @@ -500,4 +500,33 @@ class DistributedSigSpecification extends SigmaTestingCommons verifier.verify(prop, ctx, validProofB, fakeMessage).get._1 shouldBe true } + property("distributed message signing - AND (2 out of 2)") { + val ctx = fakeContext + val proverA = new ErgoLikeTestProvingInterpreter + val proverB = new ErgoLikeTestProvingInterpreter + val verifier: ContextEnrichingTestProvingInterpreter = new ContextEnrichingTestProvingInterpreter + + val msg = "Let's have a deal".getBytes("UTF-8") + + val pubkeyAlice = proverA.dlogSecrets.head.publicImage + val pubkeyBob = proverB.dlogSecrets.head.publicImage + + val sigmaTree = CAND(Seq(pubkeyAlice, pubkeyBob)) + + val hintsFromBob: HintsBag = proverB.generateCommitments(sigmaTree) + val bagA = HintsBag(hintsFromBob.realCommitments) + + val sigAlice = proverA.signMessage(sigmaTree, msg, bagA).get + + val bagB = proverB.bagForMultisig(ctx, sigmaTree, sigAlice, Seq(pubkeyAlice)) + .addHint(hintsFromBob.ownCommitments.head) + + val sigBob = proverB.signMessage(sigmaTree, msg, bagB).get + + // Proof generated by Alice without getting Bob's part is not correct + verifier.verifySignature(sigmaTree, msg, sigAlice) shouldBe false + + // Compound proof from Bob is correct + verifier.verifySignature(sigmaTree, msg, sigBob) shouldBe true + } }