зеркало из https://github.com/microsoft/CCF.git
Fix signature auth on read-only member endpoints (#2044)
This commit is contained in:
Родитель
30055d4b02
Коммит
24361ea1d4
|
@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
### Changed
|
||||
|
||||
- To avoid accidentally unauthenticated endpoints, a vector of authentication policies must now be specified at construction (as a new argument to `make_endpoint`) rather than by calling `add_authentication`. The value `ccf::no_auth_required` must be used to explicitly indicate an unauthenticated endpoint.
|
||||
- All `/gov` endpoints accept signature authentication alone correctly, regardless of session authentication.
|
||||
- `ccf.CCFClient` now allows separate `session_auth` and `signing_auth` to be passed as construction time. `ccf.CCFClient.call()` no longer takes a `signed` argument, clients with a `signing_auth` always sign. Similarly, the `disable_session_auth` constructor argument is removed, the same effect can be achieved by setting `session_auth` to `None`.
|
||||
|
||||
## [0.16.2]
|
||||
|
||||
|
|
|
@ -631,6 +631,14 @@ if(BUILD_TESTS)
|
|||
ADDITIONAL_ARGS --oe-binary ${OE_BINDIR} --initial-operator-count 1
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME governance_no_session_auth_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/governance.py
|
||||
CONSENSUS cft
|
||||
ADDITIONAL_ARGS --oe-binary ${OE_BINDIR} --initial-operator-count 1
|
||||
--disable-member-session-auth
|
||||
)
|
||||
|
||||
add_e2e_test(
|
||||
NAME ca_certs_test
|
||||
PYTHON_SCRIPT ${CMAKE_SOURCE_DIR}/tests/ca_certs.py
|
||||
|
|
|
@ -79,6 +79,16 @@ class Request:
|
|||
return string
|
||||
|
||||
|
||||
@dataclass
|
||||
class Identity:
|
||||
#: Path to file containing private key
|
||||
key: str
|
||||
#: Path to file containing certificate
|
||||
cert: str
|
||||
#: Identity description
|
||||
description: str
|
||||
|
||||
|
||||
def int_or_none(v):
|
||||
return int(v) if v is not None else None
|
||||
|
||||
|
@ -238,15 +248,12 @@ class CurlClient:
|
|||
These commands could also be run manually, or used by another client tool.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, host, port, ca=None, cert=None, key=None, disable_client_auth=False
|
||||
):
|
||||
def __init__(self, host, port, ca=None, session_auth=None, signing_auth=None):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ca = ca
|
||||
self.cert = cert
|
||||
self.key = key
|
||||
self.disable_client_auth = disable_client_auth
|
||||
self.session_auth = session_auth
|
||||
self.signing_auth = signing_auth
|
||||
|
||||
ca_curve = get_curve(self.ca)
|
||||
if ca_curve.name == "secp256k1":
|
||||
|
@ -255,9 +262,9 @@ class CurlClient:
|
|||
"Use RequestClient class instead."
|
||||
)
|
||||
|
||||
def request(self, request, signed=False, timeout=DEFAULT_REQUEST_TIMEOUT_SEC):
|
||||
def request(self, request, timeout=DEFAULT_REQUEST_TIMEOUT_SEC):
|
||||
with tempfile.NamedTemporaryFile() as nf:
|
||||
if signed:
|
||||
if self.signing_auth:
|
||||
cmd = ["scurl.sh"]
|
||||
else:
|
||||
cmd = ["curl"]
|
||||
|
@ -304,16 +311,15 @@ class CurlClient:
|
|||
|
||||
if self.ca:
|
||||
cmd.extend(["--cacert", self.ca])
|
||||
if self.key:
|
||||
cmd.extend(["--key", self.key])
|
||||
if self.cert:
|
||||
cmd.extend(["--cert", self.cert])
|
||||
if self.session_auth:
|
||||
cmd.extend(["--key", self.session_auth.key])
|
||||
cmd.extend(["--cert", self.session_auth.cert])
|
||||
if self.signing_auth:
|
||||
cmd.extend(["--signing-key", self.signing_auth.key])
|
||||
cmd.extend(["--signing-cert", self.signing_auth.cert])
|
||||
|
||||
cmd_s = " ".join(cmd)
|
||||
env = {k: v for k, v in os.environ.items()}
|
||||
if self.disable_client_auth:
|
||||
env["DISABLE_CLIENT_AUTH"] = "1"
|
||||
cmd_s = f"DISABLE_CLIENT_AUTH=1 {cmd_s}"
|
||||
|
||||
LOG.debug(f"Running: {cmd_s}")
|
||||
rc = subprocess.run(cmd, capture_output=True, check=False, env=env)
|
||||
|
@ -375,43 +381,37 @@ class RequestClient:
|
|||
host: str,
|
||||
port: int,
|
||||
ca: str,
|
||||
cert: Optional[str] = None,
|
||||
key: Optional[str] = None,
|
||||
disable_client_auth: bool = False,
|
||||
key_id: Optional[int] = False,
|
||||
session_auth: Optional[Identity] = None,
|
||||
signing_auth: Optional[Identity] = None,
|
||||
):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ca = ca
|
||||
self.cert = cert
|
||||
self.key = key
|
||||
self.session_auth = session_auth
|
||||
self.signing_auth = signing_auth
|
||||
self.key_id = None
|
||||
self.session = requests.Session()
|
||||
self.session.verify = self.ca
|
||||
if (self.cert is not None and self.key is not None) and not disable_client_auth:
|
||||
self.session.cert = (self.cert, self.key)
|
||||
if self.cert:
|
||||
with open(self.cert) as cert_file:
|
||||
if self.session_auth:
|
||||
self.session.cert = (self.session_auth.cert, self.session_auth.key)
|
||||
if self.signing_auth:
|
||||
with open(self.signing_auth.cert) as cert_file:
|
||||
self.key_id = hashlib.sha256(cert_file.read().encode()).hexdigest()
|
||||
self.session.mount("https://", TlsAdapter(self.ca))
|
||||
|
||||
def request(
|
||||
self,
|
||||
request: Request,
|
||||
signed: bool = False,
|
||||
timeout: int = DEFAULT_REQUEST_TIMEOUT_SEC,
|
||||
):
|
||||
extra_headers = {}
|
||||
extra_headers.update(request.headers)
|
||||
|
||||
auth_value = None
|
||||
if signed:
|
||||
if self.key is None:
|
||||
raise ValueError("Cannot sign request if client has no key")
|
||||
|
||||
if self.signing_auth is not None:
|
||||
auth_value = HTTPSignatureAuth_AlwaysDigest(
|
||||
algorithm="ecdsa-sha256",
|
||||
key=open(self.key, "rb").read(),
|
||||
key=open(self.signing_auth.key, "rb").read(),
|
||||
key_id=self.key_id,
|
||||
headers=["(request-target)", "Digest", "Content-Length"],
|
||||
)
|
||||
|
@ -471,17 +471,16 @@ class WSClient:
|
|||
host: str,
|
||||
port: int,
|
||||
ca: str,
|
||||
cert: Optional[str] = None,
|
||||
key: Optional[str] = None,
|
||||
disable_client_auth: bool = False,
|
||||
session_auth: Optional[Identity] = None,
|
||||
signing_auth: Optional[Identity] = None,
|
||||
):
|
||||
assert signing_auth is None, "WSClient does not support signing requests"
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.ca = ca
|
||||
self.cert = cert
|
||||
self.key = key
|
||||
self.session_auth = session_auth
|
||||
self.ws = None
|
||||
self.disable_client_auth = disable_client_auth
|
||||
|
||||
ca_curve = get_curve(self.ca)
|
||||
if ca_curve.name == "secp256k1":
|
||||
|
@ -493,24 +492,19 @@ class WSClient:
|
|||
def request(
|
||||
self,
|
||||
request: Request,
|
||||
signed: bool = False,
|
||||
timeout: int = DEFAULT_REQUEST_TIMEOUT_SEC,
|
||||
):
|
||||
if signed:
|
||||
raise ValueError(
|
||||
"Client signatures over WebSocket are not supported by CCF"
|
||||
)
|
||||
|
||||
if self.ws is None:
|
||||
LOG.info("Creating WSS connection")
|
||||
try:
|
||||
sslopt = {"ca_certs": self.ca}
|
||||
if self.session_auth:
|
||||
sslopt["certfile"] = self.session_auth.cert
|
||||
sslopt["keyfile"] = self.session_auth.key
|
||||
self.ws = websocket.create_connection(
|
||||
f"wss://{self.host}:{self.port}",
|
||||
sslopt={
|
||||
"certfile": self.cert if not self.disable_client_auth else None,
|
||||
"keyfile": self.key if not self.disable_client_auth else None,
|
||||
"ca_certs": self.ca,
|
||||
},
|
||||
sslopt=sslopt,
|
||||
timeout=timeout,
|
||||
)
|
||||
except Exception as exc:
|
||||
|
@ -555,12 +549,11 @@ class CCFClient:
|
|||
:param str host: RPC IP address or domain name of node to connect to.
|
||||
:param int port: RPC port number of node to connect to.
|
||||
:param str ca: Path to CCF network certificate.
|
||||
:param str cert: Path to client certificate (optional).
|
||||
:param str key: Path to client private key (optional).
|
||||
:param Identity session_auth: Path to private key and certificate to be used as client authentication for the session (optional).
|
||||
:param Identity signing_auth: Path to private key and certificate to be used to sign requests for the session (optional).
|
||||
:param int connection_timeout: Maximum time to wait for successful connection establishment before giving up.
|
||||
:param str description: Message to print on each request emitted with this client.
|
||||
:param bool ws: Use WebSocket client (experimental).
|
||||
:param bool disable_client_auth: Do not use the provided client identity to authenticate. The client identity will still be used to sign if ``signed`` is set to True when making requests.
|
||||
|
||||
A :py:exc:`CCFConnectionException` exception is raised if the connection is not established successfully within ``connection_timeout`` seconds.
|
||||
"""
|
||||
|
@ -572,28 +565,25 @@ class CCFClient:
|
|||
host: str,
|
||||
port: int,
|
||||
ca: str,
|
||||
cert: Optional[str] = None,
|
||||
key: Optional[str] = None,
|
||||
session_auth: Optional[Identity] = None,
|
||||
signing_auth: Optional[Identity] = None,
|
||||
connection_timeout: int = DEFAULT_CONNECTION_TIMEOUT_SEC,
|
||||
description: Optional[str] = None,
|
||||
ws: bool = False,
|
||||
disable_client_auth: bool = False,
|
||||
):
|
||||
self.connection_timeout = connection_timeout
|
||||
self.description = description
|
||||
self.name = f"[{host}:{port}]"
|
||||
self.description = description or self.name
|
||||
self.is_connected = False
|
||||
self.auth = bool(session_auth)
|
||||
self.sign = bool(signing_auth)
|
||||
|
||||
if os.getenv("CURL_CLIENT"):
|
||||
self.client_impl = CurlClient(
|
||||
host, port, ca, cert, key, disable_client_auth
|
||||
)
|
||||
self.client_impl = CurlClient(host, port, ca, session_auth, signing_auth)
|
||||
elif os.getenv("WEBSOCKETS_CLIENT") or ws:
|
||||
self.client_impl = WSClient(host, port, ca, cert, key, disable_client_auth)
|
||||
self.client_impl = WSClient(host, port, ca, session_auth, signing_auth)
|
||||
else:
|
||||
self.client_impl = RequestClient(
|
||||
host, port, ca, cert, key, disable_client_auth
|
||||
)
|
||||
self.client_impl = RequestClient(host, port, ca, session_auth, signing_auth)
|
||||
|
||||
def _response(self, response: Response) -> Response:
|
||||
LOG.info(response)
|
||||
|
@ -605,22 +595,15 @@ class CCFClient:
|
|||
body: Optional[Union[str, dict, bytes]] = None,
|
||||
http_verb: str = "POST",
|
||||
headers: Optional[dict] = None,
|
||||
signed: bool = False,
|
||||
timeout: int = DEFAULT_REQUEST_TIMEOUT_SEC,
|
||||
log_capture: Optional[list] = None,
|
||||
) -> Response:
|
||||
description = ""
|
||||
if self.description:
|
||||
description = f"{self.description}{signed * 's'}"
|
||||
else:
|
||||
description = self.name
|
||||
|
||||
if headers is None:
|
||||
headers = {}
|
||||
r = Request(path, body, http_verb, headers)
|
||||
|
||||
flush_info([f"{description} {r}"], log_capture, 3)
|
||||
response = self.client_impl.request(r, signed, timeout)
|
||||
flush_info([f"{self.description} {r}"], log_capture, 3)
|
||||
response = self.client_impl.request(r, timeout)
|
||||
flush_info([str(response)], log_capture, 3)
|
||||
return response
|
||||
|
||||
|
@ -630,7 +613,6 @@ class CCFClient:
|
|||
body: Optional[Union[str, dict, bytes]] = None,
|
||||
http_verb: str = "POST",
|
||||
headers: Optional[dict] = None,
|
||||
signed: bool = False,
|
||||
timeout: int = DEFAULT_REQUEST_TIMEOUT_SEC,
|
||||
log_capture: Optional[list] = None,
|
||||
) -> Response:
|
||||
|
@ -642,7 +624,6 @@ class CCFClient:
|
|||
:type body: str or dict or bytes
|
||||
:param str http_verb: HTTP verb (e.g. "POST" or "GET").
|
||||
:param dict headers: HTTP request headers (optional).
|
||||
:param bool signed: Sign request with client private key.
|
||||
:param int timeout: Maximum time to wait for a response before giving up.
|
||||
:param list log_capture: Rather than emit to default handler, capture log lines to list (optional).
|
||||
|
||||
|
@ -654,7 +635,7 @@ class CCFClient:
|
|||
logs: List[str] = []
|
||||
|
||||
if self.is_connected:
|
||||
r = self._call(path, body, http_verb, headers, signed, timeout, logs)
|
||||
r = self._call(path, body, http_verb, headers, timeout, logs)
|
||||
flush_info(logs, log_capture, 2)
|
||||
return r
|
||||
|
||||
|
@ -662,9 +643,7 @@ class CCFClient:
|
|||
while True:
|
||||
try:
|
||||
logs = []
|
||||
response = self._call(
|
||||
path, body, http_verb, headers, signed, timeout, logs
|
||||
)
|
||||
response = self._call(path, body, http_verb, headers, timeout, logs)
|
||||
# Only the first request gets this timeout logic - future calls
|
||||
# call _call
|
||||
self.is_connected = True
|
||||
|
|
|
@ -44,7 +44,9 @@ assert r.status_code == http.HTTPStatus.OK
|
|||
# SNIPPET_END: anonymous_requests
|
||||
|
||||
# SNIPPET: authenticated_client
|
||||
user_client = ccf.clients.CCFClient(host, port, ca, cert, key)
|
||||
user_client = ccf.clients.CCFClient(
|
||||
host, port, ca, ccf.clients.Identity(key, cert, "client")
|
||||
)
|
||||
|
||||
# SNIPPET_START: authenticated_post_requests
|
||||
r = user_client.post("/app/log/private", body={"id": 0, "msg": "Private message"})
|
||||
|
@ -99,11 +101,16 @@ proposal, vote = ccf.proposal_generator.open_network()
|
|||
# >>> proposal
|
||||
# {'script': {'text': 'return Calls:call("open_network")'}}
|
||||
|
||||
member_client = ccf.clients.CCFClient(host, port, ca, member_cert, member_key)
|
||||
member_client = ccf.clients.CCFClient(
|
||||
host,
|
||||
port,
|
||||
ca,
|
||||
session_auth=ccf.clients.Identity(member_key, member_cert, "member"),
|
||||
signing_auth=ccf.clients.Identity(member_key, member_cert, "member"),
|
||||
)
|
||||
response = member_client.post(
|
||||
"/gov/proposals",
|
||||
body=proposal,
|
||||
signed=True,
|
||||
)
|
||||
# SNIPPET_END: dict_proposal
|
||||
|
||||
|
@ -115,6 +122,5 @@ with open("my_open_network_proposal.json", "w") as f:
|
|||
response = member_client.post(
|
||||
"/gov/proposals",
|
||||
body="@my_open_network_proposal.json",
|
||||
signed=True,
|
||||
)
|
||||
# SNIPPET_END: json_proposal_with_file
|
||||
|
|
|
@ -11,6 +11,8 @@ next_is_data=false
|
|||
next_is_command=false
|
||||
next_is_privk=false
|
||||
next_is_cert=false
|
||||
next_is_signing_privk=false
|
||||
next_is_signing_cert=false
|
||||
|
||||
url=$1
|
||||
command="post"
|
||||
|
@ -31,18 +33,20 @@ for item in "$@" ; do
|
|||
next_is_command=false
|
||||
fi
|
||||
if [ "$next_is_privk" == true ]; then
|
||||
privk=$item
|
||||
next_is_privk=false
|
||||
if [ -n "$DISABLE_CLIENT_AUTH" ]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
if [ "$next_is_cert" == true ]; then
|
||||
cert=$item
|
||||
next_is_cert=false
|
||||
if [ -n "$DISABLE_CLIENT_AUTH" ]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
if [ "$next_is_signing_privk" == true ]; then
|
||||
signing_privk=$item
|
||||
next_is_signing_privk=false
|
||||
continue
|
||||
fi
|
||||
if [ "$next_is_signing_cert" == true ]; then
|
||||
signing_cert=$item
|
||||
next_is_signing_privk=false
|
||||
continue
|
||||
fi
|
||||
if [ "$item" == "--url" ]; then
|
||||
next_is_url=true
|
||||
|
@ -55,15 +59,17 @@ for item in "$@" ; do
|
|||
fi
|
||||
if [ "$item" == "--key" ]; then
|
||||
next_is_privk=true
|
||||
if [ -n "$DISABLE_CLIENT_AUTH" ]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
if [ "$item" == "--cert" ]; then
|
||||
next_is_cert=true
|
||||
if [ -n "$DISABLE_CLIENT_AUTH" ]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
if [ "$item" == "--signing-key" ]; then
|
||||
next_is_signing_privk=true
|
||||
continue
|
||||
fi
|
||||
if [ "$item" == "--signing-cert" ]; then
|
||||
next_is_signing_cert=true
|
||||
continue
|
||||
fi
|
||||
if [ "$item" == "--print-digest-to-sign" ]; then
|
||||
is_print_digest_to_sign=true
|
||||
|
@ -72,12 +78,12 @@ for item in "$@" ; do
|
|||
fwd_args+=("$item")
|
||||
done
|
||||
|
||||
if [ -z "$cert" ]; then
|
||||
echo "Error: No certificate found in arguments (--cert)"
|
||||
if [ -z "$signing_cert" ]; then
|
||||
echo "Error: No signing certificate found in arguments (--signing-cert)"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$privk" ] && [ "$is_print_digest_to_sign" == false ]; then
|
||||
echo "Error: No private key found in arguments (--key)"
|
||||
if [ -z "$signing_privk" ] && [ "$is_print_digest_to_sign" == false ]; then
|
||||
echo "Error: No signing private key found in arguments (--signing-key)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
@ -114,7 +120,7 @@ content-length: $content_length"
|
|||
signature_algorithm="hs2019"
|
||||
|
||||
# Compute key ID
|
||||
key_id=$(openssl dgst -sha256 "$cert" | cut -d ' ' -f 2)
|
||||
key_id=$(openssl dgst -sha256 "$signing_cert" | cut -d ' ' -f 2)
|
||||
|
||||
if [ "$is_print_digest_to_sign" == true ]; then
|
||||
hash_to_sign=$(echo -n "$string_to_sign" | openssl dgst -binary -sha384 | openssl base64 -A)
|
||||
|
@ -127,7 +133,7 @@ if [ "$is_print_digest_to_sign" == true ]; then
|
|||
fi
|
||||
|
||||
# Compute signature
|
||||
signed_raw=$(echo -n "$string_to_sign" | openssl dgst -sha384 -sign "$privk" | openssl base64 -A)
|
||||
signed_raw=$(echo -n "$string_to_sign" | openssl dgst -sha384 -sign "$signing_privk" | openssl base64 -A)
|
||||
|
||||
curl \
|
||||
-H "Digest: SHA-256=$req_digest" \
|
||||
|
|
|
@ -604,14 +604,6 @@ namespace ccf
|
|||
*/
|
||||
void install(Endpoint& endpoint)
|
||||
{
|
||||
if (endpoint.authn_policies.empty())
|
||||
{
|
||||
LOG_FAIL_FMT(
|
||||
"Endpoint {} /{} does not have any authentication policy",
|
||||
endpoint.dispatch.verb.c_str(),
|
||||
endpoint.dispatch.uri_path);
|
||||
}
|
||||
|
||||
// A single empty auth policy is semantically equivalent to no policy, but
|
||||
// no policy is faster
|
||||
if (
|
||||
|
|
|
@ -1549,9 +1549,9 @@ namespace ccf
|
|||
HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
|
||||
}
|
||||
|
||||
MemberId member_id;
|
||||
MemberId vote_member_id;
|
||||
if (!get_member_id_from_path(
|
||||
ctx.rpc_ctx->get_request_path_params(), member_id, error))
|
||||
ctx.rpc_ctx->get_request_path_params(), vote_member_id, error))
|
||||
{
|
||||
return make_error(
|
||||
HTTP_STATUS_BAD_REQUEST, ccf::errors::InvalidResourceName, error);
|
||||
|
@ -1567,7 +1567,7 @@ namespace ccf
|
|||
fmt::format("Proposal {} does not exist.", proposal_id));
|
||||
}
|
||||
|
||||
const auto vote_it = proposal->votes.find(member_id);
|
||||
const auto vote_it = proposal->votes.find(vote_member_id);
|
||||
if (vote_it == proposal->votes.end())
|
||||
{
|
||||
return make_error(
|
||||
|
@ -1575,7 +1575,7 @@ namespace ccf
|
|||
ccf::errors::VoteNotFound,
|
||||
fmt::format(
|
||||
"Member {} has not voted for proposal {}.",
|
||||
member_id,
|
||||
vote_member_id,
|
||||
proposal_id));
|
||||
}
|
||||
|
||||
|
|
|
@ -178,11 +178,11 @@ def test_anonymous_caller(network, args):
|
|||
primary, _ = network.find_primary()
|
||||
|
||||
# Create a new user but do not record its identity
|
||||
network.create_user(4, args.participants_curve, record=False)
|
||||
network.create_user(5, args.participants_curve, record=False)
|
||||
|
||||
log_id = 101
|
||||
msg = "This message is anonymous"
|
||||
with primary.client("user4") as c:
|
||||
with primary.client("user5") as c:
|
||||
r = c.post("/app/log/private/anonymous", {"id": log_id, "msg": msg})
|
||||
assert r.body.json() == True
|
||||
r = c.get(f"/app/log/private?id={log_id}")
|
||||
|
@ -240,13 +240,25 @@ def test_multi_auth(network, args):
|
|||
require_new_response(r)
|
||||
|
||||
LOG.info("Authenticate as a user, via HTTP signature")
|
||||
with primary.client("user0", disable_client_auth=True) as c:
|
||||
r = c.get("/app/multi_auth", signed=True)
|
||||
with primary.client(None, "user0") as c:
|
||||
r = c.get("/app/multi_auth")
|
||||
require_new_response(r)
|
||||
|
||||
LOG.info("Authenticate as a member, via HTTP signature")
|
||||
with primary.client("member0", disable_client_auth=True) as c:
|
||||
r = c.get("/app/multi_auth", signed=True)
|
||||
with primary.client(None, "member0") as c:
|
||||
r = c.get("/app/multi_auth")
|
||||
require_new_response(r)
|
||||
|
||||
LOG.info("Authenticate as user2 but sign as user1")
|
||||
with primary.client("user2", "user1") as c:
|
||||
r = c.get("/app/multi_auth")
|
||||
require_new_response(r)
|
||||
|
||||
network.create_user(5, args.participants_curve, record=False)
|
||||
|
||||
LOG.info("Authenticate as invalid user5 but sign as valid user3")
|
||||
with primary.client("user5", "user3") as c:
|
||||
r = c.get("/app/multi_auth")
|
||||
require_new_response(r)
|
||||
|
||||
LOG.info("Authenticate via JWT token")
|
||||
|
@ -656,6 +668,6 @@ if __name__ == "__main__":
|
|||
else:
|
||||
args.package = "liblogging"
|
||||
args.nodes = infra.e2e_args.max_nodes(args, f=0)
|
||||
args.initial_user_count = 2
|
||||
args.initial_user_count = 4
|
||||
args.initial_member_count = 2
|
||||
run(args)
|
||||
|
|
|
@ -9,7 +9,6 @@ import infra.path
|
|||
import infra.proc
|
||||
import infra.net
|
||||
import infra.e2e_args
|
||||
import infra.proposal
|
||||
import suite.test_requirements as reqs
|
||||
import infra.logging_app as app
|
||||
import ssl
|
||||
|
@ -20,8 +19,6 @@ from cryptography.hazmat.backends import default_backend
|
|||
from cryptography.hazmat.primitives import serialization
|
||||
from loguru import logger as LOG
|
||||
|
||||
import ccf
|
||||
|
||||
|
||||
@reqs.description("Test quotes")
|
||||
@reqs.supports_methods("quote", "quotes")
|
||||
|
@ -171,28 +168,6 @@ def test_user_id(network, args):
|
|||
return network
|
||||
|
||||
|
||||
@reqs.description("Test signed proposal over unauthenticated connection")
|
||||
def test_proposal_over_unauthenticated_connection(network, args):
|
||||
primary, backups = network.find_nodes()
|
||||
proposing_member = network.consortium.get_any_active_member()
|
||||
user_id = 0
|
||||
|
||||
proposal_body, _ = ccf.proposal_generator.set_user_data(
|
||||
user_id,
|
||||
{"property": "value"},
|
||||
)
|
||||
proposal = proposing_member.propose(
|
||||
primary, proposal_body, disable_client_auth=True
|
||||
)
|
||||
assert proposal.state == infra.proposal.ProposalState.Open
|
||||
|
||||
proposal = proposing_member.propose(
|
||||
backups[0], proposal_body, disable_client_auth=True
|
||||
)
|
||||
assert proposal.state == infra.proposal.ProposalState.Open
|
||||
return network
|
||||
|
||||
|
||||
@reqs.description("Check node/ids endpoint")
|
||||
def test_node_ids(network, args):
|
||||
nodes = network.find_nodes()
|
||||
|
@ -217,7 +192,6 @@ def run(args):
|
|||
network = test_user(network, args)
|
||||
network = test_no_quote(network, args)
|
||||
network = test_user_id(network, args)
|
||||
network = test_proposal_over_unauthenticated_connection(network, args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -27,6 +27,7 @@ class Consortium:
|
|||
member_ids=None,
|
||||
curve=None,
|
||||
remote_node=None,
|
||||
authenticate_session=True,
|
||||
):
|
||||
self.common_dir = common_dir
|
||||
self.members = []
|
||||
|
@ -34,6 +35,7 @@ class Consortium:
|
|||
self.share_script = share_script
|
||||
self.members = []
|
||||
self.recovery_threshold = None
|
||||
self.authenticate_session = authenticate_session
|
||||
# If a list of member IDs is passed in, generate fresh member identities.
|
||||
# Otherwise, recover the state of the consortium from the state of CCF.
|
||||
if member_ids is not None:
|
||||
|
@ -47,6 +49,7 @@ class Consortium:
|
|||
has_share,
|
||||
key_generator,
|
||||
m_data,
|
||||
authenticate_session=authenticate_session,
|
||||
)
|
||||
if has_share:
|
||||
self.recovery_threshold += 1
|
||||
|
@ -74,6 +77,7 @@ class Consortium:
|
|||
self.common_dir,
|
||||
share_script,
|
||||
is_recovery_member="encryption_pub_key" in info,
|
||||
authenticate_session=authenticate_session,
|
||||
)
|
||||
status = info["status"]
|
||||
if (
|
||||
|
@ -96,6 +100,11 @@ class Consortium:
|
|||
)
|
||||
self.recovery_threshold = r.body.json()["recovery_threshold"]
|
||||
|
||||
def set_authenticate_session(self, flag):
|
||||
self.authenticate_session = flag
|
||||
for member in self.members:
|
||||
member.authenticate_session = flag
|
||||
|
||||
def make_proposal(self, proposal_name, *args, **kwargs):
|
||||
func = getattr(ccf.proposal_generator, proposal_name)
|
||||
proposal, vote = func(*args, **kwargs)
|
||||
|
@ -136,6 +145,7 @@ class Consortium:
|
|||
self.share_script,
|
||||
is_recovery_member=recovery_member,
|
||||
key_generator=self.key_generator,
|
||||
authenticate_session=self.authenticate_session,
|
||||
)
|
||||
|
||||
proposal_body, careful_vote = self.make_proposal(
|
||||
|
@ -252,7 +262,8 @@ class Consortium:
|
|||
"""
|
||||
|
||||
proposals = []
|
||||
with remote_node.client(f"member{self.get_any_active_member().member_id}") as c:
|
||||
member = self.get_any_active_member()
|
||||
with remote_node.client(*member.auth()) as c:
|
||||
r = c.post("/gov/query", {"text": script})
|
||||
assert r.status_code == http.HTTPStatus.OK.value
|
||||
for proposal_id, attr in r.body.json().items():
|
||||
|
@ -272,7 +283,8 @@ class Consortium:
|
|||
proposal = self.get_any_active_member().propose(remote_node, proposal_body)
|
||||
self.vote_using_majority(remote_node, proposal, careful_vote)
|
||||
|
||||
with remote_node.client(f"member{self.get_any_active_member().member_id}") as c:
|
||||
member = self.get_any_active_member()
|
||||
with remote_node.client(*member.auth(write=True)) as c:
|
||||
r = c.post(
|
||||
"/gov/read",
|
||||
{"table": "public:ccf.gov.nodes", "key": node_to_retire.node_id},
|
||||
|
@ -449,7 +461,8 @@ class Consortium:
|
|||
"""
|
||||
# When opening the service in BFT, the first transaction to be
|
||||
# completed when f = 1 takes a significant amount of time
|
||||
with remote_node.client(f"member{self.get_any_active_member().member_id}") as c:
|
||||
member = self.get_any_active_member()
|
||||
with remote_node.client(*member.auth()) as c:
|
||||
r = c.post(
|
||||
"/gov/query",
|
||||
{
|
||||
|
@ -488,7 +501,8 @@ class Consortium:
|
|||
), f"Service status {current_status} (expected {status.name})"
|
||||
|
||||
def _check_node_exists(self, remote_node, node_id, node_status=None):
|
||||
with remote_node.client(f"member{self.get_any_active_member().member_id}") as c:
|
||||
member = self.get_any_active_member()
|
||||
with remote_node.client(*member.auth()) as c:
|
||||
r = c.post("/gov/read", {"table": "public:ccf.gov.nodes", "key": node_id})
|
||||
|
||||
if r.status_code != http.HTTPStatus.OK.value or (
|
||||
|
|
|
@ -255,7 +255,12 @@ def cli_args(add=lambda x: None, parser=None, accept_unknown=False):
|
|||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--long-tests", help="Enable extended tests", action="store_true", default=False
|
||||
"--long-tests", help="Enable extended tests", action="store_true"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--disable-member-session-auth",
|
||||
help="Disable session auth for members",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
add(parser)
|
||||
|
|
|
@ -41,6 +41,7 @@ class Member:
|
|||
is_recovery_member=True,
|
||||
key_generator=None,
|
||||
member_data=None,
|
||||
authenticate_session=True,
|
||||
):
|
||||
self.common_dir = common_dir
|
||||
self.member_id = member_id
|
||||
|
@ -48,6 +49,7 @@ class Member:
|
|||
self.share_script = share_script
|
||||
self.member_data = member_data
|
||||
self.is_recovery_member = is_recovery_member
|
||||
self.authenticate_session = authenticate_session
|
||||
|
||||
self.member_info = MemberInfo(
|
||||
f"member{self.member_id}_cert.pem",
|
||||
|
@ -93,6 +95,15 @@ class Member:
|
|||
) as md:
|
||||
json.dump(member_data, md)
|
||||
|
||||
def auth(self, write=False):
|
||||
if self.authenticate_session:
|
||||
if write:
|
||||
return (f"member{self.member_id}", f"member{self.member_id}")
|
||||
else:
|
||||
return (f"member{self.member_id}", None)
|
||||
else:
|
||||
return (None, f"member{self.member_id}")
|
||||
|
||||
def is_active(self):
|
||||
return self.status_code == MemberStatus.ACTIVE
|
||||
|
||||
|
@ -100,15 +111,9 @@ class Member:
|
|||
# Use this with caution (i.e. only when the network is opening)
|
||||
self.status_code = MemberStatus.ACTIVE
|
||||
|
||||
def propose(self, remote_node, proposal, disable_client_auth=False):
|
||||
with remote_node.client(
|
||||
f"member{self.member_id}", disable_client_auth=disable_client_auth
|
||||
) as mc:
|
||||
r = mc.post(
|
||||
"/gov/proposals",
|
||||
proposal,
|
||||
signed=True,
|
||||
)
|
||||
def propose(self, remote_node, proposal):
|
||||
with remote_node.client(*self.auth(write=True)) as mc:
|
||||
r = mc.post("/gov/proposals", proposal)
|
||||
if r.status_code != http.HTTPStatus.OK.value:
|
||||
raise infra.proposal.ProposalNotCreated(r)
|
||||
|
||||
|
@ -121,36 +126,28 @@ class Member:
|
|||
)
|
||||
|
||||
def vote(self, remote_node, proposal, ballot):
|
||||
with remote_node.client(f"member{self.member_id}") as mc:
|
||||
r = mc.post(
|
||||
f"/gov/proposals/{proposal.proposal_id}/votes",
|
||||
body=ballot,
|
||||
signed=True,
|
||||
)
|
||||
with remote_node.client(*self.auth(write=True)) as mc:
|
||||
r = mc.post(f"/gov/proposals/{proposal.proposal_id}/votes", body=ballot)
|
||||
|
||||
return r
|
||||
|
||||
def withdraw(self, remote_node, proposal):
|
||||
with remote_node.client(f"member{self.member_id}") as c:
|
||||
r = c.post(f"/gov/proposals/{proposal.proposal_id}/withdraw", signed=True)
|
||||
with remote_node.client(*self.auth(write=True)) as c:
|
||||
r = c.post(f"/gov/proposals/{proposal.proposal_id}/withdraw")
|
||||
if r.status_code == http.HTTPStatus.OK.value:
|
||||
proposal.state = infra.proposal.ProposalState.Withdrawn
|
||||
return r
|
||||
|
||||
def update_ack_state_digest(self, remote_node):
|
||||
with remote_node.client(f"member{self.member_id}") as mc:
|
||||
with remote_node.client(*self.auth()) as mc:
|
||||
r = mc.post("/gov/ack/update_state_digest")
|
||||
assert r.status_code == 200, f"Error ack/update_state_digest: {r}"
|
||||
return r.body.json()
|
||||
|
||||
def ack(self, remote_node):
|
||||
state_digest = self.update_ack_state_digest(remote_node)
|
||||
with remote_node.client(f"member{self.member_id}") as mc:
|
||||
r = mc.post(
|
||||
"/gov/ack",
|
||||
body=state_digest,
|
||||
signed=True,
|
||||
)
|
||||
with remote_node.client(*self.auth(write=True)) as mc:
|
||||
r = mc.post("/gov/ack", body=state_digest)
|
||||
assert r.status_code == 200, f"Error ACK: {r}"
|
||||
self.status_code = MemberStatus.ACTIVE
|
||||
return r
|
||||
|
@ -159,7 +156,7 @@ class Member:
|
|||
if not self.is_recovery_member:
|
||||
raise ValueError(f"Member {self.member_id} does not have a recovery share")
|
||||
|
||||
with remote_node.client(f"member{self.member_id}") as mc:
|
||||
with remote_node.client(*self.auth()) as mc:
|
||||
r = mc.get("/gov/recovery_share")
|
||||
if r.status_code != http.HTTPStatus.OK.value:
|
||||
raise NoRecoveryShareFound(r)
|
||||
|
|
|
@ -382,6 +382,7 @@ class Network:
|
|||
self.share_script,
|
||||
initial_members_info,
|
||||
args.participants_curve,
|
||||
authenticate_session=not args.disable_member_session_auth,
|
||||
)
|
||||
initial_users = list(range(max(0, args.initial_user_count)))
|
||||
self.create_users(initial_users, args.participants_curve)
|
||||
|
@ -802,10 +803,10 @@ class Network:
|
|||
if current_commit_seqno >= seqno:
|
||||
with node.client(
|
||||
f"member{self.consortium.get_any_active_member().member_id}"
|
||||
) as c:
|
||||
) as nc:
|
||||
# Using update_state_digest here as a convenient write tx
|
||||
# that is app agnostic
|
||||
r = c.post("/gov/ack/update_state_digest")
|
||||
r = nc.post("/gov/ack/update_state_digest")
|
||||
assert (
|
||||
r.status_code == 200
|
||||
), f"Error ack/update_state_digest: {r}"
|
||||
|
|
|
@ -305,24 +305,31 @@ class Node:
|
|||
def get_committed_snapshots(self, pre_condition_func=lambda src_dir, _: True):
|
||||
return self.remote.get_committed_snapshots(pre_condition_func)
|
||||
|
||||
def client_certs(self, identity=None):
|
||||
def identity(self, name=None):
|
||||
if name is not None:
|
||||
return ccf.clients.Identity(
|
||||
os.path.join(self.common_dir, f"{name}_privk.pem"),
|
||||
os.path.join(self.common_dir, f"{name}_cert.pem"),
|
||||
name,
|
||||
)
|
||||
|
||||
def session_auth(self, name=None):
|
||||
return {
|
||||
"cert": os.path.join(self.common_dir, f"{identity}_cert.pem")
|
||||
if identity
|
||||
else None,
|
||||
"key": os.path.join(self.common_dir, f"{identity}_privk.pem")
|
||||
if identity
|
||||
else None,
|
||||
"session_auth": self.identity(name),
|
||||
"ca": os.path.join(self.common_dir, "networkcert.pem"),
|
||||
}
|
||||
|
||||
def client(self, identity=None, **kwargs):
|
||||
akwargs = self.client_certs(identity)
|
||||
akwargs.update(
|
||||
{
|
||||
"description": f"[{self.node_id}{'|' + identity if identity is not None else ''}]",
|
||||
}
|
||||
)
|
||||
def signing_auth(self, name=None):
|
||||
return {
|
||||
"signing_auth": self.identity(name),
|
||||
}
|
||||
|
||||
def client(self, identity=None, signing_identity=None, **kwargs):
|
||||
akwargs = self.session_auth(identity)
|
||||
akwargs.update(self.signing_auth(signing_identity))
|
||||
akwargs[
|
||||
"description"
|
||||
] = f"[{self.node_id}|{identity or ''}|{signing_identity or ''}]"
|
||||
akwargs.update(kwargs)
|
||||
return ccf.clients.client(self.pubhost, self.pubport, **akwargs)
|
||||
|
||||
|
|
|
@ -14,9 +14,7 @@ def test_custom_auth(network, args):
|
|||
primary, _ = network.find_nodes()
|
||||
|
||||
with primary.client("user0") as c:
|
||||
r = c.get(
|
||||
"/app/custom_auth", headers={"Authorization": "Bearer 42"}, signed=False
|
||||
)
|
||||
r = c.get("/app/custom_auth", headers={"Authorization": "Bearer 42"})
|
||||
assert r.status_code == http.HTTPStatus.OK, r.status_code
|
||||
assert r.body.json()
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ def test_missing_signature(network, args):
|
|||
primary, _ = network.find_primary()
|
||||
member = network.consortium.get_any_active_member()
|
||||
with primary.client(f"member{member.member_id}") as mc:
|
||||
r = mc.post("/gov/proposals", signed=False)
|
||||
r = mc.post("/gov/proposals")
|
||||
assert r.status_code == http.HTTPStatus.UNAUTHORIZED, r.status_code
|
||||
www_auth = "www-authenticate"
|
||||
assert www_auth in r.headers, r.headers
|
||||
|
|
|
@ -67,10 +67,10 @@ def run(args, additional_attack_args):
|
|||
attack_cmd += ["--targets", vegeta_targets]
|
||||
attack_cmd += ["--format", "json"]
|
||||
attack_cmd += ["--duration", "10s"]
|
||||
certs = primary.client_certs("user0")
|
||||
attack_cmd += ["--cert", certs["cert"]]
|
||||
attack_cmd += ["--key", certs["key"]]
|
||||
attack_cmd += ["--root-certs", certs["ca"]]
|
||||
sa = primary.session_auth("user0")
|
||||
attack_cmd += ["--cert", sa["session_auth"].cert]
|
||||
attack_cmd += ["--key", sa["session_auth"].key]
|
||||
attack_cmd += ["--root-certs", sa["ca"]]
|
||||
attack_cmd += additional_attack_args
|
||||
|
||||
attack_cmd_s = " ".join(attack_cmd)
|
||||
|
|
Загрузка…
Ссылка в новой задаче