Welcome to WebAuthn workshop

Please check that you have obtained FIDO U2F Security Key

 

Please check that you have installed LATEST Nodejs(v6.x), NPM, Git and Google Chrome.

Authenticating

your Web

Yuriy Ackermann

Sr. Certification Engineer @FIDOAlliance

twitter/github: @herrjemand

Todays plan:

  • Learn what is webauthn
  • Learn how to webauthn
  • Learn how to make
  • ...manage
  • ...and assert creds.

So... before we start check that:

  • You have installed latest NodeJS
  • ...and NPM
  • Chrome(67)
  • Git
  • Text editor

Short recap of CredManAPI

  • JS API for credentials management
  • Official W3C spec
  • Basically JS API for autofill
  • Read/watch:
  • https://www.w3.org/TR/credential-management/
  • https://developers.google.com/web/fundamentals/security/credential-management/
  • https://pusher.com/sessions/meetup/js-monthly-london/building-a-better-login-with-the-credential-management-api
navigator.credentials
    .get({ 'password': true })
    .then(credential => {
        if (!credential) {
            throw new Error('No credentials returned!')
        }

        let credentials = {
            'username': credential.id,
            'password': credential.password
        }

        return fetch('https://example.com/loginEndpoint', {
            method: 'POST',
            body: JSON.stringify(credentials),
            credentials: 'include'
        })
    })
    .then((response) => {
        ...
    })


        navigator.credentials.store({
            'type': 'password',
            'id': 'alice',
            'password': 'VeryRandomPassword123456'
        })

What is WebAuthn?

  • PublicKey extension to credential management API
  • An official W3C standard
  • A sub-spec of FIDO2 specs
  • Basically public keys for authentication in browsers
  • Read:
  • https://w3c.github.io/webauthn/
  • https://webauthn.org/
  • http://slides.com/herrjemand/webauthn-isig

MakeCredentials request

   
   
    var publicKey = {
        challenge: new Uint8Array([21,31,105, ..., 55]),

        rp: {
            name: "ACME Corporation"
        },

        user: {
            id: Uint8Array.from(window.atob("MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII="), c=>c.charCodeAt(0)),
            name: "alex.p.mueller@example.com",
            displayName: "Alex P. Müller"
        },

        attestation: 'direct',

        pubKeyCredParams: [
            {
                type: "public-key", alg: -7 // "ES256" IANA COSE Algorithms registry
            },
            {
                type: "public-key", alg: -257 // "RS256" IANA COSE Algorithms registry
            }
        ]
    }

    navigator.credentials.create({ publicKey })
        .then((newCredentialInfo) => {
            /* Public key credential */
        }).catch((error) => {
            /* Error */
        })

Random 32byte challenge buffer

Friendly RP name

User names and userid buffer

Signature algorithm negotiation

You want attestation object or not

Let's try it our selves:

        
        var randomChallengeBuffer = new Uint8Array(32);
        window.crypto.getRandomValues(randomChallengeBuffer);

        var base64id = 'MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII='
        var idBuffer = Uint8Array.from(window.atob(base64id), c=>c.charCodeAt(0))

        var publicKey = {
            challenge: randomChallengeBuffer,

            rp: { name: "FIDO Example Corporation" },

            user: {
                id: idBuffer,
                name: "alice@example.com",
                displayName: "Alice von Wunderland"
            },

            attestation: 'direct',

            pubKeyCredParams: [
                { type: 'public-key',  alg: -7  }, // ES256
                { type: 'public-key', alg: -257 }  // RS256
            ]
        }

        navigator.credentials.create({ publicKey })
            .then((newCredentialInfo) => {
                console.log('SUCCESS', newCredentialInfo)
            })
            .catch((error) => {
                console.log('FAIL', error)
            })

Go to https://webauthn.bin.coffee and run in the console

MakeCredentials response

Inner authr id. id == base64url(rawId) 

{
	"challenge": "HDhbiI5a6F_ndjVmnkCYKM_Mjt5Nv7OQwrYAeI8zX5E",
	"hashAlgorithm": "SHA-256",
	"origin": "https://webauthn.bin.coffee"
}

Client collected data

Authr assertion

Okay, hands on deck, lets code

  • Open terminal
  • git clone https://github.com/fido-alliance/webauthn-demo
  • cd webauthn-demo
  • npm install
  • node app.js
  • In Google Chrome http://localhost:3000

Request challenge

Process challenge

Return response

What are we doing?

