Skip to content

Commit

Permalink
feat: Implement the WebAuthn credProps extension (#45252)
Browse files Browse the repository at this point in the history
* Add credProps support to webauthn protos

* Update generated protos

* Implement the credProps extension
  • Loading branch information
codingllama authored Aug 15, 2024
1 parent 079f806 commit b1b26f5
Show file tree
Hide file tree
Showing 11 changed files with 556 additions and 76 deletions.
16 changes: 16 additions & 0 deletions api/proto/teleport/legacy/types/webauthn/webauthn.proto
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ message AuthenticationExtensionsClientInputs {
// Only available if using U2F compatibility mode.
// https://www.w3.org/TR/webauthn-2/#sctn-appid-extension.
string app_id = 1;

// Enables the credProps extension.
// https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension
bool cred_props = 2;
}

// Extensions supplied by the authenticator to the Relying Party, during
Expand All @@ -183,6 +187,18 @@ message AuthenticationExtensionsClientOutputs {
// the rpIdHash accordingly.
// https://www.w3.org/TR/webauthn-2/#sctn-appid-extension.
bool app_id = 1;

// Credential properties per credProps extension.
// https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension.
CredentialPropertiesOutput cred_props = 2;
}

// CredentialPropertiesOutput is the output of the credProps extension.
message CredentialPropertiesOutput {
// If true, the created credential is a resident key (regardless of the
// AuthenticatorSelection.require_resident_key value).
// OPTIONAL by specification.
bool rk = 1;
}

// Authenticator selection criteria.
Expand Down
424 changes: 354 additions & 70 deletions api/types/webauthn/webauthn.pb.go

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions lib/auth/mocku2f/mocku2f.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ type Key struct {
// AllowResidentKey should be paired only with WebAuthn registration methods,
// as it makes Key mimic a WebAuthn device.
AllowResidentKey bool
// ReplyWithCredProps sets the credProps extension (rk=true) in
// SignCredentialCreation responses, regardless of other parameters.
// Useful for extension testing.
ReplyWithCredProps bool

counter uint32
}
Expand Down
12 changes: 11 additions & 1 deletion lib/auth/mocku2f/webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,23 @@ func (muk *Key) SignCredentialCreation(origin string, cc *wantypes.CredentialCre
muk.UserHandle = cc.Response.User.ID
}

var exts *wantypes.AuthenticationExtensionsClientOutputs
if muk.ReplyWithCredProps {
exts = &wantypes.AuthenticationExtensionsClientOutputs{
CredProps: &wantypes.CredentialPropertiesOutput{
RK: true,
},
}
}

return &wantypes.CredentialCreationResponse{
PublicKeyCredential: wantypes.PublicKeyCredential{
Credential: wantypes.Credential{
ID: base64.RawURLEncoding.EncodeToString(muk.KeyHandle),
Type: string(protocol.PublicKeyCredentialType),
},
RawID: muk.KeyHandle,
RawID: muk.KeyHandle,
Extensions: exts,
},
AttestationResponse: wantypes.AuthenticatorAttestationResponse{
AuthenticatorResponse: wantypes.AuthenticatorResponse{
Expand Down
19 changes: 17 additions & 2 deletions lib/auth/webauthn/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,15 @@ func (f *RegistrationFlow) Begin(ctx context.Context, user string, passwordless
if err != nil {
return nil, trace.Wrap(err)
}
cc, sessionData, err := web.BeginRegistration(u, wan.WithExclusions(exclusions))
cc, sessionData, err := web.BeginRegistration(
u,
wan.WithExclusions(exclusions),
wan.WithExtensions(protocol.AuthenticationExtensions{
// Query authenticator on whether the resulting credential is resident,
// despite our requirements.
wantypes.CredPropsExtension: true,
}),
)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -347,7 +355,7 @@ func (f *RegistrationFlow) Finish(ctx context.Context, req RegisterResponse) (*t
Aaguid: credential.Authenticator.AAGUID,
SignatureCounter: credential.Authenticator.SignCount,
AttestationObject: req.CreationResponse.AttestationResponse.AttestationObject,
ResidentKey: req.Passwordless,
ResidentKey: req.Passwordless || hasCredPropsRK(req.CreationResponse),
CredentialRpId: f.Webauthn.RPID,
CredentialBackupEligible: &gogotypes.BoolValue{
Value: credential.Flags.BackupEligible,
Expand Down Expand Up @@ -391,3 +399,10 @@ func parseCredentialCreationResponse(resp *wantypes.CredentialCreationResponse)
parsedResp, err := protocol.ParseCredentialCreationResponseBody(bytes.NewReader(body))
return parsedResp, trace.Wrap(err)
}

func hasCredPropsRK(ccr *wantypes.CredentialCreationResponse) bool {
return ccr != nil &&
ccr.Extensions != nil &&
ccr.Extensions.CredProps != nil &&
ccr.Extensions.CredProps.RK
}
41 changes: 41 additions & 0 deletions lib/auth/webauthn/register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
gogotypes "github.com/gogo/protobuf/types"
"github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/types"
Expand Down Expand Up @@ -543,6 +544,46 @@ func TestIssue31187_errorParsingAttestationResponse(t *testing.T) {
require.NoError(t, err, "ParseCredentialCreationResponseBody failed")
}

func TestRegistrationFlow_credProps(t *testing.T) {
key, err := mocku2f.Create()
require.NoError(t, err, "Create failed")

const user = "llama"
const origin = "https://localhost"
ctx := context.Background()

rf := &wanlib.RegistrationFlow{
Webauthn: &types.Webauthn{
RPID: "localhost",
},
Identity: newFakeIdentity(user),
}

// Begin ceremony.
cc, err := rf.Begin(ctx, user, false /* passwordless */)
require.NoError(t, err, "Begin failed")

// Verify that the server requested credProps.
val, ok := cc.Response.Extensions[wantypes.CredPropsExtension]
require.True(t, ok, "CredentialCreation lacks credProps extension", cc.Response.Extensions)
credPropsRequested, ok := val.(bool)
require.True(t, ok && credPropsRequested, "CredentialCreation: credProps not set to true", cc.Response.Extensions)

// Sign with credProps.
key.ReplyWithCredProps = true
ccr, err := key.SignCredentialCreation(origin, cc)
require.NoError(t, err, "SignCredentialCreation failed")

// Finish ceremony and verify mfaDev.
mfaDev, err := rf.Finish(ctx, wanlib.RegisterResponse{
User: user,
DeviceName: "mydevice",
CreationResponse: ccr,
})
require.NoError(t, err, "Finish failed")
assert.True(t, mfaDev.GetWebauthn().ResidentKey, "mfaDev.ResidentKey flag mismatch")
}

func derToPEMs(certs [][]byte) []string {
res := make([]string, len(certs))
for i, cert := range certs {
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/webauthntypes/extensions.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ package webauthntypes
// AppIDExtension is the key for the appid extension.
// https://www.w3.org/TR/webauthn-2/#sctn-appid-extension.
const AppIDExtension = "appid"

// CredPropsExtension is the key for the credProps extension.
// https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension
const CredPropsExtension = "credProps"
37 changes: 35 additions & 2 deletions lib/auth/webauthntypes/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,24 +196,44 @@ func inputExtensionsToProto(exts AuthenticationExtensions) *wanpb.Authentication
if len(exts) == 0 {
return nil
}

res := &wanpb.AuthenticationExtensionsClientInputs{}

// appid (string).
if value, ok := exts[AppIDExtension]; ok {
// Type should always be string, since we are the ones setting it, but let's
// play it safe and check anyway.
if appID, ok := value.(string); ok {
res.AppId = appID
}
}

// credProps (bool).
if val, ok := exts[CredPropsExtension]; ok {
b, ok := val.(bool)
res.CredProps = ok && b
}

return res
}

func outputExtensionsToProto(exts *AuthenticationExtensionsClientOutputs) *wanpb.AuthenticationExtensionsClientOutputs {
if exts == nil {
return nil
}
return &wanpb.AuthenticationExtensionsClientOutputs{

res := &wanpb.AuthenticationExtensionsClientOutputs{
AppId: exts.AppID,
}

// credProps.
if credProps := exts.CredProps; credProps != nil {
res.CredProps = &wanpb.CredentialPropertiesOutput{
Rk: credProps.RK,
}
}

return res
}

func rpEntityToProto(rp RelyingPartyEntity) *wanpb.RelyingPartyEntity {
Expand Down Expand Up @@ -305,16 +325,29 @@ func inputExtensionsFromProto(exts *wanpb.AuthenticationExtensionsClientInputs)
if exts.AppId != "" {
res[AppIDExtension] = exts.AppId
}
if exts.CredProps {
res[CredPropsExtension] = true
}
return res
}

func outputExtensionsFromProto(exts *wanpb.AuthenticationExtensionsClientOutputs) *AuthenticationExtensionsClientOutputs {
if exts == nil {
return nil
}
return &AuthenticationExtensionsClientOutputs{

res := &AuthenticationExtensionsClientOutputs{
AppID: exts.AppId,
}

// credProps.
if credProps := exts.CredProps; credProps != nil {
res.CredProps = &CredentialPropertiesOutput{
RK: credProps.Rk,
}
}

return res
}

func publicKeyCredentialCreationOptionsFromProto(pubKey *wanpb.PublicKeyCredentialCreationOptions) PublicKeyCredentialCreationOptions {
Expand Down
65 changes: 65 additions & 0 deletions lib/auth/webauthntypes/proto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ package webauthntypes_test
import (
"testing"

"github.com/go-webauthn/webauthn/protocol"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

wanpb "github.com/gravitational/teleport/api/types/webauthn"
Expand Down Expand Up @@ -153,3 +156,65 @@ func TestConversionFromProto_nils(t *testing.T) {
})
}
}

func TestCredPropsConversions(t *testing.T) {
t.Parallel()

ccExtensions := protocol.AuthenticationExtensions{
wantypes.CredPropsExtension: true,
}

t.Run("CredentialCreation", func(t *testing.T) {
t.Parallel()

// CC -> proto -> CC.
cc := wantypes.CredentialCreationFromProto(
wantypes.CredentialCreationToProto(
&wantypes.CredentialCreation{
Response: wantypes.PublicKeyCredentialCreationOptions{
Extensions: ccExtensions,
},
},
),
)
if diff := cmp.Diff(ccExtensions, cc.Response.Extensions); diff != "" {
t.Errorf("CredentialCreation.Response.Extensions mismatch (-want +got)\n%s", diff)
}
})

t.Run("CredentialCreation from protocol", func(t *testing.T) {
t.Parallel()

// protocol -> CC.
cc := wantypes.CredentialCreationFromProtocol(&protocol.CredentialCreation{
Response: protocol.PublicKeyCredentialCreationOptions{
Extensions: map[string]any{
wantypes.CredPropsExtension: true,
},
},
})
if diff := cmp.Diff(ccExtensions, cc.Response.Extensions); diff != "" {
t.Errorf("CredentialCreation.Response.Extensions mismatch (-want +got)\n%s", diff)
}
})

t.Run("CredentialCreationResponse", func(t *testing.T) {
t.Parallel()

// CCR -> proto -> CCR.
ccr := wantypes.CredentialCreationResponseFromProto(
wantypes.CredentialCreationResponseToProto(
&wantypes.CredentialCreationResponse{
PublicKeyCredential: wantypes.PublicKeyCredential{
Extensions: &wantypes.AuthenticationExtensionsClientOutputs{
CredProps: &wantypes.CredentialPropertiesOutput{
RK: true,
},
},
},
},
),
)
assert.True(t, ccr.Extensions.CredProps.RK, "ccr.Extensions.CredProps.RK mismatch")
})
}
9 changes: 8 additions & 1 deletion lib/auth/webauthntypes/webauthn.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,14 @@ type Credential protocol.Credential
// [protocol.AuthenticationExtensionsClientOutputs], materialized here to keep a
// stable JSON marshal/unmarshal representation.
type AuthenticationExtensionsClientOutputs struct {
AppID bool `json:"appid,omitempty"`
AppID bool `json:"appid,omitempty"`
CredProps *CredentialPropertiesOutput `json:"credProps,omitempty"`
}

// CredentialPropertiesOutput is the output of the credProps extension.
// https://w3c.github.io/webauthn/#sctn-authenticator-credential-properties-extension.
type CredentialPropertiesOutput struct {
RK bool `json:"rk,omitempty"`
}

// SessionData is a clone of [webauthn.SessionData], materialized here to keep a
Expand Down
1 change: 1 addition & 0 deletions web/packages/teleport/src/services/auth/makeMfa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export function makeWebauthnCreationResponse(res) {
type: res.type,
extensions: {
appid: Boolean(clientExtentions?.appid),
credProps: clientExtentions?.credProps,
},
rawId: bufferToBase64url(res.rawId),
response: {
Expand Down

0 comments on commit b1b26f5

Please sign in to comment.