Skip to content

Commit f8ead14

Browse files
committed
Make API deterministic by passing random challenge values as input
Also: * Use base64url instead of base64 as mandated by spec * Pass timeout value as input instead of hard-coded value * Check timeout value is not exceeded * Split "challenges" into two new tables - credential_challenges: used by init_credential() and make_credential() - assertion_challenges: used by get_credentials() and verify_assertion()
1 parent 927a0f1 commit f8ead14

23 files changed

+547
-527
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
CREATE OR REPLACE FUNCTION webauthn.base64_url_decode(text)
1+
CREATE OR REPLACE FUNCTION webauthn.base64url_decode(text)
22
RETURNS bytea
33
IMMUTABLE
44
LANGUAGE sql AS $$

FUNCTIONS/base64url_encode.sql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE OR REPLACE FUNCTION webauthn.base64url_encode(bytea)
2+
RETURNS text
3+
IMMUTABLE
4+
LANGUAGE sql AS $$
5+
SELECT translate(trim(trailing '=' from replace(encode($1,'base64'),E'\n','')),'+/','-_')
6+
$$;

FUNCTIONS/get_credentials.sql

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
CREATE OR REPLACE FUNCTION webauthn.get_credentials(username text, relaying_party text)
1+
CREATE OR REPLACE FUNCTION webauthn.get_credentials(
2+
challenge bytea,
3+
relying_party_id text,
4+
user_name text,
5+
timeout interval
6+
)
27
RETURNS jsonb
38
LANGUAGE sql
49
AS $$
510
WITH new_challenge AS (
6-
INSERT INTO webauthn.challenges (username, relaying_party)
7-
VALUES (username, relaying_party)
11+
INSERT INTO webauthn.assertion_challenges (challenge, relying_party_id, user_name, timeout)
12+
VALUES (challenge, relying_party_id, user_name, timeout)
813
RETURNING challenge
914
)
1015
SELECT jsonb_build_object(
@@ -16,17 +21,17 @@ SELECT jsonb_build_object(
1621
'allowCredentials', jsonb_agg(
1722
jsonb_build_object(
1823
'type', 'public-key',
19-
'id', encode(credentials.credential_raw_id,'base64')
24+
'id', webauthn.base64url_encode(credentials.credential_id)
2025
)
21-
ORDER BY credentials.credential_id DESC),
22-
'timeout', 60000,
23-
'challenge', encode(new_challenge.challenge,'base64'),
24-
'rpId', relaying_party
26+
ORDER BY credentials.credential_id),
27+
'timeout', (extract(epoch from get_credentials.timeout)*1000)::bigint, -- milliseconds
28+
'challenge', webauthn.base64url_encode(new_challenge.challenge),
29+
'rpId', get_credentials.relying_party_id
2530
)
2631
)
2732
FROM new_challenge
28-
JOIN webauthn.challenges ON challenges.username = get_credentials.username
29-
AND challenges.relaying_party = get_credentials.relaying_party
30-
JOIN webauthn.credentials ON credentials.challenge_id = challenges.challenge_id
31-
GROUP BY new_challenge.challenge, relaying_party
33+
JOIN webauthn.credential_challenges ON credential_challenges.relying_party_id = get_credentials.relying_party_id
34+
AND credential_challenges.user_name = get_credentials.user_name
35+
JOIN webauthn.credentials ON credentials.challenge = credential_challenges.challenge
36+
GROUP BY new_challenge.challenge, get_credentials.relying_party_id, get_credentials.timeout
3237
$$;

