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:
Package opening would work like this:
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:
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.
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:
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:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With