Skip to content

Commit

Permalink
webauthnutil: add helpers for webauthn (#2686)
Browse files Browse the repository at this point in the history
* devices: add device protobuf types

* webauthnutil: add helpers for webauthn
  • Loading branch information
calebdoxsey committed Oct 19, 2021
1 parent 961bc8a commit 1c445c4
Show file tree
Hide file tree
Showing 13 changed files with 872 additions and 2 deletions.
6 changes: 5 additions & 1 deletion go.mod
Expand Up @@ -43,6 +43,7 @@ require (
github.com/ory/dockertest/v3 v3.8.0
github.com/peterbourgon/ff/v3 v3.1.2
github.com/pomerium/csrf v1.7.0
github.com/pomerium/webauthn v0.0.0-20211014213840-422c7ce1077f
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.31.1
Expand Down Expand Up @@ -115,6 +116,7 @@ require (
github.com/fatih/color v1.12.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/fxamacker/cbor/v2 v2.3.0 // indirect
github.com/fzipp/gocyclo v0.3.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-critic/go-critic v0.5.6 // indirect
Expand Down Expand Up @@ -142,6 +144,7 @@ require (
github.com/golangci/misspell v0.3.5 // indirect
github.com/golangci/revgrep v0.0.0-20210208091834-cd28932614b5 // indirect
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
github.com/google/go-tpm v0.3.2 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20210225214923-2e10b2664254 // indirect
Expand Down Expand Up @@ -227,6 +230,7 @@ require (
github.com/ultraware/funlen v0.0.3 // indirect
github.com/ultraware/whitespace v0.0.4 // indirect
github.com/uudashr/gocognit v1.0.5 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
Expand All @@ -242,7 +246,7 @@ require (
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.22.0 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
honnef.co/go/tools v0.2.1 // indirect
mvdan.cc/gofumpt v0.1.1 // indirect
Expand Down
16 changes: 15 additions & 1 deletion go.sum
Expand Up @@ -436,6 +436,8 @@ github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWp
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM=
github.com/fxamacker/cbor/v2 v2.3.0 h1:aM45YGMctNakddNNAezPxDUpv38j44Abh+hifNuqXik=
github.com/fxamacker/cbor/v2 v2.3.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/fzipp/gocyclo v0.3.1 h1:A9UeX3HJSXTBzvHzhqoYVuE0eAhe+aM8XBCCwsPMZOc=
github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
Expand Down Expand Up @@ -611,6 +613,12 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-jsonnet v0.17.0 h1:/9NIEfhK1NQRKl3sP2536b2+x5HnZMdql7x3yK/l8JY=
github.com/google/go-jsonnet v0.17.0/go.mod h1:sOcuej3UW1vpPTZOr8L7RQimqai1a57bt5j22LzGZCw=
github.com/google/go-tpm v0.1.2-0.20190725015402-ae6dd98980d4/go.mod h1:H9HbmUG2YgV/PHITkO7p6wxEEj/v5nlsVWIwumwH2NI=
github.com/google/go-tpm v0.3.0/go.mod h1:iVLWvrPp/bHeEkxTFi9WG6K9w0iy2yIszHwZGHPbzAw=
github.com/google/go-tpm v0.3.2 h1:3iQQ2dlEf+1no7CLlfLPYzxhQy7j2G/emBqU5okydaw=
github.com/google/go-tpm v0.3.2/go.mod h1:j71sMBTfp3X5jPHz852ZOfQMUOf65Gb/Th8pRmp7fvg=
github.com/google/go-tpm-tools v0.0.0-20190906225433-1614c142f845/go.mod h1:AVfHadzbdzHo54inR2x1v640jdi1YSi3NauM2DUsxk0=
github.com/google/go-tpm-tools v0.2.0/go.mod h1:npUd03rQ60lxN7tzeBJreG38RvWwme2N1reF/eeiBk4=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
Expand Down Expand Up @@ -1044,6 +1052,8 @@ github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349 h1:Kq/3kL0k
github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
github.com/pomerium/csrf v1.7.0 h1:Qp4t6oyEod3svQtKfJZs589mdUTWKVf7q0PgCKYCshY=
github.com/pomerium/csrf v1.7.0/go.mod h1:hAPZV47mEj2T9xFs+ysbum4l7SF1IdrryYaY6PdoIqw=
github.com/pomerium/webauthn v0.0.0-20211014213840-422c7ce1077f h1:442shkoI4Oh4RHdzFaGma1t9Ji/T+8pfCxQQzmY5kj8=
github.com/pomerium/webauthn v0.0.0-20211014213840-422c7ce1077f/go.mod h1:wgH3ualWdXu/qwbhOoSQedXzco+38Iz7qKKGCJcKPXg=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
Expand Down Expand Up @@ -1275,6 +1285,8 @@ github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
Expand Down Expand Up @@ -1575,6 +1587,7 @@ golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -1914,8 +1927,9 @@ gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
Expand Down
61 changes: 61 additions & 0 deletions pkg/webauthnutil/credential_storage.go
@@ -0,0 +1,61 @@
package webauthnutil

import (
"context"

"github.com/btcsuite/btcutil/base58"
"github.com/pomerium/webauthn"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/pomerium/pomerium/pkg/grpc/databroker"
"github.com/pomerium/pomerium/pkg/grpc/device"
)

// CredentialStorage stores credentials in the databroker.
type CredentialStorage struct {
client databroker.DataBrokerServiceClient
}

// NewCredentialStorage creates a new CredentialStorage.
func NewCredentialStorage(client databroker.DataBrokerServiceClient) *CredentialStorage {
return &CredentialStorage{
client: client,
}
}

// GetCredential gets a credential from the databroker.
func (storage *CredentialStorage) GetCredential(
ctx context.Context,
credentialID []byte,
) (*webauthn.Credential, error) {
record, err := device.GetOwnerCredentialRecord(ctx, storage.client, credentialID)
if status.Code(err) == codes.NotFound {
return nil, webauthn.ErrCredentialNotFound
} else if err != nil {
return nil, err
}
return &webauthn.Credential{
ID: record.GetId(),
OwnerID: record.GetOwnerId(),
PublicKey: record.GetPublicKey(),
}, nil
}

// SetCredential sets the credential for the enrollment.
func (storage *CredentialStorage) SetCredential(
ctx context.Context,
credential *webauthn.Credential,
) error {
record := &device.OwnerCredentialRecord{
Id: credential.ID,
OwnerId: credential.OwnerID,
PublicKey: credential.PublicKey,
}
return device.PutOwnerCredentialRecord(ctx, storage.client, record)
}

// GetDeviceCredentialID gets the device credential id from a public key credential id.
func GetDeviceCredentialID(credentialID []byte) string {
return base58.Encode(credentialID)
}
60 changes: 60 additions & 0 deletions pkg/webauthnutil/credential_storage_test.go
@@ -0,0 +1,60 @@
package webauthnutil

import (
"context"
"testing"

"github.com/pomerium/webauthn"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/pomerium/pomerium/pkg/grpc/databroker"
)

type mockDataBrokerServiceClient struct {
databroker.DataBrokerServiceClient

get func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error)
put func(ctx context.Context, in *databroker.PutRequest, opts ...grpc.CallOption) (*databroker.PutResponse, error)
}

func (m mockDataBrokerServiceClient) Get(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
return m.get(ctx, in, opts...)
}

func (m mockDataBrokerServiceClient) Put(ctx context.Context, in *databroker.PutRequest, opts ...grpc.CallOption) (*databroker.PutResponse, error) {
return m.put(ctx, in, opts...)
}

func TestCredentialStorage(t *testing.T) {
m := map[string]*databroker.Record{}
client := &mockDataBrokerServiceClient{
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
record, ok := m[in.GetType()+"/"+in.GetId()]
if !ok {
return nil, status.Error(codes.NotFound, "record not found")
}
return &databroker.GetResponse{
Record: record,
}, nil
},
put: func(ctx context.Context, in *databroker.PutRequest, opts ...grpc.CallOption) (*databroker.PutResponse, error) {
m[in.GetRecord().GetType()+"/"+in.GetRecord().GetId()] = in.GetRecord()
return &databroker.PutResponse{
Record: in.GetRecord(),
}, nil
},
}
storage := NewCredentialStorage(client)
_, err := storage.GetCredential(context.Background(), []byte{0, 1, 2, 3, 4})
assert.ErrorIs(t, err, webauthn.ErrCredentialNotFound)
err = storage.SetCredential(context.Background(), &webauthn.Credential{
ID: []byte{0, 1, 2, 3, 4},
})
assert.NoError(t, err)
c, err := storage.GetCredential(context.Background(), []byte{0, 1, 2, 3, 4})
assert.NoError(t, err)
assert.Equal(t, []byte{0, 1, 2, 3, 4}, c.ID)
}
52 changes: 52 additions & 0 deletions pkg/webauthnutil/device_type.go
@@ -0,0 +1,52 @@
package webauthnutil

import (
"context"

"github.com/pomerium/webauthn/cose"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/pomerium/pomerium/pkg/grpc/databroker"
"github.com/pomerium/pomerium/pkg/grpc/device"
)

var predefinedDeviceTypes = map[string]*device.Type{
"default": {
Id: "default",
Name: "default",
Specifier: &device.Type_Webauthn{
Webauthn: &device.Type_WebAuthn{
Options: &device.WebAuthnOptions{
Attestation: device.WebAuthnOptions_DIRECT.Enum(),
AuthenticatorSelection: &device.WebAuthnOptions_AuthenticatorSelectionCriteria{
UserVerification: device.WebAuthnOptions_USER_VERIFICATION_PREFERRED.Enum(),
},
PubKeyCredParams: []*device.WebAuthnOptions_PublicKeyCredentialParameters{
{Type: device.WebAuthnOptions_PUBLIC_KEY, Alg: int64(cose.AlgorithmES256)},
{Type: device.WebAuthnOptions_PUBLIC_KEY, Alg: int64(cose.AlgorithmRS256)},
{Type: device.WebAuthnOptions_PUBLIC_KEY, Alg: int64(cose.AlgorithmRS1)},
},
},
},
},
},
}

// GetDeviceType gets the device type from the databroker. If the device type does not exist in the databroker
// a pre-defined device type may be returned.
func GetDeviceType(
ctx context.Context,
client databroker.DataBrokerServiceClient,
deviceTypeID string,
) (*device.Type, error) {
deviceType, err := device.GetType(ctx, client, deviceTypeID)
if status.Code(err) == codes.NotFound {
var ok bool
deviceType, ok = predefinedDeviceTypes[deviceTypeID]
if ok {
err = nil
}
}
return deviceType, err
}
61 changes: 61 additions & 0 deletions pkg/webauthnutil/device_type_test.go
@@ -0,0 +1,61 @@
package webauthnutil

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/anypb"

"github.com/pomerium/pomerium/pkg/grpc/databroker"
"github.com/pomerium/pomerium/pkg/grpc/device"
)

func TestGetDeviceType(t *testing.T) {
ctx := context.Background()

t.Run("from databroker", func(t *testing.T) {
client := &mockDataBrokerServiceClient{
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
assert.Equal(t, "type.googleapis.com/pomerium.device.Type", in.GetType())
assert.Equal(t, "default", in.GetId())
any, _ := anypb.New(&device.Type{
Id: "default",
Name: "Example",
})
return &databroker.GetResponse{
Record: &databroker.Record{
Type: in.GetType(),
Id: in.GetId(),
Data: any,
},
}, nil
},
}
deviceType, err := GetDeviceType(ctx, client, "default")
assert.NoError(t, err)
assert.Equal(t, "Example", deviceType.GetName())
})
t.Run("default", func(t *testing.T) {
client := &mockDataBrokerServiceClient{
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
return nil, status.Error(codes.NotFound, "not found")
},
}
deviceType, err := GetDeviceType(ctx, client, "default")
assert.NoError(t, err)
assert.Equal(t, "default", deviceType.GetName())
})
t.Run("not found", func(t *testing.T) {
client := &mockDataBrokerServiceClient{
get: func(ctx context.Context, in *databroker.GetRequest, opts ...grpc.CallOption) (*databroker.GetResponse, error) {
return nil, status.Error(codes.NotFound, "not found")
},
}
_, err := GetDeviceType(ctx, client, "example")
assert.Error(t, err)
})
}
35 changes: 35 additions & 0 deletions pkg/webauthnutil/enrollment_token.go
@@ -0,0 +1,35 @@
package webauthnutil

import (
"time"

"github.com/google/uuid"

"github.com/pomerium/pomerium/pkg/cryptutil"
)

// NewEnrollmentToken creates a new EnrollmentToken.
func NewEnrollmentToken(key []byte, ttl time.Duration, deviceEnrollmentID string) (string, error) {
id, err := uuid.Parse(deviceEnrollmentID)
if err != nil {
return "", err
}

secureToken := cryptutil.GenerateSecureToken(key, time.Now().Add(ttl), cryptutil.Token(id))
return secureToken.String(), nil
}

// ParseAndVerifyEnrollmentToken parses and verifies an enrollment token
func ParseAndVerifyEnrollmentToken(key []byte, rawEnrollmentToken string) (string, error) {
secureToken, ok := cryptutil.SecureTokenFromString(rawEnrollmentToken)
if !ok {
return "", cryptutil.ErrInvalid
}

err := secureToken.Verify(key, time.Now())
if err != nil {
return "", err
}

return secureToken.Token().UUID().String(), nil
}
18 changes: 18 additions & 0 deletions pkg/webauthnutil/enrollment_token_test.go
@@ -0,0 +1,18 @@
package webauthnutil

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestEnrollmentToken(t *testing.T) {
key := []byte{1, 2, 3}
deviceEnrollmentID := "19be0131-184e-4873-acab-2be79321c30b"
token, err := NewEnrollmentToken(key, time.Second*30, deviceEnrollmentID)
assert.NoError(t, err)
id, err := ParseAndVerifyEnrollmentToken(key, token)
assert.NoError(t, err)
assert.Equal(t, deviceEnrollmentID, id)
}

0 comments on commit 1c445c4

Please sign in to comment.