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