0

I have two Smart Cards:

  1. YubiKey 5C NFC from Yubico
  2. SafeNet eToken 5110 Series

I installed all necessary drivers and tools for both Smart Cards.

Now the goal is to create a C# method that signs an unsigned XML Claim File with the private key of those Smart Cards. This signed XML Claims File should then be send to an API with a POST request that verifies the signature by using the public key. We can upload the public key to the API by using the Yubikey with FIDO2 and then follow a procedure which then makes it possible to upload PIV certificates to the API. However this API currently exclusively supports Yubikey from Yubico and no other Smart Cards.

The unsigned XML Claims File:

<?xml version="1.0"?>
<claim xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.[THE-API].net/claim/"
    xsi:schemaLocation="http://www.[THE-API].ch/claim/ file:/C:/path/to/claim.xsd">
    <domain>[THE-API]</domain>
    <canton>ZH</canton>
    <authTokens>
        <authToken>22291119-8888-8361-12h6-fhfaw1g2c666</authToken>
    </authTokens>
</claim>

The signed XML Claims File:

<?xml version="1.0"?>
<claim xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.[THE-API].net/claim/"
    xsi:schemaLocation="http://www.[THE-API].ch/claim/ file:/C:/path/to/claim.xsd">
    <domain>[THE-API]</domain>
    <canton>ZH</canton>
    <authTokens>
        <authToken>22291119-8888-8361-12h6-fhfaw1g2c666</authToken>
    </authTokens>
Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
        <DigestValue>zL9#Rg&5QwPfS!v2XcY@p7qG%mK4lEh$A3oT8u6bM+dH1eZsF</DigestValue>
      </Reference>
    </SignedInfo> <SignatureValue>Kpdz5AVasU6nOMaFrhxHbGT8Dj1WcmvJItEZC92ilRFNusmqDxPwSvzxKrgH6JQeOTuzI7A51LGE8Dh7plgnOKw5uoBEdKPLMQd9TMyjvzeVZ6sOjAcHx1o7oavkRJi8RWuLp2YRr2iozOJ03hMz6aa6DP10b5HOLqbdwwXouHdKzqz6NNeEIRnY6vF2v4UsUtxyXj5jrUjsw3cOfIjRzVRBfzUUT8yYpmqfNNFWLokkxz3s9Vd06yex3kryMbs3QQ3ynDf8gBWzBke6wbB6Xn3MihsCT6xZ4DG2fhcaTEfMDCe66mBpTkEz2vbgBbcEfC1LbQzCjXNoVdYiQcwwLV2ukvXpB1WHTf6XxXVFAJQMs88j7nQFFkl4vQMy25asqEwM==</SignatureValue>
    <KeyInfo>
      <X509Data>
        <X509SubjectName>CN=Max Mustermann, G=Max, SN=Mustermann, O=Paper Company, OID.1.1.1.11=CHE-111.111.111, C=CH</X509SubjectName>
        <X509Certificate>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</X509Certificate>
      </X509Data>
      <KeyValue>
        <RSAKeyValue>
       <Modulus>Fzbsmd8Qt0R9Ko4O6UVvn3EnrDaTGW1g0udvbQi7Ozuk17kC8T6oMbBb95AYwtONxJ4j5v6Bbaxwslpxu2NpNVH6O8I5S6gRLK1xofa4AnHehCEmUaetjmVOTBZuR5vTc8W3WdP6ZSkW2wiYxBWT5bGogQdYlqNRp5T9NhPlZfchKpYmamLdHzY7LbyRVhNKKYovmvxXKOD1eIitINp7ng5tt6sOlWKhOExQ3G8pJq9A8QhNEhSLeEWUjY0K8lkRQAZ0PeVnhxGr2GcH1f6MDRrJucC3OTaNXftOSzX5umfEwURITw98yHhs8EMg6ND4YRo6C3NBz7qYqYnViJz4skLobbnq6I6pIL4xg2yp37bWmbH4Qqorh6lnZmmEeXcNMMEVcJ9Z9Qab5zIfAvkJWw8vb8yUhDQVq9EKJL3G4dDbF5IvbdwdBEzUvKJ8Bp2gEJMSI7FjjvUoXxqJhz4pAfTbozprOw1YFpZc==</Modulus>
          <Exponent>AQAB</Exponent>
        </RSAKeyValue>
      </KeyValue>
    </KeyInfo>
  </Signature>
