Repository navigation
PKCS#11 store: pair a certificate without a CKA_ID only with its own key - #4826
Open
wolfgangcolsman wants to merge 4 commits into
Open
wolfgangcolsman wants to merge 4 commits into
wolfgangcolsman wants to merge 4 commits into
Conversation
LoadPrivateKeyAsync skipped a certificate whose CKA_ID cannot be read, but a certificate with an empty CKA_ID still reached FindPrivateKey, which then selected a key by the configured object filters alone and, without any filter, returned the first private key of the token's key type: a key that may belong to another certificate. A certificate without a CKA_ID is now paired with the key whose public key it carries (the lookup AddAsync uses to link a certificate it writes). When no key carries it and an object label or id is configured, the key that filter selects is attached only after it signs a random challenge that the certificate's public key verifies; a shared label does not establish the pairing. Without a filter, no key is attached. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
wolfgangcolsman
added a commit
to zontal/UA-.NETStandard
that referenced
this pull request
Oct 18, 2026
…Foundation#4792) # Description Pkcs11CertificateStore.AddAsync returned success without writing anything, so a certificate added to a token backed store (a trust list update, a renewed application certificate) was silently lost. DeleteAsync always threw NotSupportedException. AddAsync now writes the certificate as an X.509 certificate object with CKA_VALUE, CKA_SUBJECT, CKA_ISSUER, CKA_SERIAL_NUMBER, CKA_LABEL and a CKA_ID that links it to the key it belongs to: the id the store's object filter selects, else the id of a certificate on the token with the same public key, else the id of the key whose modulus (RSA) or EC point matches, else the SHA-1 hash of the public key. A renewed certificate for a key generated on the token is therefore paired with that key by LoadPrivateKeyAsync. A certificate the token already holds is not written again. A private key offered with the certificate is still never written to the token; it is counted in RejectedPrivateKeyWrites as before. DeleteAsync destroys the certificate objects with the thumbprint and returns whether there were any. Key objects are never destroyed. The PKCS#11 test double now supports C_CreateObject and C_DestroyObject. Tests cover both operations against it and, in the token backed fixture, against SoftHSM2. The two tests that pinned the old behaviour (a dropped write, a delete that throws) are removed. ## Related Issues None. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> (cherry picked from commit 1b7db89) Also carries the net change of the follow-up OPCFoundation#4826 (820ba8a): a certificate without a CKA_ID is paired only with the key that carries its public key, or with a key the object filter selects after it signs a challenge the certificate verifies; the package README describes it.
wolfgangcolsman
added a commit
to zontal/UA-.NETStandard
that referenced
this pull request
Oct 18, 2026
…Foundation#4792) # Description Pkcs11CertificateStore.AddAsync returned success without writing anything, so a certificate added to a token backed store (a trust list update, a renewed application certificate) was silently lost. DeleteAsync always threw NotSupportedException. AddAsync now writes the certificate as an X.509 certificate object with CKA_VALUE, CKA_SUBJECT, CKA_ISSUER, CKA_SERIAL_NUMBER, CKA_LABEL and a CKA_ID that links it to the key it belongs to: the id the store's object filter selects, else the id of a certificate on the token with the same public key, else the id of the key whose modulus (RSA) or EC point matches, else the SHA-1 hash of the public key. A renewed certificate for a key generated on the token is therefore paired with that key by LoadPrivateKeyAsync. A certificate the token already holds is not written again. A private key offered with the certificate is still never written to the token; it is counted in RejectedPrivateKeyWrites as before. DeleteAsync destroys the certificate objects with the thumbprint and returns whether there were any. Key objects are never destroyed. The PKCS#11 test double now supports C_CreateObject and C_DestroyObject. Tests cover both operations against it and, in the token backed fixture, against SoftHSM2. The two tests that pinned the old behaviour (a dropped write, a delete that throws) are removed. ## Related Issues None. ## Checklist - [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce (new) build or analyzer warnings. - [x] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed. - [x] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received. --------- Also carries the net change of the follow-up OPCFoundation#4826 (820ba8a): a certificate without a CKA_ID is paired only with the key that carries its public key, or with a key the object filter selects after it signs a challenge the certificate verifies; the package README describes it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> (cherry picked from commit 1b7db89)
|
|
||
| Certificate? withKey = AttachKey(token, candidate, entry.Value); | ||
| byte[]? keyId = entry.Value.Length > 0 ? entry.Value : FindKeyId(token, candidate); | ||
| bool provePossession = keyId == null; |
| keyId = []; | ||
| } | ||
|
|
||
| Certificate? withKey = AttachKey(token, candidate, keyId, provePossession); |
Comment on lines
+768
to
+778
| byte[] challenge = CreateChallenge(); | ||
|
|
||
| try | ||
| { | ||
| byte[] signature = key.SignHash(challenge, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); | ||
| return publicKey.VerifyHash(challenge, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); | ||
| } | ||
| catch (Pkcs11Exception) | ||
| { | ||
| return false; | ||
| } |
… proof of possession Review feedback on the pairing of a certificate without a CKA_ID: - The CKA_ID of the key object that carries the certificate's public key may be shared by other keys; the key it selected was attached without a check. Every key with that CKA_ID is now tried, and only a key that signs a challenge the certificate's public key verifies is attached. - When that CKA_ID selects no private key (e.g. only the EC public-key object carries it), the keys the configured object label or id selects are tried as well, with the same proof. - The RSA challenge falls back to PSS when the token refuses PKCS#1 v1.5 for the key, so a key permitted only for PSS can prove possession. Pkcs11Token.FindPrivateKeys returns every matching key; FindPrivateKey returns its first. Tests cover a shared CKA_ID, the fallback after an inferred CKA_ID without a private key, and a PSS-only key. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ttached The fake PKCS#11 module signs with a per-object key where one is given, so tests can place a key that signs for the certificate after the token's own key: with the inferred CKA_ID and with the object label, for RSA and EC. Each test checks that the attached key signs for the certificate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Description
Follow-up to #4792.
Pkcs11CertificateStore.LoadPrivateKeyAsyncskipped a certificate whose CKA_ID cannot be read, but a certificate with an empty CKA_ID still reachedPkcs11Token.FindPrivateKey, which then selects a key by the configured object filters alone and, without any filter, returns the first private key of the token's key type. That key may belong to another certificate, so the store could return an identity whose signatures its certificate does not verify.A certificate without a CKA_ID is now paired with:
AddAsyncuses to link a certificate it writes: RSA modulus and exponent, or EC point and curve), elseObjectLabel/ObjectIdselects, but only after that key signs a random challenge that the certificate's public key verifies (a shared label does not establish the pairing), elseBefore this change, a certificate without a CKA_ID on a token without object filters got the first key of its type. A certificate with a CKA_ID is handled as before. No public API changes. The package README describes the pairing.
Related Issues
Follow-up to #4792.
Checklist