package applications import ( "crypto/sha256" "git.openprivacy.ca/cwtch.im/tapir" "git.openprivacy.ca/cwtch.im/tapir/primitives/core" "git.openprivacy.ca/openprivacy/log" ristretto "github.com/gtank/ristretto255" ) // ProofOfWorkApplication forces the incoming connection to do proof of work before granting a capability type ProofOfWorkApplication struct { TranscriptApp } // transcript constants const ( PoWApp = "pow-app" PoWSeed = "pow-seed" PoWChallenge = "pow-challenge" PoWPRNG = "pow-prng" PoWSolution = "pow-solution" ) // SuccessfulProofOfWorkCapability is given when a successfully PoW Challenge has been Completed const SuccessfulProofOfWorkCapability = tapir.Capability("SuccessfulProofOfWorkCapability") // NewInstance should always return a new instantiation of the application. func (powapp *ProofOfWorkApplication) NewInstance() tapir.Application { return new(ProofOfWorkApplication) } // Init is run when the connection is first started. func (powapp *ProofOfWorkApplication) Init(connection tapir.Connection) { powapp.Transcript().NewProtocol(PoWApp) if connection.IsOutbound() { powapp.Transcript().AddToTranscript(PoWSeed, connection.Expect()) solution := powapp.solveChallenge(powapp.Transcript().CommitToTranscript(PoWChallenge), powapp.transcript.CommitToPRNG(PoWPRNG)) powapp.transcript.AddToTranscript(PoWSolution, solution) connection.Send(solution) connection.SetCapability(SuccessfulProofOfWorkCapability) // We can self grant.because the server will close the connection on failure return } // We may be the first application, in which case we need to randomize the transcript challenge // We use the random hostname of the inbound server (if we've authenticated them then the challenge will // already be sufficiently randomized, so this doesn't hurt) // It does sadly mean an additional round trip. powapp.Transcript().AddToTranscript(PoWSeed, []byte(connection.Hostname())) connection.Send([]byte(connection.Hostname())) solution := connection.Expect() challenge := powapp.Transcript().CommitToTranscript(PoWChallenge) // soft-commitment to the prng, doesn't force the client to use it (but we could technically check that it did, not necessary for the security of this App) powapp.transcript.CommitToPRNG(PoWPRNG) powapp.transcript.AddToTranscript(PoWSolution, solution) if powapp.validateChallenge(challenge, solution) { connection.SetCapability(SuccessfulProofOfWorkCapability) return } } // SolveChallenge takes in a challenge and a message and returns a solution // The solution is a 24 byte nonce which when hashed with the challenge and the message // produces a sha256 hash with Difficulty leading 0s func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core.PRNG) []byte { solved := false var sum [32]byte solution := []byte{} solve := make([]byte, len(challenge)+32) // reuse our allocation buf := make([]byte, 64) next := new(ristretto.Scalar) encodedSolution := make([]byte, 0, 32) for !solved { err := prng.Next(buf, next) if err != nil { // this will cause the challenge to fail... log.Errorf("error completing challenge: %v", err) return nil } //lint:ignore SA1019 API this is "deprecated", but without it it will cause an allocation on every single check solution = next.Encode(encodedSolution) copy(solve[0:], solution[:]) copy(solve[len(solution):], challenge[:]) sum = sha256.Sum256(solve) solved = true for i := 0; i < 2; i++ { if sum[i] != 0x00 { solved = false } } // reuse this allocated memory next time... encodedSolution = encodedSolution[:0] } log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum) return solution[:] } // ValidateChallenge returns true if the message and spamguard pass the challenge func (powapp *ProofOfWorkApplication) validateChallenge(challenge []byte, solution []byte) bool { if len(solution) != 32 { return false } solve := make([]byte, len(challenge)+32) copy(solve[0:], solution[0:32]) copy(solve[32:], challenge[:]) sum := sha256.Sum256(solve) for i := 0; i < 2; i++ { if sum[i] != 0x00 { return false } } log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum) return true }