Skip to content

Commit

Permalink
initial public release
Browse files Browse the repository at this point in the history
  • Loading branch information
gtank committed Jul 7, 2017
0 parents commit 2a5cbbc
Show file tree
Hide file tree
Showing 164 changed files with 42,890 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/.GOPATH
/bin
26 changes: 26 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Copyright (c) 2017, Cloudflare. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
142 changes: 142 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# The import path is where your repository can be found.
# To import subpackages, always prepend the full import path.
# If you change this, run `make clean`. Read more: https://git.io/vM7zV
IMPORT_PATH := github.com/cloudflare/btd

# V := 1 # When V is set, print commands and build progress.

# Space separated patterns of packages to skip in list, test, format.
IGNORED_PACKAGES := /vendor/

.PHONY: all
all: build

.PHONY: build btd
build: btd

btd: .GOPATH/.ok
$Q go install $(if $V,-v) $(VERSION_FLAGS) $(IMPORT_PATH)/server
$Q mv bin/server bin/btd

### Code not in the repository root? Another binary? Add to the path like this.
# .PHONY: otherbin
# otherbin: .GOPATH/.ok
# $Q go install $(if $V,-v) $(VERSION_FLAGS) $(IMPORT_PATH)/cmd/otherbin

##### ^^^^^^ EDIT ABOVE ^^^^^^ #####

##### =====> Utility targets <===== #####

.PHONY: clean test list cover format

clean:
$Q rm -rf bin .GOPATH

test: .GOPATH/.ok
$Q go test $(if $V,-v) -i -race $(allpackages) # install -race libs to speed up next run
ifndef CI
$Q go vet $(allpackages)
$Q GODEBUG=cgocheck=2 go test -race $(allpackages)
else
$Q ( go vet $(allpackages); echo $$? ) | \
tee .GOPATH/test/vet.txt | sed '$$ d'; exit $$(tail -1 .GOPATH/test/vet.txt)
$Q ( GODEBUG=cgocheck=2 go test -v -race $(allpackages); echo $$? ) | \
tee .GOPATH/test/output.txt | sed '$$ d'; exit $$(tail -1 .GOPATH/test/output.txt)
endif

list: .GOPATH/.ok
@echo $(allpackages)

