test: client-certificate follow-ups (#2508)

This commit is contained in:
Max Schmitt 2024-08-01 19:45:51 +02:00 коммит произвёл GitHub
Родитель 65658108c6
Коммит 86c0191b67
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
9 изменённых файлов: 319 добавлений и 46 удалений

Просмотреть файл

@ -206,7 +206,6 @@ class ElementHandle(JSHandle):
"setInputFiles",
{
"timeout": timeout,
"noWaitAfter": noWaitAfter,
**converted,
},
)
@ -246,7 +245,6 @@ class ElementHandle(JSHandle):
position=position,
timeout=timeout,
force=force,
noWaitAfter=noWaitAfter,
trial=trial,
)
else:
@ -254,7 +252,6 @@ class ElementHandle(JSHandle):
position=position,
timeout=timeout,
force=force,
noWaitAfter=noWaitAfter,
trial=trial,
)

Просмотреть файл

@ -703,7 +703,6 @@ class Frame(ChannelOwner):
"selector": selector,
"strict": strict,
"timeout": timeout,
"noWaitAfter": noWaitAfter,
**converted,
},
)
@ -792,7 +791,6 @@ class Frame(ChannelOwner):
position=position,
timeout=timeout,
force=force,
noWaitAfter=noWaitAfter,
strict=strict,
trial=trial,
)
@ -802,7 +800,6 @@ class Frame(ChannelOwner):
position=position,
timeout=timeout,
force=force,
noWaitAfter=noWaitAfter,
strict=strict,
trial=trial,
)

Просмотреть файл

@ -213,7 +213,7 @@ class Locator:
noWaitAfter: bool = None,
force: bool = None,
) -> None:
await self.fill("", timeout=timeout, noWaitAfter=noWaitAfter, force=force)
await self.fill("", timeout=timeout, force=force)
def locator(
self,
@ -631,7 +631,7 @@ class Locator:
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
await self.type(text, delay=delay, timeout=timeout, noWaitAfter=noWaitAfter)
await self.type(text, delay=delay, timeout=timeout)
async def uncheck(
self,
@ -685,7 +685,6 @@ class Locator:
position=position,
timeout=timeout,
force=force,
noWaitAfter=noWaitAfter,
trial=trial,
)
else:
@ -693,7 +692,6 @@ class Locator:
position=position,
timeout=timeout,
force=force,
noWaitAfter=noWaitAfter,
trial=trial,
)

Просмотреть файл

@ -1279,7 +1279,6 @@ class Page(ChannelOwner):
position=position,
timeout=timeout,
force=force,
noWaitAfter=noWaitAfter,
strict=strict,
trial=trial,
)
@ -1289,7 +1288,6 @@ class Page(ChannelOwner):
position=position,
timeout=timeout,
force=force,
noWaitAfter=noWaitAfter,
strict=strict,
trial=trial,
)