App architecture

  • Simple Express
  • app.js + config.json - app config
  • routes/ - express routers + db
  • utils.js - help functions + crypto
  • static/ - static frontend

Frontend architecture

  • Simple HTML framework + jQuery
  • js/password.auth.js - well... you get it
  • js/helpers.js - help functions
  • js/view.js - some gui help functions

For registration:

  • Get username and name(password field is obsolete, lol)
  • Send them to the server
  • Server responds with challenge
  • MakeCredential
  • Send response to the server
  • Check that server likes it
  • PROFIT!
  • Remove password section from registration form in index.html
  • In "password.auth.js" comment #register handler
  • In "webauthn.auth.js" add:

        /* Handle for register form submission */
        $('#register').submit(function(event) {
            event.preventDefault();

            let username = this.username.value;
            let name     = this.name.value;

            if(!username || !name) {
                alert('Name or username is missing!')
                return
            }

        })
  • Then we need to get MakeCred challenge. For that we will have /webauthn/register endpoint
  • Adding getMakeCredentialsChallenge fetch function to "webauthn.auth.js"
        
let getMakeCredentialsChallenge = (formBody) => {
    return fetch('/webauthn/register', {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formBody)
    })
    .then((response) => response.json())
    .then((response) => {
        if(response.status !== 'ok')
            throw new Error(`Server responed with error. The message is: ${response.message}`);

        return response
    })
}

POST request

Its a JSON request, and it takes JS object and JSON encodes it

Server responds with JSON

response key "status" can be "ok" or "failed"

  • Adding "getMakeCredentialsChallenge" to "#register" form processor in "webauthn.auth.js"

        /* Handle for register form submission */
        $('#register').submit(function(event) {
            event.preventDefault();

            let username = this.username.value;
            let name     = this.name.value;

            if(!username || !name) {
                alert('Name or username is missing!')
                return
            }

            getMakeCredentialsChallenge({username, name})
                .then((response) => {
                    console.log(response)
                })
            
        })
  • It receives name and username
  • Adds adds them to the DB tagged as not registered
  • Generates registration request
  • And sends it back to the browser

Now for the /webauthn/register endpoint

  • Now lets create /webauthn/register endpoint
  • In routes/webauthn.js insert this code
router.post('/register', (request, response) => {
    if(!request.body || !request.body.username || !request.body.name) {
        response.json({
            'status': 'failed',
            'message': 'Request missing name or username field!'
        })

        return
    }

    let username = request.body.username;
    let name     = request.body.name;

    if(database[username] && database[username].registered) {
        response.json({
            'status': 'failed',
            'message': `Username ${username} already exists`
        })

        return
    }

    database[username] = {
        'name': name,
        'registered': false,
        'id': utils.randomBase64URLBuffer(),
        'authenticators': []
    }

    
})

Check all field. The body is the request.body

Check that user does not exist or he is not registered

Creating user

Generating user random ID

This is where we store registered authenticators

  • To generate makeCredential challenge utils have "generateServerMakeCredRequest" method


   let generateServerMakeCredRequest = (username, displayName, id) => {
        return {
            challenge: randomBase64URLBuffer(32),
    
            rp: {
                name: "FIDO Examples Corporation"
            },
    
            user: {
                id: id,
                name: username,
                displayName: displayName
            },
    
            pubKeyCredParams: [
                {
                    type: "public-key", alg: -7 // "ES256" IANA COSE Algorithms registry
                }
            ]
        }
    }
   

router.post('/register', (request, response) => {

    ...


    let challengeMakeCred    = utils.generateServerMakeCredRequest(username, 
                                                            name, database[username].id)
    challengeMakeCred.status = 'ok'

    request.session.challenge = challengeMakeCred.challenge;
    request.session.username  = username;

    response.json(challengeMakeCred)

})

Generate makeCred challenge: passing username, name, and id

Saving username and challenge in session for later

Don't forget to let browser know that you are ok!

Send response

In routes/webauthn.js add new block of code

Back to the html...

Remember this?

        
        var randomChallengeBuffer = new Uint8Array(32);
        window.crypto.getRandomValues(randomChallengeBuffer);

        var base64id = 'MIIBkzCCATigAwIBAjCCAZMwggE4oAMCAQIwggGTMII='
        var idBuffer = Uint8Array.from(window.atob(base64id), c=>c.charCodeAt(0))

        var publicKey = {
            challenge: randomChallengeBuffer,

            rp: { name: "FIDO Example Corporation" },

            user: {
                id: idBuffer,
                name: "alice@example.com",
                displayName: "Alice von Wunderland"
            },

            pubKeyCredParams: [
                { type: 'public-key',  alg: -7  }, // ES256
                { type: 'public-key', alg: -257 }  // RS256
            ]
        }

        // Note: The following call will cause the authenticator to display UI.
        navigator.credentials.create({ publicKey })
            .then((newCredentialInfo) => {
                console.log('SUCCESS', newCredentialInfo)
            })
            .catch((error) => {
                console.log('FAIL', error)
            })

