mirror of
https://github.com/Yubico/python-fido2
synced 2024-11-12 04:12:18 +01:00
Move stuff around.
- Rename fido2/pyu2f -> fido2/_pyu2f - Partial docstrings, with type information. - Rewrote server example.
This commit is contained in:
parent
7b2b5ba232
commit
1edb0d9d4e
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@
|
||||
build/
|
||||
dist/
|
||||
.eggs/
|
||||
.idea/
|
||||
.ropeproject/
|
||||
ChangeLog
|
||||
man/*.1
|
||||
|
@ -4,4 +4,4 @@ repos:
|
||||
hooks:
|
||||
- id: flake8
|
||||
- id: double-quote-string-fixer
|
||||
exclude: '^(fido2|test)/pyu2f/.*'
|
||||
exclude: '^(fido2|test)/_pyu2f/.*'
|
||||
|
@ -1,4 +1,4 @@
|
||||
== fido2
|
||||
== python-fido2
|
||||
image:https://travis-ci.org/Yubico/python-fido2.svg?branch=master["Travis CI Status", link="https://travis-ci.org/Yubico/python-fido2"]
|
||||
image:https://ci.appveyor.com/api/projects/status/8orx9nbdfq52w47s/branch/master?svg=true["Appveyor Status", link="https://ci.appveyor.com/project/Yubico53275/python-fido-host/branch/master"]
|
||||
|
||||
|
@ -38,193 +38,96 @@ Now navigate to https://localhost:5000 in a supported web browser.
|
||||
from __future__ import print_function, absolute_import, unicode_literals
|
||||
|
||||
from fido2.client import ClientData
|
||||
from fido2.server import Fido2Server
|
||||
from fido2.ctap2 import AttestationObject, AuthenticatorData
|
||||
from flask import Flask, request
|
||||
from fido2 import cbor
|
||||
from flask import Flask, session, request, redirect, abort
|
||||
|
||||
import os
|
||||
|
||||
|
||||
HTML = """
|
||||
<html>
|
||||
<head><title>Fido 2.0 webauthn demo</title></head>
|
||||
<body>
|
||||
<h1>Webauthn demo</h1>
|
||||
<p>
|
||||
<strong>This demo requires a browser supporting the WebAuthn API!</strong>
|
||||
</p>
|
||||
<hr>
|
||||
{content}
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
app = Flask(__name__, static_url_path='')
|
||||
app.secret_key = os.urandom(32) # Used for session.
|
||||
|
||||
INDEX_HTML = HTML.format(content="""
|
||||
<a href="/register">Register</a><br>
|
||||
<a href="/authenticate">Authenticate</a><br>
|
||||
""")
|
||||
|
||||
REGISTER_HTML = HTML.format(content="""
|
||||
<h2>Register a credential</h2>
|
||||
<p>Touch your authenticator device now...</p>
|
||||
<script>
|
||||
navigator.credentials.create({{
|
||||
publicKey: {{
|
||||
rp: {{
|
||||
id: document.domain,
|
||||
name: 'Demo server'
|
||||
}},
|
||||
user: {{
|
||||
id: {user_id},
|
||||
name: 'a_user',
|
||||
displayName: 'A. User',
|
||||
icon: 'https://example.com/image.png'
|
||||
}},
|
||||
challenge: {challenge},
|
||||
pubKeyCredParams: [
|
||||
{{
|
||||
alg: -7,
|
||||
type: 'public-key'
|
||||
}}
|
||||
],
|
||||
excludeCredentials: [],
|
||||
attestation: 'direct',
|
||||
timeout: 60000
|
||||
}}
|
||||
}}).then(function(attestation) {{
|
||||
console.log(attestation);
|
||||
console.log(JSON.stringify({{
|
||||
attestationObject: Array.from(new Uint8Array(attestation.response.attestationObject)),
|
||||
clientData: Array.from(new Uint8Array(attestation.response.clientDataJSON))
|
||||
}}));
|
||||
fetch('/register', {{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({{
|
||||
attestationObject: Array.from(new Uint8Array(attestation.response.attestationObject)),
|
||||
clientData: Array.from(new Uint8Array(attestation.response.clientDataJSON))
|
||||
}})
|
||||
}}).then(function() {{
|
||||
alert('Registration successful. More details in server log...');
|
||||
window.location = '/';
|
||||
}});
|
||||
}}, function(reason) {{
|
||||
console.log('Failed', reason);
|
||||
}});
|
||||
</script>
|
||||
""") # noqa
|
||||
server = Fido2Server('localhost')
|
||||
rp = {
|
||||
'id': 'localhost',
|
||||
'name': 'Demo server'
|
||||
}
|
||||
|
||||
|
||||
AUTH_HTML = HTML.format(content="""
|
||||
<h2>Authenticate using a credential</h2>
|
||||
<p>Touch your authenticator device now...</p>
|
||||
<script>
|
||||
navigator.credentials.get({{
|
||||
publicKey: {{
|
||||
rpId: document.domain,
|
||||
challenge: {challenge},
|
||||
allowCredentials: [
|
||||
{{
|
||||
type: 'public-key',
|
||||
id: {credential_id}
|
||||
}}
|
||||
],
|
||||
timeout: 60000
|
||||
}}
|
||||
}}).then(function(attestation) {{
|
||||
console.log(attestation);
|
||||
fetch('/authenticate', {{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({{
|
||||
authenticatorData: Array.from(new Uint8Array(attestation.response.authenticatorData)),
|
||||
clientData: Array.from(new Uint8Array(attestation.response.clientDataJSON)),
|
||||
signature: Array.from(new Uint8Array(attestation.response.signature))
|
||||
}})
|
||||
}}).then(function() {{
|
||||
alert('Authentication successful. More details in server log...');
|
||||
window.location = '/';
|
||||
}});
|
||||
}}, function(reason) {{
|
||||
console.log('Failed', reason);
|
||||
}});
|
||||
</script>
|
||||
""") # noqa
|
||||
|
||||
|
||||
def to_js_array(value):
|
||||
return 'new Uint8Array(%r)' % list(bytearray(value))
|
||||
|
||||
|
||||
def from_js_array(value):
|
||||
return bytes(bytearray(value))
|
||||
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
global credential, last_challenge
|
||||
credential, last_challenge = None, None
|
||||
# Registered credentials are stored globally, in memory only. Single user
|
||||
# support, state is lost when the server terminates.
|
||||
credentials = []
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return INDEX_HTML
|
||||
return redirect('/index.html')
|
||||
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
def register():
|
||||
global credential, last_challenge
|
||||
if request.method == 'POST':
|
||||
data = request.get_json(force=True)
|
||||
client_data = ClientData(from_js_array(data['clientData']))
|
||||
att_obj = AttestationObject(from_js_array(data['attestationObject']))
|
||||
print('clientData', client_data)
|
||||
print('AttestationObject:', att_obj)
|
||||
@app.route('/api/register/begin', methods=['POST'])
|
||||
def register_begin():
|
||||
registration_data = server.register_begin(rp, {
|
||||
'id': b'user_id',
|
||||
'name': 'a_user',
|
||||
'displayName': 'A. User',
|
||||
'icon': 'https://example.com/image.png'
|
||||
}, credentials)
|
||||
|
||||
# Verify the challenge
|
||||
if client_data.challenge != last_challenge:
|
||||
raise ValueError('Challenge mismatch!')
|
||||
|
||||
# Verify the signature
|
||||
att_obj.verify(client_data.hash)
|
||||
credential = att_obj.auth_data.credential_data
|
||||
print('REGISTERED CREDENTIAL:', credential)
|
||||
return 'OK'
|
||||
|
||||
last_challenge = os.urandom(32)
|
||||
return REGISTER_HTML.format(
|
||||
user_id=to_js_array(b'user_id'),
|
||||
challenge=to_js_array(last_challenge)
|
||||
)
|
||||
session['challenge'] = registration_data['publicKey']['challenge']
|
||||
print('\n\n\n\n')
|
||||
print(registration_data)
|
||||
print('\n\n\n\n')
|
||||
return cbor.dumps(registration_data)
|
||||
|
||||
|
||||
@app.route('/authenticate', methods=['GET', 'POST'])
|
||||
def authenticate():
|
||||
global credential, last_challenge
|
||||
if not credential:
|
||||
return HTML.format(content='No credential registered!')
|
||||
@app.route('/api/register/complete', methods=['POST'])
|
||||
def register_complete():
|
||||
data = cbor.loads(request.get_data())[0]
|
||||
client_data = ClientData(data['clientDataJSON'])
|
||||
att_obj = AttestationObject(data['attestationObject'])
|
||||
print('clientData', client_data)
|
||||
print('AttestationObject:', att_obj)
|
||||
|
||||
if request.method == 'POST':
|
||||
data = request.get_json(force=True)
|
||||
client_data = ClientData(from_js_array(data['clientData']))
|
||||
auth_data = AuthenticatorData(from_js_array(data['authenticatorData']))
|
||||
signature = from_js_array(data['signature'])
|
||||
print('clientData', client_data)
|
||||
print('AuthenticatorData', auth_data)
|
||||
auth_data = server.register_complete(
|
||||
session['challenge'], client_data, att_obj)
|
||||
|
||||
# Verify the challenge
|
||||
if client_data.challenge != last_challenge:
|
||||
raise ValueError('Challenge mismatch!')
|
||||
credentials.append(auth_data.credential_data)
|
||||
print('REGISTERED CREDENTIAL:', auth_data.credential_data)
|
||||
return cbor.dumps({'status': 'OK'})
|
||||
|
||||
# Verify the signature
|
||||
credential.public_key.verify(auth_data + client_data.hash, signature)
|
||||
print('ASSERTION OK')
|
||||
return 'OK'
|
||||
|
||||
last_challenge = os.urandom(32)
|
||||
return AUTH_HTML.format(
|
||||
challenge=to_js_array(last_challenge),
|
||||
credential_id=to_js_array(credential.credential_id)
|
||||
)
|
||||
@app.route('/api/authenticate/begin', methods=['POST'])
|
||||
def authenticate_begin():
|
||||
if not credentials:
|
||||
abort(404)
|
||||
|
||||
auth_data = server.authenticate_begin(rp['id'], credentials)
|
||||
session['challenge'] = auth_data['publicKey']['challenge']
|
||||
return cbor.dumps(auth_data)
|
||||
|
||||
|
||||
@app.route('/api/authenticate/complete', methods=['POST'])
|
||||
def authenticate_complete():
|
||||
if not credentials:
|
||||
abort(404)
|
||||
|
||||
data = cbor.loads(request.get_data())[0]
|
||||
credential_id = data['credentialId']
|
||||
client_data = ClientData(data['clientDataJSON'])
|
||||
auth_data = AuthenticatorData(data['authenticatorData'])
|
||||
signature = data['signature']
|
||||
print('clientData', client_data)
|
||||
print('AuthenticatorData', auth_data)
|
||||
|
||||
server.authenticate_complete(credentials, credential_id,
|
||||
session.pop('challenge'), client_data,
|
||||
auth_data, signature)
|
||||
print('ASSERTION OK')
|
||||
return cbor.dumps({'status': 'OK'})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(__doc__)
|
||||
|
||||
app.run(ssl_context='adhoc', debug=True)
|
||||
|
47
examples/server/static/authenticate.html
Normal file
47
examples/server/static/authenticate.html
Normal file
@ -0,0 +1,47 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Fido 2.0 webauthn demo</title>
|
||||
<script src="/cbor.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Webauthn demo</h1>
|
||||
<p>
|
||||
<strong>This demo requires a browser supporting the WebAuthn API!</strong>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<h2>Authenticate using a credential</h2>
|
||||
<p>Touch your authenticator device now...</p>
|
||||
<script>
|
||||
fetch('/api/authenticate/begin', {
|
||||
method: 'POST',
|
||||
}).then(function(response) {
|
||||
return response.arrayBuffer();
|
||||
}).then(function(data) {
|
||||
return CBOR.decode(data);
|
||||
}).then(function(options) {
|
||||
navigator.credentials.get(options).then(function(assertion) {
|
||||
console.log(assertion);
|
||||
fetch('/api/authenticate/complete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/cbor'},
|
||||
body: CBOR.encode({
|
||||
"credentialId": new Uint8Array(assertion.rawId),
|
||||
"authenticatorData": new Uint8Array(assertion.response.authenticatorData),
|
||||
"clientDataJSON": new Uint8Array(assertion.response.clientDataJSON),
|
||||
"signature": new Uint8Array(assertion.response.signature)
|
||||
})
|
||||
}).then(function() {
|
||||
alert('Authentication successful. More details in server log...');
|
||||
window.location = '/index.html';
|
||||
});
|
||||
}, function(reason) {
|
||||
console.log('Failed', reason);
|
||||
alert(reason);
|
||||
window.location = '/index.html';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
406
examples/server/static/cbor.js
Normal file
406
examples/server/static/cbor.js
Normal file
@ -0,0 +1,406 @@
|
||||
/*
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
(function(global, undefined) { "use strict";
|
||||
var POW_2_24 = 5.960464477539063e-8,
|
||||
POW_2_32 = 4294967296,
|
||||
POW_2_53 = 9007199254740992;
|
||||
|
||||
function encode(value) {
|
||||
var data = new ArrayBuffer(256);
|
||||
var dataView = new DataView(data);
|
||||
var lastLength;
|
||||
var offset = 0;
|
||||
|
||||
function prepareWrite(length) {
|
||||
var newByteLength = data.byteLength;
|
||||
var requiredLength = offset + length;
|
||||
while (newByteLength < requiredLength)
|
||||
newByteLength <<= 1;
|
||||
if (newByteLength !== data.byteLength) {
|
||||
var oldDataView = dataView;
|
||||
data = new ArrayBuffer(newByteLength);
|
||||
dataView = new DataView(data);
|
||||
var uint32count = (offset + 3) >> 2;
|
||||
for (var i = 0; i < uint32count; ++i)
|
||||
dataView.setUint32(i << 2, oldDataView.getUint32(i << 2));
|
||||
}
|
||||
|
||||
lastLength = length;
|
||||
return dataView;
|
||||
}
|
||||
function commitWrite() {
|
||||
offset += lastLength;
|
||||
}
|
||||
function writeFloat64(value) {
|
||||
commitWrite(prepareWrite(8).setFloat64(offset, value));
|
||||
}
|
||||
function writeUint8(value) {
|
||||
commitWrite(prepareWrite(1).setUint8(offset, value));
|
||||
}
|
||||
function writeUint8Array(value) {
|
||||
var dataView = prepareWrite(value.length);
|
||||
for (var i = 0; i < value.length; ++i)
|
||||
dataView.setUint8(offset + i, value[i]);
|
||||
commitWrite();
|
||||
}
|
||||
function writeUint16(value) {
|
||||
commitWrite(prepareWrite(2).setUint16(offset, value));
|
||||
}
|
||||
function writeUint32(value) {
|
||||
commitWrite(prepareWrite(4).setUint32(offset, value));
|
||||
}
|
||||
function writeUint64(value) {
|
||||
var low = value % POW_2_32;
|
||||
var high = (value - low) / POW_2_32;
|
||||
var dataView = prepareWrite(8);
|
||||
dataView.setUint32(offset, high);
|
||||
dataView.setUint32(offset + 4, low);
|
||||
commitWrite();
|
||||
}
|
||||
function writeTypeAndLength(type, length) {
|
||||
if (length < 24) {
|
||||
writeUint8(type << 5 | length);
|
||||
} else if (length < 0x100) {
|
||||
writeUint8(type << 5 | 24);
|
||||
writeUint8(length);
|
||||
} else if (length < 0x10000) {
|
||||
writeUint8(type << 5 | 25);
|
||||
writeUint16(length);
|
||||
} else if (length < 0x100000000) {
|
||||
writeUint8(type << 5 | 26);
|
||||
writeUint32(length);
|
||||
} else {
|
||||
writeUint8(type << 5 | 27);
|
||||
writeUint64(length);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeItem(value) {
|
||||
var i;
|
||||
|
||||
if (value === false)
|
||||
return writeUint8(0xf4);
|
||||
if (value === true)
|
||||
return writeUint8(0xf5);
|
||||
if (value === null)
|
||||
return writeUint8(0xf6);
|
||||
if (value === undefined)
|
||||
return writeUint8(0xf7);
|
||||
|
||||
switch (typeof value) {
|
||||
case "number":
|
||||
if (Math.floor(value) === value) {
|
||||
if (0 <= value && value <= POW_2_53)
|
||||
return writeTypeAndLength(0, value);
|
||||
if (-POW_2_53 <= value && value < 0)
|
||||
return writeTypeAndLength(1, -(value + 1));
|
||||
}
|
||||
writeUint8(0xfb);
|
||||
return writeFloat64(value);
|
||||
|
||||
case "string":
|
||||
var utf8data = [];
|
||||
for (i = 0; i < value.length; ++i) {
|
||||
var charCode = value.charCodeAt(i);
|
||||
if (charCode < 0x80) {
|
||||
utf8data.push(charCode);
|
||||
} else if (charCode < 0x800) {
|
||||
utf8data.push(0xc0 | charCode >> 6);
|
||||
utf8data.push(0x80 | charCode & 0x3f);
|
||||
} else if (charCode < 0xd800) {
|
||||
utf8data.push(0xe0 | charCode >> 12);
|
||||
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
|
||||
utf8data.push(0x80 | charCode & 0x3f);
|
||||
} else {
|
||||
charCode = (charCode & 0x3ff) << 10;
|
||||
charCode |= value.charCodeAt(++i) & 0x3ff;
|
||||
charCode += 0x10000;
|
||||
|
||||
utf8data.push(0xf0 | charCode >> 18);
|
||||
utf8data.push(0x80 | (charCode >> 12) & 0x3f);
|
||||
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
|
||||
utf8data.push(0x80 | charCode & 0x3f);
|
||||
}
|
||||
}
|
||||
|
||||
writeTypeAndLength(3, utf8data.length);
|
||||
return writeUint8Array(utf8data);
|
||||
|
||||
default:
|
||||
var length;
|
||||
if (Array.isArray(value)) {
|
||||
length = value.length;
|
||||
writeTypeAndLength(4, length);
|
||||
for (i = 0; i < length; ++i)
|
||||
encodeItem(value[i]);
|
||||
} else if (value instanceof Uint8Array) {
|
||||
writeTypeAndLength(2, value.length);
|
||||
writeUint8Array(value);
|
||||
} else {
|
||||
var keys = Object.keys(value);
|
||||
length = keys.length;
|
||||
writeTypeAndLength(5, length);
|
||||
for (i = 0; i < length; ++i) {
|
||||
var key = keys[i];
|
||||
encodeItem(key);
|
||||
encodeItem(value[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encodeItem(value);
|
||||
|
||||
if ("slice" in data)
|
||||
return data.slice(0, offset);
|
||||
|
||||
var ret = new ArrayBuffer(offset);
|
||||
var retView = new DataView(ret);
|
||||
for (var i = 0; i < offset; ++i)
|
||||
retView.setUint8(i, dataView.getUint8(i));
|
||||
return ret;
|
||||
}
|
||||
|
||||
function decode(data, tagger, simpleValue) {
|
||||
var dataView = new DataView(data);
|
||||
var offset = 0;
|
||||
|
||||
if (typeof tagger !== "function")
|
||||
tagger = function(value) { return value; };
|
||||
if (typeof simpleValue !== "function")
|
||||
simpleValue = function() { return undefined; };
|
||||
|
||||
function commitRead(length, value) {
|
||||
offset += length;
|
||||
return value;
|
||||
}
|
||||
function readArrayBuffer(length) {
|
||||
return commitRead(length, new Uint8Array(data, offset, length));
|
||||
}
|
||||
function readFloat16() {
|
||||
var tempArrayBuffer = new ArrayBuffer(4);
|
||||
var tempDataView = new DataView(tempArrayBuffer);
|
||||
var value = readUint16();
|
||||
|
||||
var sign = value & 0x8000;
|
||||
var exponent = value & 0x7c00;
|
||||
var fraction = value & 0x03ff;
|
||||
|
||||
if (exponent === 0x7c00)
|
||||
exponent = 0xff << 10;
|
||||
else if (exponent !== 0)
|
||||
exponent += (127 - 15) << 10;
|
||||
else if (fraction !== 0)
|
||||
return (sign ? -1 : 1) * fraction * POW_2_24;
|
||||
|
||||
tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13);
|
||||
return tempDataView.getFloat32(0);
|
||||
}
|
||||
function readFloat32() {
|
||||
return commitRead(4, dataView.getFloat32(offset));
|
||||
}
|
||||
function readFloat64() {
|
||||
return commitRead(8, dataView.getFloat64(offset));
|
||||
}
|
||||
function readUint8() {
|
||||
return commitRead(1, dataView.getUint8(offset));
|
||||
}
|
||||
function readUint16() {
|
||||
return commitRead(2, dataView.getUint16(offset));
|
||||
}
|
||||
function readUint32() {
|
||||
return commitRead(4, dataView.getUint32(offset));
|
||||
}
|
||||
function readUint64() {
|
||||
return readUint32() * POW_2_32 + readUint32();
|
||||
}
|
||||
function readBreak() {
|
||||
if (dataView.getUint8(offset) !== 0xff)
|
||||
return false;
|
||||
offset += 1;
|
||||
return true;
|
||||
}
|
||||
function readLength(additionalInformation) {
|
||||
if (additionalInformation < 24)
|
||||
return additionalInformation;
|
||||
if (additionalInformation === 24)
|
||||
return readUint8();
|
||||
if (additionalInformation === 25)
|
||||
return readUint16();
|
||||
if (additionalInformation === 26)
|
||||
return readUint32();
|
||||
if (additionalInformation === 27)
|
||||
return readUint64();
|
||||
if (additionalInformation === 31)
|
||||
return -1;
|
||||
throw "Invalid length encoding";
|
||||
}
|
||||
function readIndefiniteStringLength(majorType) {
|
||||
var initialByte = readUint8();
|
||||
if (initialByte === 0xff)
|
||||
return -1;
|
||||
var length = readLength(initialByte & 0x1f);
|
||||
if (length < 0 || (initialByte >> 5) !== majorType)
|
||||
throw "Invalid indefinite length element";
|
||||
return length;
|
||||
}
|
||||
|
||||
function appendUtf16Data(utf16data, length) {
|
||||
for (var i = 0; i < length; ++i) {
|
||||
var value = readUint8();
|
||||
if (value & 0x80) {
|
||||
if (value < 0xe0) {
|
||||
value = (value & 0x1f) << 6
|
||||
| (readUint8() & 0x3f);
|
||||
length -= 1;
|
||||
} else if (value < 0xf0) {
|
||||
value = (value & 0x0f) << 12
|
||||
| (readUint8() & 0x3f) << 6
|
||||
| (readUint8() & 0x3f);
|
||||
length -= 2;
|
||||
} else {
|
||||
value = (value & 0x0f) << 18
|
||||
| (readUint8() & 0x3f) << 12
|
||||
| (readUint8() & 0x3f) << 6
|
||||
| (readUint8() & 0x3f);
|
||||
length -= 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (value < 0x10000) {
|
||||
utf16data.push(value);
|
||||
} else {
|
||||
value -= 0x10000;
|
||||
utf16data.push(0xd800 | (value >> 10));
|
||||
utf16data.push(0xdc00 | (value & 0x3ff));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decodeItem() {
|
||||
var initialByte = readUint8();
|
||||
var majorType = initialByte >> 5;
|
||||
var additionalInformation = initialByte & 0x1f;
|
||||
var i;
|
||||
var length;
|
||||
|
||||
if (majorType === 7) {
|
||||
switch (additionalInformation) {
|
||||
case 25:
|
||||
return readFloat16();
|
||||
case 26:
|
||||
return readFloat32();
|
||||
case 27:
|
||||
return readFloat64();
|
||||
}
|
||||
}
|
||||
|
||||
length = readLength(additionalInformation);
|
||||
if (length < 0 && (majorType < 2 || 6 < majorType))
|
||||
throw "Invalid length";
|
||||
|
||||
switch (majorType) {
|
||||
case 0:
|
||||
return length;
|
||||
case 1:
|
||||
return -1 - length;
|
||||
case 2:
|
||||
if (length < 0) {
|
||||
var elements = [];
|
||||
var fullArrayLength = 0;
|
||||
while ((length = readIndefiniteStringLength(majorType)) >= 0) {
|
||||
fullArrayLength += length;
|
||||
elements.push(readArrayBuffer(length));
|
||||
}
|
||||
var fullArray = new Uint8Array(fullArrayLength);
|
||||
var fullArrayOffset = 0;
|
||||
for (i = 0; i < elements.length; ++i) {
|
||||
fullArray.set(elements[i], fullArrayOffset);
|
||||
fullArrayOffset += elements[i].length;
|
||||
}
|
||||
return fullArray;
|
||||
}
|
||||
return readArrayBuffer(length);
|
||||
case 3:
|
||||
var utf16data = [];
|
||||
if (length < 0) {
|
||||
while ((length = readIndefiniteStringLength(majorType)) >= 0)
|
||||
appendUtf16Data(utf16data, length);
|
||||
} else
|
||||
appendUtf16Data(utf16data, length);
|
||||
return String.fromCharCode.apply(null, utf16data);
|
||||
case 4:
|
||||
var retArray;
|
||||
if (length < 0) {
|
||||
retArray = [];
|
||||
while (!readBreak())
|
||||
retArray.push(decodeItem());
|
||||
} else {
|
||||
retArray = new Array(length);
|
||||
for (i = 0; i < length; ++i)
|
||||
retArray[i] = decodeItem();
|
||||
}
|
||||
return retArray;
|
||||
case 5:
|
||||
var retObject = {};
|
||||
for (i = 0; i < length || length < 0 && !readBreak(); ++i) {
|
||||
var key = decodeItem();
|
||||
retObject[key] = decodeItem();
|
||||
}
|
||||
return retObject;
|
||||
case 6:
|
||||
return tagger(decodeItem(), length);
|
||||
case 7:
|
||||
switch (length) {
|
||||
case 20:
|
||||
return false;
|
||||
case 21:
|
||||
return true;
|
||||
case 22:
|
||||
return null;
|
||||
case 23:
|
||||
return undefined;
|
||||
default:
|
||||
return simpleValue(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ret = decodeItem();
|
||||
if (offset !== data.byteLength)
|
||||
throw "Remaining bytes";
|
||||
return ret;
|
||||
}
|
||||
|
||||
var obj = { encode: encode, decode: decode };
|
||||
|
||||
if (typeof define === "function" && define.amd)
|
||||
define("cbor/cbor", obj);
|
||||
else if (typeof module !== "undefined" && module.exports)
|
||||
module.exports = obj;
|
||||
else if (!global.CBOR)
|
||||
global.CBOR = obj;
|
||||
|
||||
})(this);
|
17
examples/server/static/index.html
Normal file
17
examples/server/static/index.html
Normal file
@ -0,0 +1,17 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Fido 2.0 webauthn demo</title>
|
||||
<script src="/cbor.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Webauthn demo</h1>
|
||||
<p>
|
||||
<strong>This demo requires a browser supporting the WebAuthn API!</strong>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<a href="/register.html">Register</a><br>
|
||||
<a href="/authenticate.html">Authenticate</a><br>
|
||||
|
||||
</body>
|
||||
</html>
|
46
examples/server/static/register.html
Normal file
46
examples/server/static/register.html
Normal file
@ -0,0 +1,46 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Fido 2.0 webauthn demo</title>
|
||||
<script src="/cbor.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Webauthn demo</h1>
|
||||
<p>
|
||||
<strong>This demo requires a browser supporting the WebAuthn API!</strong>
|
||||
</p>
|
||||
<hr>
|
||||
|
||||
<h2>Register a credential</h2>
|
||||
<p>Touch your authenticator device now...</p>
|
||||
<script>
|
||||
fetch('/api/register/begin', {
|
||||
method: 'POST',
|
||||
}).then(function(response) {
|
||||
return response.arrayBuffer();
|
||||
}).then(function(data) {
|
||||
return CBOR.decode(data);
|
||||
}).then(function(options) {
|
||||
navigator.credentials.create(options).then(function(attestation) {
|
||||
console.log(attestation.response);
|
||||
console.log(CBOR.encode(attestation.response));
|
||||
fetch('/api/register/complete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/cbor'},
|
||||
body: CBOR.encode({
|
||||
"attestationObject": new Uint8Array(attestation.response.attestationObject),
|
||||
"clientDataJSON": new Uint8Array(attestation.response.clientDataJSON),
|
||||
})
|
||||
}).then(function() {
|
||||
alert('Registration successful. More details in server log...');
|
||||
window.location = '/index.html';
|
||||
});
|
||||
}, function(reason) {
|
||||
console.log('Failed', reason);
|
||||
alert(reason);
|
||||
window.location = '/index.html';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -34,7 +34,6 @@ if six.PY2:
|
||||
class ABC(object):
|
||||
pass
|
||||
abc.ABC = ABC
|
||||
abc.abstractclassmethod = abc.abstractmethod
|
||||
|
||||
|
||||
__version__ = '0.3.1-dev0'
|
||||
|
@ -210,7 +210,7 @@ class UsbHidTransport(object):
|
||||
|
||||
self.packet_size = in_size
|
||||
self.read_timeout_secs = read_timeout_secs
|
||||
self.logger = logging.getLogger('pyu2f.hidtransport')
|
||||
self.logger = logging.getLogger('_pyu2f.hidtransport')
|
||||
|
||||
self.InternalInit()
|
||||
|
@ -25,7 +25,7 @@ import threading
|
||||
|
||||
from . import base
|
||||
|
||||
logger = logging.getLogger('pyu2f.macos')
|
||||
logger = logging.getLogger('_pyu2f.macos')
|
||||
|
||||
# Constants
|
||||
DEVICE_PATH_BUFFER_SIZE = 512
|
@ -64,7 +64,7 @@ def dump_list(data):
|
||||
|
||||
def _sort_keys(entry):
|
||||
key = entry[0]
|
||||
return (six.indexbytes(key, 0), len(key), key)
|
||||
return six.indexbytes(key, 0), len(key), key
|
||||
|
||||
|
||||
def dump_dict(data):
|
||||
@ -78,8 +78,8 @@ def dump_bytes(data):
|
||||
|
||||
|
||||
def dump_text(data):
|
||||
data = data.encode('utf8')
|
||||
return dump_int(len(data), mt=3) + data
|
||||
data_bytes = data.encode('utf8')
|
||||
return dump_int(len(data_bytes), mt=3) + data_bytes
|
||||
|
||||
|
||||
_SERIALIZERS = [
|
||||
|
@ -40,8 +40,9 @@ import six
|
||||
|
||||
|
||||
class ClientData(bytes):
|
||||
def __init__(self, data):
|
||||
self.data = json.loads(data.decode())
|
||||
def __init__(self, _):
|
||||
super(ClientData, self).__init__()
|
||||
self.data = json.loads(self.decode())
|
||||
|
||||
def get(self, key):
|
||||
return self.data[key]
|
||||
@ -74,7 +75,6 @@ class ClientData(bytes):
|
||||
|
||||
|
||||
class ClientError(Exception):
|
||||
|
||||
@unique
|
||||
class ERR(IntEnum):
|
||||
OTHER_ERROR = 1
|
||||
@ -161,7 +161,7 @@ class U2fClient(object):
|
||||
self.poll_delay = 0.25
|
||||
self.ctap = CTAP1(device)
|
||||
self.origin = origin
|
||||
self._verify = verify_app_id
|
||||
self._verify = verify
|
||||
|
||||
def _verify_app_id(self, app_id):
|
||||
try:
|
||||
@ -171,12 +171,12 @@ class U2fClient(object):
|
||||
pass # Fall through to ClientError
|
||||
raise ClientError.ERR.BAD_REQUEST()
|
||||
|
||||
def register(self, app_id, register_requests, registered_keys,
|
||||
timeout=None, on_keepalive=None):
|
||||
def register(self, app_id, register_requests, registered_keys, timeout=None,
|
||||
on_keepalive=None):
|
||||
self._verify_app_id(app_id)
|
||||
|
||||
version = self.ctap.get_version()
|
||||
dummy_param = b'\0'*32
|
||||
dummy_param = b'\0' * 32
|
||||
for key in registered_keys:
|
||||
if key['version'] != version:
|
||||
continue
|
||||
@ -263,12 +263,12 @@ class Fido2Client(object):
|
||||
self.origin = origin
|
||||
self._verify = verify
|
||||
try:
|
||||
self.ctap = CTAP2(device)
|
||||
self.pin_protocol = PinProtocolV1(self.ctap)
|
||||
self.ctap2 = CTAP2(device)
|
||||
self.pin_protocol = PinProtocolV1(self.ctap2)
|
||||
self._do_make_credential = self._ctap2_make_credential
|
||||
self._do_get_assertion = self._ctap2_get_assertion
|
||||
except ValueError:
|
||||
self.ctap = CTAP1(device)
|
||||
self.ctap1 = CTAP1(device)
|
||||
self._do_make_credential = self._ctap1_make_credential
|
||||
self._do_get_assertion = self._ctap1_get_assertion
|
||||
|
||||
@ -283,6 +283,7 @@ class Fido2Client(object):
|
||||
def make_credential(self, rp, user, challenge, algos=[ES256.ALGORITHM],
|
||||
exclude_list=None, extensions=None, rk=False, uv=False,
|
||||
pin=None, timeout=None, on_keepalive=None):
|
||||
|
||||
self._verify_rp_id(rp['id'])
|
||||
|
||||
client_data = ClientData.build(
|
||||
@ -304,7 +305,7 @@ class Fido2Client(object):
|
||||
extensions, rk, uv, pin, timeout, on_keepalive):
|
||||
key_params = [{'type': 'public-key', 'alg': alg} for alg in algos]
|
||||
|
||||
info = self.ctap.get_info()
|
||||
info = self.ctap2.get_info()
|
||||
pin_auth = None
|
||||
pin_protocol = None
|
||||
if pin:
|
||||
@ -326,10 +327,10 @@ class Fido2Client(object):
|
||||
if uv:
|
||||
options['uv'] = True
|
||||
|
||||
return self.ctap.make_credential(client_data.hash, rp, user,
|
||||
key_params, exclude_list,
|
||||
extensions, options, pin_auth,
|
||||
pin_protocol, timeout, on_keepalive)
|
||||
return self.ctap2.make_credential(client_data.hash, rp, user,
|
||||
key_params, exclude_list,
|
||||
extensions, options, pin_auth,
|
||||
pin_protocol, timeout, on_keepalive)
|
||||
|
||||
def _ctap1_make_credential(self, client_data, rp, user, algos, exclude_list,
|
||||
extensions, rk, uv, pin, timeout, on_keepalive):
|
||||
@ -338,27 +339,29 @@ class Fido2Client(object):
|
||||
|
||||
app_param = sha256(rp['id'].encode())
|
||||
|
||||
dummy_param = b'\0'*32
|
||||
dummy_param = b'\0' * 32
|
||||
for cred in exclude_list or []:
|
||||
key_handle = cred['id']
|
||||
try:
|
||||
self.ctap.authenticate(dummy_param, app_param, key_handle, True)
|
||||
self.ctap1.authenticate(
|
||||
dummy_param, app_param, key_handle, True)
|
||||
raise ClientError.ERR.OTHER_ERROR() # Shouldn't happen
|
||||
except ApduError as e:
|
||||
if e.code == APDU.USE_NOT_SATISFIED:
|
||||
_call_polling(self.ctap1_poll_delay, timeout, on_keepalive,
|
||||
self.ctap.register, dummy_param, dummy_param)
|
||||
self.ctap1.register, dummy_param, dummy_param)
|
||||
raise ClientError.ERR.DEVICE_INELIGIBLE()
|
||||
|
||||
return AttestationObject.from_ctap1(
|
||||
app_param,
|
||||
_call_polling(self.ctap1_poll_delay, timeout, on_keepalive,
|
||||
self.ctap.register, client_data.hash, app_param)
|
||||
self.ctap1.register, client_data.hash, app_param)
|
||||
)
|
||||
|
||||
def get_assertion(self, rp_id, challenge, allow_list=None, extensions=None,
|
||||
up=True, uv=False, pin=None, timeout=None,
|
||||
on_keepalive=None):
|
||||
|
||||
self._verify_rp_id(rp_id)
|
||||
|
||||
client_data = ClientData.build(
|
||||
@ -399,12 +402,12 @@ class Fido2Client(object):
|
||||
if len(options) == 0:
|
||||
options = None
|
||||
|
||||
assertions = [self.ctap.get_assertion(
|
||||
assertions = [self.ctap2.get_assertion(
|
||||
rp_id, client_data.hash, allow_list, extensions, options, pin_auth,
|
||||
pin_protocol, timeout, on_keepalive
|
||||
)]
|
||||
for _ in range((assertions[0].number_of_credentials or 1) - 1):
|
||||
assertions.append(self.ctap.get_next_assertion())
|
||||
assertions.append(self.ctap2.get_next_assertion())
|
||||
return assertions
|
||||
|
||||
def _ctap1_get_assertion(self, client_data, rp_id, allow_list, extensions,
|
||||
@ -418,7 +421,7 @@ class Fido2Client(object):
|
||||
try:
|
||||
auth_resp = _call_polling(
|
||||
self.ctap1_poll_delay, timeout, on_keepalive,
|
||||
self.ctap.authenticate, client_param, app_param, cred['id']
|
||||
self.ctap1.authenticate, client_param, app_param, cred['id']
|
||||
)
|
||||
return [
|
||||
AssertionResponse.from_ctap1(app_param, cred, auth_resp)
|
||||
|
@ -34,27 +34,51 @@ from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding
|
||||
|
||||
|
||||
class CoseKey(dict):
|
||||
"""A COSE formatted public key.
|
||||
|
||||
:param _: The COSE key paramters.
|
||||
:cvar ALGORITHM: COSE algorithm identifier.
|
||||
"""
|
||||
ALGORITHM = None
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Validates a digital signature over a given message.
|
||||
|
||||
:param message: The message which was signed.
|
||||
:param signature: The signature to check.
|
||||
"""
|
||||
raise NotImplementedError('Signature verification not supported.')
|
||||
|
||||
@classmethod
|
||||
def from_cryptography_key(cls, public_key):
|
||||
"""Converts a PublicKey object from Cryptography into a COSE key.
|
||||
|
||||
:param public_key: Either an EC or RSA public key.
|
||||
:return: A CoseKey.
|
||||
"""
|
||||
raise NotImplementedError('Creation from cryptography not supported.')
|
||||
|
||||
@staticmethod
|
||||
def for_alg(alg):
|
||||
"""Get a subclass of CoseKey corresponding to an algorithm identifier.
|
||||
|
||||
:param alg: The COSE identifier of the algorithm.
|
||||
:return: A CoseKey.
|
||||
"""
|
||||
for cls in CoseKey.__subclasses__():
|
||||
if getattr(cls, 'ALGORITHM', None) == alg:
|
||||
if cls.ALGORITHM == alg:
|
||||
return cls
|
||||
return UnsupportedKey
|
||||
|
||||
@staticmethod
|
||||
def parse(cose):
|
||||
|
||||
"""Create a CoseKey from a dict"""
|
||||
return CoseKey.for_alg(cose[3])(cose)
|
||||
|
||||
|
||||
class UnsupportedKey(CoseKey):
|
||||
pass
|
||||
"""A COSE key with an unsupported algorithm."""
|
||||
|
||||
|
||||
class ES256(CoseKey):
|
||||
@ -80,6 +104,11 @@ class ES256(CoseKey):
|
||||
|
||||
@classmethod
|
||||
def from_ctap1(cls, data):
|
||||
"""Creates an ES256 key from a CTAP1 formatted public key byte string.
|
||||
|
||||
:param data: A 65 byte SECP256R1 public key.
|
||||
:return: A ES256 key.
|
||||
"""
|
||||
return cls({
|
||||
1: 2,
|
||||
3: cls.ALGORITHM,
|
||||
|
@ -39,22 +39,22 @@ class CtapDevice(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def call(self, cmd, data=b'', event=None, on_keepalive=None):
|
||||
"""
|
||||
cmd is the integer value of the command.
|
||||
data is the binary string value of the payload.
|
||||
event is an instance of threading.Event which can be used to cancel the
|
||||
invocation.
|
||||
on_keepalive is an optional callback function that is invoked on
|
||||
keepalive message from the authenticator, with the keepalive status code
|
||||
as an argument. The callback is only invoked once for consecutive
|
||||
keepalive messages with the same status.
|
||||
"""Sends a command to the authenticator, and reads the response.
|
||||
|
||||
:param cmd: The integer value of the command.
|
||||
:param data: The payload of the command.
|
||||
:param event: An optional threading.Event which can be used to cancel
|
||||
the invocation.
|
||||
:param on_keepalive: An optional callback to handle keep-alive messages
|
||||
from the authenticator. The function is only called once for
|
||||
consecutive keep-alive messages with the same status.
|
||||
:return: The response from the authenticator.
|
||||
"""
|
||||
|
||||
@abc.abstractclassmethod
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def list_devices(cls):
|
||||
"""
|
||||
Generates instances of cls for discoverable devices.
|
||||
"""
|
||||
"""Generates instances of cls for discoverable devices."""
|
||||
|
||||
|
||||
class CtapError(Exception):
|
||||
|
@ -39,12 +39,20 @@ import six
|
||||
|
||||
@unique
|
||||
class APDU(IntEnum):
|
||||
"""APDU response codes."""
|
||||
OK = 0x9000
|
||||
USE_NOT_SATISFIED = 0x6985
|
||||
WRONG_DATA = 0x6a80
|
||||
|
||||
|
||||
class ApduError(Exception):
|
||||
"""An Exception thrown when a response APDU doesn't have an OK (0x9000)
|
||||
status.
|
||||
|
||||
:param code: APDU response code.
|
||||
:param data: APDU response body.
|
||||
|
||||
"""
|
||||
def __init__(self, code, data=b''):
|
||||
self.code = code
|
||||
self.data = data
|
||||
@ -55,7 +63,18 @@ class ApduError(Exception):
|
||||
|
||||
|
||||
class RegistrationData(bytes):
|
||||
"""Binary response data for a CTAP1 registration.
|
||||
|
||||
:param _: The binary contents of the response data.
|
||||
:ivar public_key: Binary representation of the credential public key.
|
||||
:ivar key_handle: Binary key handle of the credential.
|
||||
:ivar certificate: Attestation certificate of the authenticator, DER
|
||||
encoded.
|
||||
:ivar signature: Attestation signature.
|
||||
"""
|
||||
def __init__(self, _):
|
||||
super(RegistrationData, self).__init__()
|
||||
|
||||
if six.indexbytes(self, 0) != 0x05:
|
||||
raise ValueError('Reserved byte != 0x05')
|
||||
|
||||
@ -75,9 +94,16 @@ class RegistrationData(bytes):
|
||||
|
||||
@property
|
||||
def b64(self):
|
||||
"""Websafe base64 encoded string of the RegistrationData."""
|
||||
return websafe_encode(self)
|
||||
|
||||
def verify(self, app_param, client_param):
|
||||
"""Verify the included signature with regard to the given app and client
|
||||
params.
|
||||
|
||||
:param app_param: SHA256 hash of the app ID used for the request.
|
||||
:param client_param: SHA256 hash of the ClientData used for the request.
|
||||
"""
|
||||
FidoU2FAttestation.verify_signature(
|
||||
app_param, client_param, self.key_handle, self.public_key,
|
||||
self.certificate, self.signature)
|
||||
@ -98,19 +124,42 @@ class RegistrationData(bytes):
|
||||
|
||||
@classmethod
|
||||
def from_b64(cls, data):
|
||||
"""Parse a RegistrationData from a websafe base64 encoded string.
|
||||
|
||||
:param data: Websafe base64 encoded string.
|
||||
:return: The decoded and parsed RegistrationData.
|
||||
"""
|
||||
return cls(websafe_decode(data))
|
||||
|
||||
|
||||
class SignatureData(bytes):
|
||||
"""Binary response data for a CTAP1 authentication.
|
||||
|
||||
:param _: The binary contents of the response data.
|
||||
:ivar user_presence: User presence byte.
|
||||
:ivar counter: Signature counter.
|
||||
:ivar signature: Cryptographic signature.
|
||||
"""
|
||||
def __init__(self, _):
|
||||
self.user_presence, self.counter = struct.unpack('>BI', self[:5])
|
||||
super(SignatureData, self).__init__()
|
||||
|
||||
self.user_presence = six.indexbytes(self, 0)
|
||||
self.counter = struct.unpack('>I', self[1:5])[0]
|
||||
self.signature = self[5:]
|
||||
|
||||
@property
|
||||
def b64(self):
|
||||
"""str: Websafe base64 encoded string of the SignatureData."""
|
||||
return websafe_encode(self)
|
||||
|
||||
def verify(self, app_param, client_param, public_key):
|
||||
"""Verify the included signature with regard to the given app and client
|
||||
params, using the given public key.
|
||||
|
||||
:param app_param: SHA256 hash of the app ID used for the request.
|
||||
:param client_param: SHA256 hash of the ClientData used for the request.
|
||||
:param public_key: Binary representation of the credential public key.
|
||||
"""
|
||||
m = app_param + self[:5] + client_param
|
||||
ES256.from_ctap1(public_key).verify(m, self.signature)
|
||||
|
||||
@ -124,10 +173,19 @@ class SignatureData(bytes):
|
||||
|
||||
@classmethod
|
||||
def from_b64(cls, data):
|
||||
"""Parse a SignatureData from a websafe base64 encoded string.
|
||||
|
||||
:param data: Websafe base64 encoded string.
|
||||
:return: The decoded and parsed SignatureData.
|
||||
"""
|
||||
return cls(websafe_decode(data))
|
||||
|
||||
|
||||
class CTAP1(object):
|
||||
"""Implementation of the CTAP1 specification.
|
||||
|
||||
:param device: A CtapHidDevice handle supporting CTAP1.
|
||||
"""
|
||||
@unique
|
||||
class INS(IntEnum):
|
||||
REGISTER = 0x01
|
||||
@ -135,9 +193,23 @@ class CTAP1(object):
|
||||
VERSION = 0x03
|
||||
|
||||
def __init__(self, device):
|
||||
|
||||
self.device = device
|
||||
|
||||
def send_apdu(self, cla=0, ins=0, p1=0, p2=0, data=b''):
|
||||
"""Packs and sends an APDU for use in CTAP1 commands.
|
||||
This is a low-level method mainly used internally. Avoid calling it
|
||||
directly if possible, and use the get_version, register, and
|
||||
authenticate methods if possible instead.
|
||||
|
||||
:param cla: The CLA parameter of the request.
|
||||
:param ins: The INS parameter of the request.
|
||||
:param p1: The P1 parameter of the request.
|
||||
:param p2: The P2 parameter of the request.
|
||||
:param data: The body of the request.
|
||||
:return: The response APDU data of a successful request.
|
||||
:raise: ApduError
|
||||
"""
|
||||
size = len(data)
|
||||
size_h = size >> 16 & 0xff
|
||||
size_l = size & 0xffff
|
||||
@ -152,15 +224,35 @@ class CTAP1(object):
|
||||
return data
|
||||
|
||||
def get_version(self):
|
||||
"""Get the U2F version implemented by the authenticator.
|
||||
The only version specified is "U2F_V2".
|
||||
|
||||
:return: A U2F version string.
|
||||
"""
|
||||
return self.send_apdu(ins=CTAP1.INS.VERSION).decode()
|
||||
|
||||
def register(self, client_param, app_param):
|
||||
"""Register a new U2F credential.
|
||||
|
||||
:param client_param: SHA256 hash of the ClientData used for the request.
|
||||
:param app_param: SHA256 hash of the app ID used for the request.
|
||||
:return: The registration response from the authenticator.
|
||||
"""
|
||||
data = client_param + app_param
|
||||
response = self.send_apdu(ins=CTAP1.INS.REGISTER, data=data)
|
||||
return RegistrationData(response)
|
||||
|
||||
def authenticate(self, client_param, app_param, key_handle,
|
||||
check_only=False):
|
||||
"""Authenticate a previously registered credential.
|
||||
|
||||
:param client_param: SHA256 hash of the ClientData used for the request.
|
||||
:param app_param: SHA256 hash of the app ID used for the request.
|
||||
:param key_handle: The binary key handle of the credential.
|
||||
:param check_only: True to send a "check-only" request, which is used to
|
||||
determine if a key handle is known.
|
||||
:return: The authentication response from the authenticator.
|
||||
"""
|
||||
data = client_param + app_param \
|
||||
+ struct.pack('>B', len(key_handle)) + key_handle
|
||||
p1 = 0x07 if check_only else 0x03
|
||||
|
349
fido2/ctap2.py
349
fido2/ctap2.py
@ -45,16 +45,22 @@ import six
|
||||
import re
|
||||
|
||||
|
||||
def args(*args):
|
||||
def args(*params):
|
||||
"""Constructs a dict from a list of arguments for sending a CBOR command.
|
||||
None elements will be omitted.
|
||||
|
||||
:param params: Arguments, in order, to add to the command.
|
||||
:return: The input parameters as a dict.
|
||||
"""
|
||||
Constructs a dict from a list of arguments for sending a CBOR command.
|
||||
"""
|
||||
if args:
|
||||
return dict((i, v) for i, v in enumerate(args, 1) if v is not None)
|
||||
return None
|
||||
return dict((i, v) for i, v in enumerate(params, 1) if v is not None)
|
||||
|
||||
|
||||
def hexstr(bs):
|
||||
"""Formats a byte string as a human readable hex string.
|
||||
|
||||
:param bs: The bytes to format.
|
||||
:return: A readable string representation of the input.
|
||||
"""
|
||||
return "h'%s'" % b2a_hex(bs).decode()
|
||||
|
||||
|
||||
@ -66,6 +72,19 @@ def _parse_cbor(data):
|
||||
|
||||
|
||||
class Info(bytes):
|
||||
"""Binary CBOR encoded response data returned by the CTAP2 GET_INFO command.
|
||||
|
||||
:param _: The binary content of the Info data.
|
||||
:ivar versions: The versions supported by the authenticator.
|
||||
:ivar extensions: The extensions supported by the authenticator.
|
||||
:ivar aaguid: The AAGUID of the authenticator.
|
||||
:ivar options: The options supported by the authenticator.
|
||||
:ivar max_msg_size: The max message size supported by the authenticator.
|
||||
:ivar pin_protocols: The PIN protocol versions supported by the
|
||||
authenticator.
|
||||
:ivar data: The Info members, in the form of a dict.
|
||||
"""
|
||||
|
||||
@unique
|
||||
class KEY(IntEnum):
|
||||
VERSIONS = 1
|
||||
@ -75,14 +94,17 @@ class Info(bytes):
|
||||
MAX_MSG_SIZE = 5
|
||||
PIN_PROTOCOLS = 6
|
||||
|
||||
def __init__(self, data):
|
||||
data = dict((Info.KEY(k), v) for (k, v) in _parse_cbor(data).items())
|
||||
def __init__(self, _):
|
||||
super(Info, self).__init__()
|
||||
|
||||
data = dict((Info.KEY(k), v) for (k, v) in _parse_cbor(self).items())
|
||||
self.versions = data[Info.KEY.VERSIONS]
|
||||
self.extensions = data.get(Info.KEY.EXTENSIONS, [])
|
||||
self.aaguid = data[Info.KEY.AAGUID]
|
||||
self.options = data.get(Info.KEY.OPTIONS, {})
|
||||
self.max_msg_size = data.get(Info.KEY.MAX_MSG_SIZE, 1024)
|
||||
self.pin_protocols = data.get(Info.KEY.PIN_PROTOCOLS, [])
|
||||
self.pin_protocols = data.get(
|
||||
Info.KEY.PIN_PROTOCOLS, [])
|
||||
self.data = data
|
||||
|
||||
def __repr__(self):
|
||||
@ -102,11 +124,22 @@ class Info(bytes):
|
||||
|
||||
|
||||
class AttestedCredentialData(bytes):
|
||||
"""Binary encoding of the attested credential data.
|
||||
|
||||
:param _: The binary representation of the attested credential data.
|
||||
:ivar aaguid: The AAGUID of the authenticator.
|
||||
:ivar credential_id: The binary ID of the credential.
|
||||
:ivar public_key: The public key of the credential.
|
||||
"""
|
||||
|
||||
def __init__(self, _):
|
||||
self.aaguid, self.credential_id, self.public_key, rest = \
|
||||
AttestedCredentialData.parse(self)
|
||||
if rest:
|
||||
super(AttestedCredentialData, self).__init__()
|
||||
|
||||
parsed = AttestedCredentialData.parse(self)
|
||||
self.aaguid = parsed[0]
|
||||
self.credential_id = parsed[1]
|
||||
self.public_key = parsed[2]
|
||||
if parsed[3]:
|
||||
raise ValueError('Wrong length')
|
||||
|
||||
def __repr__(self):
|
||||
@ -120,6 +153,12 @@ class AttestedCredentialData(bytes):
|
||||
|
||||
@staticmethod
|
||||
def parse(data):
|
||||
"""Parse the components of an AttestedCredentialData from a binary
|
||||
string, and return them.
|
||||
|
||||
:param data: A binary string containing an attested credential data.
|
||||
:return: AAGUID, credential ID, public key, and remaining data.
|
||||
"""
|
||||
aaguid = data[:16]
|
||||
c_len = struct.unpack('>H', data[16:18])[0]
|
||||
cred_id = data[18:18+c_len]
|
||||
@ -128,16 +167,41 @@ class AttestedCredentialData(bytes):
|
||||
|
||||
@classmethod
|
||||
def create(cls, aaguid, credential_id, public_key):
|
||||
"""Create an AttestedCredentialData by providing its components.
|
||||
|
||||
:param aaguid: The AAGUID of the authenticator.
|
||||
:param credential_id: The binary ID of the credential.
|
||||
:param public_key: A COSE formatted public key.
|
||||
:return: The attested credential data.
|
||||
"""
|
||||
return cls(aaguid + struct.pack('>H', len(credential_id))
|
||||
+ credential_id + cbor.dumps(public_key))
|
||||
|
||||
@classmethod
|
||||
def unpack_from(cls, data):
|
||||
args = cls.parse(data)
|
||||
return cls.create(*args[:-1]), args[-1]
|
||||
"""Unpack an AttestedCredentialData from a byte string, returning it and
|
||||
any remaining data.
|
||||
|
||||
:param data: A binary string containing an attested credential data.
|
||||
:return: The parsed AttestedCredentialData, and any remaining data from
|
||||
the input.
|
||||
"""
|
||||
parts = cls.parse(data)
|
||||
return cls.create(*parts[:-1]), parts[-1]
|
||||
|
||||
|
||||
class AuthenticatorData(bytes):
|
||||
"""Binary encoding of the authenticator data.
|
||||
|
||||
:param _: The binary representation of the authenticator data.
|
||||
:ivar rp_id_hash: SHA256 hash of the RP ID.
|
||||
:ivar flags: The flags of the authenticator data, see
|
||||
AuthenticatorData.FLAG.
|
||||
:ivar counter: The signature counter of the authenticator.
|
||||
:ivar credential_data: Attested credential data, if available.
|
||||
:ivar extensions: Authenticator extensions, if available.
|
||||
"""
|
||||
|
||||
@unique
|
||||
class FLAG(IntEnum):
|
||||
UP = 0x01
|
||||
@ -145,9 +209,12 @@ class AuthenticatorData(bytes):
|
||||
AT = 0x40
|
||||
ED = 0x80
|
||||
|
||||
def __init__(self, data):
|
||||
def __init__(self, _):
|
||||
super(AuthenticatorData, self).__init__()
|
||||
|
||||
self.rp_id_hash = self[:32]
|
||||
self.flags, self.counter = struct.unpack('>BI', self[32:32+5])
|
||||
self.flags = six.indexbytes(self, 32)
|
||||
self.counter = struct.unpack('>I', self[33:33+4])[0]
|
||||
rest = self[37:]
|
||||
|
||||
if self.flags & AuthenticatorData.FLAG.AT:
|
||||
@ -167,6 +234,16 @@ class AuthenticatorData(bytes):
|
||||
@classmethod
|
||||
def create(cls, rp_id_hash, flags, counter, credential_data=b'',
|
||||
extensions=None):
|
||||
"""Create an AuthenticatorData instance.
|
||||
|
||||
:param rp_id_hash: SHA256 hash of the RP ID.
|
||||
:param flags: Flags of the AuthenticatorData.
|
||||
:param counter: Signature counter of the authenticator data.
|
||||
:param credential_data: Authenticated credential data (only if AT flag
|
||||
is set).
|
||||
:param extensions: Authenticator extensions (only if ED flag is set).
|
||||
:return: The authenticator data.
|
||||
"""
|
||||
return cls(
|
||||
rp_id_hash + struct.pack('>BI', flags, counter) + credential_data +
|
||||
(cbor.dumps(extensions) if extensions is not None else b'')
|
||||
@ -186,6 +263,20 @@ class AuthenticatorData(bytes):
|
||||
|
||||
|
||||
class AttestationObject(bytes):
|
||||
"""Binary CBOR encoded attestation object.
|
||||
|
||||
:param _: The binary representation of the attestation object.
|
||||
:type _: bytes
|
||||
:ivar fmt: The type of attestation used.
|
||||
:type fmt: str
|
||||
:ivar auth_data: The attested authenticator data.
|
||||
:type auth_data: AuthenticatorData
|
||||
:ivar att_statement: The attestation statement.
|
||||
:type att_statement: Dict[str, Any]
|
||||
:ivar data: The AttestationObject members, in the form of a dict.
|
||||
:type data: Dict[AttestationObject.KEY, Any]
|
||||
"""
|
||||
|
||||
@unique
|
||||
class KEY(IntEnum):
|
||||
FMT = 1
|
||||
@ -194,25 +285,40 @@ class AttestationObject(bytes):
|
||||
|
||||
@classmethod
|
||||
def for_key(cls, key):
|
||||
try:
|
||||
"""Get an AttestationObject.KEY by number or by name, using the
|
||||
numeric ID or the Webauthn key string.
|
||||
|
||||
:param key: The numeric key value, or the string name of a member.
|
||||
:type key: Union[str, int]
|
||||
:return: The KEY corresponding to the input.
|
||||
:rtype: AttestationObject.KEY
|
||||
"""
|
||||
if isinstance(key, int):
|
||||
return cls(key)
|
||||
except ValueError:
|
||||
name = re.sub('([a-z])([A-Z])', r'\1_\2', key).upper()
|
||||
return getattr(cls, name)
|
||||
name = re.sub('([a-z])([A-Z])', r'\1_\2', key).upper()
|
||||
return getattr(cls, name)
|
||||
|
||||
@property
|
||||
def string_key(self):
|
||||
"""Get the string used for this key in the Webauthn specification.
|
||||
|
||||
:return: The Webauthn string used for a key.
|
||||
:rtype: str
|
||||
"""
|
||||
value = ''.join(w.capitalize() for w in self.name.split('_'))
|
||||
return value[0].lower() + value[1:]
|
||||
|
||||
def __init__(self, data):
|
||||
def __init__(self, _):
|
||||
super(AttestationObject, self).__init__()
|
||||
|
||||
data = dict((AttestationObject.KEY.for_key(k), v) for (k, v) in
|
||||
_parse_cbor(data).items())
|
||||
_parse_cbor(self).items())
|
||||
self.fmt = data[AttestationObject.KEY.FMT]
|
||||
self.auth_data = AuthenticatorData(
|
||||
data[AttestationObject.KEY.AUTH_DATA])
|
||||
self.auth_data = AuthenticatorData(data[AttestationObject.KEY.AUTH_DATA]
|
||||
)
|
||||
data[AttestationObject.KEY.AUTH_DATA] = self.auth_data
|
||||
self.att_statement = data[AttestationObject.KEY.ATT_STMT]
|
||||
self.att_statement = data[
|
||||
AttestationObject.KEY.ATT_STMT]
|
||||
self.data = data
|
||||
|
||||
def __repr__(self):
|
||||
@ -223,6 +329,12 @@ class AttestationObject(bytes):
|
||||
return self.__repr__()
|
||||
|
||||
def verify(self, client_param):
|
||||
"""Verify the digital signature of an AttestationObject, with regard to
|
||||
the given client_param.
|
||||
|
||||
:param client_param: SHA256 hash of the ClientData for the request.
|
||||
:type client_param: bytes
|
||||
"""
|
||||
attestation = Attestation.for_type(self.fmt)
|
||||
if attestation:
|
||||
attestation().verify(self.att_statement, self.auth_data,
|
||||
@ -232,10 +344,30 @@ class AttestationObject(bytes):
|
||||
|
||||
@classmethod
|
||||
def create(cls, fmt, auth_data, att_stmt):
|
||||
"""Create an AttestationObject instance.
|
||||
|
||||
:param fmt: The type of attestation used.
|
||||
:type fmt: str
|
||||
:param auth_data: Binary representation of the authenticator data.
|
||||
:type auth_data: bytes
|
||||
:param att_stmt: The attestation statement.
|
||||
:type att_stmt: dict
|
||||
:return: The attestation object.
|
||||
:rtype: AttestationObject
|
||||
"""
|
||||
return cls(cbor.dumps(args(fmt, auth_data, att_stmt)))
|
||||
|
||||
@classmethod
|
||||
def from_ctap1(cls, app_param, registration):
|
||||
"""Create an AttestationObject from a CTAP1 RegistrationData instance.
|
||||
|
||||
:param app_param: SHA256 hash of the RP ID used for the CTAP1 request.
|
||||
:type app_param: bytes
|
||||
:param registration: The CTAP1 registration data.
|
||||
:type registration: RegistrationData
|
||||
:return: The attestation object, using the "fido-u2f" format.
|
||||
:rtype: AttestationObject
|
||||
"""
|
||||
return cls.create(
|
||||
FidoU2FAttestation.FORMAT,
|
||||
AuthenticatorData.create(
|
||||
@ -261,14 +393,37 @@ class AttestationObject(bytes):
|
||||
)
|
||||
|
||||
def with_int_keys(self):
|
||||
"""Get a copy of this AttestationObject, using CTAP2 integer values as
|
||||
map keys in the CBOR representation.
|
||||
|
||||
:return: The attestation object, using int keys.
|
||||
:rtype: AttestationObject
|
||||
"""
|
||||
return AttestationObject(cbor.dumps(self.data))
|
||||
|
||||
def with_string_keys(self):
|
||||
"""Get a copy of this AttestationObject, using Webauthn string values as
|
||||
map keys in the CBOR representation.
|
||||
|
||||
:return: The attestation object, using str keys.
|
||||
:rtype: AttestationObject
|
||||
"""
|
||||
return AttestationObject(cbor.dumps(
|
||||
dict((k.string_key, v) for k, v in self.data.items())))
|
||||
|
||||
|
||||
class AssertionResponse(bytes):
|
||||
"""Binary CBOR encoded assertion response.
|
||||
|
||||
:param _: The binary representation of the assertion response.
|
||||
:ivar credential: The credential used for the assertion.
|
||||
:ivar auth_data: The authenticator data part of the response.
|
||||
:ivar signature: The digital signature of the assertion.
|
||||
:ivar user: The user data of the credential.
|
||||
:ivar number_of_credentials: The total number of responses available
|
||||
(only set for the first response, if > 1).
|
||||
"""
|
||||
|
||||
@unique
|
||||
class KEY(IntEnum):
|
||||
CREDENTIAL = 1
|
||||
@ -277,15 +432,20 @@ class AssertionResponse(bytes):
|
||||
USER = 4
|
||||
N_CREDS = 5
|
||||
|
||||
def __init__(self, data):
|
||||
def __init__(self, _):
|
||||
super(AssertionResponse, self).__init__()
|
||||
|
||||
data = dict((AssertionResponse.KEY(k), v) for (k, v) in
|
||||
_parse_cbor(data).items())
|
||||
self.credential = data[AssertionResponse.KEY.CREDENTIAL]
|
||||
_parse_cbor(self).items())
|
||||
self.credential = data.get(
|
||||
AssertionResponse.KEY.CREDENTIAL)
|
||||
self.auth_data = AuthenticatorData(
|
||||
data[AssertionResponse.KEY.AUTH_DATA])
|
||||
self.signature = data[AssertionResponse.KEY.SIGNATURE]
|
||||
self.user = data.get(AssertionResponse.KEY.USER)
|
||||
self.number_of_credentials = data.get(AssertionResponse.KEY.N_CREDS)
|
||||
self.user = data.get(
|
||||
AssertionResponse.KEY.USER)
|
||||
self.number_of_credentials = data.get(
|
||||
AssertionResponse.KEY.N_CREDS)
|
||||
self.data = data
|
||||
|
||||
def __repr__(self):
|
||||
@ -301,15 +461,38 @@ class AssertionResponse(bytes):
|
||||
return self.__repr__()
|
||||
|
||||
def verify(self, client_param, public_key):
|
||||
"""Verify the digital signature of the response with regard to the
|
||||
client_param, using the given public key.
|
||||
|
||||
:param client_param: SHA256 hash of the ClientData used for the request.
|
||||
:param public_key: The public key of the credential, to verify.
|
||||
"""
|
||||
public_key.verify(self.auth_data + client_param, self.signature)
|
||||
|
||||
@classmethod
|
||||
def create(cls, credential, auth_data, signature, user=None, n_creds=None):
|
||||
"""Create an AssertionResponse instance.
|
||||
|
||||
:param credential: The credential used for the response.
|
||||
:param auth_data: The binary encoded authenticator data.
|
||||
:param signature: The digital signature of the response.
|
||||
:param user: The user data of the credential, if any.
|
||||
:param n_creds: The number of responses available.
|
||||
:return: The assertion response.
|
||||
"""
|
||||
return cls(cbor.dumps(args(credential, auth_data, signature, user,
|
||||
n_creds)))
|
||||
|
||||
@classmethod
|
||||
def from_ctap1(cls, app_param, credential, authentication):
|
||||
"""Create an AssertionResponse from a CTAP1 SignatureData instance.
|
||||
|
||||
:param app_param: SHA256 hash of the RP ID used for the CTAP1 request.
|
||||
:param credential: Credential used for the CTAP1 request (from the
|
||||
allowList).
|
||||
:param authentication: The CTAP1 signature data.
|
||||
:return: The assertion response.
|
||||
"""
|
||||
return cls.create(
|
||||
credential,
|
||||
AuthenticatorData.create(
|
||||
@ -322,6 +505,11 @@ class AssertionResponse(bytes):
|
||||
|
||||
|
||||
class CTAP2(object):
|
||||
"""Implementation of the CTAP2 specification.
|
||||
|
||||
:param device: A CtapHidDevice handle supporting CTAP2.
|
||||
"""
|
||||
|
||||
@unique
|
||||
class CMD(IntEnum):
|
||||
MAKE_CREDENTIAL = 0x01
|
||||
@ -338,10 +526,22 @@ class CTAP2(object):
|
||||
|
||||
def send_cbor(self, cmd, data=None, timeout=None, parse=_parse_cbor,
|
||||
on_keepalive=None):
|
||||
"""
|
||||
Sends a CBOR message to the device, and waits for a response.
|
||||
|
||||
"""Sends a CBOR message to the device, and waits for a response.
|
||||
|
||||
The optional parameter 'timeout' can either be a numeric time in seconds
|
||||
or a threading.Event object used to cancel the request.
|
||||
|
||||
:param cmd: The command byte of the request.
|
||||
:param data: The payload to send (to be CBOR encoded).
|
||||
:param timeout: Optional timeout in seconds, or an instance of
|
||||
threading.Event used to cancel the command.
|
||||
:param parse: Function used to parse the binary response data, defaults
|
||||
to parsing the CBOR.
|
||||
:param on_keepalive: Optional function called when keep-alive is sent by
|
||||
the authenticator.
|
||||
:return: The result of calling the parse function on the response data
|
||||
(defaults to the CBOR decoded value).
|
||||
"""
|
||||
request = struct.pack('>B', cmd)
|
||||
if data is not None:
|
||||
@ -360,6 +560,23 @@ class CTAP2(object):
|
||||
exclude_list=None, extensions=None, options=None,
|
||||
pin_auth=None, pin_protocol=None, timeout=None,
|
||||
on_keepalive=None):
|
||||
"""CTAP2 makeCredential operation,
|
||||
|
||||
:param client_data_hash: SHA256 hash of the ClientData.
|
||||
:param rp: PublicKeyCredentialRpEntity parameters.
|
||||
:param user: PublicKeyCredentialUserEntity parameters.
|
||||
:param key_params: List of acceptable credential types.
|
||||
:param exclude_list: Optional list of PublicKeyCredentialDescriptors.
|
||||
:param extensions: Optional dict of extensions.
|
||||
:param options: Optional dict of options.
|
||||
:param pin_auth: Optional PIN auth parameter.
|
||||
:param pin_protocol: The version of PIN protocol used, if any.
|
||||
:param timeout: Optional timeout in seconds, or threading.Event object
|
||||
used to cancel the request.
|
||||
:param on_keepalive: Optional callback function to handle keep-alive
|
||||
messages from the authenticator.
|
||||
:return: The new credential.
|
||||
"""
|
||||
return self.send_cbor(CTAP2.CMD.MAKE_CREDENTIAL, args(
|
||||
client_data_hash,
|
||||
rp,
|
||||
@ -375,6 +592,21 @@ class CTAP2(object):
|
||||
def get_assertion(self, rp_id, client_data_hash, allow_list=None,
|
||||
extensions=None, options=None, pin_auth=None,
|
||||
pin_protocol=None, timeout=None, on_keepalive=None):
|
||||
"""CTAP2 getAssertion command.
|
||||
|
||||
:param rp_id: SHA256 hash of the RP ID of the credential.
|
||||
:param client_data_hash: SHA256 hash of the ClientData used.
|
||||
:param allow_list: Optional list of PublicKeyCredentialDescriptors.
|
||||
:param extensions: Optional dict of extensions.
|
||||
:param options: Optional dict of options.
|
||||
:param pin_auth: Optional PIN auth parameter.
|
||||
:param pin_protocol: The version of PIN protocol used, if any.
|
||||
:param timeout: Optional timeout in seconds, or threading.Event object
|
||||
used to cancel the request.
|
||||
:param on_keepalive: Optional callback function to handle keep-alive
|
||||
messages from the authenticator.
|
||||
:return: The new assertion.
|
||||
"""
|
||||
return self.send_cbor(CTAP2.CMD.GET_ASSERTION, args(
|
||||
rp_id,
|
||||
client_data_hash,
|
||||
@ -386,10 +618,24 @@ class CTAP2(object):
|
||||
), timeout, AssertionResponse, on_keepalive)
|
||||
|
||||
def get_info(self):
|
||||
"""CTAP2 getInfo command.
|
||||
|
||||
:return: Information about the authenticator.
|
||||
"""
|
||||
return self.send_cbor(CTAP2.CMD.GET_INFO, parse=Info)
|
||||
|
||||
def client_pin(self, pin_protocol, sub_cmd, key_agreement=None,
|
||||
pin_auth=None, new_pin_enc=None, pin_hash_enc=None):
|
||||
"""CTAP2 clientPin command, used for various PIN operations.
|
||||
|
||||
:param pin_protocol: The PIN protocol version to use.
|
||||
:param sub_cmd: A clientPin sub command.
|
||||
:param key_agreement: The keyAgreement parameter.
|
||||
:param pin_auth: The pinAuth parameter.
|
||||
:param new_pin_enc: The newPinEnc parameter.
|
||||
:param pin_hash_enc: The pinHashEnc parameter.
|
||||
:return: The response of the command, decoded.
|
||||
"""
|
||||
return self.send_cbor(CTAP2.CMD.CLIENT_PIN, args(
|
||||
pin_protocol,
|
||||
sub_cmd,
|
||||
@ -400,10 +646,21 @@ class CTAP2(object):
|
||||
))
|
||||
|
||||
def reset(self, timeout=None, on_keepalive=None):
|
||||
"""CTAP2 reset command, erases all credentials and PIN.
|
||||
|
||||
:param timeout: Optional timeout in seconds, or threading.Event object
|
||||
used to cancel the request.
|
||||
:param on_keepalive: Optional callback function to handle keep-alive
|
||||
messages from the authenticator.
|
||||
"""
|
||||
self.send_cbor(CTAP2.CMD.RESET, timeout=timeout,
|
||||
on_keepalive=on_keepalive)
|
||||
|
||||
def get_next_assertion(self):
|
||||
"""CTAP2 getNextAssertion command.
|
||||
|
||||
:return: The next available assertion response.
|
||||
"""
|
||||
return self.send_cbor(CTAP2.CMD.GET_NEXT_ASSERTION,
|
||||
parse=AssertionResponse)
|
||||
|
||||
@ -421,6 +678,12 @@ def _pad_pin(pin):
|
||||
|
||||
|
||||
class PinProtocolV1(object):
|
||||
"""Implementation of the CTAP1 PIN protocol v1.
|
||||
|
||||
:param ctap: An instance of a CTAP2 object.
|
||||
:cvar VERSION: The version number of the PIV protocol.
|
||||
:cvar IV: An all-zero IV used for some cryptographic operations.
|
||||
"""
|
||||
VERSION = 1
|
||||
IV = b'\x00' * 16
|
||||
|
||||
@ -462,6 +725,11 @@ class PinProtocolV1(object):
|
||||
return key_agreement, shared_secret
|
||||
|
||||
def get_pin_token(self, pin):
|
||||
"""Get a PIN token from the authenticator.
|
||||
|
||||
:param pin: The PIN of the authenticator.
|
||||
:return: A PIN token.
|
||||
"""
|
||||
key_agreement, shared_secret = self._init_shared_secret()
|
||||
|
||||
be = default_backend()
|
||||
@ -479,11 +747,21 @@ class PinProtocolV1(object):
|
||||
return dec.update(resp[PinProtocolV1.RESULT.PIN_TOKEN]) + dec.finalize()
|
||||
|
||||
def get_pin_retries(self):
|
||||
"""Get the number of PIN retries remaining.
|
||||
|
||||
:return: The number or PIN attempts until the authenticator is locked.
|
||||
"""
|
||||
resp = self.ctap.client_pin(PinProtocolV1.VERSION,
|
||||
PinProtocolV1.CMD.GET_RETRIES)
|
||||
return resp[PinProtocolV1.RESULT.RETRIES]
|
||||
|
||||
def set_pin(self, pin):
|
||||
"""Set the PIN of the autenticator.
|
||||
This only works when no PIN is set. To change the PIN when set, use
|
||||
change_pin.
|
||||
|
||||
:param pin: A PIN to set.
|
||||
"""
|
||||
pin = _pad_pin(pin)
|
||||
key_agreement, shared_secret = self._init_shared_secret()
|
||||
|
||||
@ -499,6 +777,13 @@ class PinProtocolV1(object):
|
||||
pin_auth=pin_auth)
|
||||
|
||||
def change_pin(self, old_pin, new_pin):
|
||||
"""Change the PIN of the authenticator.
|
||||
This only works when a PIN is already set. If no PIN is set, use
|
||||
set_pin.
|
||||
|
||||
:param old_pin: The currently set PIN.
|
||||
:param new_pin: The new PIN to set.
|
||||
"""
|
||||
new_pin = _pad_pin(new_pin)
|
||||
key_agreement, shared_secret = self._init_shared_secret()
|
||||
|
||||
|
17
fido2/hid.py
17
fido2/hid.py
@ -2,7 +2,7 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
from .ctap import CtapDevice, CtapError
|
||||
from .pyu2f import hidtransport
|
||||
from ._pyu2f import hidtransport
|
||||
|
||||
from enum import IntEnum, unique
|
||||
from threading import Event
|
||||
@ -59,6 +59,8 @@ class _SingleEvent(object):
|
||||
class CtapHidDevice(CtapDevice):
|
||||
"""
|
||||
CtapDevice implementation using the HID transport.
|
||||
|
||||
:cvar descriptor: Device descriptor.
|
||||
"""
|
||||
|
||||
def __init__(self, descriptor, dev):
|
||||
@ -70,14 +72,20 @@ class CtapHidDevice(CtapDevice):
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
"""CTAP HID protocol version.
|
||||
|
||||
:rtype: Tuple[int, int, int]
|
||||
"""
|
||||
return self._dev.u2fhid_version
|
||||
|
||||
@property
|
||||
def device_version(self):
|
||||
"""Device version number."""
|
||||
return self._dev.device_version
|
||||
|
||||
@property
|
||||
def capabilities(self):
|
||||
"""Capabilities supported by the device."""
|
||||
return self._dev.capabilities
|
||||
|
||||
def call(self, cmd, data=b'', event=None, on_keepalive=None):
|
||||
@ -108,12 +116,19 @@ class CtapHidDevice(CtapDevice):
|
||||
raise CtapError(CtapError.ERR.KEEPALIVE_CANCEL)
|
||||
|
||||
def wink(self):
|
||||
"""Causes the authenticator to blink."""
|
||||
self.call(CTAPHID.WINK)
|
||||
|
||||
def ping(self, msg=b'Hello FIDO'):
|
||||
"""Sends data to the authenticator, which echoes it back.
|
||||
|
||||
:param msg: The data to send.
|
||||
:return: The response from the authenticator.
|
||||
"""
|
||||
return self.call(CTAPHID.PING, msg)
|
||||
|
||||
def lock(self, lock_time=10):
|
||||
"""Locks the channel."""
|
||||
self.call(CTAPHID.LOCK, struct.pack('>B', lock_time))
|
||||
|
||||
@classmethod
|
||||
|
@ -50,6 +50,12 @@ with open(tld_fname, 'rb') as f:
|
||||
|
||||
|
||||
def verify_rp_id(rp_id, origin):
|
||||
"""Checks if a Webauthn RP ID is usable for a given origin.
|
||||
|
||||
:param rp_id: The RP ID to validate.
|
||||
:param origin: The origin of the request.
|
||||
:return: True if the RP ID is usable by the origin, False if not.
|
||||
"""
|
||||
if isinstance(rp_id, six.binary_type):
|
||||
rp_id = rp_id.decode()
|
||||
if not rp_id:
|
||||
@ -69,6 +75,12 @@ def verify_rp_id(rp_id, origin):
|
||||
|
||||
|
||||
def verify_app_id(app_id, origin):
|
||||
"""Checks if a FIDO U2F App ID is usable for a given origin.
|
||||
|
||||
:param app_id: The App ID to validate.
|
||||
:param origin: The origin of the request.
|
||||
:return: True if the App ID is usable by the origin, False if not.
|
||||
"""
|
||||
if isinstance(app_id, six.binary_type):
|
||||
app_id = app_id.decode()
|
||||
url = urlparse(app_id)
|
||||
|
120
fido2/server.py
Normal file
120
fido2/server.py
Normal file
@ -0,0 +1,120 @@
|
||||
# Copyright (c) 2018 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from .rpid import verify_rp_id
|
||||
from .cose import ES256
|
||||
from .utils import sha256
|
||||
|
||||
import os
|
||||
from cryptography.hazmat.primitives import constant_time
|
||||
|
||||
|
||||
def _verify_origin_for_rp(rp_id):
|
||||
return lambda o: verify_rp_id(rp_id, o)
|
||||
|
||||
|
||||
class Fido2Server(object):
|
||||
def __init__(self, rp_id, attestation=None, verify_origin=None):
|
||||
self.rp_id = rp_id
|
||||
self._verify = verify_origin or _verify_origin_for_rp(rp_id)
|
||||
self.timeout = 30
|
||||
self.attestation = attestation or 'none'
|
||||
self.cred_algorithms = [ES256.ALGORITHM]
|
||||
|
||||
def register_begin(self, rp, user, credentials=None):
|
||||
challenge = os.urandom(32)
|
||||
return {
|
||||
'publicKey': {
|
||||
'rp': rp,
|
||||
'user': user,
|
||||
'challenge': challenge,
|
||||
'pubKeyCredParams': [
|
||||
{
|
||||
'type': 'public-key',
|
||||
'alg': alg
|
||||
} for alg in self.cred_algorithms
|
||||
],
|
||||
'excludeCredentials': [
|
||||
{
|
||||
'type': 'public-key',
|
||||
'id': cred.credential_id
|
||||
} for cred in credentials or []
|
||||
],
|
||||
'timeout': int(self.timeout * 1000),
|
||||
'attestation': self.attestation
|
||||
}
|
||||
}
|
||||
|
||||
def register_complete(self, challenge, client_data, attestation_object):
|
||||
if client_data.get('type') != 'webauthn.create':
|
||||
raise ValueError('Incorrect type in ClientData.')
|
||||
if not self._verify(client_data.get('origin')):
|
||||
raise ValueError('Invalid origin in ClientData.')
|
||||
if not constant_time.bytes_eq(challenge, client_data.challenge):
|
||||
raise ValueError('Wrong challenge in response.')
|
||||
if not constant_time.bytes_eq(sha256(self.rp_id.encode()),
|
||||
attestation_object.auth_data.rp_id_hash):
|
||||
raise ValueError('Wrong RP ID hash in response.')
|
||||
# TODO: Ensure that we're using an acceptable attestation format.
|
||||
attestation_object.verify(client_data.hash)
|
||||
return attestation_object.auth_data
|
||||
|
||||
def authenticate_begin(self, rp_id, credentials):
|
||||
challenge = os.urandom(32)
|
||||
return {
|
||||
'publicKey': {
|
||||
'rpId': rp_id,
|
||||
'challenge': challenge,
|
||||
'allowCredentials': [
|
||||
{
|
||||
'type': 'public-key',
|
||||
'id': cred.credential_id
|
||||
} for cred in credentials
|
||||
],
|
||||
'timeout': int(self.timeout * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
def authenticate_complete(self, credentials, credential_id, challenge,
|
||||
client_data, auth_data, signature):
|
||||
if client_data.get('type') != 'webauthn.get':
|
||||
raise ValueError('Incorrect type in ClientData.')
|
||||
if not self._verify(client_data.get('origin')):
|
||||
raise ValueError('Invalid origin in ClientData.')
|
||||
if challenge != client_data.challenge:
|
||||
raise ValueError('Wrong challenge in response.')
|
||||
if not constant_time.bytes_eq(sha256(self.rp_id.encode()),
|
||||
auth_data.rp_id_hash):
|
||||
raise ValueError('Wrong RP ID hash in response.')
|
||||
|
||||
for cred in credentials:
|
||||
if cred.credential_id == credential_id:
|
||||
cred.public_key.verify(auth_data + client_data.hash, signature)
|
||||
return cred
|
||||
raise ValueError('Unknown credential ID.')
|
@ -25,13 +25,18 @@
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""Various utility functions.
|
||||
|
||||
This module contains various functions used throughout the rest of the project.
|
||||
"""
|
||||
|
||||
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
||||
from threading import Timer, Event
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hmac, hashes
|
||||
from threading import Timer, Event
|
||||
from binascii import b2a_hex
|
||||
from numbers import Number
|
||||
import six
|
||||
import numbers
|
||||
|
||||
__all__ = [
|
||||
'Timeout',
|
||||
@ -45,32 +50,61 @@ __all__ = [
|
||||
|
||||
|
||||
def sha256(data):
|
||||
"""Produces a SHA256 hash of the input.
|
||||
|
||||
:param data: The input data to hash.
|
||||
:return: The resulting hash.
|
||||
"""
|
||||
h = hashes.Hash(hashes.SHA256(), default_backend())
|
||||
h.update(data)
|
||||
return h.finalize()
|
||||
|
||||
|
||||
def hmac_sha256(key, data):
|
||||
"""Performs an HMAC-SHA256 operation on the given data, using the given key.
|
||||
|
||||
:param key: The key to use.
|
||||
:param data: The input data to hash.
|
||||
:return: The resulting hash.
|
||||
"""
|
||||
h = hmac.HMAC(key, hashes.SHA256(), default_backend())
|
||||
h.update(data)
|
||||
return h.finalize()
|
||||
|
||||
|
||||
def bytes2int(value):
|
||||
"""Parses an arbitrarily sized integer from a byte string.
|
||||
|
||||
:param value: A byte string encoding a big endian unsigned integer.
|
||||
:return: The parsed int.
|
||||
"""
|
||||
return int(b2a_hex(value), 16)
|
||||
|
||||
|
||||
def int2bytes(value, minlen=-1):
|
||||
"""Encodes an int as a byte string.
|
||||
|
||||
:param value: The integer value to encode.
|
||||
:param minlen: An optional minimum length for the resulting byte string.
|
||||
:return: The value encoded as a big endian byte string.
|
||||
"""
|
||||
ba = []
|
||||
while value > 0xff:
|
||||
ba.append(0xff & value)
|
||||
value >>= 8
|
||||
ba.append(value)
|
||||
ba.extend([0]*(minlen - len(ba)))
|
||||
ba.extend([0] * (minlen - len(ba)))
|
||||
return bytes(bytearray(reversed(ba)))
|
||||
|
||||
|
||||
def websafe_decode(data):
|
||||
"""Decodes a websafe-base64 encoded string (bytes or str).
|
||||
See: "Base 64 Encoding with URL and Filename Safe Alphabet" from Section 5
|
||||
in RFC4648 without padding.
|
||||
|
||||
:param data: The input to decode.
|
||||
:return: The decoded bytes.
|
||||
"""
|
||||
if isinstance(data, six.text_type):
|
||||
data = data.encode('ascii')
|
||||
data += b'=' * (-len(data) % 4)
|
||||
@ -78,16 +112,28 @@ def websafe_decode(data):
|
||||
|
||||
|
||||
def websafe_encode(data):
|
||||
if isinstance(data, six.text_type):
|
||||
data = data.encode('ascii')
|
||||
"""Encodes a byte string into websafe-base64 encoding.
|
||||
|
||||
:param data: The input to encode.
|
||||
:return: The encoded string.
|
||||
"""
|
||||
return urlsafe_b64encode(data).replace(b'=', b'').decode('ascii')
|
||||
|
||||
|
||||
class Timeout(object):
|
||||
"""Utility class for adding a timeout to an event.
|
||||
|
||||
:param time_or_event: A number, in seconds, or a threading.Event object.
|
||||
:ivar event: The Event associated with the Timeout.
|
||||
:ivar timer: The Timer associated with the Timeout, if any.
|
||||
"""
|
||||
|
||||
def __init__(self, time_or_event):
|
||||
if isinstance(time_or_event, numbers.Number):
|
||||
|
||||
if isinstance(time_or_event, Number):
|
||||
self.event = Event()
|
||||
self.timer = Timer(time_or_event, self.event.set)
|
||||
self.timer = Timer(time_or_event,
|
||||
self.event.set)
|
||||
else:
|
||||
self.event = time_or_event
|
||||
self.timer = None
|
||||
|
@ -12,14 +12,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for pyu2f.hidtransport."""
|
||||
"""Tests for _pyu2f.hidtransport."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import six
|
||||
import mock
|
||||
|
||||
from fido2.pyu2f import hidtransport
|
||||
from fido2._pyu2f import hidtransport
|
||||
from . import util
|
||||
|
||||
import unittest
|
@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for pyu2f.hid.linux."""
|
||||
"""Tests for _pyu2f.hid.linux."""
|
||||
|
||||
import base64
|
||||
import os
|
||||
@ -22,7 +22,7 @@ import mock
|
||||
|
||||
import six
|
||||
from six.moves import builtins
|
||||
from fido2.pyu2f import linux
|
||||
from fido2._pyu2f import linux
|
||||
|
||||
try:
|
||||
from pyfakefs import fake_filesystem # pylint: disable=g-import-not-at-top
|
@ -12,13 +12,13 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for pyu2f.hid.macos."""
|
||||
"""Tests for _pyu2f.hid.macos."""
|
||||
|
||||
import ctypes
|
||||
import sys
|
||||
import mock
|
||||
|
||||
from fido2.pyu2f import macos
|
||||
from fido2._pyu2f import macos
|
||||
|
||||
|
||||
if sys.version_info[:2] < (2, 7):
|
@ -12,14 +12,14 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Testing utilties for pyu2f.
|
||||
"""Testing utilties for _pyu2f.
|
||||
|
||||
Testing utilities such as a fake implementation of the pyhidapi device object
|
||||
that implements parts of the U2FHID frame protocol. This makes it easy to tests
|
||||
of higher level abstractions without having to use mock to mock out low level
|
||||
framing details.
|
||||
"""
|
||||
from fido2.pyu2f import base, hidtransport
|
||||
from fido2._pyu2f import base, hidtransport
|
||||
|
||||
|
||||
class UnsupportedCommandError(Exception):
|
@ -12,7 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Tests for pyu2f.tests.lib.util."""
|
||||
"""Tests for _pyu2f.tests.lib.util."""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
@ -42,7 +42,7 @@ class TestAttestationObject(unittest.TestCase):
|
||||
attestation = Attestation.for_type('__unsupported__')()
|
||||
self.assertIsInstance(attestation, UnsupportedAttestation)
|
||||
with self.assertRaises(NotImplementedError):
|
||||
attestation.verify({}, 0, 0)
|
||||
attestation.verify({}, 0, b'')
|
||||
|
||||
def test_none_attestation(self):
|
||||
attestation = Attestation.for_type('none')()
|
||||
|
@ -331,9 +331,9 @@ class TestFido2Client(unittest.TestCase):
|
||||
dev = mock.Mock()
|
||||
dev.capabilities = CAPABILITY.CBOR
|
||||
client = Fido2Client(dev, APP_ID)
|
||||
client.ctap = mock.MagicMock()
|
||||
client.ctap.get_info.return_value = Info(_INFO_NO_PIN)
|
||||
client.ctap.make_credential.side_effect = CtapError(
|
||||
client.ctap2 = mock.MagicMock()
|
||||
client.ctap2.get_info.return_value = Info(_INFO_NO_PIN)
|
||||
client.ctap2.make_credential.side_effect = CtapError(
|
||||
CtapError.ERR.CREDENTIAL_EXCLUDED)
|
||||
|
||||
try:
|
||||
@ -347,16 +347,16 @@ class TestFido2Client(unittest.TestCase):
|
||||
except ClientError as e:
|
||||
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
|
||||
|
||||
client.ctap.get_info.assert_called_with()
|
||||
client.ctap.make_credential.assert_called_once()
|
||||
client.ctap2.get_info.assert_called_with()
|
||||
client.ctap2.make_credential.assert_called_once()
|
||||
|
||||
def test_make_credential_ctap2(self):
|
||||
dev = mock.Mock()
|
||||
dev.capabilities = CAPABILITY.CBOR
|
||||
client = Fido2Client(dev, APP_ID)
|
||||
client.ctap = mock.MagicMock()
|
||||
client.ctap.get_info.return_value = Info(_INFO_NO_PIN)
|
||||
client.ctap.make_credential.return_value = AttestationObject(_MC_RESP)
|
||||
client.ctap2 = mock.MagicMock()
|
||||
client.ctap2.get_info.return_value = Info(_INFO_NO_PIN)
|
||||
client.ctap2.make_credential.return_value = AttestationObject(_MC_RESP)
|
||||
|
||||
attestation, client_data = client.make_credential(
|
||||
rp,
|
||||
@ -368,8 +368,8 @@ class TestFido2Client(unittest.TestCase):
|
||||
self.assertIsInstance(attestation, AttestationObject)
|
||||
self.assertIsInstance(client_data, ClientData)
|
||||
|
||||
client.ctap.get_info.assert_called_with()
|
||||
client.ctap.make_credential.assert_called_with(
|
||||
client.ctap2.get_info.assert_called_with()
|
||||
client.ctap2.make_credential.assert_called_with(
|
||||
client_data.hash,
|
||||
rp,
|
||||
user,
|
||||
@ -392,9 +392,9 @@ class TestFido2Client(unittest.TestCase):
|
||||
dev.capabilities = 0 # No CTAP2
|
||||
client = Fido2Client(dev, APP_ID)
|
||||
|
||||
client.ctap = mock.MagicMock()
|
||||
client.ctap.get_version.return_value = 'U2F_V2'
|
||||
client.ctap.register.return_value = REG_DATA
|
||||
client.ctap1 = mock.MagicMock()
|
||||
client.ctap1.get_version.return_value = 'U2F_V2'
|
||||
client.ctap1.register.return_value = REG_DATA
|
||||
|
||||
attestation, client_data = client.make_credential(
|
||||
rp,
|
||||
@ -406,7 +406,7 @@ class TestFido2Client(unittest.TestCase):
|
||||
self.assertIsInstance(attestation, AttestationObject)
|
||||
self.assertIsInstance(client_data, ClientData)
|
||||
|
||||
client.ctap.register.assert_called_with(
|
||||
client.ctap1.register.assert_called_with(
|
||||
client_data.hash,
|
||||
sha256(rp['id'].encode()),
|
||||
)
|
||||
|
@ -28,7 +28,6 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from fido2.ctap1 import CTAP1, ApduError
|
||||
from fido2.client import ClientData
|
||||
from binascii import a2b_hex
|
||||
import unittest
|
||||
import mock
|
||||
@ -82,7 +81,6 @@ class TestCTAP1(unittest.TestCase):
|
||||
self.assertEqual(resp.certificate, a2b_hex('3082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df')) # noqa
|
||||
self.assertEqual(resp.signature, a2b_hex('304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871')) # noqa
|
||||
|
||||
client_data = ClientData(b'{"typ":"navigator.id.finishEnrollment","challenge":"vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo","cid_pubkey":{"kty":"EC","crv":"P-256","x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8","y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},"origin":"http://example.com"}') # noqa
|
||||
resp.verify(app_param, client_param)
|
||||
|
||||
def test_authenticate(self):
|
||||
@ -103,7 +101,6 @@ class TestCTAP1(unittest.TestCase):
|
||||
self.assertEqual(resp.signature, a2b_hex('304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f')) # noqa
|
||||
|
||||
public_key = a2b_hex(b'04d368f1b665bade3c33a20f1e429c7750d5033660c019119d29aa4ba7abc04aa7c80a46bbe11ca8cb5674d74f31f8a903f6bad105fb6ab74aefef4db8b0025e1d') # noqa
|
||||
client_data = ClientData(b'{"typ":"navigator.id.getAssertion","challenge":"opsXqUifDriAAmWclinfbS0e-USY0CgyJHe_Otd7z8o","cid_pubkey":{"kty":"EC","crv":"P-256","x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8","y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},"origin":"http://example.com"}') # noqa
|
||||
resp.verify(app_param, client_param, public_key)
|
||||
|
||||
key_handle = b'\4'*8
|
||||
|
@ -87,10 +87,6 @@ class TestWebSafe(unittest.TestCase):
|
||||
self.assertEqual(websafe_encode(b'fooba'), u'Zm9vYmE')
|
||||
self.assertEqual(websafe_encode(b'foobar'), u'Zm9vYmFy')
|
||||
|
||||
def test_websafe_encode_unicode(self):
|
||||
self.assertEqual(websafe_encode(u''), u'')
|
||||
self.assertEqual(websafe_encode(u'foobar'), u'Zm9vYmFy')
|
||||
|
||||
|
||||
class TestTimeout(unittest.TestCase):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user