Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
webauthnutil: add helpers for webauthn (#2686)
* devices: add device protobuf types * webauthnutil: add helpers for webauthn
- Loading branch information
1 parent
961bc8a
commit 1c445c4
Showing
13 changed files
with
872 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Oops, something went wrong.