Просмотреть файл

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIEyzCCArOgAwIBAgIUYps4gh4MqFYg8zqQhHYL7zYfbLkwDQYJKoZIhvcNAQEL
BQAwDjEMMAoGA1UEAwwDQm9iMB4XDTI0MDcxOTEyNDc0MFoXDTI1MDcxOTEyNDc0
MFowDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEA179eTsqcc1c3AOQHzCZEyYLPta2CCAscUFqcEZ9vWvjW0uzOv9TDlB33Unov
jch4CElZOBhzTadVsbmnYKpxwyVU89WCuQKvedz4k1vu7S1YryfNbmS8PWbnQ4ds
9NB7SgJNHZILvx9DXuWeFEyzRIo1984z4HheBzrkf791LqpYKaKziANUo8h8t0dm
TX/boOz8cEnQNwtTC0ZX3aD0obG/UAhr/22ZGPo/E659fh4ptyYX2LrIUHGy+Eux
nJ9Y4cTqa88Ee6K6AkDiT/AoNQNxE4X++jqLuie8j/ZYpI1Oll38GwKVOyy1msRL
toGmISNwkMIQDGABrJlxgpP4QQAQ+08v9srzXOlkdxdr7OCP81r+ccBXiSQEe7BA
kdJ8l98l5dprJ++GJ+SZcV4+/iGR0dKU2IdAG5HiKZIFn6ch9Ux+UMqeGaYCpkHr
TiietHwcXgtVBlE0jFmB/HspmI/O0abK+grMmueaH7XtTI8YHnw0mUpL8+yp7mfA
7zFusgFgyiBPXeD/NQgg8vja67k++d1VGoXm2xr+5WPQCSbgQoMkkOBMLHWJTefd
6F4Z5M+oI0VwYbf6eQW246wJgpCHSPR0Vdijd6MAGRWKUuLfDsA9+12iGbKvwJ2e
nJlStft2V2LZcjBfdIMbigW1aSVNN5w6m6YVrQPry3WPkWcCAwEAAaMhMB8wHQYD
VR0OBBYEFPxKWTFQJSg4HD2qjxL0dnXX/z4qMA0GCSqGSIb3DQEBCwUAA4ICAQBz
4H1d5eGRU9bekUvi7LbZ5CP/I6w6PL/9AlXqO3BZKxplK7fYGHd3uqyDorJEsvjV
hxwvFlEnS0JIU3nRzhJU/h4Yaivf1WLRFwGZ4TPBjX9KFU27exFWD3rppazkWybJ
i4WuEdP3TJMdKLcNTtXWUDroDOgPlS66u6oZ+mUyUROil+B+fgQgVDhjRc5fvRgZ
Lng8wuejCo3ExQyxkwn2G5guyIimgHmOQghPtLO5xlc67Z4GPUZ1m4tC+BCiFO4D
YIXl3QiIpmU7Pss39LLKMGXXAgLRqyMzqE52lsznu18v5vDLfTaRH4u/wjzULhXz
SrV1IUJmhgEXta4EeDmPH0itgKtkbwjgCOD7drrFrJq/EnvIaJ5cpxiI1pFmYD8g
VVD7/KT/CyT1Uz1dI8QaP/JX8XEgtMJaSkPfjPErIViN9rh9ECCNLgFyv7Y0Plar
A6YlvdyV1Rta/BHndf5Hqz9QWNhbFCMQRGVQNEcoKwpFyjAE9SXoKJvFIK/w5WXu
qKzIYA26QXE3p734Xu1n8QiFJIyltVHbyUlD0k06194t5a2WK+/eDeReIsk0QOI8
FGqhyPZ7YjR5tSZTmgljtViqBO5AA23QOVFqtjOUrjXP5pTbPJel99Z/FTkqSwvB
Rt4OX7HfuokWQDTT0TMn5jVtJyi54cH7f9MmsNJ23g==
-----END CERTIFICATE-----

Просмотреть файл

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEUzCCAjsCAQAwDjEMMAoGA1UEAwwDQm9iMIICIjANBgkqhkiG9w0BAQEFAAOC
Ag8AMIICCgKCAgEA179eTsqcc1c3AOQHzCZEyYLPta2CCAscUFqcEZ9vWvjW0uzO
v9TDlB33Unovjch4CElZOBhzTadVsbmnYKpxwyVU89WCuQKvedz4k1vu7S1YryfN
bmS8PWbnQ4ds9NB7SgJNHZILvx9DXuWeFEyzRIo1984z4HheBzrkf791LqpYKaKz
iANUo8h8t0dmTX/boOz8cEnQNwtTC0ZX3aD0obG/UAhr/22ZGPo/E659fh4ptyYX
2LrIUHGy+EuxnJ9Y4cTqa88Ee6K6AkDiT/AoNQNxE4X++jqLuie8j/ZYpI1Oll38
GwKVOyy1msRLtoGmISNwkMIQDGABrJlxgpP4QQAQ+08v9srzXOlkdxdr7OCP81r+
ccBXiSQEe7BAkdJ8l98l5dprJ++GJ+SZcV4+/iGR0dKU2IdAG5HiKZIFn6ch9Ux+
UMqeGaYCpkHrTiietHwcXgtVBlE0jFmB/HspmI/O0abK+grMmueaH7XtTI8YHnw0
mUpL8+yp7mfA7zFusgFgyiBPXeD/NQgg8vja67k++d1VGoXm2xr+5WPQCSbgQoMk
kOBMLHWJTefd6F4Z5M+oI0VwYbf6eQW246wJgpCHSPR0Vdijd6MAGRWKUuLfDsA9
+12iGbKvwJ2enJlStft2V2LZcjBfdIMbigW1aSVNN5w6m6YVrQPry3WPkWcCAwEA
AaAAMA0GCSqGSIb3DQEBCwUAA4ICAQCb07d2IjUy1PeHCj/2k/z9FrZSo6K3c8y6
b/u/MZ0AXPKLPDSo7UYpOJ8Z2cBiJ8jQapjTSEL8POUYqcvCmP55R6u68KmvINHo
+Ly7pP+xPrbA4Q0WmPnz37hQn+I1he0GuEQyjZZqUln9zwp67TsWNKxKtCH+1j8M
Ltzx6kuHCdPtDUtv291yhVRqvbjiDs+gzdQYNJtAkUbHwHFxu8oZhg8QZGyXYMN8
TGoQ1LTezFZXJtX69K7WnrDGrjsgB6EMvwkqAFSYNH0LFvI0xo13OOgXr9mrwohA
76uZtjXL9B15EqrMce6mdUZi46QJuQ2avTi57Lz+fqvsBYdQO89VcFSmqu2nfspN
QZDrooyjHrlls8MpoBd8fde9oT4uA4/d9SJtuHUnjgGN7Qr7eTruWXL8wVMwFnvL
igWE4detO9y2gpRLq6uEqzWYMGtN9PXJCGU8C8m9E2EBUKMrT/bpNbboatLcgRrW
acj0BRVqoVzk1sRq7Sa6ejywqgARvIhTehg6DqdMdcENCPQ7rxDRu5PSDM8/mwIj
0KYl8d2PlECB4ofRyLcy17BZzjP6hSnkGzcFk0/bChZOSIRnwvKbvfXnB45hhPk8
XwT/6UNSwC2STP3gtOmLqrWj+OE0gy0AkDMvP3UnQVGMUvgfYg+N4ROCVtlqzxe9
W65c05Mm1g==
-----END CERTIFICATE REQUEST-----

