Skip to content

Commit 3f8c984

Browse files
thefrog-ghchromium-wpt-export-bot
authored andcommitted
Add first set of DBSC web platform tests
The two test files added are: - create-session.https.html - not-secure-connection.html ".py" files are either server endpoints or helper files used by server endpoints. __init__.py is required to use `import importlib`, which is required to import modules under directories with a hyphen (like device-bound- session-credentials). Bug: 353767385 Change-Id: I3178c0d0d3f0b08ca1f77c01cdd40c4c9028f3f0 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6309272 Reviewed-by: Weizhong Xia <[email protected]> Commit-Queue: thefrog <[email protected]> Reviewed-by: Daniel Rubery <[email protected]> Cr-Commit-Position: refs/heads/main@{#1426552}
1 parent 7bd6ffc commit 3f8c984

File tree

11 files changed

+261
-0
lines changed

11 files changed

+261
-0
lines changed

device-bound-session-credentials/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import importlib
2+
session_provider = importlib.import_module('device-bound-session-credentials.session_provider')
3+
4+
def main(request, response):
5+
session_provider.clear_server_state()
6+
return (200, [("Clear-Site-Data", '"cookies"')], "")
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>DBSC session-creating tests</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script src="/common/get-host-info.sub.js"></script>
7+
<script src="helper.js" type="module"></script>
8+
9+
<script type="module">
10+
import { expireCookie, documentHasCookie, waitForCookie, addCookieAndServerCleanup } from "./helper.js";
11+
12+
promise_test(async t => {
13+
const expectedCookieAndValue = "auth_cookie=abcdef0123";
14+
const expectedCookieAndAttributes = `${expectedCookieAndValue};Domain=${get_host_info().ORIGINAL_HOST};Path=/device-bound-session-credentials`;
15+
addCookieAndServerCleanup(t, expectedCookieAndAttributes);
16+
17+
// Prompt starting a session, and wait until registration completes.
18+
const login_response = await fetch('login.py');
19+
assert_equals(login_response.status, 200);
20+
assert_true(await waitForCookie(expectedCookieAndValue));
21+
22+
// Confirm that a request has the cookie set.
23+
const auth_response = await fetch('verify_authenticated.py');
24+
assert_equals(auth_response.status, 200);
25+
26+
// Confirm that expiring the cookie still leads to a request with the cookie set (refresh occurs).
27+
expireCookie(expectedCookieAndAttributes);
28+
assert_false(documentHasCookie(expectedCookieAndValue));
29+
const auth_response_after_expiry = await fetch('verify_authenticated.py');
30+
assert_equals(auth_response_after_expiry.status, 200);
31+
assert_true(documentHasCookie(expectedCookieAndValue));
32+
}, "An established session can refresh a cookie");
33+
</script>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export function documentHasCookie(cookieAndValue) {
2+
return document.cookie.split(';').some(item => item.includes(cookieAndValue));
3+
}
4+
5+
export function waitForCookie(cookieAndValue) {
6+
const startTime = Date.now();
7+
return new Promise(resolve => {
8+
const interval = setInterval(() => {
9+
if (documentHasCookie(cookieAndValue)) {
10+
clearInterval(interval);
11+
resolve(true);
12+
}
13+
if (Date.now() - startTime >= 1000) {
14+
clearInterval(interval);
15+
resolve(false);
16+
}
17+
}, 100);
18+
});
19+
}
20+
21+
export function expireCookie(cookieAndAttributes) {
22+
document.cookie =
23+
cookieAndAttributes + '; expires=Thu, 01 Jan 1970 00:00:00 UTC;';
24+
}
25+
26+
export function addCookieAndServerCleanup(test, cookieAndAttributes) {
27+
// Clean up any set cookies once the test completes.
28+
test.add_cleanup(async () => {
29+
const response = await fetch('clear_server_state_and_end_sessions.py');
30+
assert_equals(response.status, 200);
31+
expireCookie(cookieAndAttributes);
32+
});
33+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import json
2+
import base64
3+
from cryptography.hazmat.primitives import serialization
4+
from cryptography.hazmat.primitives import hashes
5+
from cryptography.hazmat.primitives.asymmetric import rsa, padding
6+
7+
# This method decodes the JWT and verifies the signature. If a key is provided,
8+
# that will be used for signature verification. Otherwise, the key sent within
9+
# the JWT payload will be used instead.
10+
# This returns a tuple of (decoded_header, decoded_payload, verify_succeeded).
11+
def decode_jwt(token, key=None):
12+
try:
13+
# Decode the header and payload.
14+
header, payload, signature = token.split('.')
15+
decoded_header = decode_base64_json(header)
16+
decoded_payload = decode_base64_json(payload)
17+
18+
# If decoding failed, return nothing.
19+
if not decoded_header or not decoded_payload:
20+
return None, None, False
21+
22+
# If there is a key passed in (for refresh), use that for checking the signature below.
23+
# Otherwise (for registration), use the key sent within the JWT to check the signature.
24+
if key == None:
25+
key = decoded_payload.get('key')
26+
public_key = serialization.load_pem_public_key(jwk_to_pem(key))
27+
# Verifying the signature will throw an exception if it fails.
28+
verify_rs256_signature(header, payload, signature, public_key)
29+
return decoded_header, decoded_payload, True
30+
except Exception:
31+
return None, None, False
32+
33+
def jwk_to_pem(jwk_data):
34+
jwk = json.loads(jwk_data) if isinstance(jwk_data, str) else jwk_data
35+
key_type = jwk.get("kty")
36+
37+
if key_type != "RSA":
38+
raise ValueError(f"Unsupported key type: {key_type}")
39+
40+
n = int.from_bytes(decode_base64url(jwk["n"]), 'big')
41+
e = int.from_bytes(decode_base64url(jwk["e"]), 'big')
42+
public_key = rsa.RSAPublicNumbers(e, n).public_key()
43+
pem_public_key = public_key.public_bytes(
44+
encoding=serialization.Encoding.PEM,
45+
format=serialization.PublicFormat.SubjectPublicKeyInfo
46+
)
47+
return pem_public_key
48+
49+
def verify_rs256_signature(encoded_header, encoded_payload, signature, public_key):
50+
message = (encoded_header + '.' + encoded_payload).encode('utf-8')
51+
signature_bytes = decode_base64(signature)
52+
# This will throw an exception if verification fails.
53+
public_key.verify(
54+
signature_bytes,
55+
message,
56+
padding.PKCS1v15(),
57+
hashes.SHA256()
58+
)
59+
60+
def add_base64_padding(encoded_data):
61+
remainder = len(encoded_data) % 4
62+
if remainder > 0:
63+
encoded_data += '=' * (4 - remainder)
64+
return encoded_data
65+
66+
def decode_base64url(encoded_data):
67+
encoded_data = add_base64_padding(encoded_data)
68+
encoded_data = encoded_data.replace("-", "+").replace("_", "/")
69+
return base64.b64decode(encoded_data)
70+
71+
def decode_base64(encoded_data):
72+
encoded_data = add_base64_padding(encoded_data)
73+
return base64.urlsafe_b64decode(encoded_data)
74+
75+
def decode_base64_json(encoded_data):
76+
return json.loads(decode_base64(encoded_data))
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def main(request, response):
2+
headers = [('Sec-Session-Registration', '(RS256);challenge="login_challenge_value";path="/device-bound-session-credentials/start_session.py"')]
3+
return (200, headers, "")
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!DOCTYPE html>
2+
<meta charset="utf-8">
3+
<title>No DBSC if connection is HTTP</title>
4+
<script src="/resources/testharness.js"></script>
5+
<script src="/resources/testharnessreport.js"></script>
6+
<script src="/common/get-host-info.sub.js"></script>
7+
<script src="helper.js" type="module"></script>
8+
9+
<script type="module">
10+
import { expireCookie, waitForCookie, addCookieAndServerCleanup } from "./helper.js";
11+
12+
promise_test(async t => {
13+
const expectedCookieAndValue = "auth_cookie=abcdef0123";
14+
const expectedCookieAndAttributes = `${expectedCookieAndValue};Domain=${get_host_info().ORIGINAL_HOST};Path=/device-bound-session-credentials`;
15+
addCookieAndServerCleanup(t, expectedCookieAndAttributes);
16+
17+
// Prompt starting a session, and wait until registration completes.
18+
const login_response = await fetch('login.py');
19+
assert_equals(login_response.status, 200);
20+
// For HTTP, this call will time out, because the cookie is never set.
21+
assert_false(await waitForCookie(expectedCookieAndValue));
22+
}, "Try to establish a session over HTTP");
23+
</script>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import importlib
2+
jwt_helper = importlib.import_module('device-bound-session-credentials.jwt_helper')
3+
session_provider = importlib.import_module('device-bound-session-credentials.session_provider')
4+
5+
def main(request, response):
6+
session_id_header = request.headers.get("Sec-Session-Id")
7+
if session_id_header == None:
8+
return (400, response.headers, "")
9+
session_id = session_id_header.decode('utf-8')
10+
session_key = session_provider.get_session_key(session_id)
11+
if session_key == None:
12+
return (400, response.headers, "")
13+
14+
challenge = "refresh_challenge_value"
15+
if request.headers.get("Sec-Session-Response") == None:
16+
return (401, [('Sec-Session-Challenge', '"' + challenge + '";id="' + session_id + '"')], "")
17+
18+
jwt_header, jwt_payload, verified = jwt_helper.decode_jwt(request.headers.get("Sec-Session-Response").decode('utf-8'), session_key)
19+
20+
if not verified or jwt_payload.get("jti") != challenge:
21+
return (400, response.headers, "")
22+
23+
return session_provider.get_session_instructions_response(session_id, request)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import json
2+
3+
session_to_key_map = {}
4+
5+
def create_new_session():
6+
session_id = str(len(session_to_key_map))
7+
session_to_key_map[session_id] = None
8+
return session_id
9+
10+
def set_session_key(session_id, key):
11+
if session_id not in session_to_key_map:
12+
return False
13+
session_to_key_map[session_id] = key
14+
return True
15+
16+
def get_session_key(session_id):
17+
return session_to_key_map.get(session_id)
18+
19+
def clear_server_state():
20+
global session_to_key_map
21+
session_to_key_map = {}
22+
23+
def get_session_instructions_response(session_id, request):
24+
refresh_url = "/device-bound-session-credentials/refresh_session.py"
25+
26+
response_body = {
27+
"session_identifier": session_id,
28+
"refresh_url": refresh_url,
29+
"scope": {
30+
"include_site": True,
31+
"scope_specification" : [
32+
{ "type": "exclude", "domain": request.url_parts.hostname, "path": "/device-bound-session-credentials/clear_server_state_and_end_sessions.py" },
33+
]
34+
},
35+
"credentials": [{
36+
"type": "cookie",
37+
"name": "auth_cookie",
38+
"attributes": "Domain=" + request.url_parts.hostname + "; Path=/device-bound-session-credentials"
39+
}]
40+
}
41+
headers = [
42+
("Content-Type", "application/json"),
43+
("Cache-Control", "no-store"),
44+
("Set-Cookie", "auth_cookie=abcdef0123; Domain=" + request.url_parts.hostname + "; Path=/device-bound-session-credentials")
45+
]
46+
return (200, headers, json.dumps(response_body))
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import importlib
2+
jwt_helper = importlib.import_module('device-bound-session-credentials.jwt_helper')
3+
session_provider = importlib.import_module('device-bound-session-credentials.session_provider')
4+
5+
def main(request, response):
6+
jwt_header, jwt_payload, verified = jwt_helper.decode_jwt(request.headers.get("Sec-Session-Response").decode('utf-8'))
7+
session_id = session_provider.create_new_session()
8+
session_provider.set_session_key(session_id, jwt_payload.get('key'))
9+
10+
if not verified or jwt_payload.get("jti") != "login_challenge_value":
11+
return (400, response.headers, "")
12+
13+
return session_provider.get_session_instructions_response(session_id, request)

0 commit comments

Comments
 (0)