Skip to content
Snippets Groups Projects

Add support for WebAuthN registrations

Merged ale requested to merge webauthn into master
Files
3
@@ -16,11 +16,16 @@
package compositetypes
import (
"crypto/elliptic"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/tstranex/u2f"
"github.com/duo-labs/webauthn/protocol/webauthncose"
"github.com/duo-labs/webauthn/webauthn"
"github.com/fxamacker/cbor/v2"
)
// AppSpecificPassword stores information on an application-specific
@@ -98,62 +103,152 @@ func UnmarshalEncryptedKey(s string) (*EncryptedKey, error) {
}, nil
}
// U2FRegistration stores information on a single U2F device
// U2FRegistration stores information on a single WebAuthN/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. Note that the public key itself is a
// serialization of the elliptic curve parameters.
// The public key is expected to be in raw COSE format. Note that on
// the wire (i.e. when serialized as JSON) both the public key and the
// key handle are base64-encoded.
//
// The data in U2FRegistration is still encoded, but it can be turned
// into a usable form (github.com/tstranex/u2f.Registration) later.
// It is possible to obtain a usable webauthn.Credential object at
// run-time by calling Decode().
type U2FRegistration struct {
KeyHandle []byte `json:"key_handle"`
PublicKey []byte `json:"public_key"`
Comment string `json:"comment"`
Legacy bool `json:"-"`
}
// 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)
data, err := json.Marshal(r)
if err != nil {
panic(err)
}
return string(data)
}
const (
legacySerializedU2FKeySize = 65
minU2FKeySize = 64
)
// UnmarshalU2FRegistration parses a U2FRegistration from its serialized format.
func UnmarshalU2FRegistration(s string) (*U2FRegistration, error) {
if len(s) < 65 {
// Try JSON first.
var reg U2FRegistration
if err := json.NewDecoder(strings.NewReader(s)).Decode(&reg); err == nil {
return &reg, nil
}
// Deserialize legacy format, and perform a conversion of the
// public key to COSE format.
if len(s) < legacySerializedU2FKeySize {
return nil, errors.New("badly encoded u2f registration")
}
b := []byte(s)
return &U2FRegistration{
PublicKey: b[:65],
KeyHandle: b[65:],
PublicKey: u2fToCOSE(b[:legacySerializedU2FKeySize]),
KeyHandle: b[legacySerializedU2FKeySize:],
Legacy: true,
}, 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")
// ParseLegacyU2FRegistrationFromStrings parses the legacy U2F format used
// in manual key specifications etc. which consists of a
// base64(url)-encoded key handle, and a hex-encoded public key (in
// legacy U2F format).
func ParseLegacyU2FRegistrationFromStrings(keyHandle, publicKey string) (*U2FRegistration, error) {
// U2F key handles are base64(url)-encoded (no trailing =s).
kh, err := base64.RawURLEncoding.DecodeString(keyHandle)
if err != nil {
return nil, fmt.Errorf("error decoding key handle: %w", err)
}
var reg u2f.Registration
reg.PubKey.Curve = elliptic.P256()
reg.PubKey.X = x
reg.PubKey.Y = y
reg.KeyHandle = r.KeyHandle
return &reg, nil
// U2F public keys are hex-encoded.
pk, err := hex.DecodeString(publicKey)
if err != nil {
return nil, fmt.Errorf("error decoding public key: %w", err)
}
// Simple sanity check for non-empty fields.
if len(kh) == 0 {
return nil, errors.New("missing key handle")
}
if len(pk) < minU2FKeySize {
return nil, errors.New("public key missing or too short")
}
return &U2FRegistration{
PublicKey: u2fToCOSE(pk),
KeyHandle: kh,
Legacy: true,
}, 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)
// ParseU2FRegistrationFromStrings parses the U2F registration format
// used in manual key specifications that is used by Fido2-aware
// programs such as pamu2fcfg >= 1.0.0. Both parameters are
// base64-encoded, public key should be in COSE format.
func ParseU2FRegistrationFromStrings(keyHandle, publicKey string) (*U2FRegistration, error) {
kh, err := base64.StdEncoding.DecodeString(keyHandle)
if err != nil {
return nil, fmt.Errorf("error decoding key handle: %w", err)
}
pk, err := base64.StdEncoding.DecodeString(publicKey)
if err != nil {
return nil, fmt.Errorf("error decoding public key: %w", err)
}
// Simple sanity check for non-empty fields.
if len(kh) == 0 {
return nil, errors.New("missing key handle")
}
if len(pk) < minU2FKeySize {
return nil, errors.New("public key missing or too short")
}
return &U2FRegistration{
PublicKey: pk,
KeyHandle: reg.KeyHandle,
}
KeyHandle: kh,
}, nil
}
// Decode returns a u2f.Registration object with the decoded public
// key ready for use in verification.
func (r *U2FRegistration) Decode() (webauthn.Credential, error) {
return webauthn.Credential{
ID: r.KeyHandle,
PublicKey: r.PublicKey,
}, nil
}
// Convert a legacy U2F public key to COSE format.
func u2fToCOSE(pk []byte) []byte {
var key webauthncose.EC2PublicKeyData
key.KeyType = int64(webauthncose.EllipticKey)
key.Algorithm = int64(webauthncose.AlgES256)
key.XCoord = pk[1:33]
key.YCoord = pk[33:]
data, _ := cbor.Marshal(&key) // nolint: errcheck
return data
}
// Faster, but more questionable, implementation of the above:
//
// func u2fToCOSE(pk []byte) []byte {
// x := pk[1:33]
// y := pk[33:]
// out := []byte{
// 0xa4,
// 0x01, 0x02,
// 0x03, 0x26,
// 0x21, 0x58, 0x20,
// }
// out = append(out, x...)
// out = append(out, []byte{
// 0x22, 0x58, 0x20,
// }...)
// out = append(out, y...)
// return out
// }
Loading