Просмотреть файл

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
dg6m3MAyZ1IXjHlogOdmGvnq07/w
-----END PRIVATE KEY-----

Просмотреть файл

@ -15,16 +15,20 @@
import sys
import threading
from pathlib import Path
from typing import Dict, Generator, cast
from typing import Dict, Generator, Optional, cast
import OpenSSL.crypto
import OpenSSL.SSL
import pytest
from twisted.internet import reactor as _twisted_reactor
from twisted.internet import ssl
from twisted.internet.selectreactor import SelectReactor
from twisted.web import resource, server
from twisted.web.http import Request
from playwright.async_api import Browser, BrowserType, Playwright, Request, expect
from playwright.async_api import Browser, BrowserType, Playwright, expect
ssl.optionsForClientTLS
reactor = cast(SelectReactor, _twisted_reactor)
@ -34,17 +38,61 @@ def _skip_webkit_darwin(browser_name: str) -> None:
pytest.skip("WebKit does not proxy localhost on macOS")
class Simple(resource.Resource):
class HttpsResource(resource.Resource):
serverCertificate: ssl.PrivateCertificate
isLeaf = True
def _verify_cert_chain(self, cert: Optional[OpenSSL.crypto.X509]) -> bool:
if not cert:
return False
store = OpenSSL.crypto.X509Store()
store.add_cert(self.serverCertificate.original)
store_ctx = OpenSSL.crypto.X509StoreContext(store, cert)
try:
store_ctx.verify_certificate()
return True
except OpenSSL.crypto.X509StoreContextError:
return False
def render_GET(self, request: Request) -> bytes:
return b"<html>Hello, world!</html>"
tls_socket: OpenSSL.SSL.Connection = request.transport.getHandle() # type: ignore
cert = tls_socket.get_peer_certificate()
parts = []
if self._verify_cert_chain(cert):
request.setResponseCode(200)
parts.append(
{
"key": "message",
"value": f"Hello {cert.get_subject().CN}, your certificate was issued by {cert.get_issuer().CN}!", # type: ignore
}
)
elif cert and cert.get_subject():
request.setResponseCode(403)
parts.append(
{
"key": "message",
"value": f"Sorry {cert.get_subject().CN}, certificates from {cert.get_issuer().CN} are not welcome here.",
}
)
else:
request.setResponseCode(401)
parts.append(
{
"key": "message",
"value": "Sorry, but you need to provide a client certificate to continue.",
}
)
return b"".join(
[
f'<div data-testid="{part["key"]}">{part["value"]}</div>'.encode()
for part in parts
]
)
@pytest.fixture(scope="session", autouse=True)
def _client_certificate_server(assetdir: Path) -> Generator[None, None, None]:
server.Site(Simple())
certAuthCert = ssl.Certificate.loadPEM(
(assetdir / "client-certificates/server/server_cert.pem").read_text()
)
@ -54,7 +102,10 @@ def _client_certificate_server(assetdir: Path) -> Generator[None, None, None]:
)
contextFactory = serverCert.options(certAuthCert)
site = server.Site(Simple())
contextFactory.requireCertificate = False
resource = HttpsResource()
resource.serverCertificate = serverCert
site = server.Site(resource)
def _run() -> None:
reactor.listenSSL(8000, site, contextFactory)
@ -65,6 +116,27 @@ def _client_certificate_server(assetdir: Path) -> Generator[None, None, None]:
thread.join()
async def test_should_throw_with_untrusted_client_certs(
playwright: Playwright, assetdir: Path
) -> None:
serverURL = "https://localhost:8000/"
request = await playwright.request.new_context(
# TODO: Remove this once we can pass a custom CA.
ignore_https_errors=True,
client_certificates=[
{
"origin": serverURL,
"certPath": assetdir
/ "client-certificates/client/self-signed/cert.pem",
"keyPath": assetdir / "client-certificates/client/self-signed/key.pem",
}
],
)
with pytest.raises(Exception, match="alert unknown ca"):
await request.get(serverURL)
await request.dispose()
async def test_should_work_with_new_context(browser: Browser, assetdir: Path) -> None:
context = await browser.new_context(
# TODO: Remove this once we can pass a custom CA.
@ -79,14 +151,24 @@ async def test_should_work_with_new_context(browser: Browser, assetdir: Path) ->
)
page = await context.new_page()
await page.goto("https://localhost:8000")
await expect(page.get_by_text("alert certificate required")).to_be_visible()
await expect(page.get_by_test_id("message")).to_have_text(
"Sorry, but you need to provide a client certificate to continue."
)
await page.goto("https://127.0.0.1:8000")
await expect(page.get_by_text("Hello, world!")).to_be_visible()
await expect(page.get_by_test_id("message")).to_have_text(
"Hello Alice, your certificate was issued by localhost!"
)
with pytest.raises(Exception, match="alert certificate required"):
await page.context.request.get("https://localhost:8000")
response = await page.context.request.get("https://localhost:8000")
assert (
"Sorry, but you need to provide a client certificate to continue."
in await response.text()
)
response = await page.context.request.get("https://127.0.0.1:8000")
assert "Hello, world!" in await response.text()
assert (
"Hello Alice, your certificate was issued by localhost!"
in await response.text()
)
await context.close()
@ -108,9 +190,13 @@ async def test_should_work_with_new_persistent_context(
)
page = await context.new_page()
await page.goto("https://localhost:8000")
await expect(page.get_by_text("alert certificate required")).to_be_visible()
await expect(page.get_by_test_id("message")).to_have_text(
"Sorry, but you need to provide a client certificate to continue."
)
await page.goto("https://127.0.0.1:8000")
await expect(page.get_by_text("Hello, world!")).to_be_visible()
await expect(page.get_by_test_id("message")).to_have_text(
"Hello Alice, your certificate was issued by localhost!"
)
await context.close()
@ -128,8 +214,14 @@ async def test_should_work_with_global_api_request_context(
}
],
)
with pytest.raises(Exception, match="alert certificate required"):
await request.get("https://localhost:8000")
response = await request.get("https://localhost:8000")
assert (
"Sorry, but you need to provide a client certificate to continue."
in await response.text()
)
response = await request.get("https://127.0.0.1:8000")
assert "Hello, world!" in await response.text()
assert (
"Hello Alice, your certificate was issued by localhost!"
in await response.text()
)
await request.dispose()