FUNCTIONS/init_credential.sql

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
1-
CREATE OR REPLACE FUNCTION webauthn.init_credential(username text, relaying_party text)
1+
CREATE OR REPLACE FUNCTION webauthn.init_credential(
2+
challenge bytea,
3+
relying_party_name text,
4+
relying_party_id text,
5+
user_name text,
6+
user_id bytea,
7+
user_display_name text,
8+
timeout interval
9+
)
210
RETURNS jsonb
311
LANGUAGE sql
412
AS $$
13+
-- https://www.w3.org/TR/webauthn-2/#dictionary-makecredentialoptions
514
WITH new_challenge AS (
6-
INSERT INTO webauthn.challenges (username, challenge, relaying_party)
7-
VALUES (username, gen_random_bytes(32), relaying_party)
8-
RETURNING challenge
15+
INSERT INTO webauthn.credential_challenges (challenge, relying_party_name, relying_party_id, user_name, user_id, user_display_name, timeout)
16+
VALUES (challenge, relying_party_name, relying_party_id, user_name, user_id, user_display_name, timeout)
17+
RETURNING TRUE
918
)
1019
SELECT jsonb_build_object(
1120
'publicKey', jsonb_build_object(
12-
'challenge', encode(challenge,'base64'),
1321
'rp', jsonb_build_object(
14-
'name', relaying_party,
15-
'id', relaying_party
22+
'name', relying_party_name,
23+
'id', relying_party_id
1624
),
1725
'user', jsonb_build_object(
18-
'name', username,
19-
'displayName', username,
20-
'id', encode(username::bytea,'base64')
26+
'name', user_name,
27+
'displayName', user_display_name,
28+
'id', webauthn.base64url_encode(user_id)
2129
),
30+
'challenge', webauthn.base64url_encode(challenge),
2231
'pubKeyCredParams', jsonb_build_array(
2332
jsonb_build_object(
2433
'type', 'public-key',
@@ -29,7 +38,7 @@ SELECT jsonb_build_object(
2938
'requireResidentKey', false,
3039
'userVerification', 'discouraged'
3140
),
32-
'timeout', 60000,
41+
'timeout', (extract(epoch from timeout)*1000)::bigint, -- milliseconds
3342
'extensions', jsonb_build_object(
3443
'txAuthSimple', ''
3544
),

FUNCTIONS/make_credential.sql

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,30 @@
1-
CREATE OR REPLACE FUNCTION webauthn.make_credential(username text, challenge text, credential_raw_id text, credential_type text, attestation_object text, client_data_json text, relaying_party text)
2-
RETURNS bigint
1+
CREATE OR REPLACE FUNCTION webauthn.make_credential(
2+
credential_id text,
3+
credential_type text,
4+
attestation_object text,
5+
client_data_json text,
6+
relying_party_id text
7+
)
8+
RETURNS boolean
39
LANGUAGE sql
410
AS $$
511
WITH
612
consume_challenge AS (
7-
UPDATE webauthn.challenges SET
13+
UPDATE webauthn.credential_challenges SET
814
consumed_at = now()
9-
WHERE challenges.username = make_credential.username
10-
AND challenges.relaying_party = make_credential.relaying_party
11-
AND challenges.challenge = decode(make_credential.challenge,'base64')
12-
AND challenges.challenge = webauthn.base64_url_decode(webauthn.from_utf8(decode(client_data_json,'base64'))::jsonb->>'challenge')
13-
AND challenges.consumed_at IS NULL
14-
RETURNING challenge_id
15+
WHERE credential_challenges.relying_party_id = make_credential.relying_party_id
16+
AND credential_challenges.challenge = webauthn.base64url_decode(webauthn.from_utf8(webauthn.base64url_decode(client_data_json))::jsonb->>'challenge')
17+
AND credential_challenges.consumed_at IS NULL
18+
AND credential_challenges.created_at + credential_challenges.timeout > now()
19+
RETURNING challenge
1520
)
16-
INSERT INTO webauthn.credentials (challenge_id,credential_raw_id,credential_type,attestation_object,client_data_json)
21+
INSERT INTO webauthn.credentials (credential_id,challenge,credential_type,attestation_object,client_data_json)
1722
SELECT
18-
consume_challenge.challenge_id,
19-
decode(credential_raw_id,'base64'),
23+
webauthn.base64url_decode(credential_id),
24+
consume_challenge.challenge,
2025
credential_type,
21-
decode(attestation_object,'base64'),
22-
decode(client_data_json,'base64')
26+
webauthn.base64url_decode(attestation_object),
27+
webauthn.base64url_decode(client_data_json)
2328
FROM consume_challenge
24-
RETURNING credential_id
29+
RETURNING TRUE
2530
$$;

FUNCTIONS/verify_assertion.sql

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,52 @@
1-
CREATE OR REPLACE FUNCTION webauthn.verify_assertion(credential_raw_id text, credential_type text, authenticator_data text, client_data_json text, signature text, user_handle text, relaying_party text)
2-
RETURNS bigint
1+
CREATE OR REPLACE FUNCTION webauthn.verify_assertion(
2+
credential_id text,
3+
credential_type text,
4+
authenticator_data text,
5+
client_data_json text,
6+
signature text,
7+
user_handle text,
8+
relying_party_id text
9+
)
10+
RETURNS boolean
311
LANGUAGE sql
412
AS $$
513
WITH
6-
input AS (
14+
decoded_input AS (
715
SELECT
8-
decode(credential_raw_id,'base64') AS credential_raw_id,
16+
webauthn.base64url_decode(credential_id) AS credential_id,
917
credential_type,
10-
decode(authenticator_data,'base64') AS authenticator_data,
11-
decode(client_data_json,'base64') AS client_data_json,
12-
decode(signature,'base64') AS signature,
13-
decode(user_handle,'base64') AS user_handle
18+
webauthn.base64url_decode(authenticator_data) AS authenticator_data,
19+
webauthn.base64url_decode(client_data_json) AS client_data_json,
20+
webauthn.base64url_decode(signature) AS signature,
21+
webauthn.base64url_decode(user_handle) AS user_id
1422
),
1523
consume_challenge AS (
16-
UPDATE webauthn.challenges SET
24+
UPDATE webauthn.assertion_challenges SET
1725
consumed_at = now()
18-
WHERE challenges.challenge = webauthn.base64_url_decode(webauthn.from_utf8(decode(client_data_json,'base64'))::jsonb->>'challenge')
19-
AND challenges.relaying_party = verify_assertion.relaying_party
20-
AND challenges.consumed_at IS NULL
21-
RETURNING challenge_id
26+
WHERE assertion_challenges.challenge = webauthn.base64url_decode(webauthn.from_utf8(webauthn.base64url_decode(client_data_json))::jsonb->>'challenge')
27+
AND assertion_challenges.relying_party_id = verify_assertion.relying_party_id
28+
AND assertion_challenges.consumed_at IS NULL
29+
RETURNING challenge
2230
)
23-
INSERT INTO webauthn.assertions (credential_id, challenge_id, authenticator_data, client_data_json, signature, user_handle)
31+
INSERT INTO webauthn.assertions (credential_id, challenge, authenticator_data, client_data_json, signature)
2432
SELECT
2533
credentials.credential_id,
26-
consume_challenge.challenge_id,
27-
input.authenticator_data,
28-
input.client_data_json,
29-
input.signature,
30-
input.user_handle
34+
consume_challenge.challenge,
35+
decoded_input.authenticator_data,
36+
decoded_input.client_data_json,
37+
decoded_input.signature
3138
FROM consume_challenge
32-
CROSS JOIN input
33-
JOIN webauthn.credentials ON credentials.credential_raw_id = input.credential_raw_id
34-
AND credentials.credential_type = input.credential_type
39+
CROSS JOIN decoded_input
40+
JOIN webauthn.credentials ON credentials.credential_id = decoded_input.credential_id
41+
AND credentials.credential_type = decoded_input.credential_type
42+
JOIN webauthn.credential_challenges ON credential_challenges.challenge = credentials.challenge
43+
AND credential_challenges.user_id = decoded_input.user_id
3544
WHERE ecdsa_verify(
3645
public_key := credentials.public_key,
37-
input_data := substring(input.authenticator_data,1,37) || digest(input.client_data_json,'sha256'),
38-
signature := webauthn.decode_asn1_der_signature(input.signature),
46+
input_data := substring(decoded_input.authenticator_data,1,37) || digest(decoded_input.client_data_json,'sha256'),
47+
signature := webauthn.decode_asn1_der_signature(decoded_input.signature),
3948
hash_func := 'sha256',
4049
curve_name := 'secp256r1'
4150
)
42-
RETURNING assertions.assertion_id
51+
RETURNING TRUE
4352
$$;

Makefile

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
EXTENSION = webauthn
22
DATA = webauthn--1.0.sql
3-
REGRESS = valid_signature invalid_signature invalid_base64
3+
REGRESS = valid_signature invalid_signature invalid_base64url base64url
44
EXTRA_CLEAN = webauthn--1.0.sql
55

66
PG_CONFIG = pg_config
@@ -11,16 +11,18 @@ all: webauthn--1.0.sql
1111

1212
SQL_SRC = \
1313
FUNCTIONS/complain_header.sql \
14-
FUNCTIONS/base64_url_decode.sql \
14+
FUNCTIONS/base64url_decode.sql \
15+
FUNCTIONS/base64url_encode.sql \
1516
FUNCTIONS/decode_cbor.sql \
1617
FUNCTIONS/cbor_to_json.sql \
1718
FUNCTIONS/cose_ecdha_to_pkcs.sql \
1819
FUNCTIONS/decode_asn1_der_signature.sql \
1920
FUNCTIONS/from_utf8.sql \
2021
FUNCTIONS/parse_authenticator_data.sql \
2122
FUNCTIONS/parse_attestation_object.sql \
22-
TABLES/challenges.sql \
23+
TABLES/credential_challenges.sql \
2324
TABLES/credentials.sql \
25+
TABLES/assertion_challenges.sql \
2426
TABLES/assertions.sql \
2527
FUNCTIONS/init_credential.sql \
2628
FUNCTIONS/make_credential.sql \

0 commit comments

Comments
 (0)