Skip to content

PKCS#11 store: pair a certificate without a CKA_ID only with its own key - #4826

Open
wolfgangcolsman wants to merge 4 commits into
OPCFoundation:masterfrom
zontal:fix/pkcs11-certificate-without-id
Open

wolfgangcolsman wants to merge 4 commits into
OPCFoundation:masterfrom
zontal:fix/pkcs11-certificate-without-id

Conversation

@wolfgangcolsman

Copy link
Copy Markdown
Contributor

Description

Follow-up to #4792. Pkcs11CertificateStore.LoadPrivateKeyAsync skipped a certificate whose CKA_ID cannot be read, but a certificate with an empty CKA_ID still reached Pkcs11Token.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:

  • the key that carries its public key (the same lookup AddAsync uses to link a certificate it writes: RSA modulus and exponent, or EC point and curve), else
  • the key that the configured ObjectLabel / ObjectId selects, but only after that key signs a random challenge that the certificate's public key verifies (a shared label does not establish the pairing), else
  • no key.

Before 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

  • I have signed the CLA and read the CONTRIBUTING doc.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added all necessary documentation.
  • I have verified that my changes do not introduce (new) build or analyzer warnings.
  • I ran all tests locally using the UA.slnx solution against at least .net framework and .net 10, and all passed. (Opc.Ua.Security.Pkcs11.Tests on .NET 10, 112 passed / 10 SoftHSM skipped; the library builds for net48, net8.0, net9.0, net10.0.)
  • I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings.
  • I have addressed all PR feedback received.

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Duplicate IDs can still attach unrelated keys, while incomplete fallback and signing-mechanism handling can reject valid identities.

3 open issues

Review effort: Balanced


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;
}
@marcschier marcschier added the needs changes PR needs more changes label Oct 18, 2026
… 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

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs changes PR needs more changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants