Skip to content

Commit 2ce5bf7

Browse files
committed
Add prf example
1 parent 56475d6 commit 2ce5bf7

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed

examples/prf.py

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# Copyright (c) 2024 Yubico AB
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or
5+
# without modification, are permitted provided that the following
6+
# conditions are met:
7+
#
8+
# 1. Redistributions of source code must retain the above copyright
9+
# notice, this list of conditions and the following disclaimer.
10+
# 2. Redistributions in binary form must reproduce the above
11+
# copyright notice, this list of conditions and the following
12+
# disclaimer in the documentation and/or other materials provided
13+
# with the distribution.
14+
#
15+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17+
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
18+
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
19+
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
20+
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
21+
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
25+
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
# POSSIBILITY OF SUCH DAMAGE.
27+
28+
"""
29+
Connects to the first FIDO device found which supports the PRF extension,
30+
creates a new credential for it with the extension enabled, and uses it to
31+
derive two separate secrets.
32+
"""
33+
from fido2.hid import CtapHidDevice
34+
from fido2.server import Fido2Server
35+
from fido2.client import Fido2Client, WindowsClient, UserInteraction
36+
from getpass import getpass
37+
import ctypes
38+
import sys
39+
import os
40+
41+
try:
42+
from fido2.pcsc import CtapPcscDevice
43+
except ImportError:
44+
CtapPcscDevice = None
45+
46+
47+
def enumerate_devices():
48+
for dev in CtapHidDevice.list_devices():
49+
yield dev
50+
if CtapPcscDevice:
51+
for dev in CtapPcscDevice.list_devices():
52+
yield dev
53+
54+
55+
# Handle user interaction
56+
class CliInteraction(UserInteraction):
57+
def prompt_up(self):
58+
print("\nTouch your authenticator device now...\n")
59+
60+
def request_pin(self, permissions, rd_id):
61+
return getpass("Enter PIN: ")
62+
63+
def request_uv(self, permissions, rd_id):
64+
print("User Verification required.")
65+
return True
66+
67+
68+
uv = "discouraged"
69+
rk = "discouraged"
70+
71+
if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin():
72+
# Use the Windows WebAuthn API if available, and we're not running as admin
73+
client = WindowsClient("https://example.com")
74+
rk = "required" # Windows requires resident key for hmac-secret
75+
else:
76+
# Locate a device
77+
for dev in enumerate_devices():
78+
client = Fido2Client(
79+
dev,
80+
"https://example.com",
81+
user_interaction=CliInteraction(),
82+
)
83+
if "hmac-secret" in client.info.extensions:
84+
break
85+
else:
86+
print("No Authenticator with the PRF extension found!")
87+
sys.exit(1)
88+
89+
server = Fido2Server({"id": "example.com", "name": "Example RP"}, attestation="none")
90+
user = {"id": b"user_id", "name": "A. User"}
91+
92+
# Prepare parameters for makeCredential
93+
create_options, state = server.register_begin(
94+
user,
95+
resident_key_requirement=rk,
96+
user_verification=uv,
97+
authenticator_attachment="cross-platform",
98+
)
99+
100+
# Create a credential
101+
result = client.make_credential(
102+
{
103+
**create_options["publicKey"],
104+
"extensions": {"prf": {}},
105+
}
106+
)
107+
108+
# Complete registration
109+
auth_data = server.register_complete(
110+
state, result.client_data, result.attestation_object
111+
)
112+
credentials = [auth_data.credential_data]
113+
114+
# PRF result:
115+
if not result.extension_results.get("prf", {}).get("enabled"):
116+
print("Failed to create credential with PRF", result.extension_results)
117+
sys.exit(1)
118+
119+
credential = result.attestation_object.auth_data.credential_data
120+
print("New credential created, with the PRF extension.")
121+
122+
# Prepare parameters for getAssertion
123+
allow_list = [{"type": "public-key", "id": credential.credential_id}]
124+
125+
# Generate a salt for PRF:
126+
salt = os.urandom(32)
127+
print("Authenticate with salt:", salt.hex())
128+
129+
130+
# Prepare parameters for getAssertion
131+
request_options, state = server.authenticate_begin(credentials, user_verification=uv)
132+
133+
# Authenticate the credential
134+
result = client.get_assertion(
135+
{
136+
**request_options["publicKey"],
137+
"extensions": {"prf": {"eval": {"first": salt}}},
138+
}
139+
)
140+
141+
# Only one cred in allowCredentials, only one response.
142+
result = result.get_response(0)
143+
144+
output1 = result.extension_results["prf"]["results"]["first"]
145+
print("Authenticated, secret:", output1.hex())
146+
147+
# Authenticate again, using two salts to generate two secrets:
148+
149+
# Generate a second salt for PRF:
150+
salt2 = os.urandom(32)
151+
print("Authenticate with second salt:", salt2.hex())
152+
153+
# The first salt is reused, which should result in the same secret.
154+
155+
result = client.get_assertion(
156+
{
157+
**request_options["publicKey"],
158+
"extensions": {"prf": {"eval": {"first": salt, "second": salt2}}},
159+
}
160+
)
161+
162+
# Only one cred in allowCredentials, only one response.
163+
result = result.get_response(0)
164+
165+
output = result.extension_results["prf"]["results"]
166+
print("Old secret:", output["first"].hex())
167+
print("New secret:", output["second"].hex())

0 commit comments

Comments
 (0)