Fix signature auth on read-only member endpoints (#2044)

This commit is contained in:
Amaury Chamayou 2021-01-06 16:22:56 +00:00 коммит произвёл GitHub
Родитель 30055d4b02
Коммит 24361ea1d4
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
17 изменённых файлов: 200 добавлений и 199 удалений

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

@ -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)