</claim>

Don't be surprised about the weird looking characters. I replaced them with random characters for privacy.

Now here is how the unsigned XML Claims File gets signed:

  1. Check the Test-Method a bit lower. It accesses the methods from the XMLSign.cs Class. So I execute the Sign_XML_With_Certificate() Test Method.
  2. Then it accesses the Windows Certificate Store and gets the certificate of the Smart Card.
  3. Now the problem is the PIN Prompt only appears with the SafeNet eToken 5110 Series Smart Card. Next the PIN Prompt opens up which you can see as an image in the next step.
  4. PIN Prompt for SafeNet eToken 5110 Series
  5. If the PIN is correct the signed XML Claims File gets saved in the path that was declared in the signedClaimsLocation string variable inside the Sign_XML_With_Certificate() Test Method. You can check out how a signed Claims File looks like when you scroll a bit further up in this post.
  6. You can check out how a signed XML Claims File looks a bit further up in this post.

The Problem: This doesn't work with the Yubikey from Yubico. When I execute the Test Method no prompt appears that asks for the PIN of the Yubikey. Then an exception occurs which basically says that it couldn't get the private key of course.

Now when we try to send the signed XML Claims file to the API we get an error back that says that the signature could not be verified of course.

XMLSign.cs

namespace XMLSignModule
{
    public class XMLSign
    {
        public static void SignXmlFile(string fileName, string signedFileName, string certificateSubject)
        {
            // Load the XML file.
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.PreserveWhitespace = true;
            xmlDoc.Load(fileName);

            // Load the certificate.
            X509Certificate2 cert = GetCertificateBySubject(certificateSubject);

            // Sign the XML document.
            SignedXml signedXml = new SignedXml(xmlDoc);
            signedXml.SigningKey = cert.PrivateKey;
            Reference reference = new Reference();
            reference.Uri = "";
            signedXml.AddReference(reference);
            signedXml.KeyInfo = new KeyInfo();
            signedXml.KeyInfo.AddClause(new KeyInfoX509Data(cert));

            // Compute the signature and add it to the XML document.
            signedXml.ComputeSignature();
            xmlDoc.DocumentElement?.AppendChild(xmlDoc.ImportNode(signedXml.GetXml(), true));

            // Save the signed XML document.
            xmlDoc.Save(signedFileName + "_" + DateTime.Now.ToString("yyyyMMddHHmmssfff") + ".xml");
        }

        private static X509Certificate2 GetCertificateBySubject(string certificateSubject)
        {
            X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
            store.Open(OpenFlags.ReadOnly);

            X509Certificate2Collection certCollection = store.Certificates;
            X509Certificate2Enumerator enumerator = certCollection.GetEnumerator();

            while (enumerator.MoveNext())
            {
                X509Certificate2 cert = enumerator.Current;

                if (cert != null && cert.Subject == certificateSubject)
                {
                    return cert;
                }
            }
            throw new Exception("The certificate was not found");
        }
    }
}

Test Method

[TestMethod()]
public void Sign_XML_With_Certificate()
{
    const string claimsLocation = @"C:\DEV\TestXML\unsigned_claims.xml";
    const string signedClaimsLocation = @"C:\DEV\TestXML\signed_claims";
    const string certificateSubject = "CN=Max Mustermann, G=Max, SN=Mustermann, O=Paper Company, OID.1.1.1.11=CHE-111.111.111, C=CH";
            
    XMLSign.SignXmlFile(claimsLocation, signedClaimsLocation, certificateSubject);
}

Since the API currently only supports the YubiKey from Yubico my main question is how can I sign the XML File successfully with YubiKey? A hybrid solution by using both Smart Cards would work, however I want to do it only with the YubiKey. The YubiKey supports generating self-signed certificates and also CSR to sign to a Certificate Authority. I can also export the certificate from SafeNet eToken 5110 Series and import it to the YubiKey. The YubiKey Manager allows to generate PIV certificates for signatures.

So a quick fix would be importing the certificate from the SafeNet eToken 5110 Series to the YubiKey from Yubico and register this certificate with the YubiKey on the API. And signing files would be done with the SafeNet eToken 5110 Series. However we want to have a solution where we only need the YubiKey from Yubico.

The main problem is that I can't sign the XML Claims File with the YubiKey from Yubico. The PIN prompt just doesn't appear and I can't get access to the private key.

Also there is a Yubico .NET SDK which I tried to use, however I didn't come far.

Does anyone know how I can sign the unsigned XML Claims File with the YubiKey from Yubico. Ideally on the same way like I described it for the SafeNet eToken 5110 Series Smart Key.

AztecCodes
  • 1,130
  • 7
  • 23

1 Answers1

0

The Solution

I could find a solution where you have to enter the PIN as a parameter in the code and it uses the first Yubikey that appears. But it works perfectly fine. If anyone has a better idea or wants to optimize the code, feel free to do it.

Here is my method to sign with Yubikey by Yubico:

Code:

public XmlDocument SignYubikeyClaimsDocument(XmlDocument claimsDocument, X509Certificate2 cert, string pin)
    {
        Pkcs11InteropFactories factories = new Pkcs11InteropFactories();
        using (IPkcs11Library pkcs11Library = factories.Pkcs11LibraryFactory.LoadPkcs11Library(factories, @"C:\Program Files (x86)\OpenSC Project\OpenSC\pkcs11\opensc-pkcs11.dll", AppType.SingleThreaded))
        {
            // Find the YubiKey slot
            ISlot slot = pkcs11Library.GetSlotList(SlotsType.WithTokenPresent).FirstOrDefault();
            
            if (slot == null)
            {
                throw new Exception("YubiKey not found");
            }

            // Open a session with the YubiKey from Yubico
            using (ISession session = slot.OpenSession(SessionType.ReadOnly))
            {
                // Login (if required)
                session.Login(CKU.CKU_USER, pin);

                // Find the private key and perform signing
                // Define search template for private key
                List<IObjectAttribute> searchTemplate = new List<IObjectAttribute>
                {
                    factories.ObjectAttributeFactory.Create(CKA.CKA_CLASS, CKO.CKO_PRIVATE_KEY),
                    factories.ObjectAttributeFactory.Create(CKA.CKA_KEY_TYPE, CKK.CKK_RSA),
                    // Add other attributes to match the specific key if necessary
                };

                // Find the private key
                List<IObjectHandle> foundObjects = session.FindAllObjects(searchTemplate);
                if (foundObjects.Count == 0)
                {
                    throw new Exception("Private key not found");
                }
                
                IObjectHandle privateKeyHandle = foundObjects[0];
                YubiKeyRSA yubiKeyRsa = new YubiKeyRSA(session, privateKeyHandle, factories);
                SignedXml signedXml = new SignedXml(claimsDocument);
                signedXml.SigningKey = yubiKeyRsa;

                // Creating the SignedInfo Element
                signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigCanonicalizationUrl;

                Reference reference = new Reference("");
                reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());

                signedXml.AddReference(reference);
                signedXml.KeyInfo = new KeyInfo();
                signedXml.KeyInfo.AddClause(new KeyInfoX509Data(cert));

                // Compute the Signature and add it to the Document
                signedXml.ComputeSignature();
                XmlElement xmlDigitalSignature = signedXml.GetXml();
                claimsDocument.DocumentElement?.AppendChild(
                    claimsDocument.ImportNode(xmlDigitalSignature, true));


                // Logout and close the session
                session.Logout();
            }
        }

        return claimsDocument; // Modify to include the signature
    }

For any questions about this code or Yubikey Signatures in general, feel free to contact me.

AztecCodes
  • 1,130
  • 7
  • 23