challenge is a buffer

id is a buffer

Here is our server response

        {
            "challenge": "IAomGjp6nnS9GvPhdRdd3ATQWdL0PXTOAHDR6pPgeXM",
            "rp": {
                "name": "ACME Corporation"
            },
            "user": {
                "id": "38cuhE0p0bN5PM9hSp7WEE8oTS08OQE0igXtuBifxfo",
                "name": "alice",
                "displayName": "Alice"
            },
            "pubKeyCredParams": [{
                "type": "public-key",
                "alg": -7
            }],
            "status": "ok"
        }

Oh boy, challenge is not BUFFER! ...cause no buffers in JSON

...and id as well

Good that helpers.js have "preformatMakeCredReq" method

    
    var preformatMakeCredReq = (makeCredReq) => {
        makeCredReq.challenge = base64url.decode(makeCredReq.challenge);
        makeCredReq.user.id = base64url.decode(makeCredReq.user.id);
    
        return makeCredReq
    }
   





    /* Handle for register form submission */
    $('#register').submit(function(event) {
        event.preventDefault();
    
        let username = this.username.value;
        let name     = this.name.value;
    
        if(!username || !name) {
            alert('Name or username is missing!')
            return
        }
    
        getMakeCredentialsChallenge({username, name})
            .then((response) => {
                let publicKey = preformatMakeCredReq(response);
                return navigator.credentials.create({ publicKey })
            })
            .then((newCred) => {
                console.log(newCred)
            })
    })

Updating #registration processor in webauthn.auth.js

Back to MakeCredentials response

BUFFER

BUFFER

BUFFER

