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:
So... before we start check that:
Short recap of CredManAPI
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?
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
Request challenge
Process challenge
Return response
What are we doing?
App architecture
Frontend architecture
For registration:
/* 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
}
})
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"
/* 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)
})
})
Now for the /webauthn/register endpoint
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
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
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:
Sending response to the server
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:
For authentication:
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...
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