diff --git a/backend/ldap.go b/backend/ldap.go
index eee6090c6c4fd8fb87dd2051bd6a4aa82f668ed9..8832df79e02b0c7ad777b59f19b74fd359a03759 100644
--- a/backend/ldap.go
+++ b/backend/ldap.go
@@ -1,7 +1,6 @@
 package backend
 
 import (
-	"bytes"
 	"context"
 	"errors"
 	"fmt"
@@ -9,6 +8,7 @@ import (
 	"strings"
 
 	ldaputil "git.autistici.org/ai3/go-common/ldap"
+	ct "git.autistici.org/ai3/go-common/ldap/compositetypes"
 	"gopkg.in/ldap.v3"
 )
 
@@ -136,11 +136,12 @@ func NewLDAPBackend(config *LDAPConfig) (*ldapBackend, error) {
 
 // The encrypted private keys are a compound object in LDAP (in
 // "id:key" format), we can safely ignore the key id here.
-func decodePrivateKey(enc []byte) []byte {
-	if n := bytes.IndexByte(enc, ':'); n >= 0 {
-		return enc[n+1:]
+func decodePrivateKey(enc string) ([]byte, error) {
+	key, err := ct.UnmarshalEncryptedKey(enc)
+	if err != nil {
+		return nil, err
 	}
-	return enc
+	return key.EncryptedKey, nil
 }
 
 func (b *ldapBackend) GetPrivateKeys(ctx context.Context, username string) ([][]byte, error) {
@@ -152,7 +153,9 @@ func (b *ldapBackend) GetPrivateKeys(ctx context.Context, username string) ([][]
 	var out [][]byte
 	for _, ent := range result.Entries {
 		for _, val := range ent.GetAttributeValues(b.config.Query.PrivateKeyAttr) {
-			out = append(out, decodePrivateKey([]byte(val)))
+			if key, err := decodePrivateKey(val); err == nil {
+				out = append(out, key)
+			}
 		}
 	}
 	return out, nil
diff --git a/vendor/git.autistici.org/ai3/go-common/ldap/compositetypes/composite_types.go b/vendor/git.autistici.org/ai3/go-common/ldap/compositetypes/composite_types.go
new file mode 100644
index 0000000000000000000000000000000000000000..32a1978c066daad0bf8edc452e61f73f8ac5d818
--- /dev/null
+++ b/vendor/git.autistici.org/ai3/go-common/ldap/compositetypes/composite_types.go
@@ -0,0 +1,158 @@
+// Package compositetypes provides Go types for the composite values
+// stored in our LDAP database, so that various authentication
+// packages can agree on their serialized representation.
+//
+// These are normally 1-to-many associations that are wrapped into
+// repeated LDAP attributes instead of separate nested objects, for
+// simplicity and latency reasons.
+//
+// Whenever there is an 'id' field, it's a unique (per-user)
+// identifier used to recognize a specific entry on modify/delete.
+//
+// The serialized values can be arbitrary []byte sequences (the LDAP
+// schema should specify the right types for the associated
+// attributes).
+//
+package compositetypes
+
+import (
+	"crypto/elliptic"
+	"errors"
+	"strings"
+
+	"github.com/tstranex/u2f"
+)
+
+// AppSpecificPassword stores information on an application-specific
+// password.
+//
+// Serialized as colon-separated fields with the format:
+//
+//     id:service:encrypted_password:comment
+//
+// Where 'comment' is free-form and can contain colons, no escaping is
+// performed.
+type AppSpecificPassword struct {
+	ID                string `json:"id"`
+	Service           string `json:"service"`
+	EncryptedPassword string `json:"encrypted_password"`
+	Comment           string `json:"comment"`
+}
+
+// Marshal returns the serialized format.
+func (p *AppSpecificPassword) Marshal() string {
+	return strings.Join([]string{
+		p.ID,
+		p.Service,
+		p.EncryptedPassword,
+		p.Comment,
+	}, ":")
+}
+
+// UnmarshalAppSpecificPassword parses a serialized representation of
+// an AppSpecificPassword.
+func UnmarshalAppSpecificPassword(s string) (*AppSpecificPassword, error) {
+	parts := strings.SplitN(s, ":", 4)
+	if len(parts) != 4 {
+		return nil, errors.New("badly encoded app-specific password")
+	}
+	return &AppSpecificPassword{
+		ID:                parts[0],
+		Service:           parts[1],
+		EncryptedPassword: parts[2],
+		Comment:           parts[3],
+	}, nil
+}
+
+// EncryptedKey stores a password-encrypted secret key.
+//
+// Serialized as colon-separated fields with the format:
+//
+//     id:encrypted_key
+//
+// The encrypted key is stored as a raw, unencoded byte sequence.
+type EncryptedKey struct {
+	ID           string `json:"id"`
+	EncryptedKey []byte `json:"encrypted_key"`
+}
+
+// Marshal returns the serialized format.
+func (k *EncryptedKey) Marshal() string {
+	var b []byte
+	b = append(b, []byte(k.ID)...)
+	b = append(b, ':')
+	b = append(b, k.EncryptedKey...)
+	return string(b)
+}
+
+// UnmarshalEncryptedKey parses the serialized representation of an
+// EncryptedKey.
+func UnmarshalEncryptedKey(s string) (*EncryptedKey, error) {
+	idx := strings.IndexByte(s, ':')
+	if idx < 0 {
+		return nil, errors.New("badly encoded key")
+	}
+	return &EncryptedKey{
+		ID:           s[:idx],
+		EncryptedKey: []byte(s[idx+1:]),
+	}, nil
+}
+
+// U2FRegistration stores information on a single U2F device
+// registration.
+//
+// The serialized format follows part of the U2F standard and just
+// stores 64 bytes of the public key immediately followed by the key
+// handle data, with no encoding.
+//
+// The data in U2FRegistration is still encoded, but it can be turned
+// into a usable form (github.com/tstranex/u2f.Registration) later.
+type U2FRegistration struct {
+	KeyHandle []byte `json:"key_handle"`
+	PublicKey []byte `json:"public_key"`
+}
+
+// Marshal returns the serialized format.
+func (r *U2FRegistration) Marshal() string {
+	var b []byte
+	b = append(b, r.PublicKey...)
+	b = append(b, r.KeyHandle...)
+	return string(b)
+}
+
+// UnmarshalU2FRegistration parses a U2FRegistration from its serialized format.
+func UnmarshalU2FRegistration(s string) (*U2FRegistration, error) {
+	if len(s) < 64 {
+		return nil, errors.New("badly encoded u2f registration")
+	}
+	b := []byte(s)
+	return &U2FRegistration{
+		PublicKey: b[:64],
+		KeyHandle: b[64:],
+	}, nil
+}
+
+// Decode returns a u2f.Registration object with the decoded public
+// key ready for use in verification.
+func (r *U2FRegistration) Decode() (*u2f.Registration, error) {
+	x, y := elliptic.Unmarshal(elliptic.P256(), r.PublicKey)
+	if x == nil {
+		return nil, errors.New("invalid public key")
+	}
+	var reg u2f.Registration
+	reg.PubKey.Curve = elliptic.P256()
+	reg.PubKey.X = x
+	reg.PubKey.Y = y
+	reg.KeyHandle = r.KeyHandle
+	return &reg, nil
+}
+
+// NewU2FRegistrationFromData creates a U2FRegistration from a
+// u2f.Registration object.
+func NewU2FRegistrationFromData(reg *u2f.Registration) *U2FRegistration {
+	pk := elliptic.Marshal(reg.PubKey.Curve, reg.PubKey.X, reg.PubKey.Y)
+	return &U2FRegistration{
+		PublicKey: pk,
+		KeyHandle: reg.KeyHandle,
+	}
+}
diff --git a/vendor/github.com/tstranex/u2f/LICENSE b/vendor/github.com/tstranex/u2f/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..3c7279c6fc954181e6686bbd495e31cec53365d0
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 The Go FIDO U2F Library Authors
+
+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/vendor/github.com/tstranex/u2f/README.md b/vendor/github.com/tstranex/u2f/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..95de78f8b5fc415857337860ac7dcc874e03e72e
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/README.md
@@ -0,0 +1,97 @@
+# Go FIDO U2F Library
+
+This Go package implements the parts of the FIDO U2F specification required on
+the server side of an application.
+
+[![Build Status](https://travis-ci.org/tstranex/u2f.svg?branch=master)](https://travis-ci.org/tstranex/u2f)
+
+## Features
+
+- Native Go implementation
+- No dependancies other than the Go standard library
+- Token attestation certificate verification
+
+## Usage
+
+Please visit http://godoc.org/github.com/tstranex/u2f for the full
+documentation.
+
+### How to enrol a new token
+
+```go
+app_id := "http://localhost"
+
+// Send registration request to the browser.
+c, _ := NewChallenge(app_id, []string{app_id})
+req, _ := c.RegisterRequest()
+
+// Read response from the browser.
+var resp RegisterResponse
+reg, err := Register(resp, c, nil)
+if err != nil {
+    // Registration failed.
+}
+
+// Store registration in the database.
+```
+
+### How to perform an authentication
+
+```go
+// Fetch registration and counter from the database.
+var reg Registration
+var counter uint32
+
+// Send authentication request to the browser.
+c, _ := NewChallenge(app_id, []string{app_id})
+req, _ := c.SignRequest(reg)
+
+// Read response from the browser.
+var resp SignResponse
+newCounter, err := reg.Authenticate(resp, c, counter)
+if err != nil {
+    // Authentication failed.
+}
+
+// Store updated counter in the database.
+```
+
+## Installation
+
+```
+$ go get github.com/tstranex/u2f
+```
+
+## Example
+
+See u2fdemo/main.go for an full example server. To run it:
+
+```
+$ go install github.com/tstranex/u2f/u2fdemo
+$ ./bin/u2fdemo
+```
+
+Open https://localhost:3483 in Chrome.
+Ignore the SSL warning (due to the self-signed certificate for localhost).
+You can then test registering and authenticating using your token.
+
+## Changelog
+
+- 2016-12-18: The package has been updated to work with the new
+  U2F Javascript 1.1 API specification. This causes some breaking changes.
+
+  `SignRequest` has been replaced by `WebSignRequest` which now includes
+  multiple registrations. This is useful when the user has multiple devices
+  registered since you can now authenticate against any of them with a single
+  request.
+
+  `WebRegisterRequest` has been introduced, which should generally be used
+  instead of using `RegisterRequest` directly. It includes the list of existing
+  registrations with the new registration request. If the user's device already
+  matches one of the existing registrations, it will refuse to re-register.
+
+  `Challenge.RegisterRequest` has been replaced by `NewWebRegisterRequest`.
+
+## License
+
+The Go FIDO U2F Library is licensed under the MIT License.
diff --git a/vendor/github.com/tstranex/u2f/auth.go b/vendor/github.com/tstranex/u2f/auth.go
new file mode 100644
index 0000000000000000000000000000000000000000..05c25f573101d96c10337932bd28dd87e3f02503
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/auth.go
@@ -0,0 +1,136 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+package u2f
+
+import (
+	"crypto/ecdsa"
+	"crypto/sha256"
+	"encoding/asn1"
+	"errors"
+	"math/big"
+	"time"
+)
+
+// SignRequest creates a request to initiate an authentication.
+func (c *Challenge) SignRequest(regs []Registration) *WebSignRequest {
+	var sr WebSignRequest
+	sr.AppID = c.AppID
+	sr.Challenge = encodeBase64(c.Challenge)
+	for _, r := range regs {
+		rk := getRegisteredKey(c.AppID, r)
+		sr.RegisteredKeys = append(sr.RegisteredKeys, rk)
+	}
+	return &sr
+}
+
+// ErrCounterTooLow is raised when the counter value received from the device is
+// lower than last stored counter value. This may indicate that the device has
+// been cloned (or is malfunctioning). The application may choose to disable
+// the particular device as precaution.
+var ErrCounterTooLow = errors.New("u2f: counter too low")
+
+// Authenticate validates a SignResponse authentication response.
+// An error is returned if any part of the response fails to validate.
+// The counter should be the counter associated with appropriate device
+// (i.e. resp.KeyHandle).
+// The latest counter value is returned, which the caller should store.
+func (reg *Registration) Authenticate(resp SignResponse, c Challenge, counter uint32) (newCounter uint32, err error) {
+	if time.Now().Sub(c.Timestamp) > timeout {
+		return 0, errors.New("u2f: challenge has expired")
+	}
+	if resp.KeyHandle != encodeBase64(reg.KeyHandle) {
+		return 0, errors.New("u2f: wrong key handle")
+	}
+
+	sigData, err := decodeBase64(resp.SignatureData)
+	if err != nil {
+		return 0, err
+	}
+
+	clientData, err := decodeBase64(resp.ClientData)
+	if err != nil {
+		return 0, err
+	}
+
+	ar, err := parseSignResponse(sigData)
+	if err != nil {
+		return 0, err
+	}
+
+	if ar.Counter < counter {
+		return 0, ErrCounterTooLow
+	}
+
+	if err := verifyClientData(clientData, c); err != nil {
+		return 0, err
+	}
+
+	if err := verifyAuthSignature(*ar, &reg.PubKey, c.AppID, clientData); err != nil {
+		return 0, err
+	}
+
+	if !ar.UserPresenceVerified {
+		return 0, errors.New("u2f: user was not present")
+	}
+
+	return ar.Counter, nil
+}
+
+type ecdsaSig struct {
+	R, S *big.Int
+}
+
+type authResp struct {
+	UserPresenceVerified bool
+	Counter              uint32
+	sig                  ecdsaSig
+	raw                  []byte
+}
+
+func parseSignResponse(sd []byte) (*authResp, error) {
+	if len(sd) < 5 {
+		return nil, errors.New("u2f: data is too short")
+	}
+
+	var ar authResp
+
+	userPresence := sd[0]
+	if userPresence|1 != 1 {
+		return nil, errors.New("u2f: invalid user presence byte")
+	}
+	ar.UserPresenceVerified = userPresence == 1
+
+	ar.Counter = uint32(sd[1])<<24 | uint32(sd[2])<<16 | uint32(sd[3])<<8 | uint32(sd[4])
+
+	ar.raw = sd[:5]
+
+	rest, err := asn1.Unmarshal(sd[5:], &ar.sig)
+	if err != nil {
+		return nil, err
+	}
+	if len(rest) != 0 {
+		return nil, errors.New("u2f: trailing data")
+	}
+
+	return &ar, nil
+}
+
+func verifyAuthSignature(ar authResp, pubKey *ecdsa.PublicKey, appID string, clientData []byte) error {
+	appParam := sha256.Sum256([]byte(appID))
+	challenge := sha256.Sum256(clientData)
+
+	var buf []byte
+	buf = append(buf, appParam[:]...)
+	buf = append(buf, ar.raw...)
+	buf = append(buf, challenge[:]...)
+	hash := sha256.Sum256(buf)
+
+	if !ecdsa.Verify(pubKey, hash[:], ar.sig.R, ar.sig.S) {
+		return errors.New("u2f: invalid signature")
+	}
+
+	return nil
+}
diff --git a/vendor/github.com/tstranex/u2f/certs.go b/vendor/github.com/tstranex/u2f/certs.go
new file mode 100644
index 0000000000000000000000000000000000000000..14d745a0095b565c9b8bdf00b892281f87105963
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/certs.go
@@ -0,0 +1,89 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+package u2f
+
+import (
+	"crypto/x509"
+	"log"
+)
+
+const plugUpCert = `-----BEGIN CERTIFICATE-----
+MIIBrjCCAVSgAwIBAgIJAMGSvUZlGSGVMAoGCCqGSM49BAMCMDIxMDAuBgNVBAMM
+J1BsdWctdXAgRklETyBJbnRlcm5hbCBBdHRlc3RhdGlvbiBDQSAjMTAeFw0xNDA5
+MjMxNjM3NTFaFw0zNDA5MjMxNjM3NTFaMDIxMDAuBgNVBAMMJ1BsdWctdXAgRklE
+TyBJbnRlcm5hbCBBdHRlc3RhdGlvbiBDQSAjMTBZMBMGByqGSM49AgEGCCqGSM49
+AwEHA0IABH9mscDgEHo4AUh7J8JHqRxsSVxbvsbe6Pxy5cUFKfQlWNjxRrZcbhOb
+UY3WsAwmKuUdOcghbpTILhdp8LG9z5GjUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYD
+VR0OBBYEFM+nRPKhYlDwOemShePaUOd9sDqoMB8GA1UdIwQYMBaAFM+nRPKhYlDw
+OemShePaUOd9sDqoMAoGCCqGSM49BAMCA0gAMEUCIQDVzqnX1rgvyJaZ7WZUm1ED
+hJKSsDxRXEnH+/voqpq/zgIgH4RUR6vr9YNrkzuCq5R07gF7P4qhtg/4jy+dhl7o
+NAU=
+-----END CERTIFICATE-----
+`
+
+const neowaveCert = `-----BEGIN CERTIFICATE-----
+MIICJDCCAcugAwIBAgIJAIo+0R9DGvSBMAoGCCqGSM49BAMCMG8xCzAJBgNVBAYT
+AkZSMQ8wDQYDVQQIDAZGcmFuY2UxETAPBgNVBAcMCEdhcmRhbm5lMRAwDgYDVQQK
+DAdOZW93YXZlMSowKAYDVQQDDCFOZW93YXZlIEtFWURPIEZJRE8gVTJGIENBIEJh
+dGNoIDEwHhcNMTUwMTI4MTA1ODM1WhcNMjUwMTI1MTA1ODM1WjBvMQswCQYDVQQG
+EwJGUjEPMA0GA1UECAwGRnJhbmNlMREwDwYDVQQHDAhHYXJkYW5uZTEQMA4GA1UE
+CgwHTmVvd2F2ZTEqMCgGA1UEAwwhTmVvd2F2ZSBLRVlETyBGSURPIFUyRiBDQSBC
+YXRjaCAxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEBlUmE1BRE/M/CE/ZCN+x
+eutfnVsThMwIDN+4DL9gqXoKCeRMiDQ1zwm/yQS80BYSEz7Du9RU+2mlnyhwhu+f
+BqNQME4wHQYDVR0OBBYEFF42te8/iq5HGom4sIhgkJWLq5jkMB8GA1UdIwQYMBaA
+FF42te8/iq5HGom4sIhgkJWLq5jkMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwID
+RwAwRAIgVTxBFb2Hclq5Yi5gQp6WoZAcHETfKASvTQVOE88REGQCIA5DcwGVLsZB
+QTb94Xgtb/WUieCvmwukFl/gEO15f3uA
+-----END CERTIFICATE-----
+`
+
+const yubicoRootCert = `-----BEGIN CERTIFICATE-----
+MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ
+dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw
+MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290
+IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk
+5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep
+8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw
+nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
+9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw
+LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ
+hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
+BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4
+MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt
+hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k
+LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U
+sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc
+U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==
+-----END CERTIFICATE-----
+`
+
+const entersektCert = `-----BEGIN CERTIFICATE-----
+MIICHjCCAcOgAwIBAgIBADAKBggqhkjOPQQDAjBvMQswCQYDVQQGEwJaQTEVMBMG
+A1UECAwMV2VzdGVybiBDYXBlMRUwEwYDVQQHDAxTdGVsbGVuYm9zY2gxEjAQBgNV
+BAoMCUVudGVyc2VrdDELMAkGA1UECwwCSVQxETAPBgNVBAMMCFRyYW5zYWt0MB4X
+DTE0MTEwMTExMjczNFoXDTE1MTEwMTExMjczNFowbzELMAkGA1UEBhMCWkExFTAT
+BgNVBAgMDFdlc3Rlcm4gQ2FwZTEVMBMGA1UEBwwMU3RlbGxlbmJvc2NoMRIwEAYD
+VQQKDAlFbnRlcnNla3QxCzAJBgNVBAsMAklUMREwDwYDVQQDDAhUcmFuc2FrdDBZ
+MBMGByqGSM49AgEGCCqGSM49AwEHA0IABBh10blFheMZy3k2iqW9TzLhS1DbJ/Xf
+DxqQJJkpqTLq7vI+K3O4C20YtN0jsVrj7UylWoSRlPL5F7IkbeQ6aZ6jUDBOMB0G
+A1UdDgQWBBQWRFF7mVAipWTdfBWk2B8Dv4Ab4jAfBgNVHSMEGDAWgBQWRFF7mVAi
+pWTdfBWk2B8Dv4Ab4jAMBgNVHRMEBTADAQH/MAoGCCqGSM49BAMCA0kAMEYCIQCo
+bMURXOxv6pqz6ECBh0zgL2vVhEfTOZJOW0PACGalWgIhAME0LHGi6ZS7z9yzHNqi
+cnRb+okM+PIy/hBcBuqTWCbw
+-----END CERTIFICATE-----
+`
+
+func mustLoadPool(pemCerts []byte) *x509.CertPool {
+	p := x509.NewCertPool()
+	if !p.AppendCertsFromPEM(pemCerts) {
+		log.Fatal("u2f: Error loading root cert pool.")
+		return nil
+	}
+	return p
+}
+
+var roots = mustLoadPool([]byte(yubicoRootCert + entersektCert + neowaveCert + plugUpCert))
diff --git a/vendor/github.com/tstranex/u2f/messages.go b/vendor/github.com/tstranex/u2f/messages.go
new file mode 100644
index 0000000000000000000000000000000000000000..a78038dea297e1edd9d7ecbea657d22042d67422
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/messages.go
@@ -0,0 +1,87 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+package u2f
+
+import (
+	"encoding/json"
+)
+
+// JwkKey represents a public key used by a browser for the Channel ID TLS
+// extension.
+type JwkKey struct {
+	KTy string `json:"kty"`
+	Crv string `json:"crv"`
+	X   string `json:"x"`
+	Y   string `json:"y"`
+}
+
+// ClientData as defined by the FIDO U2F Raw Message Formats specification.
+type ClientData struct {
+	Typ       string          `json:"typ"`
+	Challenge string          `json:"challenge"`
+	Origin    string          `json:"origin"`
+	CIDPubKey json.RawMessage `json:"cid_pubkey"`
+}
+
+// RegisterRequest as defined by the FIDO U2F Javascript API 1.1.
+type RegisterRequest struct {
+	Version   string `json:"version"`
+	Challenge string `json:"challenge"`
+}
+
+// WebRegisterRequest contains the parameters needed for the u2f.register()
+// high-level Javascript API function as defined by the
+// FIDO U2F Javascript API 1.1.
+type WebRegisterRequest struct {
+	AppID            string            `json:"appId"`
+	RegisterRequests []RegisterRequest `json:"registerRequests"`
+	RegisteredKeys   []RegisteredKey   `json:"registeredKeys"`
+}
+
+// RegisterResponse as defined by the FIDO U2F Javascript API 1.1.
+type RegisterResponse struct {
+	Version          string `json:"version"`
+	RegistrationData string `json:"registrationData"`
+	ClientData       string `json:"clientData"`
+}
+
+// RegisteredKey as defined by the FIDO U2F Javascript API 1.1.
+type RegisteredKey struct {
+	Version   string `json:"version"`
+	KeyHandle string `json:"keyHandle"`
+	AppID     string `json:"appId"`
+}
+
+// WebSignRequest contains the parameters needed for the u2f.sign()
+// high-level Javascript API function as defined by the
+// FIDO U2F Javascript API 1.1.
+type WebSignRequest struct {
+	AppID          string          `json:"appId"`
+	Challenge      string          `json:"challenge"`
+	RegisteredKeys []RegisteredKey `json:"registeredKeys"`
+}
+
+// SignResponse as defined by the FIDO U2F Javascript API 1.1.
+type SignResponse struct {
+	KeyHandle     string `json:"keyHandle"`
+	SignatureData string `json:"signatureData"`
+	ClientData    string `json:"clientData"`
+}
+
+// TrustedFacets as defined by the FIDO AppID and Facet Specification.
+type TrustedFacets struct {
+	Version struct {
+		Major int `json:"major"`
+		Minor int `json:"minor"`
+	} `json:"version"`
+	Ids []string `json:"ids"`
+}
+
+// TrustedFacetsEndpoint is a container of TrustedFacets.
+// It is used as the response for an appId URL endpoint.
+type TrustedFacetsEndpoint struct {
+	TrustedFacets []TrustedFacets `json:"trustedFacets"`
+}
diff --git a/vendor/github.com/tstranex/u2f/register.go b/vendor/github.com/tstranex/u2f/register.go
new file mode 100644
index 0000000000000000000000000000000000000000..da0c1cce246c2fb32547a31e6f3c3e203aa66bbb
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/register.go
@@ -0,0 +1,230 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+package u2f
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/asn1"
+	"encoding/hex"
+	"errors"
+	"time"
+)
+
+// Registration represents a single enrolment or pairing between an
+// application and a token. This data will typically be stored in a database.
+type Registration struct {
+	// Raw serialized registration data as received from the token.
+	Raw []byte
+
+	KeyHandle []byte
+	PubKey    ecdsa.PublicKey
+
+	// AttestationCert can be nil for Authenticate requests.
+	AttestationCert *x509.Certificate
+}
+
+// Config contains configurable options for the package.
+type Config struct {
+	// SkipAttestationVerify controls whether the token attestation
+	// certificate should be verified on registration. Ideally it should
+	// always be verified. However, there is currently no public list of
+	// trusted attestation root certificates so it may be necessary to skip.
+	SkipAttestationVerify bool
+
+	// RootAttestationCertPool overrides the default root certificates used
+	// to verify client attestations. If nil, this defaults to the roots that are
+	// bundled in this library.
+	RootAttestationCertPool *x509.CertPool
+}
+
+// Register validates a RegisterResponse message to enrol a new token.
+// An error is returned if any part of the response fails to validate.
+// The returned Registration should be stored by the caller.
+func Register(resp RegisterResponse, c Challenge, config *Config) (*Registration, error) {
+	if config == nil {
+		config = &Config{}
+	}
+
+	if time.Now().Sub(c.Timestamp) > timeout {
+		return nil, errors.New("u2f: challenge has expired")
+	}
+
+	regData, err := decodeBase64(resp.RegistrationData)
+	if err != nil {
+		return nil, err
+	}
+
+	clientData, err := decodeBase64(resp.ClientData)
+	if err != nil {
+		return nil, err
+	}
+
+	reg, sig, err := parseRegistration(regData)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := verifyClientData(clientData, c); err != nil {
+		return nil, err
+	}
+
+	if err := verifyAttestationCert(*reg, config); err != nil {
+		return nil, err
+	}
+
+	if err := verifyRegistrationSignature(*reg, sig, c.AppID, clientData); err != nil {
+		return nil, err
+	}
+
+	return reg, nil
+}
+
+func parseRegistration(buf []byte) (*Registration, []byte, error) {
+	if len(buf) < 1+65+1+1+1 {
+		return nil, nil, errors.New("u2f: data is too short")
+	}
+
+	var r Registration
+	r.Raw = buf
+
+	if buf[0] != 0x05 {
+		return nil, nil, errors.New("u2f: invalid reserved byte")
+	}
+	buf = buf[1:]
+
+	x, y := elliptic.Unmarshal(elliptic.P256(), buf[:65])
+	if x == nil {
+		return nil, nil, errors.New("u2f: invalid public key")
+	}
+	r.PubKey.Curve = elliptic.P256()
+	r.PubKey.X = x
+	r.PubKey.Y = y
+	buf = buf[65:]
+
+	khLen := int(buf[0])
+	buf = buf[1:]
+	if len(buf) < khLen {
+		return nil, nil, errors.New("u2f: invalid key handle")
+	}
+	r.KeyHandle = buf[:khLen]
+	buf = buf[khLen:]
+
+	// The length of the x509 cert isn't specified so it has to be inferred
+	// by parsing. We can't use x509.ParseCertificate yet because it returns
+	// an error if there are any trailing bytes. So parse raw asn1 as a
+	// workaround to get the length.
+	sig, err := asn1.Unmarshal(buf, &asn1.RawValue{})
+	if err != nil {
+		return nil, nil, err
+	}
+
+	buf = buf[:len(buf)-len(sig)]
+	fixCertIfNeed(buf)
+	cert, err := x509.ParseCertificate(buf)
+	if err != nil {
+		return nil, nil, err
+	}
+	r.AttestationCert = cert
+
+	return &r, sig, nil
+}
+
+// UnmarshalBinary implements encoding.BinaryMarshaler.
+func (r *Registration) UnmarshalBinary(data []byte) error {
+	reg, _, err := parseRegistration(data)
+	if err != nil {
+		return err
+	}
+	*r = *reg
+	return nil
+}
+
+// MarshalBinary implements encoding.BinaryUnmarshaler.
+func (r *Registration) MarshalBinary() ([]byte, error) {
+	return r.Raw, nil
+}
+
+func verifyAttestationCert(r Registration, config *Config) error {
+	if config.SkipAttestationVerify {
+		return nil
+	}
+	rootCertPool := roots
+	if config.RootAttestationCertPool != nil {
+		rootCertPool = config.RootAttestationCertPool
+	}
+
+	opts := x509.VerifyOptions{Roots: rootCertPool}
+	_, err := r.AttestationCert.Verify(opts)
+	return err
+}
+
+func verifyRegistrationSignature(
+	r Registration, signature []byte, appid string, clientData []byte) error {
+
+	appParam := sha256.Sum256([]byte(appid))
+	challenge := sha256.Sum256(clientData)
+
+	buf := []byte{0}
+	buf = append(buf, appParam[:]...)
+	buf = append(buf, challenge[:]...)
+	buf = append(buf, r.KeyHandle...)
+	pk := elliptic.Marshal(r.PubKey.Curve, r.PubKey.X, r.PubKey.Y)
+	buf = append(buf, pk...)
+
+	return r.AttestationCert.CheckSignature(
+		x509.ECDSAWithSHA256, buf, signature)
+}
+
+func getRegisteredKey(appID string, r Registration) RegisteredKey {
+	return RegisteredKey{
+		Version:   u2fVersion,
+		KeyHandle: encodeBase64(r.KeyHandle),
+		AppID:     appID,
+	}
+}
+
+// fixCertIfNeed fixes broken certificates described in
+// https://github.com/Yubico/php-u2flib-server/blob/master/src/u2flib_server/U2F.php#L84
+func fixCertIfNeed(cert []byte) {
+	h := sha256.Sum256(cert)
+	switch hex.EncodeToString(h[:]) {
+	case
+		"349bca1031f8c82c4ceca38b9cebf1a69df9fb3b94eed99eb3fb9aa3822d26e8",
+		"dd574527df608e47ae45fbba75a2afdd5c20fd94a02419381813cd55a2a3398f",
+		"1d8764f0f7cd1352df6150045c8f638e517270e8b5dda1c63ade9c2280240cae",
+		"d0edc9a91a1677435a953390865d208c55b3183c6759c9b5a7ff494c322558eb",
+		"6073c436dcd064a48127ddbf6032ac1a66fd59a0c24434f070d4e564c124c897",
+		"ca993121846c464d666096d35f13bf44c1b05af205f9b4a1e00cf6cc10c5e511":
+
+		// clear the offending byte.
+		cert[len(cert)-257] = 0
+	}
+}
+
+// NewWebRegisterRequest creates a request to enrol a new token.
+// regs is the list of the user's existing registration. The browser will
+// refuse to re-register a device if it has an existing registration.
+func NewWebRegisterRequest(c *Challenge, regs []Registration) *WebRegisterRequest {
+	req := RegisterRequest{
+		Version:   u2fVersion,
+		Challenge: encodeBase64(c.Challenge),
+	}
+
+	rr := WebRegisterRequest{
+		AppID:            c.AppID,
+		RegisterRequests: []RegisterRequest{req},
+	}
+
+	for _, r := range regs {
+		rk := getRegisteredKey(c.AppID, r)
+		rr.RegisteredKeys = append(rr.RegisteredKeys, rk)
+	}
+
+	return &rr
+}
diff --git a/vendor/github.com/tstranex/u2f/util.go b/vendor/github.com/tstranex/u2f/util.go
new file mode 100644
index 0000000000000000000000000000000000000000..f035aa417bffe8d55229e57e33501e0a628c18e1
--- /dev/null
+++ b/vendor/github.com/tstranex/u2f/util.go
@@ -0,0 +1,125 @@
+// Go FIDO U2F Library
+// Copyright 2015 The Go FIDO U2F Library Authors. All rights reserved.
+// Use of this source code is governed by the MIT
+// license that can be found in the LICENSE file.
+
+/*
+Package u2f implements the server-side parts of the
+FIDO Universal 2nd Factor (U2F) specification.
+
+Applications will usually persist Challenge and Registration objects in a
+database.
+
+To enrol a new token:
+
+    app_id := "http://localhost"
+    c, _ := NewChallenge(app_id, []string{app_id})
+    req, _ := u2f.NewWebRegisterRequest(c, existingTokens)
+    // Send the request to the browser.
+    var resp RegisterResponse
+    // Read resp from the browser.
+    reg, err := Register(resp, c)
+    if err != nil {
+         // Registration failed.
+    }
+    // Store reg in the database.
+
+To perform an authentication:
+
+    var regs []Registration
+    // Fetch regs from the database.
+    c, _ := NewChallenge(app_id, []string{app_id})
+    req, _ := c.SignRequest(regs)
+    // Send the request to the browser.
+    var resp SignResponse
+    // Read resp from the browser.
+    new_counter, err := reg.Authenticate(resp, c)
+    if err != nil {
+        // Authentication failed.
+    }
+    reg.Counter = new_counter
+    // Store updated Registration in the database.
+
+The FIDO U2F specification can be found here:
+https://fidoalliance.org/specifications/download
+*/
+package u2f
+
+import (
+	"crypto/rand"
+	"crypto/subtle"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
+	"strings"
+	"time"
+)
+
+const u2fVersion = "U2F_V2"
+const timeout = 5 * time.Minute
+
+func decodeBase64(s string) ([]byte, error) {
+	for i := 0; i < len(s)%4; i++ {
+		s += "="
+	}
+	return base64.URLEncoding.DecodeString(s)
+}
+
+func encodeBase64(buf []byte) string {
+	s := base64.URLEncoding.EncodeToString(buf)
+	return strings.TrimRight(s, "=")
+}
+
+// Challenge represents a single transaction between the server and
+// authenticator. This data will typically be stored in a database.
+type Challenge struct {
+	Challenge     []byte
+	Timestamp     time.Time
+	AppID         string
+	TrustedFacets []string
+}
+
+// NewChallenge generates a challenge for the given application.
+func NewChallenge(appID string, trustedFacets []string) (*Challenge, error) {
+	challenge := make([]byte, 32)
+	n, err := rand.Read(challenge)
+	if err != nil {
+		return nil, err
+	}
+	if n != 32 {
+		return nil, errors.New("u2f: unable to generate random bytes")
+	}
+
+	var c Challenge
+	c.Challenge = challenge
+	c.Timestamp = time.Now()
+	c.AppID = appID
+	c.TrustedFacets = trustedFacets
+	return &c, nil
+}
+
+func verifyClientData(clientData []byte, challenge Challenge) error {
+	var cd ClientData
+	if err := json.Unmarshal(clientData, &cd); err != nil {
+		return err
+	}
+
+	foundFacetID := false
+	for _, facetID := range challenge.TrustedFacets {
+		if facetID == cd.Origin {
+			foundFacetID = true
+			break
+		}
+	}
+	if !foundFacetID {
+		return errors.New("u2f: untrusted facet id")
+	}
+
+	c := encodeBase64(challenge.Challenge)
+	if len(c) != len(cd.Challenge) ||
+		subtle.ConstantTimeCompare([]byte(c), []byte(cd.Challenge)) != 1 {
+		return errors.New("u2f: challenge does not match")
+	}
+
+	return nil
+}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 85e292fa96f1b50ea1174f278b2d323456c0f394..d2f3465b6984910600e36024e04d70d104cd7d17 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -20,6 +20,12 @@
 			"revision": "2934fd63c275d37b0fe60afabb484a251662bd49",
 			"revisionTime": "2019-02-17T09:01:06Z"
 		},
+		{
+			"checksumSHA1": "X14iCbFCOfaIai/TPi4VJ/OBZjc=",
+			"path": "git.autistici.org/ai3/go-common/ldap/compositetypes",
+			"revision": "301958e3493e263eb6ea269bf7b8644fbcd97394",
+			"revisionTime": "2019-03-21T10:42:03Z"
+		},
 		{
 			"checksumSHA1": "TKGUNmKxj7KH3qhwiCh/6quUnwc=",
 			"path": "git.autistici.org/ai3/go-common/serverutil",
@@ -236,6 +242,12 @@
 			"revision": "971941c0819da74ed7c5d5329b7cebaa5a6f276c",
 			"revisionTime": "2018-10-23T08:20:22Z"
 		},
+		{
+			"checksumSHA1": "NE1kNfAZ0AAXCUbwx196os/DSUE=",
+			"path": "github.com/tstranex/u2f",
+			"revision": "d21a03e0b1d9fc1df59ff54e7a513655c1748b0c",
+			"revisionTime": "2018-05-05T18:51:14Z"
+		},
 		{
 			"checksumSHA1": "C9EIZQEMR5q5zVZCo1OtPWSV39I=",
 			"path": "go.opencensus.io",