github.com/greenpau/go-authcrunch@v1.0.50/pkg/identity/mfa_token.go (about)

     1  // Copyright 2022 Paul Greenberg greenpau@outlook.com
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package identity
    16  
    17  import (
    18  	"crypto/ecdsa"
    19  	"crypto/elliptic"
    20  	"crypto/hmac"
    21  	"crypto/rsa"
    22  	"crypto/sha1"
    23  	"crypto/sha256"
    24  	"crypto/sha512"
    25  	"crypto/subtle"
    26  	"crypto/x509"
    27  	"encoding/base64"
    28  	"encoding/binary"
    29  	"encoding/json"
    30  	"fmt"
    31  	"hash"
    32  	"math"
    33  	"math/big"
    34  	"strings"
    35  	"time"
    36  
    37  	"github.com/greenpau/go-authcrunch/pkg/errors"
    38  	"github.com/greenpau/go-authcrunch/pkg/requests"
    39  	"github.com/greenpau/go-authcrunch/pkg/util"
    40  )
    41  
    42  // MfaTokenBundle is a collection of public keys.
    43  type MfaTokenBundle struct {
    44  	tokens []*MfaToken
    45  	size   int
    46  }
    47  
    48  // MfaToken is a puiblic key in a public-private key pair.
    49  type MfaToken struct {
    50  	ID               string            `json:"id,omitempty" xml:"id,omitempty" yaml:"id,omitempty"`
    51  	Type             string            `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty"`
    52  	Algorithm        string            `json:"algorithm,omitempty" xml:"algorithm,omitempty" yaml:"algorithm,omitempty"`
    53  	Comment          string            `json:"comment,omitempty" xml:"comment,omitempty" yaml:"comment,omitempty"`
    54  	Secret           string            `json:"secret,omitempty" xml:"secret,omitempty" yaml:"secret,omitempty"`
    55  	Period           int               `json:"period,omitempty" xml:"period,omitempty" yaml:"period,omitempty"`
    56  	Digits           int               `json:"digits,omitempty" xml:"digits,omitempty" yaml:"digits,omitempty"`
    57  	Expired          bool              `json:"expired,omitempty" xml:"expired,omitempty" yaml:"expired,omitempty"`
    58  	ExpiredAt        time.Time         `json:"expired_at,omitempty" xml:"expired_at,omitempty" yaml:"expired_at,omitempty"`
    59  	CreatedAt        time.Time         `json:"created_at,omitempty" xml:"created_at,omitempty" yaml:"created_at,omitempty"`
    60  	Disabled         bool              `json:"disabled,omitempty" xml:"disabled,omitempty" yaml:"disabled,omitempty"`
    61  	DisabledAt       time.Time         `json:"disabled_at,omitempty" xml:"disabled_at,omitempty" yaml:"disabled_at,omitempty"`
    62  	Device           *MfaDevice        `json:"device,omitempty" xml:"device,omitempty" yaml:"device,omitempty"`
    63  	Parameters       map[string]string `json:"parameters,omitempty" xml:"parameters,omitempty" yaml:"parameters,omitempty"`
    64  	Flags            map[string]bool   `json:"flags,omitempty" xml:"flags,omitempty" yaml:"flags,omitempty"`
    65  	SignatureCounter uint32            `json:"signature_counter,omitempty" xml:"signature_counter,omitempty" yaml:"signature_counter,omitempty"`
    66  	pubkeyECDSA      *ecdsa.PublicKey
    67  	pubkeyRSA        *rsa.PublicKey
    68  }
    69  
    70  // MfaDevice is the hardware device associated with MfaToken.
    71  type MfaDevice struct {
    72  	Name   string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty"`
    73  	Vendor string `json:"vendor,omitempty" xml:"vendor,omitempty" yaml:"vendor,omitempty"`
    74  	Type   string `json:"type,omitempty" xml:"type,omitempty" yaml:"type,omitempty"`
    75  }
    76  
    77  // NewMfaTokenBundle returns an instance of MfaTokenBundle.
    78  func NewMfaTokenBundle() *MfaTokenBundle {
    79  	return &MfaTokenBundle{
    80  		tokens: []*MfaToken{},
    81  	}
    82  }
    83  
    84  // Add adds MfaToken to MfaTokenBundle.
    85  func (b *MfaTokenBundle) Add(k *MfaToken) {
    86  	b.tokens = append(b.tokens, k)
    87  	b.size++
    88  }
    89  
    90  // Get returns MfaToken instances of the MfaTokenBundle.
    91  func (b *MfaTokenBundle) Get() []*MfaToken {
    92  	return b.tokens
    93  }
    94  
    95  // Size returns the number of MfaToken instances in MfaTokenBundle.
    96  func (b *MfaTokenBundle) Size() int {
    97  	return b.size
    98  }
    99  
   100  // NewMfaToken returns an instance of MfaToken.
   101  func NewMfaToken(req *requests.Request) (*MfaToken, error) {
   102  	p := &MfaToken{
   103  		ID:         util.GetRandomString(40),
   104  		CreatedAt:  time.Now().UTC(),
   105  		Parameters: make(map[string]string),
   106  		Flags:      make(map[string]bool),
   107  		Comment:    req.MfaToken.Comment,
   108  		Type:       req.MfaToken.Type,
   109  	}
   110  
   111  	if req.MfaToken.Disabled {
   112  		p.Disabled = true
   113  		p.DisabledAt = time.Now().UTC()
   114  	}
   115  
   116  	switch p.Type {
   117  	case "totp":
   118  		// Shared Secret
   119  		p.Secret = req.MfaToken.Secret
   120  		// Algorithm
   121  		p.Algorithm = strings.ToLower(req.MfaToken.Algorithm)
   122  		switch p.Algorithm {
   123  		case "sha1", "sha256", "sha512":
   124  		case "":
   125  			p.Algorithm = "sha1"
   126  		default:
   127  			return nil, errors.ErrMfaTokenInvalidAlgorithm.WithArgs(p.Algorithm)
   128  		}
   129  		req.MfaToken.Algorithm = p.Algorithm
   130  
   131  		// Period
   132  		p.Period = req.MfaToken.Period
   133  		if p.Period < 30 || p.Period > 300 {
   134  			return nil, errors.ErrMfaTokenInvalidPeriod.WithArgs(p.Period)
   135  		}
   136  		// Digits
   137  		p.Digits = req.MfaToken.Digits
   138  		if p.Digits == 0 {
   139  			p.Digits = 6
   140  		}
   141  		if p.Digits < 4 || p.Digits > 8 {
   142  			return nil, errors.ErrMfaTokenInvalidDigits.WithArgs(p.Digits)
   143  		}
   144  		// Codes
   145  		if err := p.ValidateCodeWithTime(req.MfaToken.Passcode, time.Now().Add(-time.Second*time.Duration(p.Period)).UTC()); err != nil {
   146  			return nil, err
   147  		}
   148  	case "u2f":
   149  		r := &WebAuthnRegisterRequest{}
   150  		if req.WebAuthn.Register == "" {
   151  			return nil, errors.ErrWebAuthnRegisterNotFound
   152  		}
   153  		if req.WebAuthn.Challenge == "" {
   154  			return nil, errors.ErrWebAuthnChallengeNotFound
   155  		}
   156  
   157  		// Decode WebAuthn Register.
   158  		decoded, err := base64.StdEncoding.DecodeString(req.WebAuthn.Register)
   159  		if err != nil {
   160  			return nil, errors.ErrWebAuthnParse.WithArgs(err)
   161  		}
   162  		if err := json.Unmarshal([]byte(decoded), r); err != nil {
   163  			return nil, errors.ErrWebAuthnParse.WithArgs(err)
   164  		}
   165  		// Set WebAuthn Challenge as Secret.
   166  		p.Secret = req.WebAuthn.Challenge
   167  
   168  		if r.ID == "" {
   169  			return nil, errors.ErrWebAuthnEmptyRegisterID
   170  		}
   171  
   172  		switch r.Type {
   173  		case "public-key":
   174  		case "":
   175  			return nil, errors.ErrWebAuthnEmptyRegisterKeyType
   176  		default:
   177  			return nil, errors.ErrWebAuthnInvalidRegisterKeyType.WithArgs(r.Type)
   178  		}
   179  
   180  		for _, tr := range r.Transports {
   181  			switch tr {
   182  			case "usb":
   183  			case "nfc":
   184  			case "ble":
   185  			case "internal":
   186  			case "":
   187  				return nil, errors.ErrWebAuthnEmptyRegisterTransport
   188  			default:
   189  				return nil, errors.ErrWebAuthnInvalidRegisterTransport.WithArgs(tr)
   190  			}
   191  		}
   192  
   193  		if r.AttestationObject == nil {
   194  			return nil, errors.ErrWebAuthnRegisterAttestationObjectNotFound
   195  		}
   196  		if r.AttestationObject.AuthData == nil {
   197  			return nil, errors.ErrWebAuthnRegisterAuthDataNotFound
   198  		}
   199  
   200  		// Extract rpIdHash from authData.
   201  		if r.AttestationObject.AuthData.RelyingPartyID == "" {
   202  			return nil, errors.ErrWebAuthnRegisterEmptyRelyingPartyID
   203  		}
   204  		p.Parameters["rp_id_hash"] = r.AttestationObject.AuthData.RelyingPartyID
   205  
   206  		// Extract flags from authData.
   207  		if r.AttestationObject.AuthData.Flags == nil {
   208  			return nil, errors.ErrWebAuthnRegisterEmptyFlags
   209  		}
   210  		for k, v := range r.AttestationObject.AuthData.Flags {
   211  			p.Flags[k] = v
   212  		}
   213  
   214  		// Extract signature counter from authData.
   215  		p.SignatureCounter = r.AttestationObject.AuthData.SignatureCounter
   216  
   217  		// Extract public key from credentialData.
   218  		if r.AttestationObject.AuthData.CredentialData == nil {
   219  			return nil, errors.ErrWebAuthnRegisterCredentialDataNotFound
   220  		}
   221  
   222  		if r.AttestationObject.AuthData.CredentialData.PublicKey == nil {
   223  			return nil, errors.ErrWebAuthnRegisterPublicKeyNotFound
   224  		}
   225  
   226  		// See https://www.iana.org/assignments/cose/cose.xhtml#key-type
   227  		var keyType string
   228  		if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["key_type"]; exists {
   229  			switch v.(float64) {
   230  			case 2:
   231  				keyType = "ec2"
   232  			case 3:
   233  				keyType = "rsa"
   234  			default:
   235  				return nil, errors.ErrWebAuthnRegisterPublicKeyUnsupported.WithArgs(v)
   236  			}
   237  		} else {
   238  			return nil, errors.ErrWebAuthnRegisterPublicKeyTypeNotFound
   239  		}
   240  
   241  		// See https://www.iana.org/assignments/cose/cose.xhtml#algorithms
   242  		var keyAlgo string
   243  		if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["algorithm"]; exists {
   244  			switch v.(float64) {
   245  			case -7:
   246  				keyAlgo = "es256"
   247  			case -257:
   248  				keyAlgo = "rs256"
   249  			default:
   250  				return nil, errors.ErrWebAuthnRegisterPublicKeyAlgorithmUnsupported.WithArgs(v)
   251  			}
   252  		} else {
   253  			return nil, errors.ErrWebAuthnRegisterPublicKeyAlgorithmNotFound
   254  		}
   255  
   256  		switch keyType {
   257  		case "ec2":
   258  			switch keyAlgo {
   259  			case "es256":
   260  				// See https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
   261  				var curveType, curveXcoord, curveYcoord string
   262  				if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["curve_type"]; exists {
   263  					switch v.(float64) {
   264  					case 1:
   265  						curveType = "p256"
   266  					default:
   267  						return nil, errors.ErrWebAuthnRegisterPublicKeyCurveUnsupported.WithArgs(v)
   268  					}
   269  				}
   270  				if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["curve_x"]; exists {
   271  					curveXcoord = v.(string)
   272  				}
   273  				if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["curve_y"]; exists {
   274  					curveYcoord = v.(string)
   275  				}
   276  				p.Parameters["curve_type"] = curveType
   277  				p.Parameters["curve_xcoord"] = curveXcoord
   278  				p.Parameters["curve_ycoord"] = curveYcoord
   279  			default:
   280  				return nil, errors.ErrWebAuthnRegisterPublicKeyTypeAlgorithmUnsupported.WithArgs(keyType, keyAlgo)
   281  			}
   282  		default:
   283  			if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["exponent"]; exists {
   284  				p.Parameters["exponent"] = v.(string)
   285  			} else {
   286  				return nil, errors.ErrWebAuthnRegisterPublicKeyParamNotFound.WithArgs(keyType, keyAlgo, "exponent")
   287  			}
   288  			if v, exists := r.AttestationObject.AuthData.CredentialData.PublicKey["modulus"]; exists {
   289  				p.Parameters["modulus"] = v.(string)
   290  			} else {
   291  				return nil, errors.ErrWebAuthnRegisterPublicKeyParamNotFound.WithArgs(keyType, keyAlgo, "modulus")
   292  			}
   293  		}
   294  
   295  		p.Parameters["u2f_id"] = r.ID
   296  		p.Parameters["u2f_type"] = r.Type
   297  		p.Parameters["u2f_transports"] = strings.Join(r.Transports, ",")
   298  		p.Parameters["key_type"] = keyType
   299  		p.Parameters["key_algo"] = keyAlgo
   300  		//return nil, fmt.Errorf("XXX: %v", r.AttestationObject.AttestationStatement.Certificates)
   301  		//return nil, fmt.Errorf("XXX: %v", r.AttestationObject.AuthData.CredentialData)
   302  
   303  		if p.Comment == "" {
   304  			p.Comment = fmt.Sprintf("T%d", time.Now().UTC().Unix())
   305  		}
   306  	case "":
   307  		return nil, errors.ErrMfaTokenTypeEmpty
   308  	default:
   309  		return nil, errors.ErrMfaTokenInvalidType.WithArgs(p.Type)
   310  	}
   311  
   312  	return p, nil
   313  }
   314  
   315  // WebAuthnRequest processes WebAuthn requests.
   316  func (p *MfaToken) WebAuthnRequest(payload string) (*WebAuthnAuthenticateRequest, error) {
   317  	switch p.Type {
   318  	case "u2f":
   319  	default:
   320  		return nil, errors.ErrWebAuthnRequest.WithArgs("unsupported token type")
   321  	}
   322  
   323  	for _, reqParam := range []string{"u2f_id", "key_type"} {
   324  		if _, exists := p.Parameters[reqParam]; !exists {
   325  			return nil, errors.ErrWebAuthnRequest.WithArgs(reqParam + " not found")
   326  		}
   327  	}
   328  
   329  	switch p.Parameters["key_type"] {
   330  	case "ec2":
   331  		if p.pubkeyECDSA == nil {
   332  			if err := p.derivePublicKey(p.Parameters); err != nil {
   333  				return nil, err
   334  			}
   335  		}
   336  	case "rsa":
   337  		if p.pubkeyRSA == nil {
   338  			if err := p.derivePublicKey(p.Parameters); err != nil {
   339  				return nil, err
   340  			}
   341  		}
   342  	default:
   343  		return nil, errors.ErrWebAuthnRequest.WithArgs("unsupported key type")
   344  	}
   345  
   346  	decoded, err := base64.StdEncoding.DecodeString(payload)
   347  	if err != nil {
   348  		return nil, errors.ErrWebAuthnParse.WithArgs(err)
   349  	}
   350  
   351  	r := &WebAuthnAuthenticateRequest{}
   352  	if err := json.Unmarshal([]byte(decoded), r); err != nil {
   353  		return nil, errors.ErrWebAuthnParse.WithArgs(err)
   354  	}
   355  
   356  	// Validate key id.
   357  	if p.Parameters["u2f_id"] != r.ID {
   358  		return r, errors.ErrWebAuthnRequest.WithArgs("key id mismatch")
   359  	}
   360  
   361  	// Decode ClientDataJSON.
   362  	if strings.TrimSpace(r.ClientDataEncoded) == "" {
   363  		return r, errors.ErrWebAuthnRequest.WithArgs("encoded client data is empty")
   364  	}
   365  	clientDataBytes, err := base64.StdEncoding.DecodeString(r.ClientDataEncoded)
   366  	if err != nil {
   367  		return r, errors.ErrWebAuthnRequest.WithArgs("failed to decode client data")
   368  	}
   369  	clientData := &ClientData{}
   370  	if err := json.Unmarshal(clientDataBytes, clientData); err != nil {
   371  		return nil, errors.ErrWebAuthnParse.WithArgs("failed to unmarshal client data")
   372  	}
   373  	r.ClientData = clientData
   374  	r.clientDataBytes = clientDataBytes
   375  	clientDataHash := sha256.Sum256(clientDataBytes)
   376  	r.ClientDataEncoded = ""
   377  	if r.ClientData == nil {
   378  		return r, errors.ErrWebAuthnRequest.WithArgs("client data is nil")
   379  	}
   380  
   381  	// Decode Signature.
   382  	if strings.TrimSpace(r.SignatureEncoded) == "" {
   383  		return r, errors.ErrWebAuthnRequest.WithArgs("encoded signature is empty")
   384  	}
   385  	signatureBytes, err := base64.StdEncoding.DecodeString(r.SignatureEncoded)
   386  	if err != nil {
   387  		return r, errors.ErrWebAuthnRequest.WithArgs("failed to decode signature")
   388  	}
   389  	r.signatureBytes = signatureBytes
   390  	r.SignatureEncoded = ""
   391  
   392  	// Decode Authenticator Data.
   393  	// See also https://www.w3.org/TR/webauthn-2/#sctn-authenticator-data
   394  	if strings.TrimSpace(r.AuthDataEncoded) == "" {
   395  		return r, errors.ErrWebAuthnRequest.WithArgs("encoded authenticator data is empty")
   396  	}
   397  	authDataBytes, err := base64.StdEncoding.DecodeString(r.AuthDataEncoded)
   398  	if err != nil {
   399  		return r, errors.ErrWebAuthnRequest.WithArgs("failed to decode auth data")
   400  	}
   401  	if err := r.unpackAuthData(authDataBytes); err != nil {
   402  		return r, errors.ErrWebAuthnRequest.WithArgs(err)
   403  	}
   404  	r.authDataBytes = authDataBytes
   405  	if r.AuthData == nil {
   406  		return r, errors.ErrWebAuthnRequest.WithArgs("auth data is nil")
   407  	}
   408  
   409  	// Verifying an Authentication Assertion
   410  	// See also https://www.w3.org/TR/webauthn-2/#sctn-verifying-assertion
   411  
   412  	// Verify that the value of C.type is the string webauthn.get.
   413  	if r.ClientData.Type != "webauthn.get" {
   414  		return r, errors.ErrWebAuthnRequest.WithArgs("client data type is not webauthn.get")
   415  	}
   416  
   417  	// Verify that the value of C.crossOrigin is false.
   418  	if r.ClientData.CrossOrigin == true {
   419  		return r, errors.ErrWebAuthnRequest.WithArgs("client data cross origin true is not supported")
   420  	}
   421  
   422  	// TODO(greenpau): Verify that the value of C.origin matches the Relying Party's origin.
   423  
   424  	// Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by
   425  	// the Relying Party.
   426  	if r.AuthData.RelyingPartyID != p.Parameters["rp_id_hash"] {
   427  		return r, errors.ErrWebAuthnRequest.WithArgs("rpIdHash mismatch")
   428  	}
   429  
   430  	// Verify that the User Present bit of the flags in authData is set.
   431  	if r.AuthData.Flags["UP"] != true {
   432  		return r, errors.ErrWebAuthnRequest.WithArgs("authData User Present bit is not set")
   433  	}
   434  
   435  	// TODO(greenpau): If user verification is required for this assertion, verify that the User
   436  	// Verified bit of the flags in authData is set.
   437  	// This requires checking UV key in p.Flags.
   438  
   439  	// Verify signature.
   440  	signedData := append(authDataBytes, clientDataHash[:]...)
   441  	crt := &x509.Certificate{}
   442  	switch p.Parameters["key_type"] {
   443  	case "ec2":
   444  		crt.PublicKey = p.pubkeyECDSA
   445  	case "rsa":
   446  		crt.PublicKey = p.pubkeyRSA
   447  	}
   448  
   449  	switch p.Parameters["key_algo"] {
   450  	case "es256":
   451  		if err := crt.CheckSignature(x509.ECDSAWithSHA256, signedData, signatureBytes); err != nil {
   452  			return r, errors.ErrWebAuthnRequest.WithArgs(err)
   453  		}
   454  	case "rs256":
   455  		if err := crt.CheckSignature(x509.SHA256WithRSA, signedData, signatureBytes); err != nil {
   456  			return r, errors.ErrWebAuthnRequest.WithArgs(err)
   457  		}
   458  	default:
   459  		return r, errors.ErrWebAuthnRequest.WithArgs("failed signature verification due to unsupported algo")
   460  	}
   461  
   462  	return r, nil
   463  }
   464  
   465  // Disable disables MfaToken instance.
   466  func (p *MfaToken) Disable() {
   467  	p.Expired = true
   468  	p.ExpiredAt = time.Now().UTC()
   469  	p.Disabled = true
   470  	p.DisabledAt = time.Now().UTC()
   471  }
   472  
   473  // ValidateCode validates a passcode
   474  func (p *MfaToken) ValidateCode(code string) error {
   475  	switch p.Type {
   476  	case "totp":
   477  	default:
   478  		return errors.ErrMfaTokenInvalidPasscode.WithArgs("unsupported token type")
   479  	}
   480  	ts := time.Now().UTC()
   481  	return p.ValidateCodeWithTime(code, ts)
   482  }
   483  
   484  // ValidateCodeWithTime validates a passcode at a particular time.
   485  func (p *MfaToken) ValidateCodeWithTime(code string, ts time.Time) error {
   486  	code = strings.TrimSpace(code)
   487  	if code == "" {
   488  		return errors.ErrMfaTokenInvalidPasscode.WithArgs("empty")
   489  	}
   490  	if len(code) < 4 || len(code) > 8 {
   491  		return errors.ErrMfaTokenInvalidPasscode.WithArgs("not 4-8 characters long")
   492  	}
   493  	if len(code) != p.Digits {
   494  		return errors.ErrMfaTokenInvalidPasscode.WithArgs("digits length mismatch")
   495  	}
   496  	tp := uint64(math.Floor(float64(ts.Unix()) / float64(p.Period)))
   497  	tps := []uint64{}
   498  	tps = append(tps, tp)
   499  	tps = append(tps, tp+uint64(1))
   500  	tps = append(tps, tp-uint64(1))
   501  	for _, uts := range tps {
   502  		localCode, err := generateMfaCode(p.Secret, p.Algorithm, p.Digits, uts)
   503  		if err != nil {
   504  			continue
   505  		}
   506  		if subtle.ConstantTimeCompare([]byte(localCode), []byte(code)) == 1 {
   507  			return nil
   508  		}
   509  	}
   510  	return errors.ErrMfaTokenInvalidPasscode.WithArgs("failed")
   511  }
   512  
   513  func generateMfaCode(secret, algo string, digits int, ts uint64) (string, error) {
   514  	var mac hash.Hash
   515  	secretBytes := []byte(secret)
   516  	switch algo {
   517  	case "sha1":
   518  		mac = hmac.New(sha1.New, secretBytes)
   519  	case "sha256":
   520  		mac = hmac.New(sha256.New, secretBytes)
   521  	case "sha512":
   522  		mac = hmac.New(sha512.New, secretBytes)
   523  	case "":
   524  		return "", errors.ErrMfaTokenEmptyAlgorithm
   525  	default:
   526  		return "", errors.ErrMfaTokenInvalidAlgorithm.WithArgs(algo)
   527  	}
   528  
   529  	buf := make([]byte, 8)
   530  	binary.BigEndian.PutUint64(buf, ts)
   531  	mac.Write(buf)
   532  	sum := mac.Sum(nil)
   533  
   534  	off := sum[len(sum)-1] & 0xf
   535  	val := int64(((int(sum[off]) & 0x7f) << 24) |
   536  		((int(sum[off+1] & 0xff)) << 16) |
   537  		((int(sum[off+2] & 0xff)) << 8) |
   538  		(int(sum[off+3]) & 0xff))
   539  	mod := int32(val % int64(math.Pow10(digits)))
   540  	wrap := fmt.Sprintf("%%0%dd", digits)
   541  	return fmt.Sprintf(wrap, mod), nil
   542  }
   543  
   544  func (p *MfaToken) derivePublicKey(params map[string]string) error {
   545  	for _, reqParam := range []string{"key_algo"} {
   546  		if _, exists := params[reqParam]; !exists {
   547  			return errors.ErrWebAuthnRequest.WithArgs(reqParam + " not found")
   548  		}
   549  	}
   550  	switch params["key_algo"] {
   551  	case "es256":
   552  		for _, reqParam := range []string{"curve_xcoord", "curve_ycoord"} {
   553  			if _, exists := params[reqParam]; !exists {
   554  				return errors.ErrWebAuthnRequest.WithArgs(reqParam + " not found")
   555  			}
   556  		}
   557  		var coords []*big.Int
   558  		for _, ltr := range []string{"x", "y"} {
   559  			coord := "curve_" + ltr + "coord"
   560  			b, err := base64.StdEncoding.DecodeString(params[coord])
   561  			if err != nil {
   562  				return errors.ErrWebAuthnRegisterPublicKeyCurveCoord.WithArgs(ltr, err)
   563  			}
   564  			if len(b) != 32 {
   565  				return errors.ErrWebAuthnRegisterPublicKeyCurveCoord.WithArgs(ltr, "not 32 bytes in length")
   566  			}
   567  			i := new(big.Int)
   568  			i.SetBytes(b)
   569  			coords = append(coords, i)
   570  		}
   571  		p.pubkeyECDSA = &ecdsa.PublicKey{Curve: elliptic.P256(), X: coords[0], Y: coords[1]}
   572  	case "rs256":
   573  		for _, reqParam := range []string{"exponent", "modulus"} {
   574  			if _, exists := params[reqParam]; !exists {
   575  				return errors.ErrWebAuthnRequest.WithArgs(reqParam + " not found")
   576  			}
   577  		}
   578  		nb, err := base64.StdEncoding.DecodeString(params["modulus"])
   579  		if err != nil {
   580  			return errors.ErrWebAuthnRegisterPublicKeyMaterial.WithArgs("modulus", err)
   581  		}
   582  		n := new(big.Int)
   583  		n.SetBytes(nb)
   584  
   585  		/*
   586  			ne, err := base64.StdEncoding.DecodeString(params["exponent"])
   587  			if err != nil {
   588  				return errors.ErrWebAuthnRegisterPublicKeyMaterial.WithArgs("exponent", err)
   589  			}
   590  		*/
   591  		p.pubkeyRSA = &rsa.PublicKey{
   592  			N: n,
   593  			E: 65537,
   594  		}
   595  		// return errors.ErrWebAuthnRegisterPublicKeyAlgorithmUnsupported.WithArgs(params["key_algo"])
   596  	}
   597  	return nil
   598  }
   599  
   600  func (r *WebAuthnAuthenticateRequest) unpackAuthData(b []byte) error {
   601  	data := new(AuthData)
   602  	if len(b) < 37 {
   603  		return fmt.Errorf("auth data is less than 37 bytes long")
   604  	}
   605  	data.RelyingPartyID = fmt.Sprintf("%x", b[0:32])
   606  	data.Flags = make(map[string]bool)
   607  	for _, st := range []struct {
   608  		k string
   609  		v byte
   610  	}{
   611  		{"UP", 0x001},
   612  		{"RFU1", 0x002},
   613  		{"UV", 0x004},
   614  		{"RFU2a", 0x008},
   615  		{"RFU2b", 0x010},
   616  		{"RFU2c", 0x020},
   617  		{"AT", 0x040},
   618  		{"ED", 0x080},
   619  	} {
   620  		if (b[32] & st.v) == st.v {
   621  			data.Flags[st.k] = true
   622  		} else {
   623  			data.Flags[st.k] = false
   624  		}
   625  	}
   626  	data.SignatureCounter = binary.BigEndian.Uint32(b[33:37])
   627  
   628  	// TODO(greenpau): implement AT parser.
   629  	// if (data.Flags["AT"] == true) && len(b) > 37 {
   630  	//   // Extract attested credentials data.
   631  	// }
   632  
   633  	r.AuthData = data
   634  	return nil
   635  }