Recursive AI

Key-backend glue review

All onecrypto docs

An internal review of the glue between OneCrypto's operations and keys that live elsewhere: the PKCS #11 backend (OneCrypto.Keys/Pkcs11), the gpg-agent and ssh-agent clients (OneCrypto.Keys/Agents), the CSC remote-signing client (OneCrypto.Online/Csc), the core key interfaces and two-phase signing state (OneCrypto/Keys), the OpenPGP external-key path (PgpSecretKey.FromExternalKey, PgpReadOptions.ExternalKeys, Messages/SessionKeys.cs, Messages/MessageReader.cs) and CMS use of external keys (Cms/CmsRecipients.cs). This is an internal review by an engineer who did not write the code. It is not an external audit; the library is not independently audited.

Method: the code was read against the PKCS #11 v3.1 text, the Assuan manual, the ssh-agent draft, the CSC API v2.2 and RFCs 7748, 8017 and 9580; hostile peers were built as test doubles (a native-callable function table that plays a module, scripted keys, byte mutation of serialised states). Every finding below has a regression test; settled behaviour needed no oracle. Tests: tests/OneCrypto.Keys.Tests/HostilePkcs11ModuleTests.cs, KeyBackendGlueTests.cs, Pkcs11PgpTests.ATamperedSessionKeyPacketIsTheSameFailureThroughTheToken, tests/OneCrypto.Tests/Keys/PreparedSignatureTamperTests.cs, tests/OneCrypto.OpenPgp.Tests/KeyManagementTests.EveryTruncationAndOctetChangeOfAPgpState….

Findings

#SeverityWhereFindingSpecTestFixStatus
K1medium (signature)Pkcs11/Pkcs11Keys.cs Pkcs11RsaKey.SignDigest, Pkcs11EcKey.SignDigest, Pkcs11EdKey.SignA token's signature was returned as it came. Nothing between the token and the certificate, CMS or OpenPGP packet checked it, so a faulty token (an RSA-CRT fault gives a bad signature that can also leak the key), a hostile module, or a public key that is not the token key's put a signature that does not verify into the output. The agents and CSC already check theirs. Only ECDSA length was checked.RFC 8017 §8.1.2 note on fault attacks; PKCS #11 v3.1 §5.13KeyBackendGlueTests.ATokenSignatureThatDoesNotVerifyIsNeverReleased, ATokenEcdsaSignatureIsCheckedInBothEncodingsPkcs11Key.Checked verifies every signature against PublicKey before release (a platform that cannot check the scheme at all refuses it unless the session opts out: F2).Fixed.
K2high (memory safety)Pkcs11/Raw.cs FindC_FindObjects writes into a 64-entry stackalloc buffer and reports how many it found. The count was trusted: a module answering 1000 made the loop read 936 slots past the buffer. A module answering a full batch forever grew the result list without bound.PKCS #11 v3.1 §5.15.3 (at most the count asked for)HostilePkcs11ModuleTests.AFindResultLargerThanTheBatchIsRefusedBeforeItIsRead, AModuleThatNeverStopsFindingIsStoppedA count above the batch is a typed Pkcs11Exception; the result is capped at 4096 objects.Fixed.
K3medium (DoS, untyped errors)Pkcs11/Raw.cs GetAttribute, GetSlotList, GetMechanismList, Finish, DeriveEcdhLengths a module chooses were used as allocation sizes and slice lengths: an attribute of 2 GB − 1 allocated 2 GB, one of 2^32 + n wrapped to a small or negative size; a slot or mechanism count of 10^9 allocated 8 GB; on the second call a count larger than the array threw ArgumentOutOfRangeException; C_Sign/C_Decrypt reporting more octets than the buffer threw the same; a module answering CKR_BUFFER_TOO_SMALL with ever larger sizes was followed 1 M times; the ECDH secret read back was not checked against the field size.PKCS #11 v3.1 §5.2HostilePkcs11ModuleTests (the remaining five cases)Attributes ≤ 1 MiB, lists ≤ 65 536 entries and never above what was allocated, output length ≤ buffer, one BUFFER_TOO_SMALL retry, derived secret exactly the field size. All typed Pkcs11Exception.Fixed.
K4medium (misreport, oracle shape)OpenPgp/Messages/SessionKeys.cs Decrypt; Cms/CmsRecipients.cs KeyAdapter; Pkcs11/Pkcs11Keys.cs Pkcs11EcKey.AgreeEvery CryptographicException from an external key was folded into "this key cannot open the packet", so a locked token (Pkcs11PinException), a removed token or an unsupported mechanism ended as "cannot decrypt": the message was blamed for the key's trouble. The other half: a bad ephemeral point made a token refuse in C_DeriveKey with a code that surfaced as a base Pkcs11Exception, not the plain exception RSA decryption raises, so the two paths did not fail alike.RFC 9580 §13.5; RFC 8017 §7.2.2KeyBackendGlueTests.EveryWayAnExternalRsaKeyRefusesACiphertextIsTheSameFailureAsABadBody, ALockedTokenIsNotReportedAsAnUndecryptableMessage; Pkcs11PgpTests.ATamperedSessionKeyPacketIsTheSameFailureThroughTheTokenThe contract is written on IDecryptionKey/IAgreementKey: a ciphertext the key could not use is exactly CryptographicException; anything else is the key's own trouble. OpenPGP and CMS fold the former and let the latter through. PKCS #11 ECDH maps every refusal that is not about the token, PIN or key to the plain exception.Fixed.
K5low (suspect (a))SessionKeys.ExternalAgree; CmsRecipients.KeyAdapter.DeriveAgreementSecretThe in-memory X25519/X448 path refuses an all-zero shared secret; an external agreement key was believed. gpg-agent's Montgomery key does refuse it, and IAgreementKey says implementations must, but a third-party key need not. No oracle or plaintext follows (the KEK is then known to the sender, who could encrypt to the public key anyway), so low.RFC 7748 §6.1; RFC 8418 §3KeyBackendGlueTests.AnAllZeroX25519ResultFromAnExternalKeyIsRefused, …X448…Both callers refuse an all-zero result themselves.Fixed.
K6lowPkcs11Keys.cs Pkcs11RsaKey.DecryptA decryption result as long as the modulus or longer was returned; no RSAES result can be (the message is at most k − 11 / k − 2hLen − 2 octets).RFC 8017 §7.1.2, §7.2.2SoftHSM2 decrypt tests keep passing; no direct test (a hostile module that answers a modulus-sized plaintext is not scripted)Result ≥ k is the plain failure.Fixed.
K7lowKeys/PreparedSignature.cs TryDecode, Keys/SignatureSpec.cs DecodeA stored state with a public-key bit string that claimed unused bits, or a spec naming a hash outside SHA-1/2, was accepted: two byte strings for one state, and the first octet flip tried by the test survived.docs/keys.md (one encoding)PreparedSignatureTamperTests.EveryTruncationAndOctetChangeIsATypedResultA state must re-encode to the octets read (the key from its parts), and the hash name is one of the four.Fixed.
K8lowOpenPgp/KeyManagement/PgpPreparedSignature.cs CompleteA stored state whose signed header was damaged still decoded (its parts fit each other) and then raised OpenPgpParseException from Complete, not the documented PgpException.—KeyManagementTests.EveryTruncationAndOctetChangeOfAPgpState…The parse error becomes PgpException(MalformedMessage).Fixed.

Counts: signature 1 (K1), memory safety 1 (K2), DoS 1 (K3), misreport 1 (K4), low 4 (K5–K8). All fixed.

Follow-ups

  • F1 (fixed) PgpSecretKey.FromExternalKey compared nothing with the key packet. It now refuses an external key whose algorithm, curve or key material differs from the packet (PgpFailure.KeyMismatch), before any message is processed. Tests: ExternalKeyPairingTests.
  • F2 (fixed) Where the platform cannot verify a scheme at all, gpg-agent, ssh-agent, PKCS #11 and CSC now refuse the signature with AlgorithmNotSupportedException instead of releasing it unchecked. AllowUncheckedSignatures on GpgAgentOptions, SshAgentOptions, Pkcs11Session and CscKeyOptions opts out, and weakens the check. Where verification is possible nothing changed.
  • F3 CSC authentication values (CscAuthObject) and tokens are .NET strings: they are redacted in every ToString, error and recording, but cannot be zeroed. PKCS #11 PINs are copied into pinned buffers and zeroed.
  • F4 A call into a module that waits for a PIN pad holds the module-wide gate; the synchronous API has no cancellation, so other sessions of that module wait. Not documented yet.
  • F5 The key interfaces do not apply CryptoPolicy: calling a token or CSC key directly with a SHA-1 digest signs it, as an in-memory key does. Policy is enforced where the operation is: PreparedSignature.Prepare, CMS SignerPlan, OpenPGP SignatureBuilder, the X.509 builders. By design.
  • F6 A hostile agent's malformed reply during decryption (AgentProtocolException) is not folded into the plain failure; it is a protocol fault, not a property of the message.

What was checked and found sound

  • Assuan: command lines are one line (CR/LF refused) and ≤ 1000 octets; keygrips are validated as 40 hex digits before they are put in a command; descriptions are percent-escaped; replies are bounded (line 1100, reply total), data buffers wipe on growth, S-expressions are bounded in depth (8) and nodes (256); inquiries we cannot answer are cancelled. Tested in HostileAgentTests (18 cases).
  • ssh-agent: reply length is checked before allocation (default 256 KiB), the key count is bounded by what the reply can hold, the reply type must be the expected one, the returned signature must be the algorithm asked for and is verified against the public key; ssh-rsa (SHA-1) is never requested.
  • gpg-agent: signatures verified by default; a refusal that is not a cancel, bad passphrase or pinentry error becomes the plain decryption failure; agent plaintext is wiped after use; the Montgomery key refuses an all-zero result; EdDSA message size is limited with a typed error.
  • CSC: the digest always goes to credentials/authorize (the SAD is bound to it), no redirect is followed, the signature count must equal the digest count, every returned signature is normalised and verified against the credential's certificate key before release, tokens and SADs are redacted in ToString, errors and recordings (CscClientTests, CscGuardTests, CscFixtureTests).
  • Two-phase state: versioned, purpose-labelled (TSA, OCSP, CMS and X.509 flows check the label), carries only public data (key, digest or message, spec), refuses a digest that does not match its data, a scheme that does not fit its key, and a signature from another key, over other data or in another encoding. Truncation and single-octet changes end in a typed refusal (K7, K8).
  • Resources: PKCS #11 sessions close once under the module gate and unregister; key objects hold nothing to release; derived-secret objects are destroyed in a finally; the agent clients use one connection per operation, closed on every path including cancellation (AnAgentThatNeverAnswersIsCutOffByTheOperationTimeout).
  • PKCS #11: CKA_ALWAYS_AUTHENTICATE keys are refused; PINs are converted into pinned buffers and zeroed; Pkcs11PinException messages carry the token's result code only.

Timing targets to re-run

None of the changed code handles a secret by branching on its value. For completeness, the OpenPGP session-key glue (SessionKeys.cs: the exception filter, the all-zero check over the shared secret, which is a fixed pass with a single test of the OR) is the only secret-adjacent edit; the existing session-key glue target covers it.