Skip to content

Commit

Permalink
Any cert authn policy (#6608)
Browse files Browse the repository at this point in the history
Co-authored-by: Eddy Ashton <ashton.eddy@gmail.com>
  • Loading branch information
achamayou and eddyashton authored Nov 6, 2024
1 parent dad86f8 commit 7573264
Show file tree
Hide file tree
Showing 17 changed files with 162 additions and 7 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions doc/build_apps/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -86,6 +89,10 @@ Identities
:project: CCF
:members:

.. doxygenstruct:: ccf::AnyCertAuthnIdentity
:project: CCF
:members:

.. doxygenstruct:: ccf::UserCOSESign1AuthnIdentity
:project: CCF
:members:
Expand Down
1 change: 1 addition & 0 deletions doc/build_apps/js_app_bundle.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Each endpoint object contains the following information:

- ``"user_cert"``
- ``"member_cert"``
- ``"any_cert"``
- ``"jwt"``
- ``"user_cose_sign1"``
- ``"no_auth"``
Expand Down
5 changes: 5 additions & 0 deletions include/ccf/common_auth_policies.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ namespace ccf
static std::shared_ptr<MemberCertAuthnPolicy> member_cert_auth_policy =
std::make_shared<MemberCertAuthnPolicy>();

/** Authenticate using TLS session identity, but do not check
* the certificate against any table, and let the application decide */
static std::shared_ptr<AnyCertAuthnPolicy> any_cert_auth_policy =
std::make_shared<AnyCertAuthnPolicy>();

/** Authenticate using JWT, validating the token using the
* @c public:ccf.gov.jwt.public_signing_keys_metadata table */
static std::shared_ptr<JwtAuthnPolicy> jwt_auth_policy =
Expand Down
1 change: 1 addition & 0 deletions include/ccf/endpoint.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
34 changes: 34 additions & 0 deletions include/ccf/endpoints/authentication/cert_auth.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,38 @@ namespace ccf
return SECURITY_SCHEME_NAME;
};
};

struct AnyCertAuthnIdentity : public AuthnIdentity
{
// Certificate as a vector of DER-encoded bytes
std::vector<uint8_t> cert;
};

class AnyCertAuthnPolicy : public AuthnPolicy
{
protected:
std::unique_ptr<ValidityPeriodsCache> validity_periods;

public:
static constexpr auto SECURITY_SCHEME_NAME = "any_cert";

AnyCertAuthnPolicy();
virtual ~AnyCertAuthnPolicy();

std::unique_ptr<AuthnIdentity> authenticate(
ccf::kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& ctx,
std::string& error_reason) override;

std::optional<OpenAPISecuritySchema> get_openapi_security_schema()
const override
{
return get_cert_based_security_schema();
}

virtual std::string get_security_scheme_name() override
{
return SECURITY_SCHEME_NAME;
};
};
}
8 changes: 8 additions & 0 deletions include/ccf/endpoints/authentication/js.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -62,6 +66,10 @@ namespace ccf
{
return ccf::MemberCertAuthnPolicy::SECURITY_SCHEME_NAME;
}
else if constexpr (std::is_same_v<T, ccf::AnyCertAuthnIdentity>)
{
return ccf::AnyCertAuthnPolicy::SECURITY_SCHEME_NAME;
}
else if constexpr (std::is_same_v<T, ccf::JwtAuthnIdentity>)
{
return ccf::JwtAuthnPolicy::SECURITY_SCHEME_NAME;
Expand Down
20 changes: 14 additions & 6 deletions js/ccf-app/src/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand All @@ -207,6 +214,7 @@ export type AuthnIdentity =
| EmptyAuthnIdentity
| UserCertAuthnIdentity
| MemberCertAuthnIdentity
| AnyCertAuthnIdentity
| JwtAuthnIdentity
| MemberCOSESign1AuthnIdentity
| UserCOSESign1AuthnIdentity
Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
]
Expand Down
1 change: 1 addition & 0 deletions samples/apps/logging/js/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"user_cert",
"member_cert",
"any_cert",
"jwt",
"user_cose_sign1",
"no_auth"
Expand Down
6 changes: 6 additions & 0 deletions samples/apps/logging/js/src/logging.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions samples/apps/logging/logging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,17 @@ namespace loggingapp

return response;
}
else if (
auto any_cert_ident =
dynamic_cast<const ccf::AnyCertAuthnIdentity*>(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<const ccf::JwtAuthnIdentity*>(caller.get()))
Expand Down Expand Up @@ -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})
Expand Down
30 changes: 30 additions & 0 deletions src/endpoints/authentication/cert_auth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -203,4 +204,33 @@ namespace ccf
error_reason = "Could not find matching node certificate";
return nullptr;
}

AnyCertAuthnPolicy::AnyCertAuthnPolicy() :
validity_periods(std::make_unique<ValidityPeriodsCache>())
{}

AnyCertAuthnPolicy::~AnyCertAuthnPolicy() = default;

std::unique_ptr<AuthnIdentity> AnyCertAuthnPolicy::authenticate(
ccf::kv::ReadOnlyTx& tx,
const std::shared_ptr<ccf::RpcContext>& 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<AnyCertAuthnIdentity>();
identity->cert = caller_cert;
return identity;
}
}
16 changes: 16 additions & 0 deletions src/js/extensions/ccf/request.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<const ccf::AnyCertAuthnIdentity*>(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;
Expand Down
7 changes: 7 additions & 0 deletions tests/e2e_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions tests/npm-app/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -1585,6 +1585,7 @@
},
"user_cert",
"member_cert",
"any_cert",
"jwt",
"user_cose_sign1",
"no_auth"
Expand Down
10 changes: 10 additions & 0 deletions tests/npm-app/src/endpoints/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit 7573264

Please sign in to comment.