pomerium/pkg/webauthnutil/options.go
Caleb Doxsey bbed421cd8
config: remove source, remove deadcode, fix linting issues (#4118)
* remove source, remove deadcode, fix linting issues

* use github action for lint

* fix missing envoy
2023-04-21 17:25:11 -06:00

306 lines
9.7 KiB
Go

package webauthnutil
import (
"encoding/base64"
"fmt"
"net/http"
"time"
"github.com/pomerium/pomerium/pkg/cryptutil"
"github.com/pomerium/pomerium/pkg/grpc/device"
"github.com/pomerium/pomerium/pkg/grpc/user"
"github.com/pomerium/pomerium/pkg/slices"
"github.com/pomerium/webauthn"
"github.com/pomerium/webauthn/cose"
)
const (
ceremonyTimeout = time.Minute * 15
rpName = "Pomerium"
)
// GenerateChallenge generates a new Challenge.
func GenerateChallenge(key []byte, expiry time.Time) cryptutil.SecureToken {
return cryptutil.GenerateSecureToken(key, expiry, cryptutil.NewRandomToken())
}
// GenerateCreationOptions generates creation options for WebAuthn.
func GenerateCreationOptions(
r *http.Request,
key []byte,
deviceType *device.Type,
user *user.User,
) *webauthn.PublicKeyCredentialCreationOptions {
expiry := time.Now().Add(ceremonyTimeout)
return newCreationOptions(
r,
GenerateChallenge(key, expiry).Bytes(),
deviceType,
user,
)
}
// GenerateRequestOptions generates request options for WebAuthn.
func GenerateRequestOptions(
r *http.Request,
key []byte,
deviceType *device.Type,
knownDeviceCredentials []*device.Credential,
) *webauthn.PublicKeyCredentialRequestOptions {
expiry := time.Now().Add(ceremonyTimeout)
return newRequestOptions(
r,
GenerateChallenge(key, expiry).Bytes(),
deviceType,
knownDeviceCredentials,
)
}
// GetCreationOptionsForCredential gets the creation options for the public key creation credential. An error may be
// returned if the challenge used to generate the credential is invalid.
func GetCreationOptionsForCredential(
r *http.Request,
key []byte,
deviceType *device.Type,
user *user.User,
credential *webauthn.PublicKeyCreationCredential,
) (*webauthn.PublicKeyCredentialCreationOptions, error) {
clientData, err := credential.Response.UnmarshalClientData()
if err != nil {
return nil, fmt.Errorf("invalid client data: %w", err)
}
rawChallenge, err := base64.RawURLEncoding.DecodeString(clientData.Challenge)
if err != nil {
return nil, fmt.Errorf("invalid challenge: %w", err)
}
var challenge cryptutil.SecureToken
copy(challenge[:], rawChallenge)
err = challenge.Verify(key, time.Now())
if err != nil {
return nil, err
}
return newCreationOptions(r, challenge.Bytes(), deviceType, user), nil
}
// GetRequestOptionsForCredential gets the request options for the public key request credential. An error may be
// returned if the challenge used to generate the credential is invalid.
func GetRequestOptionsForCredential(
r *http.Request,
key []byte,
deviceType *device.Type,
knownDeviceCredentials []*device.Credential,
credential *webauthn.PublicKeyAssertionCredential,
) (*webauthn.PublicKeyCredentialRequestOptions, error) {
clientData, err := credential.Response.UnmarshalClientData()
if err != nil {
return nil, fmt.Errorf("invalid client data: %w", err)
}
rawChallenge, err := base64.RawURLEncoding.DecodeString(clientData.Challenge)
if err != nil {
return nil, fmt.Errorf("invalid challenge: %w", err)
}
var challenge cryptutil.SecureToken
copy(challenge[:], rawChallenge)
err = challenge.Verify(key, time.Now())
if err != nil {
return nil, err
}
return newRequestOptions(r, challenge.Bytes(), deviceType, knownDeviceCredentials), nil
}
// newCreationOptions gets the creation options for WebAuthn with the provided challenge.
func newCreationOptions(
r *http.Request,
challenge []byte,
deviceType *device.Type,
user *user.User,
) *webauthn.PublicKeyCredentialCreationOptions {
options := &webauthn.PublicKeyCredentialCreationOptions{
RP: webauthn.PublicKeyCredentialRPEntity{
Name: rpName,
ID: GetEffectiveDomain(r),
},
User: GetUserEntity(user),
Challenge: challenge,
Timeout: ceremonyTimeout,
}
if deviceOptions := deviceType.GetWebauthn().GetOptions(); deviceOptions != nil {
fillAllPublicKeyCredentialParameters(options, deviceOptions.GetPubKeyCredParams())
fillAuthenticatorSelection(options, deviceOptions.GetAuthenticatorSelection())
fillAttestationConveyance(options, deviceOptions.Attestation)
}
return options
}
// newRequestOptions gets the request options for WebAuthn with the provided challenge.
func newRequestOptions(
r *http.Request,
challenge []byte,
deviceType *device.Type,
knownDeviceCredentials []*device.Credential,
) *webauthn.PublicKeyCredentialRequestOptions {
options := &webauthn.PublicKeyCredentialRequestOptions{
Challenge: challenge,
Timeout: ceremonyTimeout,
RPID: GetEffectiveDomain(r),
}
fillRequestUserVerificationRequirement(
options,
deviceType.GetWebauthn().GetOptions().GetAuthenticatorSelection().UserVerification,
)
knownDeviceCredentialsForType := slices.Filter(knownDeviceCredentials, func(c *device.Credential) bool {
return c.GetTypeId() == deviceType.GetId()
})
for _, knownDeviceCredential := range knownDeviceCredentialsForType {
if publicKey := knownDeviceCredential.GetWebauthn(); publicKey != nil {
options.AllowCredentials = append(options.AllowCredentials, webauthn.PublicKeyCredentialDescriptor{
Type: webauthn.PublicKeyCredentialTypePublicKey,
ID: publicKey.GetId(),
})
}
}
return options
}
func fillAllPublicKeyCredentialParameters(
options *webauthn.PublicKeyCredentialCreationOptions,
allDeviceParams []*device.WebAuthnOptions_PublicKeyCredentialParameters,
) {
options.PubKeyCredParams = nil
for _, deviceParams := range allDeviceParams {
p := webauthn.PublicKeyCredentialParameters{}
fillPublicKeyCredentialParameters(&p, deviceParams)
options.PubKeyCredParams = append(options.PubKeyCredParams, p)
}
}
func fillAttestationConveyance(
options *webauthn.PublicKeyCredentialCreationOptions,
attestationConveyance *device.WebAuthnOptions_AttestationConveyancePreference,
) {
options.Attestation = ""
if attestationConveyance == nil {
return
}
switch *attestationConveyance {
case device.WebAuthnOptions_NONE:
options.Attestation = webauthn.AttestationConveyanceNone
case device.WebAuthnOptions_INDIRECT:
options.Attestation = webauthn.AttestationConveyanceIndirect
case device.WebAuthnOptions_DIRECT:
options.Attestation = webauthn.AttestationConveyanceDirect
case device.WebAuthnOptions_ENTERPRISE:
options.Attestation = webauthn.AttestationConveyanceEnterprise
}
}
func fillAuthenticatorAttachment(
criteria *webauthn.AuthenticatorSelectionCriteria,
authenticatorAttachment *device.WebAuthnOptions_AuthenticatorAttachment,
) {
criteria.AuthenticatorAttachment = ""
if authenticatorAttachment == nil {
return
}
switch *authenticatorAttachment {
case device.WebAuthnOptions_CROSS_PLATFORM:
criteria.AuthenticatorAttachment = webauthn.AuthenticatorAttachmentCrossPlatform
case device.WebAuthnOptions_PLATFORM:
criteria.AuthenticatorAttachment = webauthn.AuthenticatorAttachmentPlatform
}
}
func fillAuthenticatorSelection(
options *webauthn.PublicKeyCredentialCreationOptions,
deviceCriteria *device.WebAuthnOptions_AuthenticatorSelectionCriteria,
) {
options.AuthenticatorSelection = new(webauthn.AuthenticatorSelectionCriteria)
fillAuthenticatorAttachment(options.AuthenticatorSelection, deviceCriteria.AuthenticatorAttachment)
fillResidentKeyRequirement(options.AuthenticatorSelection, deviceCriteria.ResidentKeyRequirement)
options.AuthenticatorSelection.RequireResidentKey = deviceCriteria.GetRequireResidentKey()
fillUserVerificationRequirement(options.AuthenticatorSelection, deviceCriteria.UserVerification)
}
func fillPublicKeyCredentialParameters(
params *webauthn.PublicKeyCredentialParameters,
deviceParams *device.WebAuthnOptions_PublicKeyCredentialParameters,
) {
params.Type = ""
params.COSEAlgorithmIdentifier = 0
if deviceParams == nil {
return
}
switch deviceParams.Type {
case device.WebAuthnOptions_PUBLIC_KEY:
params.Type = webauthn.PublicKeyCredentialTypePublicKey
}
params.COSEAlgorithmIdentifier = cose.Algorithm(deviceParams.GetAlg())
}
func fillRequestUserVerificationRequirement(
options *webauthn.PublicKeyCredentialRequestOptions,
userVerificationRequirement *device.WebAuthnOptions_UserVerificationRequirement,
) {
options.UserVerification = ""
if userVerificationRequirement == nil {
return
}
switch *userVerificationRequirement {
case device.WebAuthnOptions_USER_VERIFICATION_DISCOURAGED:
options.UserVerification = webauthn.UserVerificationDiscouraged
case device.WebAuthnOptions_USER_VERIFICATION_PREFERRED:
options.UserVerification = webauthn.UserVerificationPreferred
case device.WebAuthnOptions_USER_VERIFICATION_REQUIRED:
options.UserVerification = webauthn.UserVerificationRequired
}
}
func fillResidentKeyRequirement(
criteria *webauthn.AuthenticatorSelectionCriteria,
residentKeyRequirement *device.WebAuthnOptions_ResidentKeyRequirement,
) {
criteria.ResidentKey = ""
if residentKeyRequirement == nil {
return
}
switch *residentKeyRequirement {
case device.WebAuthnOptions_RESIDENT_KEY_DISCOURAGED:
criteria.ResidentKey = webauthn.ResidentKeyDiscouraged
case device.WebAuthnOptions_RESIDENT_KEY_PREFERRED:
criteria.ResidentKey = webauthn.ResidentKeyPreferred
case device.WebAuthnOptions_RESIDENT_KEY_REQUIRED:
criteria.ResidentKey = webauthn.ResidentKeyRequired
}
}
func fillUserVerificationRequirement(
criteria *webauthn.AuthenticatorSelectionCriteria,
userVerificationRequirement *device.WebAuthnOptions_UserVerificationRequirement,
) {
criteria.UserVerification = ""
if userVerificationRequirement == nil {
return
}
switch *userVerificationRequirement {
case device.WebAuthnOptions_USER_VERIFICATION_DISCOURAGED:
criteria.UserVerification = webauthn.UserVerificationDiscouraged
case device.WebAuthnOptions_USER_VERIFICATION_PREFERRED:
criteria.UserVerification = webauthn.UserVerificationPreferred
case device.WebAuthnOptions_USER_VERIFICATION_REQUIRED:
criteria.UserVerification = webauthn.UserVerificationRequired
}
}