using System;
using System.Text;
using System.Diagnostics;
using System.IO;
using Sc14n;
using Pki = CryptoSysPKI;
/*
* $Id: TestSc14nPki.cs $
* Last updated:
* $Date: 2019-12-13 17:51 $
* $Version: 2.1.0 $
*/
/* Some tests using the SC14N .NET interface with CryptoSys PKI.
*
* Requires `Sc14n` and `CryptoSys PKI` to be installed on your system,
* Available from <http://cryptosys.net/sc14n/> and <http://cryptosys.net/pki>,
* repectively.
* Add references to .NET libraries `diSc14nNet.dll` and `diCrSysPKINet.dll`.
* Note we've used "Pki" as an alias for "CryptoSysPKI" to save typing.
*
* Test files, e.g. `olamundo.xml`, are in `sc14n-testfiles.zip`. These must be in the CWD.
*
* This is a Console Application written for target .NET Framework 2.0 and above
* Please report any bugs to <http://cryptosys.net/contact/>
*/
/******************************* LICENSE ***********************************
* Copyright (C) 2017-19 David Ireland, DI Management Services Pty Limited.
* All rights reserved. <https://di-mgt.com.au> <https://cryptosys.net>
* The code in this module is licensed under the terms of the MIT license.
* For a copy, see <http://opensource.org/licenses/MIT>
****************************************************************************
*/
namespace TestSc14nPKI
{
class TestSc14nPKI
{
static void Main(string[] args)
{
// If either of these fail, the package is not installed properly...
Console.WriteLine("Sc14n Version={0}", Sc14n.Gen.Version());
Console.WriteLine("CrPKI Version={0}", Pki.General.Version());
string fname, oname;
int n;
bool isLatin1;
// With .NET we need to "know" the encoding of the input data.
// Input XML is ISO-8859-1 encoded (aka Latin-1)
fname = "olamundo-base.xml";
oname = "olamundo-new-signed.xml";
isLatin1 = true;
Console.WriteLine("FILE: {0}", fname);
n = MakeSignedXml(oname, fname, myPriKey, myPassword, isLatin1);
Console.WriteLine("MakeSignedXml->'{0}' returns {1} (expecting 0)", oname, n);
Debug.Assert(0 == n);
// Input XML contains Chinese characters UTF-8-encoded
fname = "daiwei-base.xml";
oname = "daiwei-new-signed.xml";
isLatin1 = false;
Console.WriteLine("FILE: {0}", fname);
n = MakeSignedXml(oname, fname, myPriKey, myPassword, isLatin1);
Console.WriteLine("MakeSignedXml->'{0}' returns {1} (expecting 0)", oname, n);
Debug.Assert(0 == n);
// Input XML contains Chinese characters as character entities
// Note that digest value and signature value should be identical to previous one
fname = "daiwei-ents-base.xml";
oname = "daiwei-ents-new-signed.xml";
isLatin1 = false;
Console.WriteLine("FILE: {0}", fname);
n = MakeSignedXml(oname, fname, myPriKey, myPassword, isLatin1);
Console.WriteLine("MakeSignedXml->'{0}' returns {1} (expecting 0)", oname, n);
Debug.Assert(0 == n);
Console.WriteLine("\nALL DONE.");
}
/// <summary>
/// Create a XML-DSIG signed file given proforma XML document
/// </summary>
/// <param name="outFile">Name of outfile to create</param>
/// <param name="baseFile">Name of input XML document</param>
/// <param name="priKey">PKCS8 encrypted private key file or PEM-string</param>
/// <param name="password">Password for private key</param>
/// <param name="isLatin1">Set true if file is known to be Latin-1 encoded (ISO-8859-1) or false if UTF-8 or US-ASCII</param>
/// <returns>Zero (0) on success otherwise nonzero error code (an integer cast of <see cref="MSXerror"/> enum)</returns>
/// <remarks>Input XML document is expected to be enveloped-signature with single reference URI="",
/// C14N method REC-xml-c14n-20010315, signature method xmldsig#rsa-sha1, and digest method xmldsig#sha1.
/// KeyValue is expected to be in RSAKeyValue form.
/// Items to be replaced should be marked "%digval%", "%sigval%" and "%keyval%".
/// </remarks>
public static int MakeSignedXml(string outFile, string baseFile, string priKey, string password, bool isLatin1)
{
byte[] b, dataIn, dataOut;
string s, xmlStr, newStr;
string digval, digval_si, sigval, keyval;
int status;
// Compute digest value of body excluding <Signature> element
// (this assumes Reference URI="" and DigestMethod is SHA-1)
digval = C14n.ToDigest(baseFile, "Signature", Tran.OmitByTag, DigAlg.Sha1);
Debug.WriteLine("DIGVAL={0}", digval);
if (digval.Length == 0) {
return (int)MSXerror.TransformExclSignatureFailed;
}
// Extract the SignedInfo element into memory
// Note %digval% parameter to be completed
b = C14n.ToBytes(baseFile, "SignedInfo", Tran.SubsetByTag);
if (b.Length == 0) {
return (int)MSXerror.TransformSignedInfoFailed;
}
Debug.WriteLine("SIGNEDINFO (BASE):");
Debug.WriteLine(System.Text.Encoding.UTF8.GetString(b));
// Insert the required DigestValue we prepared earlier
// Note the SignedInfo element is *always* US-ASCII encoded,
// so we can safely use the more convenient String.Replace function
s = System.Text.Encoding.UTF8.GetString(b).Replace("%digval%", digval);
Debug.WriteLine("SIGNEDINFO (COMPLETED):");
Debug.WriteLine(s);
// Now compute the digest value of this string
digval_si = C14n.ToDigest(System.Text.Encoding.UTF8.GetBytes(s), DigAlg.Sha1);
Debug.WriteLine("SHA1(signedinfo)= {0}", digval_si);
// Compute signature value from this digest value
sigval = SigValFromDigVal(digval_si, priKey, myPassword);
Debug.WriteLine("SIG= {0}", sigval);
// Get the RSA Key Value in required XML form
keyval = KeyValFromCert(priKey);
// Now compose the output file by substituting the correct values
// (Note we make no other checks of the input XML - that's up to you)
// Read in base XML file as a byte array
dataIn = ReadABinaryFile(baseFile);
if (dataIn.Length == 0) return (int)MSXerror.ReadFileFailed;
// Convert to a string so we can use String.Replace
// We need to know the encoding
if (isLatin1)
xmlStr = System.Text.Encoding.GetEncoding("ISO-8859-1").GetString(dataIn);
else
xmlStr = System.Text.Encoding.UTF8.GetString(dataIn);
Debug.WriteLine(xmlStr);
newStr = xmlStr.Replace("%digval%", digval).Replace("%sigval%", sigval).Replace("keyval", keyval);
// Convert back to bytes then write out file
if (isLatin1)
dataOut = System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(newStr);
else
dataOut = System.Text.Encoding.UTF8.GetBytes(newStr);
status = (WriteABinaryFile(outFile, dataOut) ? 0 : (int)MSXerror.WriteFileFailed);
return status;
}
/// <summary>
/// Error codes for MakeSignedXml
/// </summary>
public enum MSXerror
{
OkSuccess = 0,
WriteFileFailed,
ReadFileFailed,
TransformExclSignatureFailed,
TransformSignedInfoFailed,
}
//**********************
// PKI HELPER FUNCTIONS
//**********************
/// <summary>
/// Compute the signature value from digest value.
/// </summary>
/// <param name="digval">Base64-encoded digest value of data to be signed</param>
/// <param name="priKey">PKCS8 encrypted private key file or PEM-string</param>
/// <param name="password">Password for private key</param>
/// <returns>Base64-encoded signature value or empty string on error</returns>
public static string SigValFromDigVal(string digval, string priKey, string password)
{
string sigval = Pki.Sig.SignDigest(Pki.Cnv.FromBase64(digval), priKey, password, Pki.SigAlgorithm.Rsa_Sha1);
return sigval;
}
/// <summary>
/// Extract XML-style RSAKeyValue from X.509 certificate.
/// </summary>
/// <param name="cert">X.509 certificate file or PEM string</param>
/// <returns>RSAKeyValue as a string or empty string on error</returns>
public static string KeyValFromCert(string cert)
{
string keyval = Pki.Rsa.ToXMLString(Pki.Rsa.ReadPublicKey(cert).ToString(), 0);
return keyval;
}
/// <summary>
/// Extract XML-style RSAKeyValue from RSA private key.
/// </summary>
/// <param name="priKey">PKCS8 encrypted private key file or PEM-string</param>
/// <param name="password">Password for private key</param>
/// <returns>RSAKeyValue as a string or empty string on error</returns>
public static string KeyValFromPriKey(string priKey, string password)
{
// CAUTION: make sure you exclude the private key parameters here
string keyval = Pki.Rsa.ToXMLString(Pki.Rsa.ReadPrivateKey(priKey, password).ToString(), Pki.Rsa.XmlOptions.ExcludePrivateParams);
return keyval;
}
/// <summary>
/// Return true if private key and certificate are matched.
/// </summary>
/// <param name="priKey">PKCS8 encrypted private key file or PEM-string</param>
/// <param name="password">Password for private key</param>
/// <param name="cert">X.509 certificate file or PEM string</param>
/// <returns>true if private key and certificate are matched or false if not</returns>
public static bool IsKeyAndCertMatch(string priKey, string password, string cert)
{
int n = Pki.Rsa.KeyMatch(Pki.Rsa.ReadPrivateKey(priKey, password), Pki.Rsa.ReadPublicKey(cert));
return (0 == n);
}
// HARD-CODED PRIVATE KEY AND CERTIFICATE (FOR OUR CONVENIENCE IN TESTING)
// Alice's PKCS8 encrypted key and X.509 certificate
// from RFC 4134 "Examples of S/MIME Messages"
// Private key password is "password"
private const string myPassword = "password"; // High security practice here!!
private const string myPriKey = @"-----BEGIN ENCRYPTED PRIVATE KEY-----
MIICojAcBgoqhkiG9w0BDAEDMA4ECFleZ90vhGrRAgIEAASCAoA9rti16XVH
K4AJVe1CNf61NIpIogu/Xs4Yn4hXflvewiOwe6/9FkxBXLbhKdbQWn1Z4p3C
njVns2VYEO/qpJR3LciHMwp5dsqedUVVia//CqFHtEV9WfvCKWgmlkkT1YEm
1aChZnPP5i6IhwVT9qvFluTZhvVmjW0YyF86OrOp0uxxVic7phPbnPrOMelf
ZPc3A3EGpzDPkxN+o0obw87tUgCL+s0KtUOr3c6Si4KQ3IQjrjZxQF4Se3t/
4PEpqUl5EpYiCx9q5uqb0Lr1kWiiQ5/inZm5ETc+qO+ENcp0KjnX523CATYd
U5iOjl/X9XZeJrMpOCXogEuhmLPRauYP1HEWnAY/hLW93v10QJXY6ALlbkL0
sd5WU8Ces7T04b/p4/12yxqYqV68QePyfHpegdraDq3vRfopSwrUxtL9cisP
jsQcJ5FL/SfloFbmld4CKIjMsromsEWqo6rfo3JqNizgTVIIWExy3jDT9VvK
d9ADH0g3JCbuFzaWVOZMmZ0wlo28PKkLQ8FkW8CG/Lq/Q/bHLPM+sPdLN+ke
gpA6fvL4wpku4ST7hmeN1vWbRLlCfuFijux77hdM7knO9/MawICsA4XdzR78
p0C2hJlc6p46IWZaINQXGstTbJMh+mJ7i1lrbG2kvZ2Twf9R+RaLp2mPHjb1
+P+3f2L3tOoC31oJ18u/L1MXEWxLEZHB0+ANg+N/0/icwImcI0D+wVN2puU4
m58j81sGZUEAB3aFEbPxoX3y+qYlOnt1OfdY7WnNdyr9ZzI09fkrTvujF4LU
nycqE+MXerf0PxkNu1qv9bQvCoH8x3J2EVdMxPBtH1Fb7SbE66cNyh//qzZo
B9Je
-----END ENCRYPTED PRIVATE KEY-----";
private const string myCert = @"-----BEGIN CERTIFICATE-----
MIICLDCCAZWgAwIBAgIQRjRrx4AAVrwR024uxBCzsDANBgkqhkiG9w0BAQUFADAS
MRAwDgYDVQQDEwdDYXJsUlNBMB4XDTk5MDkxOTAxMDg0N1oXDTM5MTIzMTIzNTk1
OVowEzERMA8GA1UEAxMIQWxpY2VSU0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
AoGBAOCJczmN2PX16Id2OX9OsAW7U4PeD7er3H3HdSkNBS5tEt+mhibU0m+qWCn8
l+z6glEPMIC+sVCeRkTxLLvYMs/GaG8H2bBgrL7uNAlqE/X3BQWT3166NVbZYf8Z
f8mB5vhs6odAcO+sbSx0ny36VTq5mXcCpkhSjE7zVzhXdFdfAgMBAAGjgYEwfzAM
BgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIGwDAfBgNVHSMEGDAWgBTp4JAnrHgg
eprTTPJCN04irp44uzAdBgNVHQ4EFgQUd9K00bdMioqjzkWdzuw8oDrj/1AwHwYD
VR0RBBgwFoEUQWxpY2VSU0FAZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADgYEA
PnBHqEjME1iPylFxa042GF0EfoCxjU3MyqOPzH1WyLzPbrMcWakgqgWBqE4lradw
FHUv9ceb0Q7pY9Jkt8ZmbnMhVN/0uiVdfUnTlGsiNnRzuErsL2Tt0z3Sp0LF6DeK
tNufZ+S9n/n+dO/q+e5jatg/SyUJtdgadq7rm9tJsCI=
-----END CERTIFICATE-----";
//*****************
// FILE UTILITIES *
//*****************
static byte[] ReadABinaryFile(string fileName)
{
byte[] b = new byte[0];
FileInfo finfo = new FileInfo(fileName);
if (finfo.Exists)
{
FileStream fsi = finfo.OpenRead();
BinaryReader br = new BinaryReader(fsi);
int count = (int)fsi.Length;
b = br.ReadBytes(count);
br.Close();
fsi.Close();
}
Debug.Assert(finfo.Exists, "File '" + fileName + "' does not exist.");
return b;
}
static bool WriteABinaryFile(string fileName, byte[] data)
{
FileStream fs;
BinaryWriter bw;
fs = new FileStream(fileName, FileMode.Create, FileAccess.Write);
bw = new BinaryWriter(fs);
bw.Write(data);
bw.Close();
fs.Close();
return true;
}
}
}