Guess what? JSON does not do buffers *(

But don't worry, utils have publicKeyCredentialToJSON method 

  
    getMakeCredentialsChallenge({username, name})
        .then((response) => {
            let publicKey = preformatMakeCredReq(response);
            return navigator.credentials.create({ publicKey })
        })
        .then((newCred) => {
            let makeCredResponse = publicKeyCredentialToJSON(newCred);
            console.log(makeCredResponse)
        })

That's better!

Updating #registration processor in webauthn.auth.js



{
	"rawId": "Gw7nqgWzci8jwIX9yXzYtynmJbQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
	"response": {
		"attestationObject": "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEcwRQIhAIdC3J6jt_rxTF3mo_HdQ_HUWOW3b9GPzpLMz-Wt78UTAiBjE0JgQNTkglEg2eEv8aJIYZCqJUzm5LO8tVax7m-gz2N4NWOBWQGCMIIBfjCCASSgAwIBAgIBATAKBggqhkjOPQQDAjA8MREwDwYDVQQDDAhTb2Z0IFUyRjEUMBIGA1UECgwLR2l0SHViIEluYy4xETAPBgNVBAsMCFNlY3VyaXR5MB4XDTE3MDcyNjIwMDkwOFoXDTI3MDcyNDIwMDkwOFowPDERMA8GA1UEAwwIU29mdCBVMkYxFDASBgNVBAoMC0dpdEh1YiBJbmMuMREwDwYDVQQLDAhTZWN1cml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPacqyQUS7Tvh_cPIxxc1PV4BKz44Mays-NSGD2AOR9r0nnSakyDZHTmwtojk_-sHVA0bFwjkGVXkz7Lk_9u3tGjFzAVMBMGCysGAQQBguUcAgEBBAQDAgMIMAoGCCqGSM49BAMCA0gAMEUCIQD-Ih2XuOrqErufQhSFD0gXZbXglZNeoaPWbQ-xbzn3IgIgZNfcL1xsOCr3ZfV4ajmwsUqXRSjvfd8hAhUbiErUQXpoYXV0aERhdGFYykmWDeWIDoxodDQXD2R2YFuP5K65ooYyx5lc87qDHZdjQQAAAAAAAAAAAAAAAAAAAAAAAAAAAEYbDueqBbNyLyPAhf3JfNi3KeYltAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAApQECAyYgASFYIJ8Lv8N1eB_A9d6z3k0B4d9ii7fHSyZChIG3lwlqsgHcIlggglrXCklNPmjLdnXDijGxDh0b2k52p2N6EDET0BScCjo",
		"clientDataJSON": "eyJjaGFsbGVuZ2UiOiJLMlEwdHdnXzVGNDJRMEtnYll6OXdxaGVEN3ZBbmlFdEJ0N190a3g3ZEo0IiwiaGFzaEFsZ29yaXRobSI6IlNIQS0yNTYiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAifQ"
	},
	"id": "Gw7nqgWzci8jwIX9yXzYtynmJbQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
	"type": "public-key"
}

For registration:

  • Get username and name(password field is obsolete, lol)
  • Send them to the server
  • Server responds with challenge
  • MakeCredential
  • Send response to the server
  • Check that server likes it
  • PROFIT!

Sending response to the server

  • Auth and Reg responses both going to the same endpoint /webauthn/response

let sendWebAuthnResponse = (body) => {
    return fetch('/webauthn/response', {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(body)
    })
    .then((response) => response.json())
    .then((response) => {
        if(response.status !== 'ok')
            throw new Error(`Server responed with error. The message is: ${response.message}`);

        return response
    })
}

    .then((response) => {
        let makeCredResponse = publicKeyCredentialToJSON(response);
        return sendWebAuthnResponse(makeCredResponse)
    })
    .then((response) => {
        if(response.status === 'ok') {
            loadMainContainer()   
        } else {
            alert(`Server responed with error. The message is: ${response.message}`);
        }
    })
    .catch((error) => alert(error))

New function to be added to webauthn.auth.js

Updating #registration processor in webauthn.auth.js

Back to the server


router.post('/response', (request, response) => {
    if(!request.body       || !request.body.id
    || !request.body.rawId || !request.body.response
    || !request.body.type  || request.body.type !== 'public-key' ) {
        response.json({
            'status': 'failed',
            'message': 'Response missing one or more of id/rawId/response/type fields, or type is not public-key!'
        })

        return
    }

    let webauthnResp = request.body
    let clientData   = JSON.parse(base64url.decode(webauthnResp.response.clientDataJSON));

    /* Check challenge... */
    if(clientData.challenge !== request.session.challenge) {
        response.json({
            'status': 'failed',
            'message': 'Challenges don\'t match!'
        })
    }

    /* ...and origin */
    if(clientData.origin !== config.origin) {
        response.json({
            'status': 'failed',
            'message': 'Origins don\'t match!'
        })
    }

})

Add this code to routes/webauthn.js

Checking response

Parsing client data

Checking that origin and challenge match

Ok, so we got the response. How do we know that it's a reg?

attestationObject

MakeCredentials

authenticatorData

GetAssertion


router.post('/response', (request, response) => {
    ...

    if(webauthnResp.response.attestationObject !== undefined) {
        /* This is create cred */

    } else if(webauthnResp.response.authenticatorData !== undefined) {
        /* This is get assertion */
        
    } else {
        response.json({
            'status': 'failed',
            'message': 'Can not determine type of response!'
        })
    }

})

Updating  /response endpoint in routes/webauthn.js

AttestationVerification

In utils.js there is a method "verifyAuthenticatorAttestationResponse"

let verifyAuthenticatorAttestationResponse = (webAuthnResponse) => {
    let attestationBuffer = base64url.toBuffer(webAuthnResponse.response.attestationObject);
    let ctapMakeCredResp  = cbor.decodeAllSync(attestationBuffer)[0];

    let response = {'verified': false};
    if(ctapMakeCredResp.fmt === 'fido-u2f') {
        let authrDataStruct = parseMakeCredAuthData(ctapMakeCredResp.authData);

        if(!(authrDataStruct.flags & U2F_USER_PRESENTED))
            throw new Error('User was NOT presented durring authentication!');

        let clientDataHash  = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
        let reservedByte    = Buffer.from([0x00]);
        let publicKey       = COSEECDHAtoPKCS(authrDataStruct.COSEPublicKey)
        let signatureBase   = Buffer.concat([reservedByte, authrDataStruct.rpIdHash, clientDataHash, authrDataStruct.credID, publicKey]);

        let PEMCertificate = ASN1toPEM(ctapMakeCredResp.attStmt.x5c[0]);
        let signature      = ctapMakeCredResp.attStmt.sig;

        response.verified = verifySignature(signature, signatureBase, PEMCertificate)

        if(response.verified) {
            response.authrInfo = {
                fmt: 'fido-u2f',
                publicKey: base64url.encode(publicKey),
                counter: authrDataStruct.counter,
                credID: base64url.encode(authrDataStruct.credID)
            }
        }
    }

    return response
}

Decode base64url encoded assertion buffer, and CBOR parse it

It's a U2F statement

Parsing raw authData buffer

Check that TUP flag is set

Generate signature base

COSE to PKCS conversion

X509 Cert buffer into PEM

Verify signature

On verification, return response with new Authr

AuthenticatorData

let parseMakeCredAuthData = (buffer) => {
    let rpIdHash      = buffer.slice(0, 32);          buffer = buffer.slice(32);
    let flagsBuf      = buffer.slice(0, 1);           buffer = buffer.slice(1);
    let flags         = flagsBuf[0];
    let counterBuf    = buffer.slice(0, 4);           buffer = buffer.slice(4);
    let counter       = counterBuf.readUInt32BE(0);
    let aaguid        = buffer.slice(0, 16);          buffer = buffer.slice(16);
    let credIDLenBuf  = buffer.slice(0, 2);           buffer = buffer.slice(2);
    let credIDLen     = credIDLenBuf.readUInt16BE(0);
    let credID        = buffer.slice(0, credIDLen);   buffer = buffer.slice(credIDLen);
    let COSEPublicKey = buffer;

    return {rpIdHash, flagsBuf, flags, counter, counterBuf, aaguid, credID, COSEPublicKey}
}

COSE PublicKey to PKCS

let COSEECDHAtoPKCS = (COSEPublicKey) => {
    /* 
       +------+-------+-------+---------+----------------------------------+
       | name | key   | label | type    | description                      |
       |      | type  |       |         |                                  |
       +------+-------+-------+---------+----------------------------------+
       | crv  | 2     | -1    | int /   | EC Curve identifier - Taken from |
       |      |       |       | tstr    | the COSE Curves registry         |
       |      |       |       |         |                                  |
       | x    | 2     | -2    | bstr    | X Coordinate                     |
       |      |       |       |         |                                  |
       | y    | 2     | -3    | bstr /  | Y Coordinate                     |
       |      |       |       | bool    |                                  |
       |      |       |       |         |                                  |
       | d    | 2     | -4    | bstr    | Private key                      |
       +------+-------+-------+---------+----------------------------------+
    */

    let coseStruct = cbor.decodeAllSync(COSEPublicKey)[0];
    let tag = Buffer.from([0x04]);
    let x   = coseStruct.get(-2);
    let y   = coseStruct.get(-3);

    return Buffer.concat([tag, x, y])
}

Final registration response


    let result;
    if(webauthnResp.response.attestationObject !== undefined) {
        /* This is create cred */
        result = utils.verifyAuthenticatorAttestationResponse(webauthnResp);

        if(result.verified) {
            database[request.session.username].authenticators.push(result.authrInfo);
            database[request.session.username].registered = true
        }
    } else if(webauthnResp.response.authenticatorData !== undefined) {
        /* This is get assertion */

    } else {
        response.json({
            'status': 'failed',
            'message': 'Can not determine type of response!'
        })
    }

    if(result.verified) {
        request.session.loggedIn = true;
        response.json({ 'status': 'ok' })
    } else {
        response.json({
            'status': 'failed',
            'message': 'Can not authenticate signature!'
        })
    }

Updating  /response endpoint in routes/webauthn.js

For registration:

  • Get username and name(password field is obsolete, lol)
  • Send them to the server
  • Server responds with challenge
  • MakeCredential
  • Send response to the server
  • Check that server likes it
  • PROFIT!

DEMO

For authentication:

  • Get username(remove password field)
  • Send to the server
  • Server responds with challenge
  • GetAssertion
  • Send response to the server
  • Check that server likes it
  • PROFIT!
  • Take username
  • Check that it's exits
  • Generate challenge
  • Send it to the browser

This time we start with server

/webauthn/login endpoint

router.post('/login', (request, response) => {
    if(!request.body || !request.body.username) {
        response.json({
            'status': 'failed',
            'message': 'Request missing username field!'
        })

        return
    }

    let username = request.body.username;

    if(!database[username] || !database[username].registered) {
        response.json({
            'status': 'failed',
            'message': `User ${username} does not exist!`
        })

        return
    }

    let getAssertion    = utils.generateServerGetAssertion(database[username].authenticators)
    getAssertion.status = 'ok'

    request.session.challenge = getAssertion.challenge;
    request.session.username  = username;

    response.json(getAssertion)
})

Save username and challenge

Adding new /login endpoint in routes/webauthn.js






let generateServerGetAssertion = (authenticators) => {
    let allowCredentials = [];
    for(let authr of authenticators) {
        allowCredentials.push({
              type: 'public-key',
              id: authr.credID,
              transports: ['usb', 'nfc', 'ble']
        })
    }
    return {
        challenge: randomBase64URLBuffer(32),
        allowCredentials: allowCredentials
    }
}

generateServerGetAssertion

Back to html...

  • Comment login handler in password.auth.js
  • Add getAssertionChallenge function to webauthn.auth.js
let getGetAssertionChallenge = (formBody) => {
    return fetch('/webauthn/login', {
        method: 'POST',
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(formBody)
    })
    .then((response) => response.json())
    .then((response) => {
        if(response.status !== 'ok')
            throw new Error(`Server responed with error. The message is: ${response.message}`);

        return response
    })
}
/* Handle for login form submission */
$('#login').submit(function(event) {
    event.preventDefault();

    let username = this.username.value;

    if(!username) {
        alert('Username is missing!')
        return
    }

    getGetAssertionChallenge({username})
        .then((response) => {
            let publicKey = preformatGetAssertReq(response);
            return navigator.credentials.get({ publicKey })
        })
        .then((response) => {
            let getAssertionResponse = publicKeyCredentialToJSON(response);
            return sendWebAuthnResponse(getAssertionResponse)
        })
        .then((response) => {
            if(response.status === 'ok') {
                loadMainContainer()   
            } else {
                alert(`Server responed with error. The message is: ${response.message}`);
            }
        })
        .catch((error) => alert(error))
})

Adding login processor to webauthn.auth.js

   

    } else if(webauthnResp.response.authenticatorData !== undefined) {
        /* This is get assertion */

    } else {

Back to /response processor in routes/webauthn.js

AssertionVerification

In utils.js there is a method "verifyAuthenticatorAssertionResponse"

let verifyAuthenticatorAssertionResponse = (webAuthnResponse, authenticators) => {
    let authr = findAuthr(webAuthnResponse.id, authenticators);
    let authenticatorData = base64url.toBuffer(webAuthnResponse.response.authenticatorData);

    let response = {'verified': false};
    if(authr.fmt === 'fido-u2f') {
        let authrDataStruct  = parseGetAssertAuthData(authenticatorData);

        if(!(authrDataStruct.flags & U2F_USER_PRESENTED))
            throw new Error('User was NOT presented durring authentication!');

        let clientDataHash   = hash(base64url.toBuffer(webAuthnResponse.response.clientDataJSON))
        let signatureBase    = Buffer.concat([authrDataStruct.rpIdHash, authrDataStruct.flagsBuf, authrDataStruct.counterBuf, clientDataHash]);

        let publicKey = ASN1toPEM(base64url.toBuffer(authr.publicKey));
        let signature = base64url.toBuffer(webAuthnResponse.response.signature);

        response.verified = verifySignature(signature, signatureBase, publicKey)

        if(response.verified) {
            if(response.counter <= authr.counter)
                throw new Error('Authr counter did not increase!');

            authr.counter = authrDataStruct.counter
        }
    }

    return response
}

Searching for authenticator specified by rawID

It's a U2F statement

Parsing raw authData buffer

Check that TUP flag is set

Generate signature base

PKCS PubKey to PEM

Verify signature

Check that counter increased

Update counter

authenticatorData parsing


    let parseGetAssertAuthData = (buffer) => {
        let rpIdHash      = buffer.slice(0, 32);          buffer = buffer.slice(32);
        let flagsBuf      = buffer.slice(0, 1);           buffer = buffer.slice(1);
        let flags         = flagsBuf[0];
        let counterBuf    = buffer.slice(0, 4);           buffer = buffer.slice(4);
        let counter       = counterBuf.readUInt32BE(0);
    
        return {rpIdHash, flagsBuf, flags, counter, counterBuf}
    }
   

    } else if(webauthnResp.response.authenticatorData !== undefined) {
        /* This is get assertion */
        result = utils.verifyAuthenticatorAssertionResponse(webauthnResp, database[request.session.username].authenticators);
    } else {

New function to be added to webauthn.auth.js

Updating assertion verification in webauthn.js

DEMO

Thanks you coming

Questions?

WORKSHOP: Authenticating your web like a boss

By FIDO Alliance

WORKSHOP: Authenticating your web like a boss

WebAuthn Workshop

  • 56,589