From 132e20df0ac17b5f22c3271220a3e4cd549cfea9 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Fri, 18 Oct 2024 11:31:09 +0200 Subject: [PATCH 1/3] tests: Improve RSA256 test Pass a real public key to ensure it's correctly loaded --- setup.py | 1 + tests/test_auth.py | 38 ++++++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/setup.py b/setup.py index 781b11ea..a06d9a9b 100755 --- a/setup.py +++ b/setup.py @@ -34,6 +34,7 @@ install_requirements = [ "Django >= 3.2", "josepy", + "pyjwt", "requests", "cryptography", ] diff --git a/tests/test_auth.py b/tests/test_auth.py index 5bdda157..54243dc5 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -1,9 +1,10 @@ import json from unittest.mock import Mock, call, patch +import jwt from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, hmac, serialization -from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric import ec, rsa from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation @@ -860,19 +861,34 @@ class OIDCAuthenticationBackendRS256WithKeyTestCase(TestCase): @override_settings(OIDC_RP_CLIENT_ID="example_id") @override_settings(OIDC_RP_CLIENT_SECRET="client_secret") @override_settings(OIDC_RP_SIGN_ALGO="RS256") - @override_settings(OIDC_RP_IDP_SIGN_KEY="sign_key") - def setUp(self): - self.backend = OIDCAuthenticationBackend() - @override_settings(OIDC_USE_NONCE=False) - @patch("mozilla_django_oidc.auth.OIDCAuthenticationBackend._verify_jws") @patch("mozilla_django_oidc.auth.requests") - def test_jwt_verify_sign_key(self, request_mock, jws_mock): + def test_jwt_verify_sign_key(self, request_mock): """Test jwt verification signature.""" + + # Generate a private key to create a test token with + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + # Make the public key available through the JWKS response + public_key = smart_str(key.public_key().public_bytes( + serialization.Encoding.PEM, + serialization.PublicFormat.PKCS1, + )) + + with override_settings(OIDC_RP_IDP_SIGN_KEY=public_key): + backend = OIDCAuthenticationBackend() + + # Generate id_token + header = { + "typ": "JWT", + "alg": "RS256", + } + data = {"name": "John Doe", "test": "test_jwt_verify_sign_key"} + id_token = jwt.encode(payload=data, key=key, algorithm="RS256", headers=header) + auth_request = RequestFactory().get("/foo", {"code": "foo", "state": "bar"}) auth_request.session = {} - jws_mock.return_value = json.dumps({"aud": "audience"}).encode("utf-8") get_json_mock = Mock() get_json_mock.json.return_value = { "nickname": "username", @@ -881,13 +897,11 @@ def test_jwt_verify_sign_key(self, request_mock, jws_mock): request_mock.get.return_value = get_json_mock post_json_mock = Mock(status_code=200) post_json_mock.json.return_value = { - "id_token": "token", + "id_token": id_token, "access_token": "access_token", } request_mock.post.return_value = post_json_mock - self.backend.authenticate(request=auth_request) - calls = [call(force_bytes("token"), "sign_key")] - jws_mock.assert_has_calls(calls) + self.assertIsNotNone(backend.authenticate(request=auth_request)) class OIDCAuthenticationBackendRS256WithJwksEndpointTestCase(TestCase): From 7374ddce50a3dc1cc8cdf204f2428921d8875f36 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Wed, 16 Oct 2024 06:14:24 +0200 Subject: [PATCH 2/3] Replace josepy with jwcrypto --- mozilla_django_oidc/auth.py | 38 ++++++++++++++++------------- mozilla_django_oidc/utils.py | 35 ++------------------------- tests/test_auth.py | 2 +- tests/test_utils.py | 46 ------------------------------------ 4 files changed, 24 insertions(+), 97 deletions(-) diff --git a/mozilla_django_oidc/auth.py b/mozilla_django_oidc/auth.py index f8243fe3..713bb4f1 100644 --- a/mozilla_django_oidc/auth.py +++ b/mozilla_django_oidc/auth.py @@ -9,11 +9,11 @@ from django.contrib.auth.backends import ModelBackend from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation from django.urls import reverse -from django.utils.encoding import force_bytes, smart_bytes, smart_str +from django.utils.encoding import force_bytes, smart_str from django.utils.module_loading import import_string -from josepy.b64 import b64decode -from josepy.jwk import JWK -from josepy.jws import JWS, Header +from jwcrypto.common import base64url_decode +from jwcrypto.jwk import JWK +from jwcrypto.jws import JWS, InvalidJWSSignature from requests.auth import HTTPBasicAuth from requests.exceptions import HTTPError @@ -127,10 +127,11 @@ def update_user(self, user, claims): def _verify_jws(self, payload, key): """Verify the given JWS payload with the given key and return the payload""" - jws = JWS.from_compact(payload) + jws = JWS() + jws.deserialize(smart_str(payload)) try: - alg = jws.signature.combined.alg.name + alg = jws.jose_header["alg"] except KeyError: msg = "No alg value found in header" raise SuspiciousOperation(msg) @@ -143,13 +144,17 @@ def _verify_jws(self, payload, key): raise SuspiciousOperation(msg) if isinstance(key, str): - # Use smart_bytes here since the key string comes from settings. - jwk = JWK.load(smart_bytes(key)) + try: + jwk = JWK.from_pem(force_bytes(key)) + except ValueError: + jwk = JWK.from_password(key) else: # The key is a json returned from the IDP JWKS endpoint. - jwk = JWK.from_json(key) + jwk = JWK(**key) - if not jws.verify(jwk): + try: + jws.verify(jwk) + except InvalidJWSSignature: msg = "JWS token verification failed." raise SuspiciousOperation(msg) @@ -167,17 +172,16 @@ def retrieve_matching_jwk(self, token): jwks = response_jwks.json() # Compute the current header from the given token to find a match - jws = JWS.from_compact(token) - json_header = jws.signature.protected - header = Header.json_loads(json_header) + jws = JWS() + jws.deserialize(smart_str(token)) key = None for jwk in jwks["keys"]: if import_from_settings("OIDC_VERIFY_KID", True) and jwk[ "kid" - ] != smart_str(header.kid): + ] != smart_str(jws.jose_header["kid"]): continue - if "alg" in jwk and jwk["alg"] != smart_str(header.alg): + if "alg" in jwk and jwk["alg"] != smart_str(jws.jose_header["alg"]): continue key = jwk if key is None: @@ -188,11 +192,11 @@ def get_payload_data(self, token, key): """Helper method to get the payload of the JWT token.""" if self.get_settings("OIDC_ALLOW_UNSECURED_JWT", False): header, payload_data, signature = token.split(b".") - header = json.loads(smart_str(b64decode(header))) + header = json.loads(base64url_decode(smart_str(header))) # If config allows unsecured JWTs check the header and return the decoded payload if "alg" in header and header["alg"] == "none": - return b64decode(payload_data) + return base64url_decode(smart_str(payload_data)) # By default fallback to verify JWT signatures return self._verify_jws(token, key) diff --git a/mozilla_django_oidc/utils.py b/mozilla_django_oidc/utils.py index a09e4ce1..fcb47a6c 100644 --- a/mozilla_django_oidc/utils.py +++ b/mozilla_django_oidc/utils.py @@ -4,8 +4,7 @@ from hashlib import sha256 from urllib.request import parse_http_list, parse_keqv_list -# Make it obvious that these aren't the usual base64 functions -import josepy.b64 +from jwcrypto.common import base64url_encode from django.conf import settings from django.core.exceptions import ImproperlyConfigured @@ -55,36 +54,6 @@ def is_authenticated(user): return user.is_authenticated -def base64_url_encode(bytes_like_obj): - """Return a URL-Safe, base64 encoded version of bytes_like_obj - - Implements base64urlencode as described in - https://datatracker.ietf.org/doc/html/rfc7636#appendix-A - """ - - s = josepy.b64.b64encode(bytes_like_obj).decode("ascii") # base64 encode - # the josepy base64 encoder (strips '='s padding) automatically - s = s.replace("+", "-") # 62nd char of encoding - s = s.replace("/", "_") # 63rd char of encoding - - return s - - -def base64_url_decode(string_like_obj): - """Return the bytes encoded in a URL-Safe, base64 encoded string. - Implements inverse of base64urlencode as described in - https://datatracker.ietf.org/doc/html/rfc7636#appendix-A - This function is not used by the OpenID client; it's just for testing PKCE related functions. - """ - s = string_like_obj - - s = s.replace("_", "/") # 63rd char of encoding - s = s.replace("-", "+") # 62nd char of encoding - b = josepy.b64.b64decode(s) # josepy base64 encoder (decodes without '='s padding) - - return b - - def generate_code_challenge(code_verifier, method): """Return a code_challege, which proves knowledge of the code_verifier. The code challenge is generated according to method which must be one @@ -99,7 +68,7 @@ def generate_code_challenge(code_verifier, method): return code_verifier elif method == "S256": - return base64_url_encode(sha256(code_verifier.encode("ascii")).digest()) + return base64url_encode(sha256(code_verifier.encode("ascii")).digest()) else: raise ValueError("code challenge method must be 'plain' or 'S256'.") diff --git a/tests/test_auth.py b/tests/test_auth.py index 54243dc5..ed857b85 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -94,7 +94,7 @@ def test_disallowed_unsecured_token(self): ) ) - with self.assertRaises(KeyError): + with self.assertRaises(SuspiciousOperation): self.backend.get_payload_data(token, None) @override_settings(OIDC_ALLOW_UNSECURED_JWT=True) diff --git a/tests/test_utils.py b/tests/test_utils.py index 5a4a740b..087a0c05 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,8 +7,6 @@ from mozilla_django_oidc.utils import ( absolutify, add_state_and_verifier_and_nonce_to_session, - base64_url_decode, - base64_url_encode, generate_code_challenge, import_from_settings, ) @@ -52,50 +50,6 @@ def test_absolutify_path_host_injection(self): self.assertEqual(url, "https://testserver/evil.com/foo/bar") -class Base64URLEncodeTestCase(TestCase): - def test_base64_url_encode(self): - """ - Tests creating a url-safe base64 encoded string from bytes. - Source: https://datatracker.ietf.org/doc/html/rfc7636#appendix-A - """ - data = bytes((3, 236, 255, 224, 193)) - encoded = base64_url_encode(data) - - # Using base64.b64encode() returns b'A+z/4ME='. - # Our implementation should strip tailing '='s padding. - # and replace '+' with '-' and '/' with '_'. - self.assertEqual(encoded, "A-z_4ME") - - # Decoding should return the original data. - decoded = base64_url_decode(encoded) - self.assertEqual(decoded, data) - - def test_base64_url_encode_empty_input(self): - """ - Tests creating a url-safe base64 encoded string from an empty bytes instance. - """ - data = bytes() - encoded = base64_url_encode(data) - self.assertEqual(encoded, "") - - decoded = base64_url_decode(encoded) - self.assertEqual(decoded, data) - - def test_base64_url_encode_double_padding(self): - """ - Test encoding a string whoose base64.b64encode encoding ends with '=='. - """ - data = bytes((3, 236, 255, 224, 193, 222, 22)) - encoded = base64_url_encode(data) - - # Using base64.b64encode() returns b'A+z/4MHeFg=='. - self.assertEqual(encoded, "A-z_4MHeFg") - - # Decoding should return the original data. - decoded = base64_url_decode(encoded) - self.assertEqual(decoded, data) - - class PKCECodeVerificationTestCase(TestCase): def test_generate_code_challenge(self): """ From 981d295ed96aaec950fa05e7dd2735f76fc18e87 Mon Sep 17 00:00:00 2001 From: Antoine LAURENT Date: Fri, 18 Oct 2024 10:53:04 +0200 Subject: [PATCH 3/3] tests: Also replace josepy with jwcrypto --- setup.py | 1 + tests/test_auth.py | 153 +++++++++++++++------------------------------ 2 files changed, 53 insertions(+), 101 deletions(-) diff --git a/setup.py b/setup.py index a06d9a9b..c060e581 100755 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ "Django >= 3.2", "josepy", "pyjwt", + "jwcrypto", "requests", "cryptography", ] diff --git a/tests/test_auth.py b/tests/test_auth.py index ed857b85..5d93fa25 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -5,6 +5,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, hmac, serialization from cryptography.hazmat.primitives.asymmetric import ec, rsa +from cryptography.hazmat.primitives import hashes, hmac from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation @@ -12,6 +13,7 @@ from django.utils.encoding import force_bytes, smart_str from josepy.b64 import b64encode from josepy.jwa import ES256 +from jwcrypto.common import base64url_encode from mozilla_django_oidc.auth import OIDCAuthenticationBackend, default_username_algo @@ -73,13 +75,10 @@ def test_allowed_unsecured_token(self): header = force_bytes(json.dumps({"alg": "none"})) payload = force_bytes(json.dumps({"foo": "bar"})) signature = "" - token = force_bytes( - "{}.{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)), signature - ) - ) + token = "{}.{}.{}".format(base64url_encode(header), base64url_encode(payload), signature) + token_bytes = force_bytes(token) - extracted_payload = self.backend.get_payload_data(token, None) + extracted_payload = self.backend.get_payload_data(token_bytes, None) self.assertEqual(payload, extracted_payload) @override_settings(OIDC_ALLOW_UNSECURED_JWT=False) @@ -88,14 +87,11 @@ def test_disallowed_unsecured_token(self): header = force_bytes(json.dumps({"alg": "none"})) payload = force_bytes(json.dumps({"foo": "bar"})) signature = "" - token = force_bytes( - "{}.{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)), signature - ) - ) + token = "{}.{}.{}".format(base64url_encode(header), base64url_encode(payload), signature) + token_bytes = force_bytes(token) with self.assertRaises(SuspiciousOperation): - self.backend.get_payload_data(token, None) + self.backend.get_payload_data(token_bytes, None) @override_settings(OIDC_ALLOW_UNSECURED_JWT=True) def test_allowed_unsecured_valid_token(self): @@ -106,17 +102,11 @@ def test_allowed_unsecured_valid_token(self): # Compute signature key = b"mysupersecuretestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) - msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) - ) + msg = "{}.{}".format(base64url_encode(header), base64url_encode(payload)) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) - token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), - smart_str(signature), - ) + token = "{}.{}.{}".format(base64url_encode(header), base64url_encode(payload), signature) token_bytes = force_bytes(token) key_text = smart_str(key) output = self.backend.get_payload_data(token_bytes, key_text) @@ -131,17 +121,11 @@ def test_disallowed_unsecured_valid_token(self): # Compute signature key = b"mysupersecuretestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) - msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) - ) + msg = "{}.{}".format(base64url_encode(header), base64url_encode(payload)) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) - token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), - smart_str(signature), - ) + token = "{}.{}.{}".format(base64url_encode(header), base64url_encode(payload), signature) token_bytes = force_bytes(token) key_text = smart_str(key) output = self.backend.get_payload_data(token_bytes, key_text) @@ -157,17 +141,11 @@ def test_allowed_unsecured_invalid_token(self): key = b"mysupersecuretestkey" fake_key = b"mysupersecurefaketestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) - msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) - ) + msg = "{}.{}".format(base64url_encode(header), base64url_encode(payload)) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) - token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), - smart_str(signature), - ) + token = "{}.{}.{}".format(base64url_encode(header), base64url_encode(payload), signature) token_bytes = force_bytes(token) key_text = smart_str(fake_key) @@ -185,17 +163,11 @@ def test_disallowed_unsecured_invalid_token(self): key = b"mysupersecuretestkey" fake_key = b"mysupersecurefaketestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) - msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) - ) + msg = "{}.{}".format(base64url_encode(header), base64url_encode(payload)) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) - token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), - smart_str(signature), - ) + token = "{}.{}.{}".format(base64url_encode(header), base64url_encode(payload), signature) token_bytes = force_bytes(token) key_text = smart_str(fake_key) @@ -980,14 +952,14 @@ def test_retrieve_matching_jwk(self, mock_requests): key = b"mysupersecuretestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) + smart_str(base64url_encode(header)), smart_str(base64url_encode(payload)) ) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), + smart_str(base64url_encode(header)), + smart_str(base64url_encode(payload)), smart_str(signature), ) @@ -1026,14 +998,14 @@ def test_retrieve_matching_jwk_same_kid(self, mock_requests): key = b"mysupersecuretestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) + smart_str(base64url_encode(header)), smart_str(base64url_encode(payload)) ) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), + smart_str(base64url_encode(header)), + smart_str(base64url_encode(payload)), smart_str(signature), ) @@ -1062,14 +1034,14 @@ def test_retrieve_mismatcing_jwk_alg(self, mock_requests): key = b"mysupersecuretestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) + smart_str(base64url_encode(header)), smart_str(base64url_encode(payload)) ) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), + smart_str(base64url_encode(header)), + smart_str(base64url_encode(payload)), smart_str(signature), ) @@ -1100,14 +1072,14 @@ def test_retrieve_mismatcing_jwk_kid(self, mock_requests): key = b"mysupersecuretestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) + smart_str(base64url_encode(header)), smart_str(base64url_encode(payload)) ) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), + smart_str(base64url_encode(header)), + smart_str(base64url_encode(payload)), smart_str(signature), ) @@ -1137,14 +1109,14 @@ def test_retrieve_jwk_optional_alg(self, mock_requests): key = b"mysupersecuretestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) + smart_str(base64url_encode(header)), smart_str(base64url_encode(payload)) ) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), + smart_str(base64url_encode(header)), + smart_str(base64url_encode(payload)), smart_str(signature), ) @@ -1168,14 +1140,14 @@ def test_retrieve_not_existing_jwk(self, mock_requests): key = b"mysupersecuretestkey" h = hmac.HMAC(key, hashes.SHA256(), backend=default_backend()) msg = "{}.{}".format( - smart_str(b64encode(header)), smart_str(b64encode(payload)) + smart_str(base64url_encode(header)), smart_str(base64url_encode(payload)) ) h.update(force_bytes(msg)) - signature = b64encode(h.finalize()) + signature = base64url_encode(h.finalize()) token = "{}.{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(payload)), + smart_str(base64url_encode(header)), + smart_str(base64url_encode(payload)), smart_str(signature), ) @@ -1249,11 +1221,6 @@ def test_es256_alg_verification(self, mock_requests): # Generate a private key to create a test token with private_key = ec.generate_private_key(ec.SECP256R1, default_backend()) - private_key_pem = private_key.private_bytes( - serialization.Encoding.PEM, - serialization.PrivateFormat.PKCS8, - serialization.NoEncryption(), - ) # Make the public key available through the JWKS response public_numbers = private_key.public_key().public_numbers() @@ -1265,37 +1232,21 @@ def test_es256_alg_verification(self, mock_requests): "kty": "EC", "alg": "ES256", "use": "sig", - "x": smart_str(b64encode(public_numbers.x.to_bytes(32, "big"))), - "y": smart_str(b64encode(public_numbers.y.to_bytes(32, "big"))), + "x": base64url_encode(public_numbers.x.to_bytes(32, "big")), + "y": base64url_encode(public_numbers.y.to_bytes(32, "big")), "crv": "P-256", } ] } mock_requests.get.return_value = get_json_mock - header = force_bytes( - json.dumps( - { - "typ": "JWT", - "alg": "ES256", - "kid": "eckid", - }, - ) - ) + header = { + "typ": "JWT", + "alg": "ES256", + "kid": "eckid", + } data = {"name": "John Doe", "test": "test_es256_alg_verification"} - - h = hmac.HMAC(private_key_pem, hashes.SHA256(), backend=default_backend()) - msg = "{}.{}".format( - smart_str(b64encode(header)), - smart_str(b64encode(force_bytes(json.dumps(data)))), - ) - h.update(force_bytes(msg)) - - signature = b64encode(ES256.sign(private_key, force_bytes(msg))) - token = "{}.{}".format( - msg, - smart_str(signature), - ) + token = jwt.encode(payload=data, key=private_key, algorithm="RS256", headers=header) # Verify the token created with the private key by using the JWKS endpoint, # where the public numbers are.