cover: bin/gocovmerge .GOPATH/.ok
@echo "NOTE: make cover does not exit 1 on failure, don't use it to check for tests success!"
$Q rm -f .GOPATH/cover/*.out .GOPATH/cover/all.merged
$(if $V,@echo "-- go test -coverpkg=./... -coverprofile=.GOPATH/cover/... ./...")
@for MOD in $(allpackages); do \
go test -coverpkg=`echo $(allpackages)|tr " " ","` \
-coverprofile=.GOPATH/cover/unit-`echo $$MOD|tr "/" "_"`.out \
$$MOD 2>&1 | grep -v "no packages being tested depend on"; \
done
$Q ./bin/gocovmerge .GOPATH/cover/*.out > .GOPATH/cover/all.merged
ifndef CI
$Q go tool cover -html .GOPATH/cover/all.merged
else
$Q go tool cover -html .GOPATH/cover/all.merged -o .GOPATH/cover/all.html
endif
@echo ""
@echo "=====> Total test coverage: <====="
@echo ""
$Q go tool cover -func .GOPATH/cover/all.merged

format: bin/goimports .GOPATH/.ok
$Q find .GOPATH/src/$(IMPORT_PATH)/ -iname \*.go | grep -v \
-e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES)) | xargs ./bin/goimports -w

##### =====> Internals <===== #####

.PHONY: setup
setup: clean .GOPATH/.ok
@if ! grep "/.GOPATH" .gitignore > /dev/null 2>&1; then \
echo "/.GOPATH" >> .gitignore; \
echo "/bin" >> .gitignore; \
fi
go get -u github.com/FiloSottile/gvt
- ./bin/gvt fetch golang.org/x/tools/cmd/goimports
- ./bin/gvt fetch github.com/wadey/gocovmerge

VERSION := $(shell git describe --tags --always --dirty="-dev")
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
VERSION_FLAGS := -ldflags='-X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"'

# cd into the GOPATH to workaround ./... not following symlinks
_allpackages = $(shell ( cd $(CURDIR)/.GOPATH/src/$(IMPORT_PATH) && \
GOPATH=$(CURDIR)/.GOPATH go list ./... 2>&1 1>&3 | \
grep -v -e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES)) 1>&2 ) 3>&1 | \
grep -v -e "^$$" $(addprefix -e ,$(IGNORED_PACKAGES)))

# memoize allpackages, so that it's executed only once and only if used
allpackages = $(if $(__allpackages),,$(eval __allpackages := $$(_allpackages)))$(__allpackages)

export GOPATH := $(CURDIR)/.GOPATH
unexport GOBIN

Q := $(if $V,,@)

.GOPATH/.ok:
$Q mkdir -p "$(dir .GOPATH/src/$(IMPORT_PATH))"
$Q ln -s ../../../.. ".GOPATH/src/$(IMPORT_PATH)"
$Q mkdir -p .GOPATH/test .GOPATH/cover
$Q mkdir -p bin
$Q ln -s ../bin .GOPATH/bin
$Q touch $@

.PHONY: bin/gocovmerge bin/goimports
bin/gocovmerge: .GOPATH/.ok
@test -d ./vendor/github.com/wadey/gocovmerge || \
{ echo "Vendored gocovmerge not found, try running 'make setup'..."; exit 1; }
$Q go install $(IMPORT_PATH)/vendor/github.com/wadey/gocovmerge
bin/goimports: .GOPATH/.ok
@test -d ./vendor/golang.org/x/tools/cmd/goimports || \
{ echo "Vendored goimports not found, try running 'make setup'..."; exit 1; }
$Q go install $(IMPORT_PATH)/vendor/golang.org/x/tools/cmd/goimports

# Based on https://github.com/cloudflare/hellogopher - v1.1 - MIT License
#
# Copyright (c) 2017 Cloudflare
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## Blind Token Daemon

This is the server implementing the second revision of the Cloudflare blinded tokens protocol. For a description of the original protocol and motivations, see the [challenge bypass specification](https://github.com/cloudflare/challenge-bypass-specification) or our talk at [Real World Crypto 2017](https://speakerdeck.com/gtank/solving-the-cloudflare-captcha-rwc2017).

The protocol is based on a variant of an OPRF [password management scheme](https://eprint.iacr.org/2016/144) by Jarecki, Kiayias, Krawczyk and Xu. When adapted to our needs, this scheme allows us to achieve the same goals using faster primitives, less bandwidth, and simpler secret-key operational logistics compared to the earlier RSA-based protocol.

## Quickstart

To run the server:

`go run server/main.go --key testdata/p256-key.pem`

To demo token issuance:

`cat testdata/bl_sig_req | nc localhost 2416`

For a full client implementation, see the [browser extension](https://github.com/cloudflare/challenge-bypass-extension).

### Definitions

A **message authentication code (MAC)** on a message is a keyed authentication tag that can be only be created and verified by the holder of the key.

A **pseudorandom function** is a function whose output cannot be efficiently distinguished from random output. This is a general class of functions; concrete examples include hashes and encryption algorithms.

An **oblivious pseudorandom function (OPRF)** is a two-party protocol between sender *S* and receiver *R* for securely computing a pseudorandom function *f_k(·)* on key *k* contributed by *S* and input *x* contributed by *R*, in such a way that receiver *R* learns only the value *f_k(x)* while sender *S* learns nothing from the interaction.

In this protocol, the Cloudflare edge is the "sender" holding k and the inputs x are the tokens. So the clients don't learn our key and we don't learn the token values.

### Protocol sketch

The core difference is that where we previously relied on asymmetric cryptography for signatures, we can instead construct a symmetric exchange in which a secret key known only to the edge is used to create per-token MAC keys for each redemption request in a way that prevents the server from learning the token values and the client from learning the secret key.

Given a group setting and two hashes H_1, H_2, we build a commitment to a random token per request using a secret key k held by the edge servers. H_1 and H_2 are hash functions onto, respectively, the group and {0, 1}^λ where λ is a security parameter.

1. Client generates random token `x` and a blinding factor `r`
2. Client calculates `a = H_1(x)^r` and sends `a` to the edge along with a CAPTCHA solution
3. Edge validates the solution and computes `b = a^k = H_1(x)^(rk)`, returns `b` to client
4. Client unblinds b to retrieve `n = b^(1/r) = H_1(x)^k`. Now both the server and the client can calculate `H_2(x, n)` as a shared key for the MAC.
5. When the client wants to redeem a token it presents `(x, MAC(request-binding-data))` where `request-binding-data` is made of information observable by the edge that is unique(ish) to that particular request.
6. The server uses `x` as a double-spend index and recalculates `n` using its secret key. Then it can validate the MAC using the shared key.
7. We know that a matching commitment value is valid because generating it requires access to `k`.

We prevent the edge from tracking users by tagging with unique keys using batch discrete-logarithm equality proofs (which are implemented but not yet deployed).

### Benefits vs blind-RSA protocol

- 10x savings in token size (~256 bits instead of ~2048)
- Simpler & faster primitives (also: available in-browser via SJCL)
- No need for public-key encryption at all, since the derived shared key used to calculate each MAC is never transmitted and cannot be calculated without knowledge of the edge key or the client's blinding factor.
- The only secret to be managed is a 32-byte scalar.
- Easier key rotation. Instead of managing RSA certificates with pinning or transparency, we can publish/pin the commitment component of a DLEQ proof to allow clients to positively verify they're in the same anonymity set with regard to k as everyone else. Alternatively or additionally, if we publish historical k values then auditors who save their b results can check our honesty.
128 changes: 128 additions & 0 deletions crypto/batch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// This implements a non-interactive version of the common-exponent batch
// Schnorr proof from Ryan Henry's thesis. This specifically applies to the
// case where a set of group elements shares a common discrete log with regard
// to a set of generators. Slightly more formally, the case:
//
// (G, q, g_1,...,g_n) and (h_1,...,h_n) ∈ (G)^n with h_i = (g_i)^x for i = 1,...,n
//
// Inspired by an observation of Ian Goldberg's that this "common-exponent" case
// is drastically simpler than a general batch proof. The general idea is that
// we can produce a linear combination of the elements and perform a
// Chaum-Pedersen proof on the resulting composite elements.
//
// See Section 3.2.3.3 for the interactive protocol:
// https://uwspace.uwaterloo.ca/bitstream/handle/10012/8621/Henry_Ryan.pdf
package crypto

import (
"crypto"
"errors"
"math/big"

"golang.org/x/crypto/sha3"
)

var (
ErrUnequalPointCounts = errors.New("batch proof had unequal numbers of points")
)

type BatchProof struct {
P *Proof
G, H *Point
M, Z []*Point
C [][]byte
}

func NewBatchProof(hash crypto.Hash, g, h *Point, m []*Point, z []*Point, x *big.Int) (*BatchProof, error) {
if len(m) != len(z) {
return nil, ErrUnequalPointCounts
}

// The underlying proof and validation steps will do consistency checks.
curve := g.Curve

// seed = H(g, h, [m], [z])
H := hash.New()
H.Write(g.Marshal())
H.Write(h.Marshal())
for i := 0; i < len(m); i++ {
H.Write(m[i].Marshal())
H.Write(z[i].Marshal())
}
seed := H.Sum(nil)

prng := sha3.NewShake256()
prng.Write(seed)

// Non-interactively generate random c_1, c_2, ... , c_n in Z/qZ
// to combine m and z elements. Here's how this works:
// For (m_1, m_2), (z_1, z_2), and (c_1, c_2) we have
// z_1 = (m_1)^x
// z_2 = (m_2)^x
// (z_1^c_1) = (m_1^c_1)^x
// (z_2^c_2) = (m_2^c_2)^x
// (z_1^c_1)(z_2^c_2) = [(m_1^c_1)(m_2^c_2)]^x
// This generalizes to produce composite elements for the entire batch that
// can be compared to the public key in the standard two-point DLEQ proof.

Mx, My, Zx, Zy := new(big.Int), new(big.Int), new(big.Int), new(big.Int)
C := make([][]byte, len(m))
for i := 0; i < len(m); i++ {
ci, _, err := randScalar(curve, prng)
if err != nil {
return nil, err
}
// cM = c[i]M[i]
cMx, cMy := curve.ScalarMult(m[i].X, m[i].Y, ci)
// cZ = c[i]Z[i]
cZx, cZy := curve.ScalarMult(z[i].X, z[i].Y, ci)
// Accumulate
Mx, My = curve.Add(cMx, cMy, Mx, My)
Zx, Zy = curve.Add(cZx, cZy, Zx, Zy)
C[i] = ci
}
compositeM := &Point{Curve: curve, X: Mx, Y: My}
compositeZ := &Point{Curve: curve, X: Zx, Y: Zy}

proof, err := NewProof(hash, g, h, compositeM, compositeZ, x)
if err != nil {
return nil, err
}
return &BatchProof{
P: proof,
G: g, H: h,
M: m, Z: z,
C: C,
}, nil
}

func (b *BatchProof) IsComplete() bool {
hasPublicKey := b.G != nil && b.H != nil
hasPointSets := b.M != nil && b.Z != nil && len(b.M) == len(b.Z)
return hasPublicKey && hasPointSets && b.C != nil
}

func (b *BatchProof) IsSane() bool {
if len(b.M) != len(b.Z) {
return false
}
if b.G.Curve != b.H.Curve {
return false
}
for i := 0; i < len(b.M); i++ {
if b.G.Curve != b.M[i].Curve || b.G.Curve != b.Z[i].Curve {
return false
}
if !b.M[i].IsOnCurve() || !b.Z[i].IsOnCurve() {
return false
}
}
return true
}

func (b *BatchProof) Verify() bool {
if !b.IsComplete() || !b.IsSane() {
return false
}
return b.P.Verify()
}
Loading

0 comments on commit 2a5cbbc

Please sign in to comment.