Solución:
X509Certificate2
carga la clave privada del archivo pfx en el Proveedor criptográfico mejorado de Microsoft v1.0 (tipo de proveedor 1
alias PROV_RSA_FULL
) que no es compatible con SHA-256.
Los proveedores criptográficos basados en CNG (introducidos en Vista y Server 2008) admiten más algoritmos que los proveedores basados en CryptoAPI, pero el código .NET todavía parece funcionar con clases basadas en CryptoAPI como RSACryptoServiceProvider
en vez de RSACng
así que tenemos que solucionar estas limitaciones.
Sin embargo, otro proveedor de CryptoAPI, Proveedor criptográfico RSA y AES mejorado de Microsoft (tipo de proveedor 24
alias PROV_RSA_AES
) es compatible con SHA-256. Entonces, si obtenemos la clave privada en este proveedor, podemos iniciar sesión con ella.
Primero, tendrás que ajustar tu X509Certificate2
constructor para permitir que la clave sea exportada fuera del proveedor que X509Certificate2
lo pone agregando el X509KeyStorageFlags.Exportable
bandera:
X509Certificate2 cert = new X509Certificate2(
@"location of pks file", "password",
X509KeyStorageFlags.Exportable);
Y exporta la clave privada:
var exportedKeyMaterial = cert.PrivateKey.ToXmlString(
/* includePrivateParameters = */ true);
Entonces crea un nuevo RSACryptoServiceProvider
instancia para un proveedor que admita SHA-256:
var key = new RSACryptoServiceProvider(
new CspParameters(24 /* PROV_RSA_AES */));
key.PersistKeyInCsp = false;
E importe la clave privada en él:
key.FromXmlString(exportedKeyMaterial);
Cuando hayas creado tu SignedXml
ejemplo, dile que use key
en vez de cert.PrivateKey
:
signedXml.SigningKey = key;
Y ahora funcionará.
Aquí está la lista de tipos de proveedores y sus códigos en MSDN.
Aquí está el código completo ajustado para su ejemplo:
CryptoConfig.AddAlgorithm(typeof(RSAPKCS1SHA256SignatureDescription), "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
X509Certificate2 cert = new X509Certificate2(@"location of pks file", "password", X509KeyStorageFlags.Exportable);
// Export private key from cert.PrivateKey and import into a PROV_RSA_AES provider:
var exportedKeyMaterial = cert.PrivateKey.ToXmlString( /* includePrivateParameters = */ true);
var key = new RSACryptoServiceProvider(new CspParameters(24 /* PROV_RSA_AES */));
key.PersistKeyInCsp = false;
key.FromXmlString(exportedKeyMaterial);
XmlDocument doc = new XmlDocument();
doc.PreserveWhitespace = true;
doc.Load(@"input.xml");
SignedXml signedXml = new SignedXml(doc);
signedXml.SigningKey = key;
signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
//
// Add a signing reference, the uri is empty and so the whole document
// is signed.
Reference reference = new Reference();
reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
reference.AddTransform(new XmlDsigExcC14NTransform());
reference.Uri = "";
signedXml.AddReference(reference);
//
// Add the certificate as key info, because of this the certificate
// with the public key will be added in the signature part.
KeyInfo keyInfo = new KeyInfo();
keyInfo.AddClause(new KeyInfoX509Data(cert));
signedXml.KeyInfo = keyInfo;
// Generate the signature.
signedXml.ComputeSignature();
Ya se ha dado como respuesta exportar y volver a importar, pero hay un par de otras opciones que debe conocer.
1. Utilice GetRSAPrivateKey y .NET 4.6.2 (actualmente en versión preliminar)
El método GetRSAPrivateKey (extensión) devuelve una instancia RSA del “mejor tipo disponible” para la clave y la plataforma (a diferencia de la propiedad PrivateKey que “todos saben” devuelve RSACryptoServiceProvider).
En el 99,99 (etc.)% de todas las claves privadas RSA, el objeto devuelto por este método es capaz de generar la firma SHA-2.
Si bien ese método se agregó en .NET 4.6 (.0), el requisito de 4.6.2 existe en este caso porque la instancia RSA devuelta por GetRSAPrivateKey no funcionó con SignedXml. Eso ha sido arreglado desde entonces (162556).
2. Vuelva a abrir la clave sin exportar
Personalmente, no me gusta este enfoque porque usa la propiedad PrivateKey (ahora heredada) y la clase RSACryptoServiceProvider. Pero tiene la ventaja de funcionar en todas las versiones de .NET Framework (aunque no en .NET Core en sistemas que no son Windows, ya que RSACryptoServiceProvider es solo para Windows).
private static RSACryptoServiceProvider UpgradeCsp(RSACryptoServiceProvider currentKey)
{
const int PROV_RSA_AES = 24;
CspKeyContainerInfo info = currentKey.CspKeyContainerInfo;
// WARNING: 3rd party providers and smart card providers may not handle this upgrade.
// You may wish to test that the info.ProviderName value is a known-convertible value.
CspParameters cspParameters = new CspParameters(PROV_RSA_AES)
{
KeyContainerName = info.KeyContainerName,
KeyNumber = (int)info.KeyNumber,
Flags = CspProviderFlags.UseExistingKey,
};
if (info.MachineKeyStore)
{
cspParameters.Flags |= CspProviderFlags.UseMachineKeyStore;
}
if (info.ProviderType == PROV_RSA_AES)
{
// Already a PROV_RSA_AES, copy the ProviderName in case it's 3rd party
cspParameters.ProviderName = info.ProviderName;
}
return new RSACryptoServiceProvider(cspParameters);
}
Si ya tiene cert.PrivateKey emitido como RSACryptoServiceProvider, puede enviarlo a través de UpgradeCsp. Dado que esto es abrir una clave existente, no habrá material adicional escrito en el disco, usa los mismos permisos que la clave existente y no requiere que hagas una exportación.
Pero (¡CUIDADO!) NO establezca PersistKeyInCsp = false, porque eso borrará la clave original cuando se cierre el clon.
Si se encuentra con este problema después de actualizar a .Net 4.7.1 o superior:
.Net 4.7 y por debajo:
SignedXml signedXml = new SignedXml(doc);
signedXml.SigningKey = cert.PrivateKey;
.Net 4.7.1 y superior:
SignedXml signedXml = new SignedXml(doc);
signedXml.SigningKey = cert.GetRSAPrivateKey();
Créditos para Vladimir Kocjancic