Files
logos-cpp-sdk/tests/sdk/test_logos_host_services.cpp
T
Dario Gabriel LipicarandClaude Opus 5 b9d7641fae feat(sdk): logos_host_services.h — the C++ veneer over the privileged surface
Phase C1. A Qt-free, header-only wrapper over the three trust-root lp_* calls
A3 added, so capability_module can become an ordinary universal module instead
of a hand-written Qt plugin reaching into TokenManager directly.

Deliberately FREE FUNCTIONS, not a LogosModuleContext seam as the plan
sketched. The grant is process-global per IMAGE (host binary and module cdylib
each link their own logos-protocol, so each has its own grant state and its own
TokenManager), so the gate lives in the caller's own image and there is nothing
per-instance to inject; a context seam would imply the privilege is a property
of one impl object, which it is not. It is also markedly cheaper: a seam would
need a new module-impl C ABI export plus lockstep changes in BOTH codegen paths
(the Qt provider glue and the cdylib wrapper).

constantTimeEquals lives here rather than in each caller: the natural spelling
(a == b) leaks the matching-prefix length through timing, and a trust root
comparing tokens with == is the exact bug this file exists to prevent. Ported
from capability_module's own implementation to std::string.

Two things the tests caught that reading had not:

* lp_inform_module_token_to takes SIX arguments (client, auth_token,
  origin_module, module_name, token, timeout_ms), not the three I first wrote.
  The wrapper now mirrors it exactly, with the protocol's own default-timeout
  semantics documented.
* sdk_tests compiles against logos_headers alone, which carries no protocol
  include path. It now resolves logos_protocol.h from LOGOS_PROTOCOL_ROOT,
  accepting either the source layout (cpp/) or a package layout (include/) and
  failing loudly on neither, rather than hard-coding the one in use today.

The suite deliberately does NOT link logos-protocol: the lp_*-calling wrappers
are `inline` and never ODR-used by these tests, so no protocol symbol is
referenced. That is itself the assertion — the veneer must not drag the
protocol library into a header-only consumer. A future test that calls one will
fail to LINK rather than silently pull it in.

Also documents a real gap found while writing it: lp_token_get performs NO
host-service check, so "token_registry" gates ENUMERATION only. The plan claims
that service covers `lp_token_get(any)`; it does not. Flagged at the call site
rather than papered over — if lookup should be gated, the gate belongs in
lp_token_get.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 09:01:46 -03:00

90 lines
3.6 KiB
C++

#include <gtest/gtest.h>
#include "logos_host_services.h"
#include <string>
// Coverage for the Qt-free host-services veneer.
//
// The GATE itself (ungranted callers get LP_ERR_UNSUPPORTED / a null
// lp_token_keys) is tested in logos-protocol, which is where the gate lives and
// where the library is linked — see tests/protocol/test_host_services_grant.cpp.
// What is tested here is what this header ADDS: constantTimeEquals, plus the
// fact that the header parses standalone in a Qt-free, protocol-unlinked TU.
//
// Note this file deliberately does NOT call tokenKeys()/informModuleTokenTo():
// they are `inline` and never ODR-used here, so no lp_* symbol is referenced
// and sdk_tests keeps linking against logos_headers alone. That is also the
// property being asserted by this file existing at all — the veneer must not
// drag the protocol library into a header-only consumer.
using logos::host::constantTimeEquals;
TEST(HostServicesConstantTimeEquals, EqualStringsMatch)
{
EXPECT_TRUE(constantTimeEquals("", ""));
EXPECT_TRUE(constantTimeEquals("a", "a"));
EXPECT_TRUE(constantTimeEquals("10d794ec-1234-5678-9abc-def012345678",
"10d794ec-1234-5678-9abc-def012345678"));
}
TEST(HostServicesConstantTimeEquals, DifferentLengthsDoNotMatch)
{
EXPECT_FALSE(constantTimeEquals("", "a"));
EXPECT_FALSE(constantTimeEquals("a", ""));
EXPECT_FALSE(constantTimeEquals("token", "token "));
EXPECT_FALSE(constantTimeEquals("token", "toke"));
}
TEST(HostServicesConstantTimeEquals, DifferenceInAnyPositionIsCaught)
{
const std::string ref = "abcdefghijklmnop";
// A comparison that early-exits would still get these right; what would
// NOT be caught by a weaker test is a loop that stops at the first
// mismatch and reports equality for the rest. Walk every index so a
// truncated loop bound fails here rather than in production.
for (std::size_t i = 0; i < ref.size(); ++i) {
std::string other = ref;
other[i] = static_cast<char>(other[i] ^ 0x01);
EXPECT_FALSE(constantTimeEquals(ref, other))
<< "difference at index " << i << " was not detected";
}
}
TEST(HostServicesConstantTimeEquals, EmbeddedNulsAreCompared)
{
// std::string is not NUL-terminated-by-convention here; a memcmp/strcmp
// regression would stop at the NUL and call these equal.
const std::string a("tok\0AAA", 7);
const std::string b("tok\0BBB", 7);
ASSERT_EQ(a.size(), b.size());
EXPECT_FALSE(constantTimeEquals(a, b));
EXPECT_TRUE(constantTimeEquals(a, std::string("tok\0AAA", 7)));
}
TEST(HostServicesConstantTimeEquals, HighBitBytesAreCompared)
{
// Signed char: 0x80 sign-extends. Without the unsigned casts in the
// implementation the XOR still works, but a naive `int` accumulator that
// dropped the cast could mask a difference — pin the behaviour.
const std::string a("\x80\x01", 2);
const std::string b("\x80\x81", 2);
EXPECT_FALSE(constantTimeEquals(a, b));
EXPECT_TRUE(constantTimeEquals(a, std::string("\x80\x01", 2)));
}
TEST(HostServicesStatus, UngrantedIsDistinguishableFromOtherFailures)
{
logos::host::Status ungranted{false, LP_ERR_UNSUPPORTED};
EXPECT_TRUE(ungranted.ungranted());
EXPECT_FALSE(static_cast<bool>(ungranted));
logos::host::Status otherFailure{false, LP_ERR_INVALID_ARG};
EXPECT_FALSE(otherFailure.ungranted())
<< "a non-gate failure must not be reported as 'not permitted'";
logos::host::Status ok = logos::host::Status::fromCode(LP_OK);
EXPECT_TRUE(static_cast<bool>(ok));
EXPECT_FALSE(ok.ungranted());
}