Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Standardized approach to digital signatures of files via .NET

I am building a system for distributing packages (.zip archives) created by different organizations. I'd like a way to verify that the publisher of a package is indeed who they claim to be, and that the file has not been tampered with.

To verify the publisher, a system similar to what is used by web browsers is required - e.g., my application contacts the root certificate authorities, who verify the identity. In other words, the 'green bar' :)

I'm guessing the package creation would work like this:

  1. Author creates zip package
  2. Author hashes package and signs the hash
  3. It is re-packaged, with:
    • A header containing the signed hash, and the public certificate
    • A body containing the contents of the zip file

Package opening would work like this:

  1. Take the body of the data
  2. Hash it using the same algorithm
  3. Decrypt the package's hash using the public key from the certificate
  4. Compare the two hashes - we now have integrity
  5. Contact the root CAs to verify the identity

This way, I have verified the identity, and also verified the contents (the contents themselves do not need to be encrypted - the goal is verification, not privacy).

So my questions are:

  1. Is the above the correct way to approach it?
  2. What hashing algorithm do people normally use? I assume it should be one-way. Would you just choose one (MD5, SHA1, SHA2?) or is it more normal to support a variety and let the package author tell you which one they used (e.g., the header of the document contains the name of the hashing function).
  3. How do you work with the root CA's? Is this the job of the X509Store class, or are there additional steps involved?
  4. What kind of certificates are involved here? The same kind of certificates used to sign .NET assemblies? (Code-signing certificates?)

Lastly, if an organization does not have a paid-for certificate and instead decide to use a self-issued certificate, I assume I can still verify the hashes (for the sake of data integrity) without having to install stuff into the computer's certificate stores or any magic like that (in these cases, I'd just display: "Published by XYZ Co. (Unverified)". Is this correct?

I have found plenty of links on how to use the X509 and RSACryptoServiceProvider, so I can probably figure the code out, I guess I'm more interested in the process and knowing I'm using the right techniques.

like image 829
Paul Stovell Avatar asked Jan 30 '09 14:01

Paul Stovell


2 Answers

I tried aku's suggestion of System.IO.Packaging and quite liked it, although getting the signatures to work was quite hard and not intuitive (to my minuscule mind, anyway). Here are the steps I took, in case anyone else needs to do it.

The main issue is the documentation just says "certificates", but I had to create a password protected PFX to make it work. Two useful links are:

  1. MSDN: Digital Signing Framework of the Open Packaging Conventions
  2. Creating self-signed PFX's

As the second link indicates, you have two options for creating the PFX. You can create a .cer, install it and then export it using the GUI, or you can download pvkimport from Microsoft.

Here are the commands I used (edit: you can use OpenSSL to do this - see bottom):

makecert -r -n "CN=Paul Stovell" -b 01/01/2000 -e 01/01/2099 -eku 1.3.6.1.5.5.7.3.3 -sv PaulStovell.pvk PaulStovell.cer
cert2spc PaulStovell.cer PaulStovell.spc
pvkimprt -pfx PaulStovell.spc PaulStovell.pvk

In the last command, a wizard appears. You are asked whether you want to export the private key, which requires the PFX to be password protected. Choose "yes". Then there is a checkbox on the next page asking whether to "Export all extended properties", to which I also chose "yes".

This leaves you with a password protected self-signed PFX file which you can use to sign documents and packages with.

Here is some code to create, sign and save a package, and then reopen and verify it.

private const string _digitalSignatureUri = "/package/services/digital-signature/_rels/origin.psdsor.rels";

