Skip to content

Implement the Link attribute, report Query object types, and enable the RSA GCM cipher suites - #741

Open
JonahMMay wants to merge 6 commits into
OpenKMIP:masterfrom
Cenvora:veeam-kmip-interop
Open

JonahMMay wants to merge 6 commits into
OpenKMIP:masterfrom
Cenvora:veeam-kmip-interop

Conversation

@JonahMMay

@JonahMMay JonahMMay commented Sep 14, 2026

Copy link
Copy Markdown

Three related fixes, found while getting PyKMIP to interoperate with a real KMIP client (Veeam Backup & Replication 13). Each is a gap that stops a standards-conforming client from completing a normal asymmetric key workflow.

1. The Link attribute is unimplemented

There are three stubs and no implementation: no Link class in attributes.py, AttributeValueFactory.create_attribute_value() raises NotImplementedError, and _get_attribute_from_managed_object() returns None. A client that calls CreateKeyPair and then asks the public key for its linked private key gets nothing, so it cannot resolve the pair.

This adds the Link class with KMIP read/write serialization, the factory method, and the engine read path.

The pairing is stored, not inferred

The tempting implementation is to recover the relationship by querying for an object of the complementary type sharing the creating object's _owner and initial_date. That is unsound: initial_date is a whole-second epoch integer and _process_create_key_pair gives both halves of a pair the same value, so two key pairs created by one owner within the same second are indistinguishable and the query returns an arbitrary one.

A client that encrypts under public key A and later asks for A's linked private key is then handed B's. Nothing fails at creation time; it surfaces as a decryption failure against data that is already written.

So the pairing is recorded on both objects immediately after the commit that assigns their identifiers — the only moment it is known for certain.

Compatibility — guidance welcome

This adds managed_objects.link_id. There is no migration machinery in the project and metadata.create_all() does not add columns to existing tables, so an existing database raises:

OperationalError: no such column: managed_objects.link_id

For SQLite the upgrade is:

ALTER TABLE managed_objects ADD COLUMN link_id INTEGER;

I am happy to rework this if you would rather not carry a schema change, but the inference-based alternative is unsound for the reason above, so I did not want to propose it as the default.

Objects created before the column existed still resolve through the old heuristic, which now refuses to guess when more than one candidate matches and logs a warning rather than returning an arbitrary key.

Incidentally

The Link setter used six.string_types, so setting a string identifier raised NameError on exactly this path now that the Python 2 shims are gone. It uses str. (This overlaps in spirit with #739, which trims more six usage — happy to defer to that if it lands first.)

2. Query reports no supported object types

_process_query returns an empty list for QUERY_OBJECTS, so the Operation list is populated but Object Types is always empty. Clients reasonably read that as "this server manages no object types" and refuse to proceed; the failure surfaces as a generic registration error with nothing pointing at Query.

Now returns the types the engine actually manages: Certificate, Symmetric Key, Public Key, Private Key, Secret Data.

3. No usable GCM cipher suite with an RSA certificate

TLS12AuthenticationSuite._default_cipher_suites includes GCM suites only in their ECDHE-ECDSA form, which requires an ECDSA server certificate. Deployed with an RSA certificate — the common case, and what bin/create_certificates.py produces — no GCM suite is available at all.

A peer enforcing OpenSSL SECLEVEL=2 offers only forward-secret GCM suites, so the two sides share nothing and the handshake fails inside OpenSSL before any application-level logging. The server logs nothing, which makes this disproportionately hard to diagnose.

Adds ECDHE-RSA-AES256-GCM-SHA384 and ECDHE-RSA-AES128-GCM-SHA256, and drops two ECDHE-ECDSA entries that were listed twice.

(Unrelated and left alone: DH-DSS-AES256-SHA256 appears twice in the existing list.)

Tests

  • Two key pairs created by one owner under a frozen clock, asserting each public key links to its own private key. The test first asserts both pairs really did land in the same second, or it would not exercise the collision it exists for.
  • The RSA GCM suites are asserted to survive OpenSSL's own parsing — SSLContext.set_ciphers() then get_ciphers() — rather than by comparing the cipher string. String equality cannot catch a malformed entry, because OpenSSL drops tokens it does not recognise instead of raising.
  • The test_query_1_* and Link factory tests asserted the previous behaviour (assertIsNone(result.object_types), NotImplementedError) and are updated.

Full unit suite: 3363 passed, 35 skipped.

Attribution

The Link implementation, the Query fix and the cipher-suite addition are by Luca Dell'Oca (@dellock6), from his pykmip-veeam fork; the commits here keep his authorship. Rebasing them onto current master, the stored-pairing change, the six fix and the test updates are mine.

🤖 Generated with Claude Code

dellock6 and others added 6 commits September 14, 2026 14:31
The Link attribute was recovered by querying for an object of the
complementary type sharing the creating object's _owner and initial_date.
initial_date is a whole-second epoch integer and both halves of a pair are
given the same value, so two key pairs created by one owner inside the same
second were indistinguishable and .first() returned an arbitrary one.

A KMIP client that encrypts under public key A and later asks for A's linked
private key could therefore be handed B's. Nothing fails at creation time;
it surfaces as a decryption failure against data that is already written.

Record the pairing on both objects once SQLAlchemy has assigned identifiers,
which is the only moment the relationship is known for certain. The old
heuristic is kept for rows created before the column existed, but it now
refuses to guess when more than one candidate matches, and logs instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A missing comma in TLS12AuthenticationSuite._default_cipher_suites made
Python concatenate two entries into one unknown token, so
ECDHE-RSA-AES256-GCM-SHA384 was never actually enabled. OpenSSL drops
unrecognised tokens silently rather than raising, so a peer restricted to
forward-secret GCM suites negotiated AES-128 instead, with nothing logged.
The two duplicated ECDHE-ECDSA entries introduced alongside it are dropped.

Link.linked_object_identifier used six.string_types, and 0.11.0 removed the
Python 2 compatibility shims, so setting a string identifier raised
NameError — on exactly the path a client takes to resolve a key pair.

The Query and Link tests still asserted the old behaviour: an empty
supported-object-type list (which a client reads as "this server supports
nothing") and NotImplementedError from the Link factory.

Adds a test that asserts the RSA GCM suites survive OpenSSL's own parsing.
Asserting the cipher string cannot catch this: a malformed entry is still a
string, which is how the missing comma went unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants