Skip to content

Commit

Permalink
Merge pull request #685 from ScorexFoundation/sign-message
Browse files Browse the repository at this point in the history
signMessage / verifyMessage for signing and verifying arbitrary messages
  • Loading branch information
aslesarenko authored Jan 22, 2021
2 parents f7ac7eb + e4ca397 commit 64bbfd8
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 26 deletions.
1 change: 0 additions & 1 deletion sigmastate/src/main/scala/sigmastate/SigSerializer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Helpers.xor
import gf2t.GF2_192_Poly
import sigmastate.basics.{SecondDiffieHellmanTupleProverMessage, ProveDHTuple}


object SigSerializer {

val hashSize = CryptoConstants.soundnessBits / 8
Expand Down
59 changes: 41 additions & 18 deletions sigmastate/src/main/scala/sigmastate/interpreter/Interpreter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -379,4 +402,4 @@ object Interpreter {

def error(msg: String) = throw new InterpreterException(msg)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
}

}
26 changes: 23 additions & 3 deletions sigmastate/src/main/scala/sigmastate/interpreter/ProverUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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") {

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

0 comments on commit 64bbfd8

Please sign in to comment.