static void Main(string[] args)
{
    var certificate = new X509Certificate2(@"T:\Sample\Input\PaulStovell.pfx", "password");
    using (var package = Package.Open("T:\\Sample\\MyPackage.zip", FileMode.Create, FileAccess.ReadWrite, FileShare.None))
    {
        CreatePart(package, @"/Files/File2.dll", @"T:\Sample\Input\File2.dll");
        CreatePart(package, @"/Files/File2.pdb", @"T:\Sample\Input\File2.pdb");
        CreatePart(package, @"/Files/File2.xml", @"T:\Sample\Input\File2.xml");
        package.PackageProperties.Creator = "Paul Stovell";
        package.PackageProperties.Title = "Paul Stovell's Package";
        package.PackageProperties.Description = "My First Package";
        package.PackageProperties.Identifier = "MyPackage";
        package.PackageProperties.Version = "1.0.0.0";

        // Sign the package
        var toSign = package.GetParts().Select(part => part.Uri).ToList();
        var uriPartSignatureOriginRelationship = PackUriHelper.CreatePartUri(new Uri(_digitalSignatureUri, UriKind.Relative));
        toSign.Add(uriPartSignatureOriginRelationship);

        var dsm = new PackageDigitalSignatureManager(package);
        dsm.CertificateOption = CertificateEmbeddingOption.InSignaturePart;
        dsm.Sign(toSign, certificate);

        package.Close();
    }

    Console.WriteLine("Package written");
    Console.WriteLine("Reading package");

    using (var package = Package.Open("T:\\Sample\\MyPackage.zip", FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        Console.WriteLine("  Package name: {0}", package.PackageProperties.Title);
        var dsm = new PackageDigitalSignatureManager(package);
        if (dsm.IsSigned)
        {
            var verificationResult = dsm.VerifySignatures(false);
            var signature = dsm.Signatures[0];
            Console.WriteLine("  Signed by: {0}", signature.Signer.Subject);
            Console.WriteLine("  Issued by: {0}", signature.Signer.Issuer);
            Console.WriteLine("  Verification: {0}", verificationResult);
        }
        else
        {
            Console.WriteLine("  Not signed.");
        }
    }
    Console.ReadKey();
}

private static void CreatePart(Package package, string relativePath, string file)
{
    var packagePartUri = new Uri(relativePath, UriKind.Relative);
    var packagePart = package.CreatePart(packagePartUri, "part/" + Path.GetExtension(file));
    using (var fileContent = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        CopyStream(fileContent, packagePart.GetStream());
    }
}

private static void CopyStream(Stream source, Stream target)
{
    // It is .NET 3.5, surely this kind of thing has gotten easier by now?
    var bufferSize = 0x1000;
    var buf = new byte[bufferSize];
    int bytesRead = 0;
    while ((bytesRead = source.Read(buf, 0, bufferSize)) > 0)
    {
        target.Write(buf, 0, bytesRead);
    }
}

On my machine, the output is:

Package written
Reading package
  Package name: Paul Stovell's Package
  Signed by: CN=Paul Stovell
  Issued by: CN=Paul Stovell
  Verification: Success

The last line is interesting. It is the output of PackageDigitalSignatureManager.VerifySignatures(). It indicates that the document has not been tampered with. If I alter or delete a file after signing, it no longer returns 'Success'.

I copied the .exe, sample files, and the .pfx, to a new machine and got the exact same output, which seems to indicate that "Verify" only checks the author of the signature, and does not ask any certification authorities. I did not install any certificates on the test machine, and it runs on its own domain.

Edit: The code above only verifies the document itself, it does not verify the certificate using the root CA authorities. To do that, use the following:

This call returns false when called against my self-signed certificate:

var verified = ((X509Certificate2) dsm.Signatures[0].Signer).Verify();

Then I double-click the .cer which I used to create the .pfx from, and install it into the default location, so that it becomes a trusted certificate authority. Once that is done, the call above returns true. So that would appear to be the correct way to verify the identity of the signer.

Edit 2: A useful note. To look at and play with the certificates on your machine, do the following:

  1. Start->Run and type "mmc"
  2. In the MMC window, click File->Add/Remove SnapIn...
  3. Click Certificates, Add->
  4. Choose one of the accounts
  5. Hit OK

In my case, when I double clicked the .cer above to install it, it was placed under the Current User store, under the Trusted Root Certification Authorities folder. You can then delete it to undo your changes.

My final verification logic looks like this:

if (dsm.IsSigned)
{
    var verificationResult = dsm.VerifySignatures(false);
    var signature = dsm.Signatures[0];
    var trusted = ((X509Certificate2)dsm.Signatures[0].Signer).Verify();

    Console.WriteLine("  Signed by: {0}", signature.Signer.Subject);
    Console.WriteLine("  Issued by: {0}", signature.Signer.Issuer);
    Console.WriteLine("  Verified: {0}", verificationResult == VerifyResult.Success);
    Console.WriteLine("  Trusted: {0}", trusted);
}
else
{
    Console.WriteLine("  Not signed.");
}

Where it is always verified (unless I tamper with the contents), but only trusted when the certificate is in the store.

Edit 3: I did a little more reading. PFX files are actually PKCS 12 files, part of a specification by the RSA group. An easier way to create them, rather than what I showed above, is to use the open source OpenSSL which has binaries available for Windows.

After installing OpenSSL, create a public/private key pair file. I found I had to run the second command below as an Administrator, so it might pay to launch this one as admin.

openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout PaulStovell.pem -out PaulStovell.cer

It will ask about 7 additional questions about you and your organization to create the certificate. Next, create the PKCS 12. It will ask you for a password:

openssl pkcs12 -export -out PaulStovell.pfx -in PaulStovell.pem -name "Paul Stovell"

Enter and re-enter your password when prompted.

At this point, System.IO.Packaging can use your PFX and verify the package signature, but it will not trust the certificate. To create a .cer certificate, so that you can install it into the trusted certificate authorities store, do the following:

openssl x509 -in PaulStovell.pem -out PaulStovell.cer

Now you can double-click the certificate and install it, and it will be treated as a trusted certificate.

like image 111
Paul Stovell Avatar answered Oct 08 '22 11:10

Paul Stovell


There is a standard API to create signed ZIP packages.

System.IO.Packaging namespace contains necessary classes to create OPS (open packaging specification) conformant ZIP packages with digital signatures.

like image 29
aku Avatar answered Oct 08 '22 11:10

aku