git » remoteu2f » commit ab01245

Initial commit

author Alberto Bertogli
2015-09-20 16:31:04 UTC
committer Alberto Bertogli
2015-10-15 19:32:31 UTC

Initial commit

.gitignore +14 -0
INSTALL.md +73 -0
LICENSE +25 -0
Makefile +18 -0
README.md +26 -0
TODO +6 -0
internal/client/config.go +118 -0
internal/client/grpc.go +204 -0
internal/proto/dummy.go +4 -0
internal/proto/remoteu2f.pb.go +215 -0
internal/proto/remoteu2f.proto +44 -0
libpam/.clang-format +7 -0
libpam/.gitignore +3 -0
libpam/Makefile +17 -0
libpam/pam_prompt_exec.c +453 -0
remoteu2f-cli/main.go +367 -0
remoteu2f-proxy/embedded_data.go +770 -0
remoteu2f-proxy/init/default/remoteu2f-proxy +8 -0
remoteu2f-proxy/init/systemd/remoteu2f-proxy.service +11 -0
remoteu2f-proxy/init/upstart/remoteu2f-proxy.conf +16 -0
remoteu2f-proxy/main.go +125 -0
remoteu2f-proxy/ratelimit.go +31 -0
remoteu2f-proxy/server.go +368 -0
remoteu2f-proxy/to_embed/authenticate.html +31 -0
remoteu2f-proxy/to_embed/register.html +29 -0
remoteu2f-proxy/to_embed/remoteu2f.js +25 -0
remoteu2f-proxy/to_embed/u2f_api.js +654 -0
remoteu2f-proxy/tools/embed.go +118 -0

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5dc7449
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+
+# vim swap files.
+.*.sw*
+
+# keys and certificates that may be used for testing.
+*.pem
+
+# token files that may be used for testing.
+tokens
+
+# binaries we generate.
+remoteu2f-proxy/remoteu2f-proxy
+remoteu2f-cli/remoteu2f-cli
+libpam/*.so
diff --git a/INSTALL.md b/INSTALL.md
new file mode 100644
index 0000000..73b7793
--- /dev/null
+++ b/INSTALL.md
@@ -0,0 +1,73 @@
+
+# Installing remoteu2f
+
+
+## Building and installing the proxy
+
+You will need a publicly available server, a valid SSL certificate, and two
+open ports (one for HTTP and another one for GRPC).
+
+First, build and install the binary:
+
+    mkdir remoteu2f; cd remoteu2f
+    export GOPATH=$PWD
+
+    go get blitiri.com.ar/go/remoteu2f/remoteu2f-proxy
+    sudo cp bin/remoteu2f-proxy /usr/local/bin/
+
+
+Then, generate some random tokens that will be used to authorize clients:
+
+    ( for i in `seq 1 10`; do
+          head -c60 /dev/urandom | sha256sum -b | cut -d ' ' -f 1 ;
+      done ) > /etc/remoteu2f-proxy/tokens
+
+Use one token per user, or one token per (user, host).
+Never share tokens between different users, this is very insecure and will
+become even more so in the future.
+Tokens are arbitrary strings, prepending the name of the user can help you
+know who you gave them to.
+
+
+Finally, launch the binary. You can use the provided upstart or systemd
+examples to help you with this, depending on your system.
+
+
+## Building and installing the ssh side
+
+You will need `pam_prompt_exec.so` and `remoteu2f-cli`:
+
+    mkdir remoteu2f; cd remoteu2f
+    export GOPATH=$PWD
+
+    go get blitiri.com.ar/go/remoteu2f/remoteu2f-cli
+    sudo cp bin/remoteu2f-cli /usr/local/bin/
+
+    cd src/blitiri.com.ar/go/remoteu2f/libpam
+    make
+    sudo cp pam_prompt_exec.so /lib/security
+
+
+Then, configure PAM for ssh (or sudo, or the service of your choice) by
+editing /etc/pam.d/sshd (or equivalent) and adding the following at the
+bottom:
+
+    auth required pam_prompt_exec.so /usr/local/bin/remoteu2f-cli pam --nullok
+
+
+### Configuring a user
+
+Once you have completed the server install above, each each user that wants to
+use remoteu2f has to configure their client.
+
+Run `remoteu2f-cli init` and follow the instructions.
+
+Take note of the backup codes so you can access without your security key in
+an emergency.
+
+
+Then use `remoteu2f-cli register` to register your security key. You can
+register as many keys as you want.
+
+Use `remoteu2f-cli auth` to verify that it works.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3f40c7e
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,25 @@
+Unless otherwise noted, this project and its files are under the MIT licence,
+which is reproduced below (taken from http://opensource.org/licenses/MIT).
+
+-----
+
+Copyright (c) 2015  Alberto Bertogli
+
+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.
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ab9a2d0
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,18 @@
+
+all: libpam remoteu2f-cli/remoteu2f-cli remoteu2f-proxy/remoteu2f-proxy
+
+libpam:
+	$(MAKE) -C libpam
+
+remoteu2f-cli/remoteu2f-cli:
+	cd remoteu2f-cli && go build
+
+remoteu2f-proxy/remoteu2f-proxy:
+	cd remoteu2f-proxy && go build
+
+clean:
+	$(MAKE) -C libpam clean
+	rm -f remoteu2f-cli/remoteu2f-cli
+	rm -f remoteu2f-proxy/remoteu2f-proxy
+
+.PHONY: all libpam clean
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..17e0d79
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+
+# remoteu2f - Use U2F for PAM remotely
+
+[remoteu2f](http://blitiri.com.ar/p/remoteu2f) is a project that enables the
+use of a [FIDO U2F](https://www.yubico.com/applications/fido/) security key
+remotely, through a lightly-trusted proxy.
+
+It is useful mainly to use as a second factor authentication for ssh and
+sudo-over-ssh. For example:
+
+ - User does "ssh server", and enters their password.
+ - Server shows a one-time randomly generated URL.
+ - User visits the URL, and inserts/touches the security key.
+ - The SSH server allows access.
+
+It is written in Go, with some C for PAM integration.
+
+For how to install and use it, please see the
+[installation instructions](INSTALL.md).
+
+
+## Contact
+
+If you want to report bugs, send patches, or have any questions or comments,
+just let me know at albertito@blitiri.com.ar.
+
diff --git a/TODO b/TODO
new file mode 100644
index 0000000..c92dbbb
--- /dev/null
+++ b/TODO
@@ -0,0 +1,6 @@
+
+- Add more documentation, specially about how it works and trust model.
+- Write a remoteu2f-agent to avoid having to open the browser.
+- Support security key counters.
+- Make the proxy rate limit an option.
+
diff --git a/internal/client/config.go b/internal/client/config.go
new file mode 100644
index 0000000..372b763
--- /dev/null
+++ b/internal/client/config.go
@@ -0,0 +1,118 @@
+package client
+
+import (
+	"bytes"
+	"crypto/rand"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"math/big"
+	"os"
+)
+
+// Default configuration directory, relative to the user's home directory.
+const DefaultConfigDir = ".remoteu2f"
+
+// Default configuration file name.
+const DefaultConfigFileName = "config"
+
+// Client configuration structure.
+// We read and write this to disk in toml format.
+type Config struct {
+	// Address of the GRPC server to use.
+	Addr string
+
+	// Client token used to request authorization to the server.
+	Token string
+
+	// U2F AppID to use. Usually "https://address/".
+	AppID string
+
+	// Backup codes, that allow emergency access.
+	BackupCodes map[string]bool
+
+	// Registrations.
+	// Map of description -> marshalled registration.
+	Registrations map[string][]byte
+}
+
+func ReadConfig(path string) (*Config, error) {
+	data, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	c := &Config{}
+	return c, json.Unmarshal(data, c)
+}
+
+func DefaultConfigFullPath(home string) string {
+	if home == "" {
+		home = os.Getenv("HOME")
+	}
+	return home + "/" + DefaultConfigDir + "/" + DefaultConfigFileName
+}
+
+func ReadDefaultConfig(home string) (*Config, error) {
+	return ReadConfig(DefaultConfigFullPath(home))
+}
+
+func (c *Config) Write(path string) error {
+	f, err := ioutil.TempFile("", "remoteu2f-config-")
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	b, err := json.Marshal(c)
+	if err != nil {
+		os.Remove(f.Name())
+		return err
+	}
+
+	// Format the json for increased readability.
+	var out bytes.Buffer
+	json.Indent(&out, b, "", "    ")
+	out.WriteTo(f)
+
+	return os.Rename(f.Name(), path)
+}
+
+func (c *Config) WriteToDefaultPath(home string) error {
+	if home == "" {
+		home = os.Getenv("HOME")
+	}
+	dir := home + "/" + DefaultConfigDir
+	path := dir + "/" + DefaultConfigFileName
+	os.MkdirAll(dir, 0700)
+	err := c.Write(path)
+	if err != nil {
+		return fmt.Errorf("error writing to %q: %v", path, err)
+	}
+	return nil
+}
+
+func (c *Config) NewBackupCodes() error {
+	codes := map[string]bool{}
+
+	// 6 codes of 6 digits each.
+	for i := 0; i < 6; i++ {
+		n, err := rand.Int(rand.Reader, big.NewInt(1000000))
+		if err != nil {
+			return err
+		}
+		sn := fmt.Sprintf("%06d", n)
+		codes[sn] = true
+	}
+
+	c.BackupCodes = codes
+	return nil
+}
+
+func (c *Config) RegistrationValues() [][]byte {
+	var rs [][]byte
+	for _, r := range c.Registrations {
+		rs = append(rs, r)
+	}
+	return rs
+}
diff --git a/internal/client/grpc.go b/internal/client/grpc.go
new file mode 100644
index 0000000..cd02483
--- /dev/null
+++ b/internal/client/grpc.go
@@ -0,0 +1,204 @@
+package client
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/tstranex/u2f"
+	"golang.org/x/net/context"
+	"golang.org/x/oauth2"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/credentials/oauth"
+
+	pb "blitiri.com.ar/go/remoteu2f/internal/proto"
+)
+
+type RemoteU2FClient struct {
+	c pb.RemoteU2FClient
+}
+
+func GRPCClient(addr, token, caFile string) (*RemoteU2FClient, error) {
+	var err error
+	var tCreds credentials.TransportAuthenticator
+	if caFile == "" {
+		tCreds = credentials.NewClientTLSFromCert(nil, "")
+	} else {
+		tCreds, err = credentials.NewClientTLSFromFile(caFile, "")
+		if err != nil {
+			return nil, fmt.Errorf("error reading CA file: %s", err)
+		}
+	}
+
+	t := oauth2.Token{
+		AccessToken: token,
+		TokenType:   "Bearer",
+	}
+	rpcCreds := oauth.NewOauthAccess(&t)
+
+	conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(tCreds),
+		grpc.WithPerRPCCredentials(rpcCreds),
+		grpc.WithBlock(),
+		grpc.WithTimeout(30*time.Second))
+	if err != nil {
+		return nil, fmt.Errorf("error connecting to server: %s", err)
+	}
+
+	c := pb.NewRemoteU2FClient(conn)
+	return &RemoteU2FClient{c}, nil
+}
+
+func (c *RemoteU2FClient) GetAppID() (string, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	r, err := c.c.GetAppID(ctx, &pb.Void{})
+	if err != nil {
+		return "", err
+	}
+
+	return r.Url, nil
+}
+
+type PendingRegister struct {
+	Key       *pb.Url
+	challenge *u2f.Challenge
+}
+
+func (c *RemoteU2FClient) PrepareRegister(msg, appID string) (*PendingRegister, error) {
+	var trustedFacets = []string{appID}
+
+	challenge, err := u2f.NewChallenge(appID, trustedFacets)
+	if err != nil {
+		return nil, fmt.Errorf("u2f.NewChallenge error: %v", err)
+	}
+
+	j, err := json.Marshal(challenge.RegisterRequest())
+	if err != nil {
+		return nil, fmt.Errorf("json marshalling error: %v", err)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	key, err := c.c.PrepareOp(ctx, &pb.Prepare{j, msg, pb.Prepare_REGISTER})
+	if err != nil {
+		return nil, fmt.Errorf("error preparing: %v", err)
+	}
+
+	return &PendingRegister{key, challenge}, nil
+}
+
+func (c *RemoteU2FClient) CompleteRegister(p *PendingRegister) ([]byte, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+	defer cancel()
+
+	resp, err := c.c.GetOpResponse(ctx, p.Key)
+	if err != nil {
+		return nil, fmt.Errorf("error registering: %v", err)
+	}
+
+	var regResp u2f.RegisterResponse
+	if err := json.Unmarshal(resp.Json, &regResp); err != nil {
+		return nil, fmt.Errorf("invalid response: %s", err)
+	}
+
+	config := u2f.Config{
+		// Unfortunately we don't have the attestation certs of many keys,
+		// so skip the check for now.
+		SkipAttestationVerify: true,
+	}
+
+	reg, err := u2f.Register(regResp, *p.challenge, &config)
+	if err != nil {
+		return nil, fmt.Errorf("u2f.Register error: %v", err)
+	}
+
+	// We save the marshalled registration object, which we use later to get
+	// it back for authorization purposes.
+	return reg.MarshalBinary()
+}
+
+type PendingAuth struct {
+	Key *pb.Url
+
+	// Registrations we sent auth requests for.
+	regs []*u2f.Registration
+
+	// Challenges matching each registration.
+	challenges []*u2f.Challenge
+}
+
+func (c *RemoteU2FClient) PrepareAuthentication(msg, appID string, marshalledRegs [][]byte) (
+	*PendingAuth, error) {
+
+	var trustedFacets = []string{appID}
+
+	pa := &PendingAuth{}
+	signReqs := []*u2f.SignRequest{}
+
+	// Generate one signature request for each registration.
+	for _, mreg := range marshalledRegs {
+		reg := &u2f.Registration{}
+		err := reg.UnmarshalBinary(mreg)
+		if err != nil {
+			return nil, fmt.Errorf("u2f.ParseRegistration: %v\n", err)
+		}
+
+		// Can/should we reuse the challenge for all the registrations?
+		challenge, err := u2f.NewChallenge(appID, trustedFacets)
+		if err != nil {
+			return nil, fmt.Errorf("u2f.NewChallenge error: %v", err)
+		}
+
+		sr := challenge.SignRequest(*reg)
+		signReqs = append(signReqs, sr)
+
+		pa.challenges = append(pa.challenges, challenge)
+		pa.regs = append(pa.regs, reg)
+	}
+
+	j, err := json.Marshal(signReqs)
+	if err != nil {
+		return nil, fmt.Errorf("json marshalling error: %v", err)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	key, err := c.c.PrepareOp(
+		ctx, &pb.Prepare{j, msg, pb.Prepare_AUTHENTICATE})
+	if err != nil {
+		return nil, fmt.Errorf("error preparing: %v", err)
+	}
+
+	pa.Key = key
+
+	return pa, nil
+}
+
+func (c *RemoteU2FClient) CompleteAuthentication(pa *PendingAuth) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+	defer cancel()
+
+	resp, err := c.c.GetOpResponse(ctx, pa.Key)
+	if err != nil {
+		return fmt.Errorf("error authenticating: %v", err)
+	}
+
+	var signResp u2f.SignResponse
+	if err := json.Unmarshal(resp.Json, &signResp); err != nil {
+		return fmt.Errorf("invalid response: %s", err)
+	}
+
+	for i, reg := range pa.regs {
+		// TODO: support counters.
+		_, err = reg.Authenticate(signResp, *pa.challenges[i], 0)
+		if err == nil {
+			return nil
+		}
+	}
+
+	return fmt.Errorf("authenticate error: matching registration not found")
+}
diff --git a/internal/proto/dummy.go b/internal/proto/dummy.go
new file mode 100644
index 0000000..16249dd
--- /dev/null
+++ b/internal/proto/dummy.go
@@ -0,0 +1,4 @@
+package remoteu2f
+
+// Generate the protobuf+grpc service.
+//go:generate protoc --go_out=plugins=grpc:. remoteu2f.proto
diff --git a/internal/proto/remoteu2f.pb.go b/internal/proto/remoteu2f.pb.go
new file mode 100644
index 0000000..ade1d3b
--- /dev/null
+++ b/internal/proto/remoteu2f.pb.go
@@ -0,0 +1,215 @@
+// Code generated by protoc-gen-go.
+// source: remoteu2f.proto
+// DO NOT EDIT!
+
+/*
+Package remoteu2f is a generated protocol buffer package.
+
+It is generated from these files:
+	remoteu2f.proto
+
+It has these top-level messages:
+	Void
+	Url
+	Prepare
+	Response
+*/
+package remoteu2f
+
+import proto "github.com/golang/protobuf/proto"
+import fmt "fmt"
+import math "math"
+
+import (
+	context "golang.org/x/net/context"
+	grpc "google.golang.org/grpc"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// Request type, used only for selecting the web page template (no
+// functional behaviour difference from an RPC perspective).
+type Prepare_RType int32
+
+const (
+	Prepare_UNKNOWN      Prepare_RType = 0
+	Prepare_REGISTER     Prepare_RType = 1
+	Prepare_AUTHENTICATE Prepare_RType = 2
+)
+
+var Prepare_RType_name = map[int32]string{
+	0: "UNKNOWN",
+	1: "REGISTER",
+	2: "AUTHENTICATE",
+}
+var Prepare_RType_value = map[string]int32{
+	"UNKNOWN":      0,
+	"REGISTER":     1,
+	"AUTHENTICATE": 2,
+}
+
+func (x Prepare_RType) String() string {
+	return proto.EnumName(Prepare_RType_name, int32(x))
+}
+
+// Generic empty message, for RPCs that don't need one.
+type Void struct {
+}
+
+func (m *Void) Reset()         { *m = Void{} }
+func (m *Void) String() string { return proto.CompactTextString(m) }
+func (*Void) ProtoMessage()    {}
+
+// Generic message containing a request key and URL.
+type Url struct {
+	Key string `protobuf:"bytes,1,opt,name=key" json:"key,omitempty"`
+	Url string `protobuf:"bytes,2,opt,name=url" json:"url,omitempty"`
+}
+
+func (m *Url) Reset()         { *m = Url{} }
+func (m *Url) String() string { return proto.CompactTextString(m) }
+func (*Url) ProtoMessage()    {}
+
+// Prepare an operation.
+type Prepare struct {
+	// Generic json content to return.
+	Json []byte `protobuf:"bytes,1,opt,name=json,proto3" json:"json,omitempty"`
+	// User-readable message to show on the web page.
+	Msg   string        `protobuf:"bytes,2,opt,name=msg" json:"msg,omitempty"`
+	Rtype Prepare_RType `protobuf:"varint,3,opt,name=rtype,enum=remoteu2f.Prepare_RType" json:"rtype,omitempty"`
+}
+
+func (m *Prepare) Reset()         { *m = Prepare{} }
+func (m *Prepare) String() string { return proto.CompactTextString(m) }
+func (*Prepare) ProtoMessage()    {}
+
+// Operation response.
+type Response struct {
+	Json []byte `protobuf:"bytes,1,opt,name=json,proto3" json:"json,omitempty"`
+}
+
+func (m *Response) Reset()         { *m = Response{} }
+func (m *Response) String() string { return proto.CompactTextString(m) }
+func (*Response) ProtoMessage()    {}
+
+func init() {
+	proto.RegisterEnum("remoteu2f.Prepare_RType", Prepare_RType_name, Prepare_RType_value)
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConn
+
+// Client API for RemoteU2F service
+
+type RemoteU2FClient interface {
+	PrepareOp(ctx context.Context, in *Prepare, opts ...grpc.CallOption) (*Url, error)
+	GetOpResponse(ctx context.Context, in *Url, opts ...grpc.CallOption) (*Response, error)
+	GetAppID(ctx context.Context, in *Void, opts ...grpc.CallOption) (*Url, error)
+}
+
+type remoteU2FClient struct {
+	cc *grpc.ClientConn
+}
+
+func NewRemoteU2FClient(cc *grpc.ClientConn) RemoteU2FClient {
+	return &remoteU2FClient{cc}
+}
+
+func (c *remoteU2FClient) PrepareOp(ctx context.Context, in *Prepare, opts ...grpc.CallOption) (*Url, error) {
+	out := new(Url)
+	err := grpc.Invoke(ctx, "/remoteu2f.RemoteU2F/PrepareOp", in, out, c.cc, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *remoteU2FClient) GetOpResponse(ctx context.Context, in *Url, opts ...grpc.CallOption) (*Response, error) {
+	out := new(Response)
+	err := grpc.Invoke(ctx, "/remoteu2f.RemoteU2F/GetOpResponse", in, out, c.cc, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *remoteU2FClient) GetAppID(ctx context.Context, in *Void, opts ...grpc.CallOption) (*Url, error) {
+	out := new(Url)
+	err := grpc.Invoke(ctx, "/remoteu2f.RemoteU2F/GetAppID", in, out, c.cc, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// Server API for RemoteU2F service
+
+type RemoteU2FServer interface {
+	PrepareOp(context.Context, *Prepare) (*Url, error)
+	GetOpResponse(context.Context, *Url) (*Response, error)
+	GetAppID(context.Context, *Void) (*Url, error)
+}
+
+func RegisterRemoteU2FServer(s *grpc.Server, srv RemoteU2FServer) {
+	s.RegisterService(&_RemoteU2F_serviceDesc, srv)
+}
+
+func _RemoteU2F_PrepareOp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) {
+	in := new(Prepare)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	out, err := srv.(RemoteU2FServer).PrepareOp(ctx, in)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func _RemoteU2F_GetOpResponse_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) {
+	in := new(Url)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	out, err := srv.(RemoteU2FServer).GetOpResponse(ctx, in)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func _RemoteU2F_GetAppID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) {
+	in := new(Void)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	out, err := srv.(RemoteU2FServer).GetAppID(ctx, in)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+var _RemoteU2F_serviceDesc = grpc.ServiceDesc{
+	ServiceName: "remoteu2f.RemoteU2F",
+	HandlerType: (*RemoteU2FServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "PrepareOp",
+			Handler:    _RemoteU2F_PrepareOp_Handler,
+		},
+		{
+			MethodName: "GetOpResponse",
+			Handler:    _RemoteU2F_GetOpResponse_Handler,
+		},
+		{
+			MethodName: "GetAppID",
+			Handler:    _RemoteU2F_GetAppID_Handler,
+		},
+	},
+	Streams: []grpc.StreamDesc{},
+}
diff --git a/internal/proto/remoteu2f.proto b/internal/proto/remoteu2f.proto
new file mode 100644
index 0000000..16be073
--- /dev/null
+++ b/internal/proto/remoteu2f.proto
@@ -0,0 +1,44 @@
+
+syntax = "proto3";
+
+package remoteu2f;
+
+// Generic empty message, for RPCs that don't need one.
+message Void {
+}
+
+// Generic message containing a request key and URL.
+message Url {
+	string key = 1;
+	string url = 2;
+}
+
+// Prepare an operation.
+message Prepare {
+	// Generic json content to return.
+	bytes json = 1;
+
+	// User-readable message to show on the web page.
+	string msg = 2;
+
+	// Request type, used only for selecting the web page template (no
+	// functional behaviour difference from an RPC perspective).
+	enum RType {
+		UNKNOWN = 0;
+		REGISTER = 1;
+		AUTHENTICATE = 2;
+	}
+	RType rtype = 3;
+}
+
+// Operation response.
+message Response {
+	bytes json = 1;
+}
+
+
+service RemoteU2F {
+	rpc PrepareOp(Prepare) returns (Url);
+	rpc GetOpResponse(Url) returns (Response);
+	rpc GetAppID(Void) returns (Url);
+}
diff --git a/libpam/.clang-format b/libpam/.clang-format
new file mode 100644
index 0000000..9819398
--- /dev/null
+++ b/libpam/.clang-format
@@ -0,0 +1,7 @@
+BasedOnStyle: LLVM
+IndentWidth: 8
+UseTab: Always
+BreakBeforeBraces: Linux
+AllowShortIfStatementsOnASingleLine: false
+IndentCaseLabels: false
+
diff --git a/libpam/.gitignore b/libpam/.gitignore
new file mode 100644
index 0000000..7f56666
--- /dev/null
+++ b/libpam/.gitignore
@@ -0,0 +1,3 @@
+
+*.so
+
diff --git a/libpam/Makefile b/libpam/Makefile
new file mode 100644
index 0000000..4c24eb1
--- /dev/null
+++ b/libpam/Makefile
@@ -0,0 +1,17 @@
+
+all: pam_prompt_exec.so
+
+pam_prompt_exec.so: pam_prompt_exec.c
+	$(CC) $(CFLAGS) -std=c11 -Wall -fPIC -shared -lpam $< -o $@
+
+
+# This is just for convenience during development.
+# Unfortunately most distributions don't include a general binary and we have
+# to pick a version.
+fmt:
+	clang-format-3.7 -i pam_prompt_exec.c
+
+clean:
+	rm -f pam_prompt_exec.so
+
+.PHONY: all fmt clean
diff --git a/libpam/pam_prompt_exec.c b/libpam/pam_prompt_exec.c
new file mode 100644
index 0000000..c5014f7
--- /dev/null
+++ b/libpam/pam_prompt_exec.c
@@ -0,0 +1,453 @@
+// pam_prompt_exec is a PAM module which calls an external command, which
+// prompts the user for input.
+//
+// It is analogous to pam_exec(8), but allows the program to print a prompt
+// (by writing to stdout) and then reading input back (by reading from stdin).
+// The command is run as the PAM user.
+
+// We use features from POSIX 2008.
+#define _POSIX_C_SOURCE 200809L
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <syslog.h>
+#include <time.h>
+#include <unistd.h>
+
+#define PAM_SM_AUTH
+#define PAM_SM_ACCOUNT
+#define PAM_SM_SESSION
+#define PAM_SM_PASSWORD
+
+#include <security/pam_modules.h>
+#include <security/pam_modutil.h>
+#include <security/pam_ext.h>
+
+// Buffer size to use when reading stdout.
+// Note this is the maximum size we read, 4k should be plenty for our
+// interactive use.
+static const int BUFSIZE = 4 * 1024;
+
+// The exit status of the child if we have to exit before exec()ing.
+// Nothing special about 217, it's just easy to find.
+static const int CHILD_ERROR = 217;
+
+// Send text to PAM, get a response back.
+static struct pam_response *pam_talk(pam_handle_t *pamh, char *text)
+{
+	int rv;
+	struct pam_conv *conv;
+	rv = pam_get_item(pamh, PAM_CONV, (void *)&conv);
+	if (rv != PAM_SUCCESS) {
+		pam_syslog(pamh, LOG_ERR, "pam_get_item() failed: %d", rv);
+		return NULL;
+	}
+
+	const struct pam_message msg = {
+	    .msg_style = PAM_PROMPT_ECHO_ON, .msg = text,
+	};
+	const struct pam_message *pmsg = &msg;
+
+	struct pam_response *resp = NULL;
+	rv = conv->conv(1, &pmsg, &resp, conv->appdata_ptr);
+	if (rv != PAM_SUCCESS) {
+		pam_syslog(pamh, LOG_ERR, "conv->conv() failed: %d", rv);
+		return NULL;
+	}
+
+	return resp;
+}
+
+// Move the given fd to an fd >= 3; exit if it fails.
+static int move_to_high_fd(pam_handle_t *pamh, int fd)
+{
+	while (fd < 3) {
+		fd = dup(fd);
+		if (fd < 0) {
+			pam_syslog(pamh, LOG_ERR, "dup() failed: %s",
+				   strerror(errno));
+			_exit(CHILD_ERROR);
+		}
+	}
+
+	return fd;
+}
+
+// Build a "NAME=VALUE" string to use in the environment list.
+static char *build_env_value(pam_handle_t *pamh, const char *name,
+			     const char *value)
+{
+	// We'll return "name=value\0"
+	// 3 extra bytes to account for the =, \0 and one more just to be
+	// extra cautious.
+	size_t bsize = strlen(name) + strlen(value) + 3;
+	char *buf = calloc(bsize, 1);
+	if (buf == NULL) {
+		pam_syslog(pamh, LOG_ERR, "calloc(env item) failed: %s",
+			   strerror(errno));
+		_exit(CHILD_ERROR);
+	}
+	snprintf(buf, bsize, "%s=%s", name, value);
+	return buf;
+}
+
+// Build the environment for the child, or die trying.
+static char **build_child_env(pam_handle_t *pamh, const char *pam_type)
+{
+	char **env = pam_getenvlist(pamh);
+	if (env == NULL) {
+		pam_syslog(pamh, LOG_ERR, "pam_getenvlist() failed: %s",
+			   strerror(errno));
+		_exit(CHILD_ERROR);
+	}
+
+	// Find how many elements are in env.
+	int envlen = 0;
+	for (envlen = 0; env[envlen] != NULL; envlen++)
+		;
+
+	// Variables to copy.
+	struct {
+		int item;
+		const char *name;
+	} items[] = {
+	    {PAM_SERVICE, "PAM_SERVICE"}, {PAM_USER, "PAM_USER"},
+	    {PAM_TTY, "PAM_TTY"},	 {PAM_RHOST, "PAM_RHOST"},
+	    {PAM_RUSER, "PAM_RUSER"},
+	};
+	const int nitems = 5;
+
+	// Realloc to account for nitems + PAM_TYPE (below) + NULL.
+	env = realloc(env, (envlen + nitems + 2) * sizeof(char *));
+	if (env == NULL) {
+		pam_syslog(pamh, LOG_ERR, "realloc() failed: %s",
+			   strerror(errno));
+		_exit(CHILD_ERROR);
+	}
+
+	// Add the items to the environment.
+	for (int i = 0; i < nitems; i++) {
+		const void *item;
+
+		// Skip items that are not found.
+		if (pam_get_item(pamh, items[i].item, &item) != PAM_SUCCESS ||
+		    item == NULL) {
+			continue;
+		}
+
+		// Add it to the environment.
+		env[envlen] =
+		    build_env_value(pamh, items[i].name, (const char *)item);
+		envlen++;
+		env[envlen] = NULL;
+	}
+
+	// And PAM_TYPE to the type we know.
+	env[envlen] = build_env_value(pamh, "PAM_TYPE", pam_type);
+	envlen++;
+	env[envlen] = NULL;
+
+	return env;
+}
+
+// Drop privileges to the user we are validating.
+// PAM modules usually (but not always) run as root, this will drop privileges
+// to that user.
+// It is only called during the child process initialization.
+static void drop_privileges(pam_handle_t *pamh)
+{
+	// Get the user information.
+	const char *user = NULL;
+	int rv = pam_get_user(pamh, &user, NULL);
+	if (rv != PAM_SUCCESS || user == NULL) {
+		pam_syslog(pamh, LOG_ERR, "could not get PAM user: %d", rv);
+		_exit(CHILD_ERROR);
+	}
+
+	struct passwd *pwd = getpwnam(user);
+	if (pwd == NULL) {
+		pam_syslog(pamh, LOG_ERR, "could not get user info: %s",
+			   strerror(errno));
+		_exit(CHILD_ERROR);
+	}
+
+	// Change the group.
+	uid_t old_gid = getegid();
+	if (old_gid != pwd->pw_gid && setegid(pwd->pw_gid)) {
+		pam_syslog(pamh, LOG_ERR, "error in setegid(): %s",
+			   strerror(errno));
+		_exit(CHILD_ERROR);
+	}
+
+	// Change the user.
+	uid_t old_uid = geteuid();
+	if (old_uid != pwd->pw_uid && seteuid(pwd->pw_uid)) {
+		pam_syslog(pamh, LOG_ERR, "error in seteuid(): %s",
+			   strerror(errno));
+		_exit(CHILD_ERROR);
+	}
+}
+
+// Like read() but either fails or returns a complete read.
+static ssize_t full_read(int fd, void *buf, size_t count)
+{
+	ssize_t rv;
+	size_t c = 0;
+
+	while (c < count) {
+		rv = read(fd, (char *)buf + c, count - c);
+		if (rv < 0) {
+			return rv;
+		} else if (rv == 0) {
+			return c;
+		}
+
+		c += rv;
+	}
+
+	return count;
+}
+
+// Like write() but either fails or returns a complete write.
+static ssize_t full_write(int fd, const void *buf, size_t count)
+{
+	ssize_t rv;
+	size_t c = 0;
+
+	while (c < count) {
+		rv = write(fd, (char *)buf + c, count - c);
+		if (rv < 0)
+			return rv;
+
+		c += rv;
+	}
+
+	return count;
+}
+
+static int prompt_exec(const char *pam_type, pam_handle_t *pamh, int argc,
+		       const char **argv)
+{
+	int binary_path_pos = -1;
+	for (int i = 0; i < argc; i++) {
+		// Stop if we got to the binary path.
+		if (argv[i][0] == '/') {
+			binary_path_pos = i;
+			break;
+		}
+
+		if (strncmp(argv[i], "type=", 5) == 0) {
+			// Ignore if we are not invoked with the expected
+			// pam type.
+			if (strcmp(pam_type, &argv[i][5]) != 0) {
+				return PAM_IGNORE;
+			}
+		}
+	}
+
+	if (binary_path_pos == -1) {
+		pam_syslog(pamh, LOG_ERR, "No program to exec");
+		return PAM_SERVICE_ERR;
+	}
+
+	// Set up stdin pipe.
+	int stdin_fds[2] = {-1, -1};
+	if (pipe(stdin_fds) != 0) {
+		pam_syslog(pamh, LOG_ERR, "Could not create stdin pipe: %s",
+			   strerror(errno));
+		return PAM_SYSTEM_ERR;
+	}
+
+	// Set up stdout pipe.
+	int stdout_fds[2] = {-1, -1};
+	if (pipe(stdout_fds) != 0) {
+		pam_syslog(pamh, LOG_ERR, "Could not create stdout pipe: %s",
+			   strerror(errno));
+		return PAM_SYSTEM_ERR;
+	}
+
+	pid_t pid = fork();
+	if (pid == -1) {
+		pam_syslog(pamh, LOG_ERR, "Could not fork(): %s",
+			   strerror(errno));
+		return PAM_SYSTEM_ERR;
+	} else if (pid > 0) { // Parent.
+		// Close the fds we don't use.
+		close(stdin_fds[0]);
+		close(stdout_fds[1]);
+
+		// Read the prompt from stdout.
+		char buf[BUFSIZE];
+		int buflen = -1;
+		memset(buf, 0, BUFSIZE);
+
+		buflen = full_read(stdout_fds[0], buf, BUFSIZE - 1);
+		if (buflen < 0) {
+			pam_syslog(pamh, LOG_ERR, "Could not from stdout: %s",
+				   strerror(errno));
+			return PAM_SYSTEM_ERR;
+		}
+		close(stdout_fds[0]);
+
+		// Talk to the user, but only if there's a prompt.
+		if (buflen > 0) {
+			// Converse via PAM.
+			struct pam_response *response = pam_talk(pamh, buf);
+			if (response == NULL) {
+				return PAM_SYSTEM_ERR;
+			}
+
+			// Send response back via stdin.
+			int rlen = strlen(response->resp);
+			int rv = full_write(stdin_fds[1], response->resp, rlen);
+			if (rv != rlen) {
+				// Note this is just informational: the child
+				// may not care about this and may have even
+				// closed it.
+				pam_syslog(pamh, LOG_NOTICE,
+					   "Could not write to stdin: %s",
+					   strerror(errno));
+			}
+
+			close(stdin_fds[1]);
+
+			free(response->resp);
+			free(response);
+		}
+
+		// Wait for the program to die.
+		pid_t rv;
+		int status = 0;
+		while ((rv = waitpid(pid, &status, 0)) == -1 && errno == EINTR)
+			;
+		if (rv != pid) {
+			pam_syslog(pamh, LOG_ERR, "waitpid() failed: %s",
+				   strerror(errno));
+			return PAM_SYSTEM_ERR;
+		}
+
+		const char *binary = argv[binary_path_pos];
+
+		if (status == 0) {
+			return PAM_SUCCESS;
+		} else if (WIFEXITED(status)) {
+			pam_error(pamh, "%s failed, exit code: %d", binary,
+				  WEXITSTATUS(status));
+			return PAM_SYSTEM_ERR;
+		} else if (WIFSIGNALED(status)) {
+			pam_error(pamh, "%s failed, signal: %d", binary,
+				  WTERMSIG(status));
+			return PAM_SYSTEM_ERR;
+		} else {
+			pam_error(pamh, "%s failed, unknown status: 0x%x",
+				  binary, status);
+			return PAM_SYSTEM_ERR;
+		}
+	} else { // Child.
+		// Close the fds we don't use.
+		close(stdin_fds[1]);
+		close(stdout_fds[0]);
+
+		// Move stdin and stdout pipes to high file descriptors, so we
+		// can dup() them safely later.
+		int stdin_fd = move_to_high_fd(pamh, stdin_fds[0]);
+		int stdout_fd = move_to_high_fd(pamh, stdout_fds[1]);
+
+		// Now move stdin and stdout to the canonical fds.
+		if (dup2(stdin_fd, STDIN_FILENO) == -1) {
+			pam_syslog(pamh, LOG_ERR, "dup2(stdin) failed: %s",
+				   strerror(errno));
+			_exit(CHILD_ERROR);
+		}
+		if (dup2(stdout_fd, STDOUT_FILENO) == -1) {
+			pam_syslog(pamh, LOG_ERR, "dup2(stdout) failed: %s",
+				   strerror(errno));
+			_exit(CHILD_ERROR);
+		}
+
+		// Close unused fds, just in case.
+		for (int i = 3; i < sysconf(_SC_OPEN_MAX); i++) {
+			close(i);
+		}
+
+		// Run in a new session, just in case.
+		if (setsid() == -1) {
+			pam_syslog(pamh, LOG_ERR, "setsid() failed: %s",
+				   strerror(errno));
+			_exit(CHILD_ERROR);
+		}
+
+		// Drop privileges by changing to the new user.
+		drop_privileges(pamh);
+
+		// Set up the environment, with the PAM environment + the
+		// variables with the PAM information to pass on.
+		char **child_env = build_child_env(pamh, pam_type);
+
+		// Set up the child's argv.
+		int child_argc = argc - binary_path_pos + 2;
+		char **child_argv = calloc(child_argc, sizeof(char *));
+		if (child_argv == NULL) {
+			pam_syslog(pamh, LOG_ERR, "calloc(argc) failed: %s",
+				   strerror(errno));
+			_exit(CHILD_ERROR);
+		}
+
+		int i, j;
+		for (i = binary_path_pos, j = 0; i < argc; i++, j++) {
+			child_argv[j] = strdup(argv[i]);
+		}
+		child_argv[j] = NULL;
+
+		// Exec!
+		execve(child_argv[0], child_argv, child_env);
+		pam_syslog(pamh, LOG_ERR, "execve(%s) failed: %s",
+			   strerror(errno), child_argv[0]);
+		_exit(CHILD_ERROR);
+	}
+}
+
+/*
+ * PAM functions for auth, session and account.
+ */
+
+int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc,
+			const char **argv)
+{
+	return prompt_exec("auth", pamh, argc, argv);
+}
+
+int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+	return PAM_IGNORE;
+}
+
+int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+	if (flags & PAM_PRELIM_CHECK)
+		return PAM_SUCCESS;
+	return prompt_exec("password", pamh, argc, argv);
+}
+
+int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv)
+{
+	return prompt_exec("account", pamh, argc, argv);
+}
+
+int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc,
+			const char **argv)
+{
+	return prompt_exec("open_session", pamh, argc, argv);
+}
+
+int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc,
+			 const char **argv)
+{
+	return prompt_exec("close_session", pamh, argc, argv);
+}
diff --git a/remoteu2f-cli/main.go b/remoteu2f-cli/main.go
new file mode 100644
index 0000000..d66123c
--- /dev/null
+++ b/remoteu2f-cli/main.go
@@ -0,0 +1,367 @@
+// remoteu2f command-line interface
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"os"
+	"os/user"
+	"sort"
+	"strings"
+
+	"github.com/codegangsta/cli"
+
+	"blitiri.com.ar/go/remoteu2f/internal/client"
+)
+
+var stdinScanner *bufio.Scanner
+
+// readLine reads a line from os.Stdin and returns it.
+// It exits the process on errors.
+func readLine() string {
+	if !stdinScanner.Scan() && stdinScanner.Err() != nil {
+		fmt.Printf("Error reading from stdin: %v\n", stdinScanner.Err())
+		os.Exit(1)
+	}
+	return strings.TrimSpace(stdinScanner.Text())
+}
+
+func main() {
+	app := cli.NewApp()
+	app.Name = "remoteu2f-cli"
+	app.Usage = "remoteu2f command line tool"
+	app.Flags = []cli.Flag{
+		cli.StringFlag{
+			Name:  "ca_file",
+			Usage: "path to the CA file (default: use system's)",
+		},
+	}
+	app.Commands = []cli.Command{
+		{
+			Name:   "init",
+			Usage:  "Create initial configuration (interactive)",
+			Action: Init,
+			Flags: []cli.Flag{
+				cli.BoolFlag{
+					Name:  "override",
+					Usage: "override the configuration if it exists",
+				},
+			},
+		},
+		{
+			Name:   "register",
+			Usage:  "Register a new security key",
+			Action: Register,
+		},
+		{
+			Name:   "auth",
+			Usage:  "Perform a test authentication",
+			Action: Authenticate,
+		},
+		{
+			Name:   "new_backup_codes",
+			Usage:  "Generate new backup codes, remove the old ones",
+			Action: NewBackupCodes,
+		},
+		{
+			Name:   "print_config",
+			Usage:  "Print the config (useful for debugging)",
+			Action: PrintConfig,
+		},
+		{
+			Name:   "pam",
+			Usage:  "Perform an authentication for PAM",
+			Action: PAM,
+			Flags: []cli.Flag{
+				cli.BoolFlag{
+					Name:  "nullok",
+					Usage: "return success if there is no configuration",
+				},
+			},
+		},
+	}
+
+	// Initialize the stdin scanner so the subcommands can use it.
+	stdinScanner = bufio.NewScanner(os.Stdin)
+
+	app.RunAndExitOnError()
+}
+
+func fatalf(format string, a ...interface{}) {
+	fmt.Printf(format, a...)
+	os.Exit(1)
+}
+
+func mustReadConfig() *client.Config {
+	conf, err := client.ReadDefaultConfig("")
+	if err != nil {
+		fatalf("Error reading config: %v\n", err)
+	}
+
+	return conf
+}
+
+func mustWriteConfig(c *client.Config, homedir string) {
+	err := c.WriteToDefaultPath(homedir)
+	if err != nil {
+		fatalf("Error writing config: %v\n", err)
+	}
+}
+
+func mustGRPCClient(addr, token, caFile string) *client.RemoteU2FClient {
+	c, err := client.GRPCClient(addr, token, caFile)
+	if err != nil {
+		fatalf("Error connecting with the server: %v\n", err)
+	}
+
+	return c
+}
+
+func mustUserInfo() (string, string) {
+	user, err := user.Current()
+	if err != nil {
+		fatalf("error getting current user: %v", err)
+	}
+
+	hostname, err := os.Hostname()
+	if err != nil {
+		fatalf("error getting hostname: %v", err)
+	}
+
+	return user.Username, hostname
+}
+
+func mustLookupHomeDir(username string) string {
+	info, err := user.Lookup(username)
+	if err != nil {
+		fatalf("Could not find $HOME for user: %v\n", err)
+	}
+
+	return info.HomeDir
+}
+
+func printBackupCodes(conf *client.Config) {
+	// Sort the codes so we get stable and more friendly output.
+	var codes []string
+	for c := range conf.BackupCodes {
+		codes = append(codes, c)
+	}
+	sort.Strings(codes)
+
+	for _, c := range codes {
+		fmt.Printf("  %v\n", c)
+	}
+}
+
+func printRegistrations(conf *client.Config) {
+	// Sort the descriptions so we get stable and more friendly output.
+	var ds []string
+	for d := range conf.Registrations {
+		ds = append(ds, d)
+	}
+	sort.Strings(ds)
+
+	for _, d := range ds {
+		fmt.Printf("  %q\n", d)
+	}
+}
+
+func Init(ctx *cli.Context) {
+	if !ctx.Bool("override") {
+		// We don't want to accidentally override the config.
+		_, err := client.ReadDefaultConfig("")
+		if err == nil {
+			fmt.Printf("Configuration already exists at %s\n",
+				client.DefaultConfigFullPath(""))
+			fmt.Printf("Use --override to continue anyway.\n")
+			os.Exit(1)
+		}
+	}
+
+	fmt.Printf("- GRPC server address to use? (e.g. 'mydomain.com:8801')\n")
+	addr := readLine()
+	fmt.Printf("- Authorization token? (given to you by the server admin)\n")
+	token := readLine()
+
+	fmt.Printf("- Contacting server...\n")
+	c, err := client.GRPCClient(addr, token, ctx.GlobalString("ca_file"))
+	if err != nil {
+		fmt.Printf("Error connecting with the server: %v\n", err)
+		fmt.Printf("Check the parameters above and try again.\n")
+		os.Exit(1)
+	}
+
+	appID, err := c.GetAppID()
+	if err != nil {
+		fmt.Printf("RPC error: %v\n", err)
+		fmt.Printf("Check the parameters above and try again.\n")
+		os.Exit(1)
+	}
+
+	fmt.Printf("It worked!  AppID: %s\n", appID)
+
+	conf := &client.Config{
+		Addr:          addr,
+		Token:         token,
+		AppID:         appID,
+		Registrations: map[string][]byte{},
+	}
+
+	err = conf.NewBackupCodes()
+	if err != nil {
+		fatalf("Error generating backup codes: %v\n", err)
+	}
+
+	mustWriteConfig(conf, "")
+	fmt.Printf("Config written to %s\n", client.DefaultConfigFullPath(""))
+
+	fmt.Printf("\n")
+	fmt.Printf("Please write down your backup codes:\n")
+	printBackupCodes(conf)
+
+	fmt.Printf("\n")
+	fmt.Printf("All done!\n")
+	fmt.Printf("To register a security key, run:  remoteu2f-cli register\n")
+}
+
+func PrintConfig(ctx *cli.Context) {
+	conf := mustReadConfig()
+	fmt.Printf("GRPC address:   %s\n", conf.Addr)
+	fmt.Printf("Client token:   %s\n", conf.Token)
+	fmt.Printf("Application ID: %s\n", conf.AppID)
+
+	fmt.Printf("Registered keys:\n")
+	printRegistrations(conf)
+
+	fmt.Printf("Backup codes:\n")
+	printBackupCodes(conf)
+}
+
+func Register(ctx *cli.Context) {
+	conf := mustReadConfig()
+	c := mustGRPCClient(conf.Addr, conf.Token, ctx.GlobalString("ca_file"))
+
+	user, hostname := mustUserInfo()
+	msg := fmt.Sprintf("%s@%s", user, hostname)
+
+	pr, err := c.PrepareRegister(msg, conf.AppID)
+	if err != nil {
+		fatalf("Error preparing registration: %v\n", err)
+	}
+	fmt.Printf("Go to:  %s\n", pr.Key.Url)
+
+	reg, err := c.CompleteRegister(pr)
+	if err != nil {
+		fatalf("Error completing registration: %v\n", err)
+	}
+
+	fmt.Printf("Description for this security key:\n")
+	desc := readLine()
+
+	if conf.Registrations == nil {
+		conf.Registrations = map[string][]byte{}
+	}
+	conf.Registrations[desc] = reg
+
+	mustWriteConfig(conf, "")
+	fmt.Println("Success, registration written to config")
+}
+
+func Authenticate(ctx *cli.Context) {
+	conf := mustReadConfig()
+	c := mustGRPCClient(conf.Addr, conf.Token, ctx.GlobalString("ca_file"))
+
+	user, hostname := mustUserInfo()
+	msg := fmt.Sprintf("%s@%s", user, hostname)
+
+	if len(conf.Registrations) == 0 {
+		fmt.Printf("Error: no registrations found\n")
+		fatalf("To register a security key, run:  remoteu2f-cli register\n")
+	}
+
+	pa, err := c.PrepareAuthentication(
+		msg, conf.AppID, conf.RegistrationValues())
+	if err != nil {
+		fatalf("Error preparing authentication: %v\n", err)
+	}
+	fmt.Printf("Go to:  %s\n", pa.Key.Url)
+
+	err = c.CompleteAuthentication(pa)
+	if err != nil {
+		fatalf("Error completing authentication: %v\n", err)
+	}
+
+	fmt.Println("Authentication succeeded")
+}
+
+func NewBackupCodes(ctx *cli.Context) {
+	conf := mustReadConfig()
+	err := conf.NewBackupCodes()
+	if err != nil {
+		fatalf("Error generating new backup codes: %v\n", err)
+	}
+
+	mustWriteConfig(conf, "")
+
+	fmt.Printf("New backup codes:\n")
+	for s, _ := range conf.BackupCodes {
+		fmt.Printf("  %v\n", s)
+	}
+}
+
+func PAM(ctx *cli.Context) {
+	// We need to find the user's home first.
+	username := os.Getenv("PAM_USER")
+	homedir := mustLookupHomeDir(username)
+
+	nullok := ctx.Bool("nullok")
+	conf, err := client.ReadDefaultConfig(homedir)
+	if err != nil {
+		if nullok {
+			os.Exit(0)
+		} else {
+			fatalf("Error reading config: %v\n", err)
+		}
+	}
+
+	if len(conf.Registrations) == 0 {
+		if nullok {
+			os.Exit(0)
+		} else {
+			fatalf("Error: no registrations found\n")
+		}
+	}
+
+	c := mustGRPCClient(conf.Addr, conf.Token, ctx.GlobalString("ca_file"))
+
+	user, hostname := mustUserInfo()
+	msg := fmt.Sprintf("%s@%s", user, hostname)
+
+	pa, err := c.PrepareAuthentication(
+		msg, conf.AppID, conf.RegistrationValues())
+	if err != nil {
+		fatalf("Error preparing authentication: %v\n", err)
+	}
+
+	fmt.Printf("Authenticate here and press enter:  %s\n", pa.Key.Url)
+
+	// Closing stdout makes pam_prompt_exec send the prompt over.
+	os.Stdout.Close()
+
+	// Read input, and check if it's a backup code.
+	// Never take a backup code of less than 6 characters, just in case some
+	// data handling error makes them appear in conf.BackupCodes.
+	input := readLine()
+	if _, ok := conf.BackupCodes[input]; len(input) >= 6 && ok {
+		delete(conf.BackupCodes, input)
+		mustWriteConfig(conf, homedir)
+		os.Exit(0)
+	}
+
+	err = c.CompleteAuthentication(pa)
+	if err != nil {
+		fatalf("Error completing authentication: %v\n", err)
+	}
+
+	os.Exit(0)
+}
diff --git a/remoteu2f-proxy/embedded_data.go b/remoteu2f-proxy/embedded_data.go
new file mode 100644
index 0000000..5c9b3ae
--- /dev/null
+++ b/remoteu2f-proxy/embedded_data.go
@@ -0,0 +1,770 @@
+
+package main
+
+// File auto-generated by embed.go.
+
+
+// to_embed/authenticate.html ----- 8< ----- 8< ----- 8< ----- 8< -----
+
+// authenticate_html contains the content of to_embed/authenticate.html.
+const authenticate_html = `<!DOCTYPE html>
+<html>
+  <head>
+  </head>
+
+  <body>
+    <h1>remoteu2f - authenticate</h1>
+
+    <h2>{{.Message}}</h2>
+
+    <p class="instructions">
+    Please insert/touch your security key to authenticate.
+    </p>
+
+    <p class="status">
+    Status: <span id="status">waiting for security key</span>
+    </p>
+
+    <script
+        type="text/javascript"
+        src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"
+        integrity="sha384-8gBf6Y4YYq7Jx97PIqmTwLPin4hxIzQw5aDmUg/DDhul9fFpbbLcLh3nTIIDJKhx"
+        crossorigin="anonymous"></script>
+    <script type="text/javascript" src="remoteu2f.js"></script>
+    <script type="text/javascript" src="u2f_api.js"></script>
+    <script>
+      u2f.sign({{.Request}}, handleKeyResponse, 2*60);
+    </script>
+
+  </body>
+</html>
+`
+
+
+// to_embed/register.html ----- 8< ----- 8< ----- 8< ----- 8< -----
+
+// register_html contains the content of to_embed/register.html.
+const register_html = `<!DOCTYPE html>
+<html>
+  <head>
+  </head>
+
+  <body>
+    <h1>remoteu2f - register</h1>
+
+    <h2>{{.Message}}</h2>
+
+    <p class="instructions">
+    Please insert/touch your security key to complete the registration.
+    </p>
+
+    <p class="status">
+    Status: <span id="status">waiting for security key</span>
+    </p>
+
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"
+            integrity="sha384-8gBf6Y4YYq7Jx97PIqmTwLPin4hxIzQw5aDmUg/DDhul9fFpbbLcLh3nTIIDJKhx"
+            crossorigin="anonymous"></script>
+    <script type="text/javascript" src="remoteu2f.js"></script>
+    <script type="text/javascript" src="u2f_api.js"></script>
+    <script>
+      u2f.register([{{.Request}}], [], handleKeyResponse, 2*60)
+    </script>
+
+  </body>
+</html>
+`
+
+
+// to_embed/remoteu2f.js ----- 8< ----- 8< ----- 8< ----- 8< -----
+
+// remoteu2f_js contains the content of to_embed/remoteu2f.js.
+const remoteu2f_js = `
+// This function gets called when the security key gives us a response to our
+// register/sign request.
+function handleKeyResponse(resp) {
+    if (resp.errorCode != undefined && resp.errorCode != u2f.ErrorCodes.OK) {
+        codeToText = {
+            0: "OK",
+            1: "General error (hardware issues?)",
+            2: "Bad request (please report)",
+            3: "Unsupported configuration (please report)",
+            4: "Device ineligible, did you forget to register it?",
+            5: "Timed out waiting for security key",
+        }
+
+        $('#status').text(
+                codeToText[resp.errorCode] + " -- "
+                + resp.errorMessage);
+        return;
+    }
+
+    $('#status').text('sending response');
+    $.post('response', JSON.stringify(resp)).done(function() {
+        $('#status').text('done');
+    });
+}
+`
+
+
+// to_embed/u2f_api.js ----- 8< ----- 8< ----- 8< ----- 8< -----
+
+// u2f_api_js contains the content of to_embed/u2f_api.js.
+const u2f_api_js = `// Copyright 2014-2015 Google Inc. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// remoteu2f note: Obtained from the reference code at
+// https://github.com/google/u2f-ref-code/
+
+/**
+ * @fileoverview The U2F api.
+ */
+
+'use strict';
+
+/** Namespace for the U2F api.
+ * @type {Object}
+ */
+var u2f = u2f || {};
+
+/**
+ * The U2F extension id
+ * @type {string}
+ * @const
+ */
+u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
+
+/**
+ * Message types for messsages to/from the extension
+ * @const
+ * @enum {string}
+ */
+u2f.MessageTypes = {
+  'U2F_REGISTER_REQUEST': 'u2f_register_request',
+  'U2F_SIGN_REQUEST': 'u2f_sign_request',
+  'U2F_REGISTER_RESPONSE': 'u2f_register_response',
+  'U2F_SIGN_RESPONSE': 'u2f_sign_response'
+};
+
+/**
+ * Response status codes
+ * @const
+ * @enum {number}
+ */
+u2f.ErrorCodes = {
+  'OK': 0,
+  'OTHER_ERROR': 1,
+  'BAD_REQUEST': 2,
+  'CONFIGURATION_UNSUPPORTED': 3,
+  'DEVICE_INELIGIBLE': 4,
+  'TIMEOUT': 5
+};
+
+/**
+ * A message type for registration requests
+ * @typedef {{
+ *   type: u2f.MessageTypes,
+ *   signRequests: Array<u2f.SignRequest>,
+ *   registerRequests: ?Array<u2f.RegisterRequest>,
+ *   timeoutSeconds: ?number,
+ *   requestId: ?number
+ * }}
+ */
+u2f.Request;
+
+/**
+ * A message for registration responses
+ * @typedef {{
+ *   type: u2f.MessageTypes,
+ *   responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
+ *   requestId: ?number
+ * }}
+ */
+u2f.Response;
+
+/**
+ * An error object for responses
+ * @typedef {{
+ *   errorCode: u2f.ErrorCodes,
+ *   errorMessage: ?string
+ * }}
+ */
+u2f.Error;
+
+/**
+ * Data object for a single sign request.
+ * @typedef {{
+ *   version: string,
+ *   challenge: string,
+ *   keyHandle: string,
+ *   appId: string
+ * }}
+ */
+u2f.SignRequest;
+
+/**
+ * Data object for a sign response.
+ * @typedef {{
+ *   keyHandle: string,
+ *   signatureData: string,
+ *   clientData: string
+ * }}
+ */
+u2f.SignResponse;
+
+/**
+ * Data object for a registration request.
+ * @typedef {{
+ *   version: string,
+ *   challenge: string,
+ *   appId: string
+ * }}
+ */
+u2f.RegisterRequest;
+
+/**
+ * Data object for a registration response.
+ * @typedef {{
+ *   registrationData: string,
+ *   clientData: string
+ * }}
+ */
+u2f.RegisterResponse;
+
+
+// Low level MessagePort API support
+
+/**
+ * Sets up a MessagePort to the U2F extension using the
+ * available mechanisms.
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
+ */
+u2f.getMessagePort = function(callback) {
+  if (typeof chrome != 'undefined' && chrome.runtime) {
+    // The actual message here does not matter, but we need to get a reply
+    // for the callback to run. Thus, send an empty signature request
+    // in order to get a failure response.
+    var msg = {
+      type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+      signRequests: []
+    };
+    chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
+      if (!chrome.runtime.lastError) {
+        // We are on a whitelisted origin and can talk directly
+        // with the extension.
+        u2f.getChromeRuntimePort_(callback);
+      } else {
+        // chrome.runtime was available, but we couldn't message
+        // the extension directly, use iframe
+        u2f.getIframePort_(callback);
+      }
+    });
+  } else if (u2f.isAndroidChrome_()) {
+    u2f.getAuthenticatorPort_(callback);
+  } else {
+    // chrome.runtime was not available at all, which is normal
+    // when this origin doesn't have access to any extensions.
+    u2f.getIframePort_(callback);
+  }
+};
+
+/**
+ * Detect chrome running on android based on the browser's useragent.
+ * @private
+ */
+u2f.isAndroidChrome_ = function() {
+  var userAgent = navigator.userAgent;
+  return userAgent.indexOf('Chrome') != -1 &&
+      userAgent.indexOf('Android') != -1;
+};
+
+/**
+ * Connects directly to the extension via chrome.runtime.connect
+ * @param {function(u2f.WrappedChromeRuntimePort_)} callback
+ * @private
+ */
+u2f.getChromeRuntimePort_ = function(callback) {
+  var port = chrome.runtime.connect(u2f.EXTENSION_ID,
+    {'includeTlsChannelId': true});
+  setTimeout(function() {
+    callback(new u2f.WrappedChromeRuntimePort_(port));
+  }, 0);
+};
+
+/**
+ * Return a 'port' abstraction to the Authenticator app.
+ * @param {function(u2f.WrappedAuthenticatorPort_)} callback
+ * @private
+ */
+u2f.getAuthenticatorPort_ = function(callback) {
+  setTimeout(function() {
+    callback(new u2f.WrappedAuthenticatorPort_());
+  }, 0);
+};
+
+/**
+ * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
+ * @param {Port} port
+ * @constructor
+ * @private
+ */
+u2f.WrappedChromeRuntimePort_ = function(port) {
+  this.port_ = port;
+};
+
+/**
+ * Format a return a sign request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ =
+    function(signRequests, timeoutSeconds, reqId) {
+  return {
+    type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+    signRequests: signRequests,
+    timeoutSeconds: timeoutSeconds,
+    requestId: reqId
+  };
+};
+
+/**
+ * Format a return a register request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {Array<u2f.RegisterRequest>} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ =
+    function(signRequests, registerRequests, timeoutSeconds, reqId) {
+  return {
+    type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
+    signRequests: signRequests,
+    registerRequests: registerRequests,
+    timeoutSeconds: timeoutSeconds,
+    requestId: reqId
+  };
+};
+
+/**
+ * Posts a message on the underlying channel.
+ * @param {Object} message
+ */
+u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
+  this.port_.postMessage(message);
+};
+
+/**
+ * Emulates the HTML 5 addEventListener interface. Works only for the
+ * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
+    function(eventName, handler) {
+  var name = eventName.toLowerCase();
+  if (name == 'message' || name == 'onmessage') {
+    this.port_.onMessage.addListener(function(message) {
+      // Emulate a minimal MessageEvent object
+      handler({'data': message});
+    });
+  } else {
+    console.error('WrappedChromeRuntimePort only supports onMessage');
+  }
+};
+
+/**
+ * Wrap the Authenticator app with a MessagePort interface.
+ * @constructor
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_ = function() {
+  this.requestId_ = -1;
+  this.requestObject_ = null;
+}
+
+/**
+ * Launch the Authenticator intent.
+ * @param {Object} message
+ */
+u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
+  var intentLocation = /** @type {string} */ (message);
+  document.location = intentLocation;
+};
+
+/**
+ * Emulates the HTML 5 addEventListener interface.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedAuthenticatorPort_.prototype.addEventListener =
+    function(eventName, handler) {
+  var name = eventName.toLowerCase();
+  if (name == 'message') {
+    var self = this;
+    /* Register a callback to that executes when
+     * chrome injects the response. */
+    window.addEventListener(
+        'message', self.onRequestUpdate_.bind(self, handler), false);
+  } else {
+    console.error('WrappedAuthenticatorPort only supports message');
+  }
+};
+
+/**
+ * Callback invoked  when a response is received from the Authenticator.
+ * @param function({data: Object}) callback
+ * @param {Object} message message Object
+ */
+u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
+    function(callback, message) {
+  var messageObject = JSON.parse(message.data);
+  var intentUrl = messageObject['intentURL'];
+
+  var errorCode = messageObject['errorCode'];
+  var responseObject = null;
+  if (messageObject.hasOwnProperty('data')) {
+    responseObject = /** @type {Object} */ (
+       JSON.parse(messageObject['data']));
+    responseObject['requestId'] = this.requestId_;
+  }
+
+  /* Sign responses from the authenticator do not conform to U2F,
+   * convert to U2F here. */
+  responseObject = this.doResponseFixups_(responseObject);
+  callback({'data': responseObject});
+};
+
+/**
+ * Fixup the response provided by the Authenticator to conform with
+ * the U2F spec.
+ * @param {Object} responseData
+ * @return {Object} the U2F compliant response object
+ */
+u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ =
+    function(responseObject) {
+  if (responseObject.hasOwnProperty('responseData')) {
+    return responseObject;
+  } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) {
+    // Only sign responses require fixups.  If this is not a response
+    // to a sign request, then an internal error has occurred.
+    return {
+      'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE,
+      'responseData': {
+        'errorCode': u2f.ErrorCodes.OTHER_ERROR,
+        'errorMessage': 'Internal error: invalid response from Authenticator'
+      }
+    };
+  }
+
+  /* Non-conformant sign response, do fixups. */
+  var encodedChallengeObject = responseObject['challenge'];
+  if (typeof encodedChallengeObject !== 'undefined') {
+    var challengeObject = JSON.parse(atob(encodedChallengeObject));
+    var serverChallenge = challengeObject['challenge'];
+    var challengesList = this.requestObject_['signData'];
+    var requestChallengeObject = null;
+    for (var i = 0; i < challengesList.length; i++) {
+	  var challengeObject = challengesList[i];
+	  if (challengeObject['keyHandle'] == responseObject['keyHandle']) {
+	    requestChallengeObject = challengeObject;
+	    break;
+	  }
+	}
+  }
+  var responseData = {
+    'errorCode': responseObject['resultCode'],
+    'keyHandle': responseObject['keyHandle'],
+    'signatureData': responseObject['signature'],
+    'clientData': encodedChallengeObject
+  };
+  return {
+    'type': u2f.MessageTypes.U2F_SIGN_RESPONSE,
+    'responseData': responseData,
+    'requestId': responseObject['requestId']
+  }
+};
+
+/**
+ * Base URL for intents to Authenticator.
+ * @const
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
+    'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
+
+/**
+ * Format a return a sign request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {number} timeoutSeconds (ignored for now)
+ * @param {number} reqId
+ * @return {string}
+ */
+u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ =
+    function(signRequests, timeoutSeconds, reqId) {
+  if (!signRequests || signRequests.length == 0) {
+    return null;
+  }
+  /* TODO(fixme): stash away requestId, as the authenticator app does
+   * not return it for sign responses. */
+  this.requestId_ = reqId;
+  /* TODO(fixme): stash away the signRequests, to deal with the legacy
+   * response format returned by the Authenticator app. */
+  this.requestObject_ = {
+    'type': u2f.MessageTypes.U2F_SIGN_REQUEST,
+    'signData': signRequests,
+    'requestId': reqId,
+    'timeout': timeoutSeconds
+  };
+
+  var appId = signRequests[0]['appId'];
+  var intentUrl =
+      u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+      ';S.appId=' + encodeURIComponent(appId) +
+      ';S.eventId=' + reqId +
+      ';S.challenges=' +
+      encodeURIComponent(
+          JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end';
+  return intentUrl;
+};
+
+/**
+ * Get the browser data objects from the challenge list
+ * @param {Array} challenges list of challenges
+ * @return {Array} list of browser data objects
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_
+    .prototype.getBrowserDataList_ = function(challenges) {
+  return challenges
+      .map(function(challenge) {
+        var browserData = {
+          'typ': 'navigator.id.getAssertion',
+          'challenge': challenge['challenge']
+        };
+        var challengeObject = {
+          'challenge' : browserData,
+          'keyHandle' : challenge['keyHandle']
+        };
+        return challengeObject;
+      });
+};
+
+/**
+ * Format a return a register request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {Array<u2f.RegisterRequest>} enrollChallenges
+ * @param {number} timeoutSeconds (ignored for now)
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ =
+    function(signRequests, enrollChallenges, timeoutSeconds, reqId) {
+  if (!enrollChallenges || enrollChallenges.length == 0) {
+    return null;
+  }
+  // Assume the appId is the same for all enroll challenges.
+  var appId = enrollChallenges[0]['appId'];
+  var registerRequests = [];
+  for (var i = 0; i < enrollChallenges.length; i++) {
+    var registerRequest = {
+      'challenge': enrollChallenges[i]['challenge'],
+      'version': enrollChallenges[i]['version']
+    };
+    if (enrollChallenges[i]['appId'] != appId) {
+      // Only include the appId when it differs from the first appId.
+      registerRequest['appId'] = enrollChallenges[i]['appId'];
+    }
+    registerRequests.push(registerRequest);
+  }
+  var registeredKeys = [];
+  if (signRequests) {
+    for (i = 0; i < signRequests.length; i++) {
+      var key = {
+        'keyHandle': signRequests[i]['keyHandle'],
+        'version': signRequests[i]['version']
+      };
+      // Only include the appId when it differs from the appId that's
+      // being registered now.
+      if (signRequests[i]['appId'] != appId) {
+        key['appId'] = signRequests[i]['appId'];
+      }
+      registeredKeys.push(key);
+    }
+  }
+  var request = {
+    'type': u2f.MessageTypes.U2F_REGISTER_REQUEST,
+    'appId': appId,
+    'registerRequests': registerRequests,
+    'registeredKeys': registeredKeys,
+    'requestId': reqId,
+    'timeoutSeconds': timeoutSeconds
+  };
+  var intentUrl =
+      u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+      ';S.request=' + encodeURIComponent(JSON.stringify(request)) +
+      ';end';
+  /* TODO(fixme): stash away requestId, this is is not necessary for
+   * register requests, but here to keep parity with sign.
+   */
+  this.requestId_ = reqId;
+  return intentUrl;
+};
+
+
+/**
+ * Sets up an embedded trampoline iframe, sourced from the extension.
+ * @param {function(MessagePort)} callback
+ * @private
+ */
+u2f.getIframePort_ = function(callback) {
+  // Create the iframe
+  var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
+  var iframe = document.createElement('iframe');
+  iframe.src = iframeOrigin + '/u2f-comms.html';
+  iframe.setAttribute('style', 'display:none');
+  document.body.appendChild(iframe);
+
+  var channel = new MessageChannel();
+  var ready = function(message) {
+    if (message.data == 'ready') {
+      channel.port1.removeEventListener('message', ready);
+      callback(channel.port1);
+    } else {
+      console.error('First event on iframe port was not "ready"');
+    }
+  };
+  channel.port1.addEventListener('message', ready);
+  channel.port1.start();
+
+  iframe.addEventListener('load', function() {
+    // Deliver the port to the iframe and initialize
+    iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
+  });
+};
+
+
+// High-level JS API
+
+/**
+ * Default extension response timeout in seconds.
+ * @const
+ */
+u2f.EXTENSION_TIMEOUT_SEC = 30;
+
+/**
+ * A singleton instance for a MessagePort to the extension.
+ * @type {MessagePort|u2f.WrappedChromeRuntimePort_}
+ * @private
+ */
+u2f.port_ = null;
+
+/**
+ * Callbacks waiting for a port
+ * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
+ * @private
+ */
+u2f.waitingForPort_ = [];
+
+/**
+ * A counter for requestIds.
+ * @type {number}
+ * @private
+ */
+u2f.reqCounter_ = 0;
+
+/**
+ * A map from requestIds to client callbacks
+ * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
+ *                       |function((u2f.Error|u2f.SignResponse)))>}
+ * @private
+ */
+u2f.callbackMap_ = {};
+
+/**
+ * Creates or retrieves the MessagePort singleton to use.
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
+ * @private
+ */
+u2f.getPortSingleton_ = function(callback) {
+  if (u2f.port_) {
+    callback(u2f.port_);
+  } else {
+    if (u2f.waitingForPort_.length == 0) {
+      u2f.getMessagePort(function(port) {
+        u2f.port_ = port;
+        u2f.port_.addEventListener('message',
+          /** @type {function(Event)} */ (u2f.responseHandler_));
+
+        // Careful, here be async callbacks. Maybe.
+        while (u2f.waitingForPort_.length)
+          u2f.waitingForPort_.shift()(u2f.port_);
+      });
+    }
+    u2f.waitingForPort_.push(callback);
+  }
+};
+
+/**
+ * Handles response messages from the extension.
+ * @param {MessageEvent.<u2f.Response>} message
+ * @private
+ */
+u2f.responseHandler_ = function(message) {
+  var response = message.data;
+  var reqId = response['requestId'];
+  if (!reqId || !u2f.callbackMap_[reqId]) {
+    console.error('Unknown or missing requestId in response.');
+    return;
+  }
+  var cb = u2f.callbackMap_[reqId];
+  delete u2f.callbackMap_[reqId];
+  cb(response['responseData']);
+};
+
+/**
+ * Dispatches an array of sign requests to available U2F tokens.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.sign = function(signRequests, callback, opt_timeoutSeconds) {
+  u2f.getPortSingleton_(function(port) {
+    var reqId = ++u2f.reqCounter_;
+    u2f.callbackMap_[reqId] = callback;
+    var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+        opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+    var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId);
+    port.postMessage(req);
+  });
+};
+
+/**
+ * Dispatches register requests to available U2F tokens. An array of sign
+ * requests identifies already registered tokens.
+ * @param {Array<u2f.RegisterRequest>} registerRequests
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.register = function(registerRequests, signRequests,
+    callback, opt_timeoutSeconds) {
+  u2f.getPortSingleton_(function(port) {
+    var reqId = ++u2f.reqCounter_;
+    u2f.callbackMap_[reqId] = callback;
+    var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+        opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+    var req = port.formatRegisterRequest_(
+        signRequests, registerRequests, timeoutSeconds, reqId);
+    port.postMessage(req);
+  });
+};
+`
+
+
+
diff --git a/remoteu2f-proxy/init/default/remoteu2f-proxy b/remoteu2f-proxy/init/default/remoteu2f-proxy
new file mode 100644
index 0000000..a54a1cd
--- /dev/null
+++ b/remoteu2f-proxy/init/default/remoteu2f-proxy
@@ -0,0 +1,8 @@
+# /etc/default/remoteu2f-proxy
+
+OPTS=" \
+-base_url=https://yourdomain:8800 \
+-tls_cert=/etc/ssl/yourcert.crt \
+-tls_key=/etc/ssl/yourkey.key \
+-token_file=/etc/remoteu2f-proxy/tokens \
+"
diff --git a/remoteu2f-proxy/init/systemd/remoteu2f-proxy.service b/remoteu2f-proxy/init/systemd/remoteu2f-proxy.service
new file mode 100644
index 0000000..a9697f1
--- /dev/null
+++ b/remoteu2f-proxy/init/systemd/remoteu2f-proxy.service
@@ -0,0 +1,11 @@
+[Unit]
+Description = remoteu2f proxy
+
+[Service]
+EnvironmentFile = /etc/default/remoteu2f-proxy
+ExecStart = /usr/local/bin/remoteu2f-proxy -logtostderr $OPTS
+Type = simple
+User = www-data
+
+[Install]
+WantedBy = multi-user.target
diff --git a/remoteu2f-proxy/init/upstart/remoteu2f-proxy.conf b/remoteu2f-proxy/init/upstart/remoteu2f-proxy.conf
new file mode 100644
index 0000000..c9e3359
--- /dev/null
+++ b/remoteu2f-proxy/init/upstart/remoteu2f-proxy.conf
@@ -0,0 +1,16 @@
+# /etc/init/remoteu2f-proxy.conf
+
+description "remoteu2f-proxy"
+
+start on filesystem
+stop on runlevel [016]
+
+respawn
+
+pre-start exec test -x /usr/local/bin/remoteu2f-proxy || { stop; exit 0; }
+
+script
+    test ! -r /etc/default/remoteu2f-proxy || . /etc/default/remoteu2f-proxy
+    exec start-stop-daemon --start --chuid www-data:www-data --exec /usr/local/bin/remoteu2f-proxy -- $OPTS
+end script
+
diff --git a/remoteu2f-proxy/main.go b/remoteu2f-proxy/main.go
new file mode 100644
index 0000000..dae8b25
--- /dev/null
+++ b/remoteu2f-proxy/main.go
@@ -0,0 +1,125 @@
+// remoteu2f-proxy is the http+grpc server for remoteu2f.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+
+	// Enable profiling via HTTP.
+	// We will only use this if --debug_addr is given.
+	_ "net/http/pprof"
+
+	"github.com/golang/glog"
+)
+
+// Command-line flags.
+var (
+	baseURL = flag.String("base_url", "",
+		"base URL to send the clients (e.g. https://domain:8800); "+
+			"must be https and not have a trailing '/'")
+
+	httpAddr = flag.String("http_addr", ":8800",
+		"address where to listen for HTTP requests")
+	grpcAddr = flag.String("grpc_addr", ":8801",
+		"address where to listen for GPRC requests")
+	debugAddr = flag.String("debug_addr", "",
+		"address where to listen for debug/trace/profile HTTP requests")
+
+	tlsCert = flag.String("tls_cert", "cert.pem",
+		"file containing the TLS certificate to use")
+	tlsKey = flag.String("tls_key", "key.pem",
+		"file containing the TLS key to use")
+	grpcCert = flag.String("tls_cert_grpc", "",
+		"if set, use this certificate for GRPC instead of --tls_cert")
+	grpcKey = flag.String("tls_key_grpc", "",
+		"if set, use this key for GRPC instead of --tls_key")
+
+	tokenFile = flag.String("token_file", "tokens",
+		"file containing the valid client access tokens")
+)
+
+func validateBaseURL(s string) error {
+	u, err := url.Parse(*baseURL)
+	if err != nil {
+		return fmt.Errorf("malformed url:", err)
+	}
+	if u.Scheme != "https" {
+		return fmt.Errorf("scheme MUST be 'https'")
+	}
+	if u.Path != "" {
+		return fmt.Errorf("path MUST be empty (not even a trailing '/')")
+	}
+
+	return nil
+}
+
+func getValidTokens(path string) (map[string]bool, error) {
+	contents, err := ioutil.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	tokens := map[string]bool{}
+	for _, t := range strings.Split(string(contents), "\n") {
+		// Remove empty lines, and make sure all tokens have a minimum lenght.
+		t = strings.TrimSpace(t)
+		if t == "" {
+			continue
+		}
+		if len(t) < 10 {
+			return nil, fmt.Errorf(
+				"token %q too short (minimum lenght: 10)", t)
+		}
+
+		tokens[t] = true
+	}
+
+	return tokens, nil
+}
+
+func main() {
+	flag.Parse()
+
+	// We have strict requirements on baseURL because we use it as the appID.
+	if err := validateBaseURL(*baseURL); err != nil {
+		glog.Fatalf("invalid --base_url: %s", err)
+	}
+
+	validTokens, err := getValidTokens(*tokenFile)
+	if err != nil {
+		glog.Fatalf("error reading token file: %s", err)
+	}
+
+	s := NewServer()
+	s.BaseURL = *baseURL
+	s.HTTPAddr = *httpAddr
+	s.GRPCAddr = *grpcAddr
+	s.ValidTokens = validTokens
+
+	s.HTTPCert = *tlsCert
+	s.HTTPKey = *tlsKey
+	s.GRPCCert = *tlsCert
+	s.GRPCKey = *tlsKey
+	if *grpcCert != "" {
+		s.GRPCCert = *grpcCert
+	}
+	if *grpcKey != "" {
+		s.GRPCKey = *grpcKey
+	}
+
+	if *debugAddr != "" {
+		// Launch the default HTTP server at the given address.
+		// This is the one pprof and grpc register automatically against.
+		go func() {
+			glog.Infof("Debug HTTP listening on %s", *debugAddr)
+			err := http.ListenAndServe(*debugAddr, nil)
+			glog.Fatalf("Debug HTTP exiting: %s", err)
+		}()
+	}
+
+	s.ListenAndServe()
+}
diff --git a/remoteu2f-proxy/ratelimit.go b/remoteu2f-proxy/ratelimit.go
new file mode 100644
index 0000000..87b8cc6
--- /dev/null
+++ b/remoteu2f-proxy/ratelimit.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+	"sync"
+	"time"
+)
+
+// Simple and trivial rate limiter.
+// Only covers from gross mis-usage, this is not accurate or featureful.
+type RateLimiter struct {
+	Interval time.Duration
+	MaxCount uint
+
+	mu       sync.Mutex
+	count    uint
+	lastTick time.Time
+}
+
+func (r *RateLimiter) Allowed() bool {
+	r.mu.Lock()
+	defer r.mu.Unlock()
+
+	if time.Since(r.lastTick) > r.Interval {
+		r.lastTick = time.Now()
+		r.count = 0
+		return true
+	}
+
+	r.count++
+	return r.count < r.MaxCount
+}
diff --git a/remoteu2f-proxy/server.go b/remoteu2f-proxy/server.go
new file mode 100644
index 0000000..86543a2
--- /dev/null
+++ b/remoteu2f-proxy/server.go
@@ -0,0 +1,368 @@
+package main
+
+// Embed the html and js files we need.
+//go:generate go run tools/embed.go to_embed/*.html to_embed/*.js
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"strings"
+	"sync"
+	"text/template"
+	"time"
+
+	"github.com/golang/glog"
+	"github.com/gorilla/mux"
+	"golang.org/x/net/context"
+	"golang.org/x/net/trace"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/metadata"
+
+	pb "blitiri.com.ar/go/remoteu2f/internal/proto"
+)
+
+// randomID generates a random ID to use as part of the URL and to identify a
+// single operation.
+func randomID() (string, error) {
+	// 64 bit from crypto/rand should be enough for our purposes.
+	// These are reasonably short-lived (2m) and we have rate limiting.
+	b := make([]byte, 8)
+	_, err := rand.Read(b)
+	if err != nil {
+		return "", err
+	}
+
+	return base64.RawURLEncoding.EncodeToString(b), nil
+}
+
+// We treat Registrations and Authentication requests the same.
+type PendingOp struct {
+	// JSON that came in the Prepare message.
+	prepared []byte
+
+	// Message for the user, from the Prepare message.
+	msg string
+
+	// Request type, from the Prepare message.
+	rtype pb.Prepare_RType
+
+	// Channel we use to send the reply.
+	reply chan []byte
+}
+
+type Server struct {
+	BaseURL     string
+	HTTPAddr    string
+	GRPCAddr    string
+	ValidTokens map[string]bool
+
+	HTTPCert string
+	HTTPKey  string
+	GRPCCert string
+	GRPCKey  string
+
+	mu  sync.Mutex
+	ops map[string]*PendingOp
+
+	ratelimiter *RateLimiter
+}
+
+func NewServer() *Server {
+	// Rate-limit requests to 50/s.
+	// TODO: Make this configurable.
+	rl := &RateLimiter{
+		Interval: 1 * time.Second,
+		MaxCount: 50,
+	}
+
+	return &Server{
+		ops:         map[string]*PendingOp{},
+		ratelimiter: rl,
+	}
+}
+
+func (s *Server) removeOp(key string) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	op, ok := s.ops[key]
+	if !ok {
+		return
+	}
+
+	delete(s.ops, key)
+	close(op.reply)
+}
+
+func (s *Server) removeOpAfter(key string, after time.Duration) {
+	<-time.After(after)
+	s.removeOp(key)
+}
+
+func (s *Server) getOp(key string) (*PendingOp, bool) {
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	op, ok := s.ops[key]
+	return op, ok
+}
+
+func (s *Server) checkOauth(ctx context.Context) error {
+	md, ok := metadata.FromContext(ctx)
+	if !ok || md == nil {
+		return grpc.Errorf(codes.PermissionDenied, "MD not found")
+	}
+
+	for _, tokenMD := range md["authorization"] {
+		// tokenMD is: <type> SP <token>. Extract the token
+		ps := strings.SplitN(tokenMD, " ", 2)
+		if len(ps) != 2 {
+			return grpc.Errorf(codes.PermissionDenied, "invalid token format")
+		}
+		token := ps[1]
+
+		if _, ok := s.ValidTokens[token]; ok {
+			return nil
+		}
+	}
+
+	return grpc.Errorf(codes.PermissionDenied, "token not authorized")
+}
+
+func (s *Server) PrepareOp(ctx context.Context, p *pb.Prepare) (*pb.Url, error) {
+	if !s.ratelimiter.Allowed() {
+		return nil, grpc.Errorf(codes.Unavailable, "rate limited")
+	}
+
+	if err := s.checkOauth(ctx); err != nil {
+		return nil, err
+	}
+
+	key, err := randomID()
+	if err != nil {
+		return nil, err
+	}
+
+	op := &PendingOp{
+		prepared: p.Json,
+		msg:      p.Msg,
+		rtype:    p.Rtype,
+
+		// Buffered channel makes us not block if the grpc client goes away.
+		reply: make(chan []byte, 1),
+	}
+
+	s.mu.Lock()
+	s.ops[key] = op
+	s.mu.Unlock()
+
+	// We don't expect to have enough pending operations for the number of
+	// goroutines to be a problem.
+	go s.removeOpAfter(key, 3*time.Minute)
+
+	return &pb.Url{
+		Url: s.BaseURL + "/" + key + "/",
+		Key: key,
+	}, nil
+}
+
+func (s *Server) GetOpResponse(ctx context.Context, url *pb.Url) (*pb.Response, error) {
+	if !s.ratelimiter.Allowed() {
+		return nil, grpc.Errorf(codes.Unavailable, "rate limited")
+	}
+
+	// We could be more paranoid and check against the token that prepared the
+	// operation, but this is good enough for now.
+	if err := s.checkOauth(ctx); err != nil {
+		return nil, err
+	}
+
+	op, ok := s.getOp(url.Key)
+	if !ok {
+		return nil, grpc.Errorf(codes.FailedPrecondition, "key not found")
+	}
+
+	reply, ok := <-op.reply
+	if !ok {
+		return nil, grpc.Errorf(codes.DeadlineExceeded,
+			"timed out waiting for reply")
+	}
+
+	// Remove the data once we've sent a reply.
+	s.removeOp(url.Key)
+
+	return &pb.Response{reply}, nil
+}
+
+func (s *Server) GetAppID(ctx context.Context, _ *pb.Void) (*pb.Url, error) {
+	if !s.ratelimiter.Allowed() {
+		return nil, grpc.Errorf(codes.Unavailable, "rate limited")
+	}
+
+	if err := s.checkOauth(ctx); err != nil {
+		return nil, err
+	}
+
+	r := &pb.Url{
+		Key: "",
+		Url: s.BaseURL,
+	}
+	return r, nil
+}
+
+func keyFromRequest(r *http.Request) string {
+	vs := mux.Vars(r)
+	return vs["key"]
+}
+
+var registerTmpl = template.Must(
+	template.New("register").Parse(register_html))
+var authenticateTmpl = template.Must(
+	template.New("authenticate").Parse(authenticate_html))
+
+// Serve the key-specific index.
+func (s *Server) IndexHandler(w http.ResponseWriter, r *http.Request) {
+	tr := trace.New("http", "index")
+	defer tr.Finish()
+
+	if !s.ratelimiter.Allowed() {
+		tr.LazyPrintf("rate limited")
+		tr.SetError()
+		http.Error(w, "too many requests", 429)
+		return
+	}
+
+	key := keyFromRequest(r)
+	tr.LazyPrintf("key: %s", key)
+	op, ok := s.getOp(key)
+	if !ok {
+		tr.LazyPrintf("404 error")
+		tr.SetError()
+		http.NotFound(w, r)
+		return
+	}
+
+	var err error
+	data := struct {
+		Message string
+		Request string
+	}{
+		op.msg,
+		string(op.prepared),
+	}
+	switch op.rtype {
+	case pb.Prepare_REGISTER:
+		err = registerTmpl.Execute(w, data)
+	case pb.Prepare_AUTHENTICATE:
+		err = authenticateTmpl.Execute(w, data)
+	default:
+		err = fmt.Errorf("unknown operation type %v", op.rtype)
+	}
+
+	if err != nil {
+		tr.LazyPrintf("render error: %v", err)
+		tr.SetError()
+		http.Error(w, "error rendering", http.StatusBadRequest)
+		return
+	}
+}
+
+// StaticHandler returns an HTTP handler for the given path and content.
+func (s *Server) StaticHandler(path, content string) func(w http.ResponseWriter, r *http.Request) {
+	return func(w http.ResponseWriter, r *http.Request) {
+		tr := trace.New("http", path)
+		defer tr.Finish()
+
+		if !s.ratelimiter.Allowed() {
+			tr.LazyPrintf("rate limited")
+			tr.SetError()
+			http.Error(w, "too many requests", 429)
+			return
+		}
+		w.Write([]byte(content))
+	}
+}
+
+// Common handler for both javascript responses.
+func (s *Server) HTTPResponse(w http.ResponseWriter, r *http.Request) {
+	tr := trace.New("http", "response")
+	defer tr.Finish()
+
+	if !s.ratelimiter.Allowed() {
+		tr.LazyPrintf("rate limited")
+		tr.SetError()
+		http.Error(w, "too many requests", 429)
+		return
+	}
+
+	key := keyFromRequest(r)
+	tr.LazyPrintf("key: %s", key)
+	op, ok := s.getOp(key)
+	if !ok {
+		tr.LazyPrintf("404 error")
+		tr.SetError()
+		http.NotFound(w, r)
+		return
+	}
+
+	buf := make([]byte, 4*1024)
+	n, err := r.Body.Read(buf)
+
+	if err != nil && err != io.EOF {
+		tr.LazyPrintf("400 error reading body: %v", err)
+		tr.SetError()
+		http.Error(w, "error reading body", http.StatusBadRequest)
+		return
+	}
+
+	op.reply <- buf[:n]
+
+	w.Write([]byte("success"))
+}
+
+func (s *Server) ListenAndServe() {
+	// Prepare and launch the HTTP server.
+	r := mux.NewRouter()
+	r.HandleFunc("/{key}/", s.IndexHandler)
+	r.HandleFunc("/{key}/response", s.HTTPResponse)
+	r.HandleFunc("/{key}/u2f_api.js",
+		s.StaticHandler("u2f_api.js", u2f_api_js))
+	r.HandleFunc("/{key}/remoteu2f.js",
+		s.StaticHandler("remoteu2f.js", remoteu2f_js))
+	httpServer := http.Server{
+		Addr:    s.HTTPAddr,
+		Handler: r,
+	}
+
+	go func() {
+		glog.Infof("HTTP listening on %s", s.HTTPAddr)
+		err := httpServer.ListenAndServeTLS(s.HTTPCert, s.HTTPKey)
+		glog.Fatalf("HTTP exiting: %s", err)
+	}()
+
+	// And now the GRPC server.
+	lis, err := net.Listen("tcp", s.GRPCAddr)
+	if err != nil {
+		glog.Errorf("failed to listen: %v", err)
+		return
+	}
+
+	ta, err := credentials.NewServerTLSFromFile(s.GRPCCert, s.GRPCKey)
+	if err != nil {
+		glog.Errorf("failed to create TLS transport auth: %v", err)
+		return
+	}
+
+	grpcServer := grpc.NewServer(grpc.Creds(ta))
+	pb.RegisterRemoteU2FServer(grpcServer, s)
+
+	glog.Infof("GRPC listening on %s", s.GRPCAddr)
+	err = grpcServer.Serve(lis)
+	glog.Infof("GRPC exiting: %s", err)
+}
diff --git a/remoteu2f-proxy/to_embed/authenticate.html b/remoteu2f-proxy/to_embed/authenticate.html
new file mode 100644
index 0000000..148fd78
--- /dev/null
+++ b/remoteu2f-proxy/to_embed/authenticate.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+  <head>
+  </head>
+
+  <body>
+    <h1>remoteu2f - authenticate</h1>
+
+    <h2>{{.Message}}</h2>
+
+    <p class="instructions">
+    Please insert/touch your security key to authenticate.
+    </p>
+
+    <p class="status">
+    Status: <span id="status">waiting for security key</span>
+    </p>
+
+    <script
+        type="text/javascript"
+        src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"
+        integrity="sha384-8gBf6Y4YYq7Jx97PIqmTwLPin4hxIzQw5aDmUg/DDhul9fFpbbLcLh3nTIIDJKhx"
+        crossorigin="anonymous"></script>
+    <script type="text/javascript" src="remoteu2f.js"></script>
+    <script type="text/javascript" src="u2f_api.js"></script>
+    <script>
+      u2f.sign({{.Request}}, handleKeyResponse, 2*60);
+    </script>
+
+  </body>
+</html>
diff --git a/remoteu2f-proxy/to_embed/register.html b/remoteu2f-proxy/to_embed/register.html
new file mode 100644
index 0000000..e19aa06
--- /dev/null
+++ b/remoteu2f-proxy/to_embed/register.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html>
+  <head>
+  </head>
+
+  <body>
+    <h1>remoteu2f - register</h1>
+
+    <h2>{{.Message}}</h2>
+
+    <p class="instructions">
+    Please insert/touch your security key to complete the registration.
+    </p>
+
+    <p class="status">
+    Status: <span id="status">waiting for security key</span>
+    </p>
+
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"
+            integrity="sha384-8gBf6Y4YYq7Jx97PIqmTwLPin4hxIzQw5aDmUg/DDhul9fFpbbLcLh3nTIIDJKhx"
+            crossorigin="anonymous"></script>
+    <script type="text/javascript" src="remoteu2f.js"></script>
+    <script type="text/javascript" src="u2f_api.js"></script>
+    <script>
+      u2f.register([{{.Request}}], [], handleKeyResponse, 2*60)
+    </script>
+
+  </body>
+</html>
diff --git a/remoteu2f-proxy/to_embed/remoteu2f.js b/remoteu2f-proxy/to_embed/remoteu2f.js
new file mode 100644
index 0000000..22718a5
--- /dev/null
+++ b/remoteu2f-proxy/to_embed/remoteu2f.js
@@ -0,0 +1,25 @@
+
+// This function gets called when the security key gives us a response to our
+// register/sign request.
+function handleKeyResponse(resp) {
+    if (resp.errorCode != undefined && resp.errorCode != u2f.ErrorCodes.OK) {
+        codeToText = {
+            0: "OK",
+            1: "General error (hardware issues?)",
+            2: "Bad request (please report)",
+            3: "Unsupported configuration (please report)",
+            4: "Device ineligible, did you forget to register it?",
+            5: "Timed out waiting for security key",
+        }
+
+        $('#status').text(
+                codeToText[resp.errorCode] + " -- "
+                + resp.errorMessage);
+        return;
+    }
+
+    $('#status').text('sending response');
+    $.post('response', JSON.stringify(resp)).done(function() {
+        $('#status').text('done');
+    });
+}
diff --git a/remoteu2f-proxy/to_embed/u2f_api.js b/remoteu2f-proxy/to_embed/u2f_api.js
new file mode 100644
index 0000000..dad9f89
--- /dev/null
+++ b/remoteu2f-proxy/to_embed/u2f_api.js
@@ -0,0 +1,654 @@
+// Copyright 2014-2015 Google Inc. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+// remoteu2f note: Obtained from the reference code at
+// https://github.com/google/u2f-ref-code/
+
+/**
+ * @fileoverview The U2F api.
+ */
+
+'use strict';
+
+/** Namespace for the U2F api.
+ * @type {Object}
+ */
+var u2f = u2f || {};
+
+/**
+ * The U2F extension id
+ * @type {string}
+ * @const
+ */
+u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
+
+/**
+ * Message types for messsages to/from the extension
+ * @const
+ * @enum {string}
+ */
+u2f.MessageTypes = {
+  'U2F_REGISTER_REQUEST': 'u2f_register_request',
+  'U2F_SIGN_REQUEST': 'u2f_sign_request',
+  'U2F_REGISTER_RESPONSE': 'u2f_register_response',
+  'U2F_SIGN_RESPONSE': 'u2f_sign_response'
+};
+
+/**
+ * Response status codes
+ * @const
+ * @enum {number}
+ */
+u2f.ErrorCodes = {
+  'OK': 0,
+  'OTHER_ERROR': 1,
+  'BAD_REQUEST': 2,
+  'CONFIGURATION_UNSUPPORTED': 3,
+  'DEVICE_INELIGIBLE': 4,
+  'TIMEOUT': 5
+};
+
+/**
+ * A message type for registration requests
+ * @typedef {{
+ *   type: u2f.MessageTypes,
+ *   signRequests: Array<u2f.SignRequest>,
+ *   registerRequests: ?Array<u2f.RegisterRequest>,
+ *   timeoutSeconds: ?number,
+ *   requestId: ?number
+ * }}
+ */
+u2f.Request;
+
+/**
+ * A message for registration responses
+ * @typedef {{
+ *   type: u2f.MessageTypes,
+ *   responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
+ *   requestId: ?number
+ * }}
+ */
+u2f.Response;
+
+/**
+ * An error object for responses
+ * @typedef {{
+ *   errorCode: u2f.ErrorCodes,
+ *   errorMessage: ?string
+ * }}
+ */
+u2f.Error;
+
+/**
+ * Data object for a single sign request.
+ * @typedef {{
+ *   version: string,
+ *   challenge: string,
+ *   keyHandle: string,
+ *   appId: string
+ * }}
+ */
+u2f.SignRequest;
+
+/**
+ * Data object for a sign response.
+ * @typedef {{
+ *   keyHandle: string,
+ *   signatureData: string,
+ *   clientData: string
+ * }}
+ */
+u2f.SignResponse;
+
+/**
+ * Data object for a registration request.
+ * @typedef {{
+ *   version: string,
+ *   challenge: string,
+ *   appId: string
+ * }}
+ */
+u2f.RegisterRequest;
+
+/**
+ * Data object for a registration response.
+ * @typedef {{
+ *   registrationData: string,
+ *   clientData: string
+ * }}
+ */
+u2f.RegisterResponse;
+
+
+// Low level MessagePort API support
+
+/**
+ * Sets up a MessagePort to the U2F extension using the
+ * available mechanisms.
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
+ */
+u2f.getMessagePort = function(callback) {
+  if (typeof chrome != 'undefined' && chrome.runtime) {
+    // The actual message here does not matter, but we need to get a reply
+    // for the callback to run. Thus, send an empty signature request
+    // in order to get a failure response.
+    var msg = {
+      type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+      signRequests: []
+    };
+    chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
+      if (!chrome.runtime.lastError) {
+        // We are on a whitelisted origin and can talk directly
+        // with the extension.
+        u2f.getChromeRuntimePort_(callback);
+      } else {
+        // chrome.runtime was available, but we couldn't message
+        // the extension directly, use iframe
+        u2f.getIframePort_(callback);
+      }
+    });
+  } else if (u2f.isAndroidChrome_()) {
+    u2f.getAuthenticatorPort_(callback);
+  } else {
+    // chrome.runtime was not available at all, which is normal
+    // when this origin doesn't have access to any extensions.
+    u2f.getIframePort_(callback);
+  }
+};
+
+/**
+ * Detect chrome running on android based on the browser's useragent.
+ * @private
+ */
+u2f.isAndroidChrome_ = function() {
+  var userAgent = navigator.userAgent;
+  return userAgent.indexOf('Chrome') != -1 &&
+      userAgent.indexOf('Android') != -1;
+};
+
+/**
+ * Connects directly to the extension via chrome.runtime.connect
+ * @param {function(u2f.WrappedChromeRuntimePort_)} callback
+ * @private
+ */
+u2f.getChromeRuntimePort_ = function(callback) {
+  var port = chrome.runtime.connect(u2f.EXTENSION_ID,
+    {'includeTlsChannelId': true});
+  setTimeout(function() {
+    callback(new u2f.WrappedChromeRuntimePort_(port));
+  }, 0);
+};
+
+/**
+ * Return a 'port' abstraction to the Authenticator app.
+ * @param {function(u2f.WrappedAuthenticatorPort_)} callback
+ * @private
+ */
+u2f.getAuthenticatorPort_ = function(callback) {
+  setTimeout(function() {
+    callback(new u2f.WrappedAuthenticatorPort_());
+  }, 0);
+};
+
+/**
+ * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
+ * @param {Port} port
+ * @constructor
+ * @private
+ */
+u2f.WrappedChromeRuntimePort_ = function(port) {
+  this.port_ = port;
+};
+
+/**
+ * Format a return a sign request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ =
+    function(signRequests, timeoutSeconds, reqId) {
+  return {
+    type: u2f.MessageTypes.U2F_SIGN_REQUEST,
+    signRequests: signRequests,
+    timeoutSeconds: timeoutSeconds,
+    requestId: reqId
+  };
+};
+
+/**
+ * Format a return a register request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {Array<u2f.RegisterRequest>} signRequests
+ * @param {number} timeoutSeconds
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ =
+    function(signRequests, registerRequests, timeoutSeconds, reqId) {
+  return {
+    type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
+    signRequests: signRequests,
+    registerRequests: registerRequests,
+    timeoutSeconds: timeoutSeconds,
+    requestId: reqId
+  };
+};
+
+/**
+ * Posts a message on the underlying channel.
+ * @param {Object} message
+ */
+u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
+  this.port_.postMessage(message);
+};
+
+/**
+ * Emulates the HTML 5 addEventListener interface. Works only for the
+ * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
+    function(eventName, handler) {
+  var name = eventName.toLowerCase();
+  if (name == 'message' || name == 'onmessage') {
+    this.port_.onMessage.addListener(function(message) {
+      // Emulate a minimal MessageEvent object
+      handler({'data': message});
+    });
+  } else {
+    console.error('WrappedChromeRuntimePort only supports onMessage');
+  }
+};
+
+/**
+ * Wrap the Authenticator app with a MessagePort interface.
+ * @constructor
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_ = function() {
+  this.requestId_ = -1;
+  this.requestObject_ = null;
+}
+
+/**
+ * Launch the Authenticator intent.
+ * @param {Object} message
+ */
+u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
+  var intentLocation = /** @type {string} */ (message);
+  document.location = intentLocation;
+};
+
+/**
+ * Emulates the HTML 5 addEventListener interface.
+ * @param {string} eventName
+ * @param {function({data: Object})} handler
+ */
+u2f.WrappedAuthenticatorPort_.prototype.addEventListener =
+    function(eventName, handler) {
+  var name = eventName.toLowerCase();
+  if (name == 'message') {
+    var self = this;
+    /* Register a callback to that executes when
+     * chrome injects the response. */
+    window.addEventListener(
+        'message', self.onRequestUpdate_.bind(self, handler), false);
+  } else {
+    console.error('WrappedAuthenticatorPort only supports message');
+  }
+};
+
+/**
+ * Callback invoked  when a response is received from the Authenticator.
+ * @param function({data: Object}) callback
+ * @param {Object} message message Object
+ */
+u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
+    function(callback, message) {
+  var messageObject = JSON.parse(message.data);
+  var intentUrl = messageObject['intentURL'];
+
+  var errorCode = messageObject['errorCode'];
+  var responseObject = null;
+  if (messageObject.hasOwnProperty('data')) {
+    responseObject = /** @type {Object} */ (
+       JSON.parse(messageObject['data']));
+    responseObject['requestId'] = this.requestId_;
+  }
+
+  /* Sign responses from the authenticator do not conform to U2F,
+   * convert to U2F here. */
+  responseObject = this.doResponseFixups_(responseObject);
+  callback({'data': responseObject});
+};
+
+/**
+ * Fixup the response provided by the Authenticator to conform with
+ * the U2F spec.
+ * @param {Object} responseData
+ * @return {Object} the U2F compliant response object
+ */
+u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ =
+    function(responseObject) {
+  if (responseObject.hasOwnProperty('responseData')) {
+    return responseObject;
+  } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) {
+    // Only sign responses require fixups.  If this is not a response
+    // to a sign request, then an internal error has occurred.
+    return {
+      'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE,
+      'responseData': {
+        'errorCode': u2f.ErrorCodes.OTHER_ERROR,
+        'errorMessage': 'Internal error: invalid response from Authenticator'
+      }
+    };
+  }
+
+  /* Non-conformant sign response, do fixups. */
+  var encodedChallengeObject = responseObject['challenge'];
+  if (typeof encodedChallengeObject !== 'undefined') {
+    var challengeObject = JSON.parse(atob(encodedChallengeObject));
+    var serverChallenge = challengeObject['challenge'];
+    var challengesList = this.requestObject_['signData'];
+    var requestChallengeObject = null;
+    for (var i = 0; i < challengesList.length; i++) {
+	  var challengeObject = challengesList[i];
+	  if (challengeObject['keyHandle'] == responseObject['keyHandle']) {
+	    requestChallengeObject = challengeObject;
+	    break;
+	  }
+	}
+  }
+  var responseData = {
+    'errorCode': responseObject['resultCode'],
+    'keyHandle': responseObject['keyHandle'],
+    'signatureData': responseObject['signature'],
+    'clientData': encodedChallengeObject
+  };
+  return {
+    'type': u2f.MessageTypes.U2F_SIGN_RESPONSE,
+    'responseData': responseData,
+    'requestId': responseObject['requestId']
+  }
+};
+
+/**
+ * Base URL for intents to Authenticator.
+ * @const
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
+    'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
+
+/**
+ * Format a return a sign request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {number} timeoutSeconds (ignored for now)
+ * @param {number} reqId
+ * @return {string}
+ */
+u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ =
+    function(signRequests, timeoutSeconds, reqId) {
+  if (!signRequests || signRequests.length == 0) {
+    return null;
+  }
+  /* TODO(fixme): stash away requestId, as the authenticator app does
+   * not return it for sign responses. */
+  this.requestId_ = reqId;
+  /* TODO(fixme): stash away the signRequests, to deal with the legacy
+   * response format returned by the Authenticator app. */
+  this.requestObject_ = {
+    'type': u2f.MessageTypes.U2F_SIGN_REQUEST,
+    'signData': signRequests,
+    'requestId': reqId,
+    'timeout': timeoutSeconds
+  };
+
+  var appId = signRequests[0]['appId'];
+  var intentUrl =
+      u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+      ';S.appId=' + encodeURIComponent(appId) +
+      ';S.eventId=' + reqId +
+      ';S.challenges=' +
+      encodeURIComponent(
+          JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end';
+  return intentUrl;
+};
+
+/**
+ * Get the browser data objects from the challenge list
+ * @param {Array} challenges list of challenges
+ * @return {Array} list of browser data objects
+ * @private
+ */
+u2f.WrappedAuthenticatorPort_
+    .prototype.getBrowserDataList_ = function(challenges) {
+  return challenges
+      .map(function(challenge) {
+        var browserData = {
+          'typ': 'navigator.id.getAssertion',
+          'challenge': challenge['challenge']
+        };
+        var challengeObject = {
+          'challenge' : browserData,
+          'keyHandle' : challenge['keyHandle']
+        };
+        return challengeObject;
+      });
+};
+
+/**
+ * Format a return a register request.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {Array<u2f.RegisterRequest>} enrollChallenges
+ * @param {number} timeoutSeconds (ignored for now)
+ * @param {number} reqId
+ * @return {Object}
+ */
+u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ =
+    function(signRequests, enrollChallenges, timeoutSeconds, reqId) {
+  if (!enrollChallenges || enrollChallenges.length == 0) {
+    return null;
+  }
+  // Assume the appId is the same for all enroll challenges.
+  var appId = enrollChallenges[0]['appId'];
+  var registerRequests = [];
+  for (var i = 0; i < enrollChallenges.length; i++) {
+    var registerRequest = {
+      'challenge': enrollChallenges[i]['challenge'],
+      'version': enrollChallenges[i]['version']
+    };
+    if (enrollChallenges[i]['appId'] != appId) {
+      // Only include the appId when it differs from the first appId.
+      registerRequest['appId'] = enrollChallenges[i]['appId'];
+    }
+    registerRequests.push(registerRequest);
+  }
+  var registeredKeys = [];
+  if (signRequests) {
+    for (i = 0; i < signRequests.length; i++) {
+      var key = {
+        'keyHandle': signRequests[i]['keyHandle'],
+        'version': signRequests[i]['version']
+      };
+      // Only include the appId when it differs from the appId that's
+      // being registered now.
+      if (signRequests[i]['appId'] != appId) {
+        key['appId'] = signRequests[i]['appId'];
+      }
+      registeredKeys.push(key);
+    }
+  }
+  var request = {
+    'type': u2f.MessageTypes.U2F_REGISTER_REQUEST,
+    'appId': appId,
+    'registerRequests': registerRequests,
+    'registeredKeys': registeredKeys,
+    'requestId': reqId,
+    'timeoutSeconds': timeoutSeconds
+  };
+  var intentUrl =
+      u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
+      ';S.request=' + encodeURIComponent(JSON.stringify(request)) +
+      ';end';
+  /* TODO(fixme): stash away requestId, this is is not necessary for
+   * register requests, but here to keep parity with sign.
+   */
+  this.requestId_ = reqId;
+  return intentUrl;
+};
+
+
+/**
+ * Sets up an embedded trampoline iframe, sourced from the extension.
+ * @param {function(MessagePort)} callback
+ * @private
+ */
+u2f.getIframePort_ = function(callback) {
+  // Create the iframe
+  var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
+  var iframe = document.createElement('iframe');
+  iframe.src = iframeOrigin + '/u2f-comms.html';
+  iframe.setAttribute('style', 'display:none');
+  document.body.appendChild(iframe);
+
+  var channel = new MessageChannel();
+  var ready = function(message) {
+    if (message.data == 'ready') {
+      channel.port1.removeEventListener('message', ready);
+      callback(channel.port1);
+    } else {
+      console.error('First event on iframe port was not "ready"');
+    }
+  };
+  channel.port1.addEventListener('message', ready);
+  channel.port1.start();
+
+  iframe.addEventListener('load', function() {
+    // Deliver the port to the iframe and initialize
+    iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
+  });
+};
+
+
+// High-level JS API
+
+/**
+ * Default extension response timeout in seconds.
+ * @const
+ */
+u2f.EXTENSION_TIMEOUT_SEC = 30;
+
+/**
+ * A singleton instance for a MessagePort to the extension.
+ * @type {MessagePort|u2f.WrappedChromeRuntimePort_}
+ * @private
+ */
+u2f.port_ = null;
+
+/**
+ * Callbacks waiting for a port
+ * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
+ * @private
+ */
+u2f.waitingForPort_ = [];
+
+/**
+ * A counter for requestIds.
+ * @type {number}
+ * @private
+ */
+u2f.reqCounter_ = 0;
+
+/**
+ * A map from requestIds to client callbacks
+ * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
+ *                       |function((u2f.Error|u2f.SignResponse)))>}
+ * @private
+ */
+u2f.callbackMap_ = {};
+
+/**
+ * Creates or retrieves the MessagePort singleton to use.
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
+ * @private
+ */
+u2f.getPortSingleton_ = function(callback) {
+  if (u2f.port_) {
+    callback(u2f.port_);
+  } else {
+    if (u2f.waitingForPort_.length == 0) {
+      u2f.getMessagePort(function(port) {
+        u2f.port_ = port;
+        u2f.port_.addEventListener('message',
+          /** @type {function(Event)} */ (u2f.responseHandler_));
+
+        // Careful, here be async callbacks. Maybe.
+        while (u2f.waitingForPort_.length)
+          u2f.waitingForPort_.shift()(u2f.port_);
+      });
+    }
+    u2f.waitingForPort_.push(callback);
+  }
+};
+
+/**
+ * Handles response messages from the extension.
+ * @param {MessageEvent.<u2f.Response>} message
+ * @private
+ */
+u2f.responseHandler_ = function(message) {
+  var response = message.data;
+  var reqId = response['requestId'];
+  if (!reqId || !u2f.callbackMap_[reqId]) {
+    console.error('Unknown or missing requestId in response.');
+    return;
+  }
+  var cb = u2f.callbackMap_[reqId];
+  delete u2f.callbackMap_[reqId];
+  cb(response['responseData']);
+};
+
+/**
+ * Dispatches an array of sign requests to available U2F tokens.
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.sign = function(signRequests, callback, opt_timeoutSeconds) {
+  u2f.getPortSingleton_(function(port) {
+    var reqId = ++u2f.reqCounter_;
+    u2f.callbackMap_[reqId] = callback;
+    var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+        opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+    var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId);
+    port.postMessage(req);
+  });
+};
+
+/**
+ * Dispatches register requests to available U2F tokens. An array of sign
+ * requests identifies already registered tokens.
+ * @param {Array<u2f.RegisterRequest>} registerRequests
+ * @param {Array<u2f.SignRequest>} signRequests
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
+ * @param {number=} opt_timeoutSeconds
+ */
+u2f.register = function(registerRequests, signRequests,
+    callback, opt_timeoutSeconds) {
+  u2f.getPortSingleton_(function(port) {
+    var reqId = ++u2f.reqCounter_;
+    u2f.callbackMap_[reqId] = callback;
+    var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
+        opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
+    var req = port.formatRegisterRequest_(
+        signRequests, registerRequests, timeoutSeconds, reqId);
+    port.postMessage(req);
+  });
+};
diff --git a/remoteu2f-proxy/tools/embed.go b/remoteu2f-proxy/tools/embed.go
new file mode 100644
index 0000000..828e254
--- /dev/null
+++ b/remoteu2f-proxy/tools/embed.go
@@ -0,0 +1,118 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"text/template"
+)
+
+// Flag definitions.
+var (
+	pkg = flag.String("package", "main", "name of the package to generate")
+	out = flag.String("out", "embedded_data.go", "file to write to")
+)
+
+func readContent(path string) (string, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+
+	buf, err := ioutil.ReadAll(f)
+	return string(buf), err
+}
+
+func escapeContent(c string) string {
+	// Escape
+	//   `
+	// with
+	//   ` + "\x60" + `
+	// which let us embed it in the template.
+	return strings.Replace(c, "`", "` + \"\\x60\" + `", -1)
+}
+
+type File struct {
+	Name           string
+	Path           string
+	EscapedContent string
+}
+
+const generatedTmpl = `
+package {{.Pkg}}
+
+// File auto-generated by embed.go.
+
+{{range .Files}}
+// {{.Path}} ----- 8< ----- 8< ----- 8< ----- 8< -----
+
+// {{.Name}} contains the content of {{.Path}}.
+const {{.Name}} = ` + "`" + `{{.EscapedContent}}` + "`" + `
+
+{{end}}
+
+`
+
+// tmpl is the compiled version of generatedTmpl above.
+var tmpl = template.Must(template.New("generated").Parse(generatedTmpl))
+
+func Embed(pkg string, paths []string, outPath string) error {
+
+	var vals struct {
+		Pkg   string
+		Files []*File
+	}
+
+	vals.Pkg = pkg
+
+	for _, path := range paths {
+		f := &File{
+			Path: path,
+			Name: strings.Replace(filepath.Base(path), ".", "_", -1),
+		}
+
+		content, err := readContent(path)
+		if err != nil {
+			return fmt.Errorf("Error reading %q: %v", path, err)
+		}
+
+		f.EscapedContent = escapeContent(content)
+
+		vals.Files = append(vals.Files, f)
+	}
+
+	out, err := os.Create(outPath)
+	if err != nil {
+		return fmt.Errorf("Error opening %q: %v", *out, err)
+	}
+
+	err = tmpl.Execute(out, vals)
+	if err != nil {
+		return fmt.Errorf("Error executing template: %v", err)
+	}
+
+	return nil
+}
+
+func main() {
+	flag.Parse()
+
+	paths := []string{}
+	for _, glob := range flag.Args() {
+		matches, err := filepath.Glob(glob)
+		if err != nil {
+			fmt.Printf("%v\n", err)
+			os.Exit(1)
+		}
+		paths = append(paths, matches...)
+	}
+
+	err := Embed(*pkg, paths, *out)
+	if err != nil {
+		fmt.Printf("%v\n", err)
+		os.Exit(1)
+	}
+}