From 75732648c15c2ed5349634cac9198929996d3816 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Wed, 6 Nov 2024 15:08:37 +0000 Subject: [PATCH] Any cert authn policy (#6608) Co-authored-by: Eddy Ashton --- CHANGELOG.md | 8 +++++ doc/build_apps/api.rst | 7 ++++ doc/build_apps/js_app_bundle.rst | 1 + include/ccf/common_auth_policies.h | 5 +++ include/ccf/endpoint.h | 1 + .../ccf/endpoints/authentication/cert_auth.h | 34 +++++++++++++++++++ include/ccf/endpoints/authentication/js.h | 8 +++++ js/ccf-app/src/endpoints.ts | 20 +++++++---- python/pyproject.toml | 2 +- samples/apps/logging/js/app.json | 1 + samples/apps/logging/js/src/logging.js | 6 ++++ samples/apps/logging/logging.cpp | 12 +++++++ src/endpoints/authentication/cert_auth.cpp | 30 ++++++++++++++++ src/js/extensions/ccf/request.cpp | 16 +++++++++ tests/e2e_logging.py | 7 ++++ tests/npm-app/app.json | 1 + tests/npm-app/src/endpoints/auth.ts | 10 ++++++ 17 files changed, 162 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 233b64b7c6e6..525b507895bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [6.0.0-dev6] + +[6.0.0-dev6]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev6 + +### Added + +- Added a `ccf::any_cert_auth_policy` (C++), or `any_cert` (JS/TS), implementing TLS client certificate authentication, but without checking for the presence of the certificate in the governance user or member tables. This enables applications wanting to do so to perform user management in application space, using application tables (#6608). + ## [6.0.0-dev5] [6.0.0-dev5]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev5 diff --git a/doc/build_apps/api.rst b/doc/build_apps/api.rst index 808ec6394eca..0ec3229e031f 100644 --- a/doc/build_apps/api.rst +++ b/doc/build_apps/api.rst @@ -63,6 +63,9 @@ Policies .. doxygenvariable:: ccf::member_cert_auth_policy :project: CCF +.. doxygenvariable:: ccf::any_cert_auth_policy + :project: CCF + .. doxygenvariable:: ccf::member_cose_sign1_auth_policy :project: CCF @@ -86,6 +89,10 @@ Identities :project: CCF :members: +.. doxygenstruct:: ccf::AnyCertAuthnIdentity + :project: CCF + :members: + .. doxygenstruct:: ccf::UserCOSESign1AuthnIdentity :project: CCF :members: diff --git a/doc/build_apps/js_app_bundle.rst b/doc/build_apps/js_app_bundle.rst index 50484646b9de..f4ba215e07b9 100644 --- a/doc/build_apps/js_app_bundle.rst +++ b/doc/build_apps/js_app_bundle.rst @@ -70,6 +70,7 @@ Each endpoint object contains the following information: - ``"user_cert"`` - ``"member_cert"`` + - ``"any_cert"`` - ``"jwt"`` - ``"user_cose_sign1"`` - ``"no_auth"`` diff --git a/include/ccf/common_auth_policies.h b/include/ccf/common_auth_policies.h index 81557157d22b..c2edb9c7b84e 100644 --- a/include/ccf/common_auth_policies.h +++ b/include/ccf/common_auth_policies.h @@ -31,6 +31,11 @@ namespace ccf static std::shared_ptr member_cert_auth_policy = std::make_shared(); + /** Authenticate using TLS session identity, but do not check + * the certificate against any table, and let the application decide */ + static std::shared_ptr any_cert_auth_policy = + std::make_shared(); + /** Authenticate using JWT, validating the token using the * @c public:ccf.gov.jwt.public_signing_keys_metadata table */ static std::shared_ptr jwt_auth_policy = diff --git a/include/ccf/endpoint.h b/include/ccf/endpoint.h index 7e97cdf0c0d9..6be3112cdd78 100644 --- a/include/ccf/endpoint.h +++ b/include/ccf/endpoint.h @@ -226,6 +226,7 @@ namespace ccf::endpoints * * @see ccf::empty_auth_policy * @see ccf::user_cert_auth_policy + * @see ccf::any_cert_auth_policy */ AuthnPolicies authn_policies; }; diff --git a/include/ccf/endpoints/authentication/cert_auth.h b/include/ccf/endpoints/authentication/cert_auth.h index 6624cc01c059..844716db7f3a 100644 --- a/include/ccf/endpoints/authentication/cert_auth.h +++ b/include/ccf/endpoints/authentication/cert_auth.h @@ -114,4 +114,38 @@ namespace ccf return SECURITY_SCHEME_NAME; }; }; + + struct AnyCertAuthnIdentity : public AuthnIdentity + { + // Certificate as a vector of DER-encoded bytes + std::vector cert; + }; + + class AnyCertAuthnPolicy : public AuthnPolicy + { + protected: + std::unique_ptr validity_periods; + + public: + static constexpr auto SECURITY_SCHEME_NAME = "any_cert"; + + AnyCertAuthnPolicy(); + virtual ~AnyCertAuthnPolicy(); + + std::unique_ptr authenticate( + ccf::kv::ReadOnlyTx& tx, + const std::shared_ptr& ctx, + std::string& error_reason) override; + + std::optional get_openapi_security_schema() + const override + { + return get_cert_based_security_schema(); + } + + virtual std::string get_security_scheme_name() override + { + return SECURITY_SCHEME_NAME; + }; + }; } diff --git a/include/ccf/endpoints/authentication/js.h b/include/ccf/endpoints/authentication/js.h index 2b843902094c..2a853d4ccbeb 100644 --- a/include/ccf/endpoints/authentication/js.h +++ b/include/ccf/endpoints/authentication/js.h @@ -24,6 +24,10 @@ namespace ccf ccf::MemberCertAuthnPolicy::SECURITY_SCHEME_NAME, ccf::member_cert_auth_policy); + policies.emplace( + ccf::AnyCertAuthnPolicy::SECURITY_SCHEME_NAME, + ccf::any_cert_auth_policy); + policies.emplace( ccf::JwtAuthnPolicy::SECURITY_SCHEME_NAME, ccf::jwt_auth_policy); @@ -62,6 +66,10 @@ namespace ccf { return ccf::MemberCertAuthnPolicy::SECURITY_SCHEME_NAME; } + else if constexpr (std::is_same_v) + { + return ccf::AnyCertAuthnPolicy::SECURITY_SCHEME_NAME; + } else if constexpr (std::is_same_v) { return ccf::JwtAuthnPolicy::SECURITY_SCHEME_NAME; diff --git a/js/ccf-app/src/endpoints.ts b/js/ccf-app/src/endpoints.ts index 60fe22687b3c..cbdf5322ba01 100644 --- a/js/ccf-app/src/endpoints.ts +++ b/js/ccf-app/src/endpoints.ts @@ -117,7 +117,18 @@ export interface EmptyAuthnIdentity extends AuthnIdentityCommon { policy: "no_auth"; } -interface UserMemberAuthnIdentityCommon extends AuthnIdentityCommon { +interface CertAuthnIdentityCommon extends AuthnIdentityCommon { + /** + * PEM-encoded certificate. + */ + cert: string; +} + +export interface AnyCertAuthnIdentity extends CertAuthnIdentityCommon { + policy: "any_cert"; +} + +interface UserMemberAuthnIdentityCommon extends CertAuthnIdentityCommon { /** * User/member ID. */ @@ -127,11 +138,6 @@ interface UserMemberAuthnIdentityCommon extends AuthnIdentityCommon { * User/member data object. */ data: any; - - /** - * PEM-encoded user/member certificate. - */ - cert: string; } export interface UserCertAuthnIdentity extends UserMemberAuthnIdentityCommon { @@ -193,6 +199,7 @@ export interface AllOfAuthnIdentity extends AuthnIdentityCommon { user_cert?: UserCertAuthnIdentity; member_cert?: MemberCertAuthnIdentity; + any_cert?: AnyCertAuthnIdentity; user_cose_sign1?: UserCOSESign1AuthnIdentity; member_cose_sign1?: MemberCOSESign1AuthnIdentity; jwt?: JwtAuthnIdentity; @@ -207,6 +214,7 @@ export type AuthnIdentity = | EmptyAuthnIdentity | UserCertAuthnIdentity | MemberCertAuthnIdentity + | AnyCertAuthnIdentity | JwtAuthnIdentity | MemberCOSESign1AuthnIdentity | UserCOSESign1AuthnIdentity diff --git a/python/pyproject.toml b/python/pyproject.toml index 9698f2c56836..81e6f2ad6afa 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ccf" -version = "6.0.0-dev5" +version = "6.0.0-dev6" authors = [ { name="CCF Team", email="CCF-Sec@microsoft.com" }, ] diff --git a/samples/apps/logging/js/app.json b/samples/apps/logging/js/app.json index ecd7b61a6de9..63791f55a1f2 100644 --- a/samples/apps/logging/js/app.json +++ b/samples/apps/logging/js/app.json @@ -24,6 +24,7 @@ }, "user_cert", "member_cert", + "any_cert", "jwt", "user_cose_sign1", "no_auth" diff --git a/samples/apps/logging/js/src/logging.js b/samples/apps/logging/js/src/logging.js index c4b30907e285..278f73ca6756 100644 --- a/samples/apps/logging/js/src/logging.js +++ b/samples/apps/logging/js/src/logging.js @@ -436,6 +436,11 @@ function describe_member_cert_ident(lines, obj) { lines.push(`The caller's cert is:\n${obj.cert}`); } +function describe_any_cert_ident(lines, obj) { + lines.push("Any TLS cert"); + lines.push(`The caller's cert is:\n${obj.cert}`); +} + function describe_jwt_ident(lines, obj) { lines.push("JWT"); lines.push( @@ -468,6 +473,7 @@ export function multi_auth(request) { const describers = { user_cert: describe_user_cert_ident, member_cert: describe_member_cert_ident, + any_cert: describe_any_cert_ident, jwt: describe_jwt_ident, user_cose_sign1: describe_cose_ident, no_auth: describe_noauth_ident, diff --git a/samples/apps/logging/logging.cpp b/samples/apps/logging/logging.cpp index bd0afec84ff4..3a97f29109d1 100644 --- a/samples/apps/logging/logging.cpp +++ b/samples/apps/logging/logging.cpp @@ -289,6 +289,17 @@ namespace loggingapp return response; } + else if ( + auto any_cert_ident = + dynamic_cast(caller.get())) + { + auto response = std::string("Any TLS cert"); + auto caller_cert = ccf::crypto::cert_der_to_pem(any_cert_ident->cert); + + response += + fmt::format("\nThe caller's cert is:\n{}", caller_cert.str()); + return response; + } else if ( auto jwt_ident = dynamic_cast(caller.get())) @@ -1168,6 +1179,7 @@ namespace loggingapp user_cert_jwt_and_sig_auth_policy, ccf::user_cert_auth_policy, ccf::member_cert_auth_policy, + ccf::any_cert_auth_policy, ccf::jwt_auth_policy, ccf::user_cose_sign1_auth_policy, ccf::empty_auth_policy}) diff --git a/src/endpoints/authentication/cert_auth.cpp b/src/endpoints/authentication/cert_auth.cpp index af23aaa89ea6..b5eafa69fd8b 100644 --- a/src/endpoints/authentication/cert_auth.cpp +++ b/src/endpoints/authentication/cert_auth.cpp @@ -117,6 +117,7 @@ namespace ccf if (!validity_periods->is_cert_valid_now(caller_cert, error_reason)) { + // Error is set by the call when necessary return nullptr; } @@ -203,4 +204,33 @@ namespace ccf error_reason = "Could not find matching node certificate"; return nullptr; } + + AnyCertAuthnPolicy::AnyCertAuthnPolicy() : + validity_periods(std::make_unique()) + {} + + AnyCertAuthnPolicy::~AnyCertAuthnPolicy() = default; + + std::unique_ptr AnyCertAuthnPolicy::authenticate( + ccf::kv::ReadOnlyTx& tx, + const std::shared_ptr& ctx, + std::string& error_reason) + { + const auto& caller_cert = ctx->get_session_context()->caller_cert; + if (caller_cert.empty()) + { + error_reason = "No caller certificate"; + return nullptr; + } + + if (!validity_periods->is_cert_valid_now(caller_cert, error_reason)) + { + // Error is set by the call when necessary + return nullptr; + } + + auto identity = std::make_unique(); + identity->cert = caller_cert; + return identity; + } } diff --git a/src/js/extensions/ccf/request.cpp b/src/js/extensions/ccf/request.cpp index b1181b5b26a8..cb0439a5f915 100644 --- a/src/js/extensions/ccf/request.cpp +++ b/src/js/extensions/ccf/request.cpp @@ -3,6 +3,7 @@ #include "ccf/js/extensions/ccf/request.h" +#include "ccf/crypto/verifier.h" #include "ccf/endpoints/authentication/all_of_auth.h" #include "ccf/endpoints/authentication/cert_auth.h" #include "ccf/endpoints/authentication/cose_auth.h" @@ -164,6 +165,21 @@ namespace ccf::js::extensions return caller; } + // For any cert, instead of an id-based lookup for the PEM cert and + // potential associated data, we directly retrieve the cert bytes as + // DER from the identity object, as provided by the session, and + // convert them to PEM. + if ( + auto any_cert_ident = + dynamic_cast(ident.get())) + { + auto policy_name = ccf::get_policy_name_from_ident(any_cert_ident); + caller.set("policy", ctx.new_string(policy_name)); + auto pem_cert = ccf::crypto::cert_der_to_pem(any_cert_ident->cert); + caller.set("cert", ctx.new_string(pem_cert.str())); + return caller; + } + char const* policy_name = nullptr; std::string id; bool is_member = false; diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index 878e5a799f15..6519a9e72080 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -665,6 +665,13 @@ def require_new_response(r): assert r.body.text().startswith("Member TLS cert"), r.body.text() require_new_response(r) + # Create a keypair that is not a user + network.create_user("not_a_user", args.participants_curve, record=False) + with primary.client("not_a_user") as c: + r = c.post("/app/multi_auth") + assert r.body.text().startswith("Any TLS cert"), r.body.text() + require_new_response(r) + LOG.info("Authenticate via JWT token") jwt_issuer = infra.jwt_issuer.JwtIssuer() jwt_issuer.register(network) diff --git a/tests/npm-app/app.json b/tests/npm-app/app.json index efbb5460fca2..5179677d65d9 100644 --- a/tests/npm-app/app.json +++ b/tests/npm-app/app.json @@ -1585,6 +1585,7 @@ }, "user_cert", "member_cert", + "any_cert", "jwt", "user_cose_sign1", "no_auth" diff --git a/tests/npm-app/src/endpoints/auth.ts b/tests/npm-app/src/endpoints/auth.ts index 52b0f6fa9c63..3d340b37e295 100644 --- a/tests/npm-app/src/endpoints/auth.ts +++ b/tests/npm-app/src/endpoints/auth.ts @@ -36,6 +36,8 @@ export function checkMultiAuth(request: ccfapp.Request): ccfapp.Response { describe_user_cert_ident(lines, request.caller); } else if (request.caller.policy === "member_cert") { describe_member_cert_ident(lines, request.caller); + } else if (request.caller.policy === "any_cert") { + describe_any_cert_ident(lines, request.caller); } else if (request.caller.policy === "jwt") { describe_jwt_ident(lines, request.caller); } else if (request.caller.policy === "user_cose_sign1") { @@ -73,6 +75,14 @@ function describe_member_cert_ident( lines.push(`The caller's cert is:\n${obj.cert}`); } +function describe_any_cert_ident( + lines: Lines, + obj: ccfapp.AnyCertAuthnIdentity, +) { + lines.push("Any TLS cert"); + lines.push(`The caller's cert is:\n${obj.cert}`); +} + function describe_jwt_ident(lines: Lines, obj: ccfapp.JwtAuthnIdentity) { lines.push("JWT"); lines.push(