Просмотреть файл

@ -15,15 +15,18 @@
import sys
import threading
from pathlib import Path
from typing import Dict, Generator, cast
from typing import Dict, Generator, Optional, cast
import OpenSSL.crypto
import OpenSSL.SSL
import pytest
from twisted.internet import reactor as _twisted_reactor
from twisted.internet import ssl
from twisted.internet.selectreactor import SelectReactor
from twisted.web import resource, server
from twisted.web.http import Request
from playwright.sync_api import Browser, BrowserType, Playwright, Request, expect
from playwright.sync_api import Browser, BrowserType, Playwright, expect
reactor = cast(SelectReactor, _twisted_reactor)
@ -34,17 +37,61 @@ def _skip_webkit_darwin(browser_name: str) -> None:
pytest.skip("WebKit does not proxy localhost on macOS")
class Simple(resource.Resource):
class HttpsResource(resource.Resource):
serverCertificate: ssl.PrivateCertificate
isLeaf = True
def _verify_cert_chain(self, cert: Optional[OpenSSL.crypto.X509]) -> bool:
if not cert:
return False
store = OpenSSL.crypto.X509Store()
store.add_cert(self.serverCertificate.original)
store_ctx = OpenSSL.crypto.X509StoreContext(store, cert)
try:
store_ctx.verify_certificate()
return True
except OpenSSL.crypto.X509StoreContextError:
return False
def render_GET(self, request: Request) -> bytes:
return b"<html>Hello, world!</html>"
tls_socket: OpenSSL.SSL.Connection = request.transport.getHandle() # type: ignore
cert = tls_socket.get_peer_certificate()
parts = []
if self._verify_cert_chain(cert):
request.setResponseCode(200)
parts.append(
{
"key": "message",
"value": f"Hello {cert.get_subject().CN}, your certificate was issued by {cert.get_issuer().CN}!", # type: ignore
}
)
elif cert and cert.get_subject():
request.setResponseCode(403)
parts.append(
{
"key": "message",
"value": f"Sorry {cert.get_subject().CN}, certificates from {cert.get_issuer().CN} are not welcome here.",
}
)
else:
request.setResponseCode(401)
parts.append(
{
"key": "message",
"value": "Sorry, but you need to provide a client certificate to continue.",
}
)
return b"".join(
[
f'<div data-testid="{part["key"]}">{part["value"]}</div>'.encode()
for part in parts
]
)
@pytest.fixture(scope="session", autouse=True)
def _client_certificate_server(assetdir: Path) -> Generator[None, None, None]:
server.Site(Simple())
certAuthCert = ssl.Certificate.loadPEM(
(assetdir / "client-certificates/server/server_cert.pem").read_text()
)
@ -54,7 +101,10 @@ def _client_certificate_server(assetdir: Path) -> Generator[None, None, None]:
)
contextFactory = serverCert.options(certAuthCert)
site = server.Site(Simple())
contextFactory.requireCertificate = False
resource = HttpsResource()
resource.serverCertificate = serverCert
site = server.Site(resource)
def _run() -> None:
reactor.listenSSL(8000, site, contextFactory)
@ -65,6 +115,27 @@ def _client_certificate_server(assetdir: Path) -> Generator[None, None, None]:
thread.join()
def test_should_throw_with_untrusted_client_certs(
playwright: Playwright, assetdir: Path
) -> None:
serverURL = "https://localhost:8000/"
request = playwright.request.new_context(
# TODO: Remove this once we can pass a custom CA.
ignore_https_errors=True,
client_certificates=[
{
"origin": serverURL,
"certPath": assetdir
/ "client-certificates/client/self-signed/cert.pem",
"keyPath": assetdir / "client-certificates/client/self-signed/key.pem",
}
],
)
with pytest.raises(Exception, match="alert unknown ca"):
request.get(serverURL)
request.dispose()
def test_should_work_with_new_context(browser: Browser, assetdir: Path) -> None:
context = browser.new_context(
# TODO: Remove this once we can pass a custom CA.
@ -79,14 +150,21 @@ def test_should_work_with_new_context(browser: Browser, assetdir: Path) -> None:
)
page = context.new_page()
page.goto("https://localhost:8000")
expect(page.get_by_text("alert certificate required")).to_be_visible()
expect(page.get_by_test_id("message")).to_have_text(
"Sorry, but you need to provide a client certificate to continue."
)
page.goto("https://127.0.0.1:8000")
expect(page.get_by_text("Hello, world!")).to_be_visible()
expect(page.get_by_test_id("message")).to_have_text(
"Hello Alice, your certificate was issued by localhost!"
)
with pytest.raises(Exception, match="alert certificate required"):
page.context.request.get("https://localhost:8000")
response = page.context.request.get("https://localhost:8000")
assert (
"Sorry, but you need to provide a client certificate to continue."
in response.text()
)
response = page.context.request.get("https://127.0.0.1:8000")
assert "Hello, world!" in response.text()
assert "Hello Alice, your certificate was issued by localhost!" in response.text()
context.close()
@ -108,9 +186,13 @@ def test_should_work_with_new_persistent_context(
)
page = context.new_page()
page.goto("https://localhost:8000")
expect(page.get_by_text("alert certificate required")).to_be_visible()
expect(page.get_by_test_id("message")).to_have_text(
"Sorry, but you need to provide a client certificate to continue."
)
page.goto("https://127.0.0.1:8000")
expect(page.get_by_text("Hello, world!")).to_be_visible()
expect(page.get_by_test_id("message")).to_have_text(
"Hello Alice, your certificate was issued by localhost!"
)
context.close()
@ -128,8 +210,11 @@ def test_should_work_with_global_api_request_context(
}
],
)
with pytest.raises(Exception, match="alert certificate required"):
request.get("https://localhost:8000")
response = request.get("https://localhost:8000")
assert (
"Sorry, but you need to provide a client certificate to continue."
in response.text()
)
response = request.get("https://127.0.0.1:8000")
assert "Hello, world!" in response.text()
assert "Hello Alice, your certificate was issued by localhost!" in response.text()
request.dispose()