/// <summary> /// Encrypts the entire XML document and digitally signs it. /// </summary> /// <param name="container"></param> /// <param name="document">The document to encrypt and sign. It will be modified in-place by this method.</param> /// <param name="encryptFor">Certificate of the document's recipient. Must contain RSA public key for a 2048-bit key pair or larger.</param> /// <param name="signWith">Certificate of the document's signer. Must be linked to RSA private key at least 2048 bits in length.</param> /// <remarks> /// The XML document may be left in a corrupted state if an exception is thrown. /// </remarks> public static void EncryptAndSign(this HelpersContainerClasses.ProtectedXml container, XmlDocument document, X509Certificate2 encryptFor, X509Certificate2 signWith) { Helpers.Argument.ValidateIsNotNull(document, nameof(document)); Helpers.Argument.ValidateIsNotNull(encryptFor, nameof(encryptFor)); Helpers.Argument.ValidateIsNotNull(signWith, nameof(signWith)); VerifyCertificateIsSaneAndUsable(encryptFor); VerifyCertificateAndPrivateKeyIsSaneAndUsable(signWith); // Part 1: encrypt. Default settings are secure and nice, surprisingly. var encryptor = new EncryptedXml(); var encryptedData = encryptor.Encrypt(document.DocumentElement, encryptFor); EncryptedXml.ReplaceElement(document.DocumentElement, encryptedData, false); // Part 2: sign. using (var signingKey = signWith.GetRSAPrivateKey()) { var signedXml = new SignedXml(document) { SigningKey = signingKey }; var whatToSign = new Reference { // The entire document is signed. Uri = "", // A nice strong algorithm without known weaknesses that are easily exploitable. DigestMethod = Sha512Algorithm }; // This signature (and other signatures) are inside the signed data, so exclude them. whatToSign.AddTransform(new XmlDsigEnvelopedSignatureTransform()); signedXml.AddReference(whatToSign); // A nice strong algorithm without known weaknesses that are easily exploitable. signedXml.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA512Url; // Canonical XML 1.0 (omit comments); I suppose it works fine, no deep thoughts about this. signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigCanonicalizationUrl; // Signer certificate must be delivered with the signature. signedXml.KeyInfo.AddClause(new KeyInfoX509Data(signWith)); // Ready to sign! Let's go! signedXml.ComputeSignature(); // Now stick the Signature element it generated back into the document and we are done. var signature = signedXml.GetXml(); document.DocumentElement.AppendChild(document.ImportNode(signature, true)); } }
/// <summary> /// Decrypts an XML document encrypted using <see cref="EncryptAndSign"/> after verifying the digital signature on it. /// </summary> /// <param name="container"></param> /// <param name="document">The encrypted and signed document. It will be modified in-place by this method.</param> /// <param name="signedBy">The certificate of the document's signer will be output into this variable.</param> /// <param name="recipientCertificates">The set of certificates whose key pairs may be used for decryption. The correct certificate and key pair will be selected automatically (if it is among the set).</param> /// <exception cref="CryptographicException"> /// Thrown if a cryptographic operation fails (e.g. because you do not have the correct decryption key). /// </exception> /// <remarks> /// The XML document may be left in a corrupted state if an exception is thrown. /// </remarks> public static void VerifyAndDecrypt(this HelpersContainerClasses.ProtectedXml container, XmlDocument document, out X509Certificate2 signedBy, params X509Certificate2[] recipientCertificates) { Helpers.Argument.ValidateIsNotNull(document, nameof(document)); Helpers.Argument.ValidateIsNotNull(recipientCertificates, nameof(recipientCertificates)); foreach (var certificate in recipientCertificates) { if (certificate == null) { throw new ArgumentException("Recipient certificate list cannot contain null values.", nameof(recipientCertificates)); } VerifyCertificateAndPrivateKeyIsSaneAndUsable(certificate); } var namespaces = new XmlNamespaceManager(document.NameTable); namespaces.AddNamespace("ds", XmlDigitalSignatureNamespace); namespaces.AddNamespace("enc", XmlEncryptionNamespace); if (document.SelectSingleNode("/enc:EncryptedData", namespaces) == null) { throw new ArgumentException("The document is not an encrypted XML document.", nameof(document)); } var signatureNodes = document.SelectNodes("/enc:EncryptedData/ds:Signature", namespaces); if (signatureNodes.Count != 1) { throw new ArgumentException("The document not carry exactly 1 XML digital signature.", nameof(document)); } // Verify signature. var signatureNode = (XmlElement)signatureNodes[0]; var signedXml = new SignedXml(document); signedXml.LoadXml(signatureNode); if (!signedXml.CheckSignature()) { throw new SecurityException("Signature failed to verify - the XML document has been tampered with!"); } var referenceUris = signedXml.SignedInfo.References.Cast <Reference>().Select(r => r.Uri).ToArray(); // It must be a whole-document signature. if (referenceUris.Length != 1 || referenceUris.Single() != "") { throw new SecurityException("The digital signature was not scoped to the entire XML document."); } // The signature must include a certificate for the signer in order to be categorized. var certificateElement = signatureNode.SelectSingleNode("ds:KeyInfo/ds:X509Data/ds:X509Certificate", namespaces); if (certificateElement == null) { throw new SecurityException("The digital signature did not contain the certificate of the signer."); } signedBy = new X509Certificate2(Convert.FromBase64String(certificateElement.InnerText)); // We do not accept signatures from weak certificates. VerifyCertificateIsSaneAndUsable(signedBy); // Decrypt. var decryptor = new EncryptedXmlWithCustomDecryptionCertificates(document) { DecryptionCertificates = recipientCertificates }; decryptor.DecryptDocument(); }