Import RSA CngKey and store in MicrosoftSoftwareKeyStorageProvider - c#

I have an exported RSAParameters private key that I'd like to import into another machine. I can save new keys into the local machine or user containers, but I'm stuck trying to import an existing key.
The code below will generate a new key pair, and I know I could just generate a new key directly into the container - but I want to be able to generate a single key and import that same key into a handful of different computers.
How can I take either an RSAParameters or a string of XML (either one) and import that into a local user (or machine) container?
public async Task<KeyGenerationResult> GenerateNewKeyAsync(int keySize)
{
var csp = new RSACng(keySize);
var privKey = await Task.Run(() => csp.ExportParameters(includePrivateParameters: true));
var pubKey = csp.ExportParameters(includePrivateParameters: false);
var pubKeyString = exportKeyToString(pubKey);
var privKeyString = exportKeyToString(privKey);
return new KeyGenerationResult
{
PrivateKey = privKey,
PublicKey = pubKey,
PrivateKeyCleartext = privKeyString,
PublicKeyCleartext = pubKeyString
};
}
private static string exportKeyToString(RSAParameters key)
{
string keyString;
var sw = new StringWriter();
var xs = new XmlSerializer(typeof(RSAParameters));
xs.Serialize(sw, key);
keyString = sw.ToString();
return keyString;
}
public void SavePrivateKeyToLocalMachine(RSAParameters privateKey, string keyName)
{
//Stuck here. :(
}
CngKey.Import() takes a byte[] and that looks promising, but I haven't been able to find any method to create the byte[] that CngKey.Import() requires.
var d = new RSACryptoServiceProvider();
d.ImportParameters(privateKey);
var keyBlob = d.ExportCspBlob(true);
var key = CngKey.Import(keyBlob, CngKeyBlobFormat.Pkcs8PrivateBlob);
That gets me a byte[] but regardless of what CngKeyBlobFormat I use, I get an exception. I'm stuck.
UPDATE
I found a way to get a byte[] using
var cp = new CngKeyCreationParameters();
cp.KeyUsage = CngKeyUsages.AllUsages;
cp.ExportPolicy = CngExportPolicies.AllowPlaintextExport | CngExportPolicies.AllowExport | CngExportPolicies.AllowArchiving | CngExportPolicies.AllowPlaintextArchiving;
cp.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(keySize), CngPropertyOptions.None));
var key = CngKey.Create(CngAlgorithm.Rsa, null, cp);
var bytes = key.Export(CngKeyBlobFormat.{I have tried them all});
And this code looks like it should let me import the byte[]
/* try to save this key to the local user container */
var keyParameters = new CngKeyCreationParameters()
{
KeyCreationOptions = CngKeyCreationOptions.OverwriteExistingKey,
Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider,
KeyUsage = CngKeyUsages.AllUsages,
ExportPolicy = CngExportPolicies.AllowPlaintextExport
};
keyParameters.KeyCreationOptions = CngKeyCreationOptions.None;
keyParameters.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(keySize), CngPropertyOptions.None));
keyParameters.Parameters.Add(new CngProperty(blobType.Format, bytes, CngPropertyOptions.None));
var newKey = CngKey.Create(CngAlgorithm.Rsa, "MyTestName", keyParameters);
...but once again, no dice. It doesn't matter what CngKeyBlobFormat I try, they all give me exceptions and I'm unable to import the key into a local key storage provider.
What magic mix of settings and parameters do I need to make this work?

Well, I finally got it working. Here's the code as it finally settled down.
public class KeyGenerationResult
{
public RSAParameters PublicKey { get; set; }
public string PublicKeyCleartext { get; set; }
public string PrivateKeyCleartext { get; set; }
public byte[] PrivateBytes { get; set; }
public int KeySize { get; set; }
public CngKeyBlobFormat BlobFormat { get; set; }
}
public async Task<KeyGenerationResult> GenerateNewKeyAsync(int keySize)
{
var cp = new CngKeyCreationParameters();
cp.KeyUsage = CngKeyUsages.AllUsages;
cp.ExportPolicy = CngExportPolicies.AllowPlaintextExport | CngExportPolicies.AllowExport | CngExportPolicies.AllowArchiving | CngExportPolicies.AllowPlaintextArchiving;
cp.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(keySize), CngPropertyOptions.None));
var key = await Task.Run(() => CngKey.Create(CngAlgorithm.Rsa, null, cp)).ConfigureAwait(false);
var blobType = CngKeyBlobFormat.GenericPrivateBlob;
var bytes = await Task.Run(() => key.Export(blobType)).ConfigureAwait(false);
var rsa = new RSACng(key);
var pubKey = rsa.ExportParameters(includePrivateParameters: false);
var pubKeyString = exportKeyToString(pubKey);
return new KeyGenerationResult
{
PublicKey = pubKey,
PrivateKeyCleartext = Convert.ToBase64String(bytes),
PublicKeyCleartext = pubKeyString,
PrivateBytes = bytes,
BlobFormat = blobType,
KeySize = keySize
};
}
private static string exportKeyToString(RSAParameters key)
{
string keyString;
var sw = new StringWriter();
var xs = new XmlSerializer(typeof(RSAParameters));
xs.Serialize(sw, key);
keyString = sw.ToString();
return keyString;
}
public void SavePrivateKeyToLocalMachine(KeyGenerationResult keyData, string keyName)
{
var myKSP = CngProvider.MicrosoftSoftwareKeyStorageProvider;
const bool MachineKey = false;
if (!CngKey.Exists(keyName, myKSP))
{
var keyParams = new CngKeyCreationParameters
{
ExportPolicy = CngExportPolicies.AllowPlaintextExport,
KeyCreationOptions = (MachineKey) ? CngKeyCreationOptions.MachineKey : CngKeyCreationOptions.None,
Provider = myKSP
};
keyParams.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(keyData.KeySize), CngPropertyOptions.None));
keyParams.Parameters.Add(new CngProperty(keyData.BlobFormat.Format, keyData.PrivateBytes, CngPropertyOptions.None));
CngKey.Create(CngAlgorithm.Rsa, keyName, keyParams);
}
else
{
throw new CryptographicException($"The key with the name '{keyName}' already exists!");
}
}

Related

C# / WPF RSA Doesn't Decrypt Text

I have a problem when I try to decrypt a string generated using RSA in C#. It Encrypts the strings well, but it throws an error when I try to decrypt the string using the private key:
Error occurred while decoding OAEP padding.
I tried changing the fOAEP parameter between true and false, changing the RSACryptoServiceProviders with 2048 as parameter and still doesn't work.
Project is a WPF App which generates 2 files with the keys, keys are loaded by the core.cs file.
Then keys are being loaded.
The example I taken only uses the public key to encrypt the string and only the private key to decrypt the string.
Core.cs File
// Keys are generated
public void GeneratePublicKey(string publicKeyFile) {
using (var rsa = new RSACryptoServiceProvider(2048)) {
rsa.PersistKeyInCsp = false;
if (File.Exists(publicKeyFile))
File.Delete(publicKeyFile);
//and the public key ...
var pubKey = rsa.ExportParameters(false);
//converting the public key into a string representation
string pubKeyString; {
//we need some buffer
var sw = new System.IO.StringWriter();
//we need a serializer
var xs = new System.Xml.Serialization.XmlSerializer(typeof(RSAParameters));
//serialize the key into the stream
xs.Serialize(sw, pubKey);
//get the string from the stream
pubKeyString = sw.ToString();
}
File.WriteAllText(publicKeyFile, pubKeyString);
}
}
public void GeneratePrivateKey(string privateKeyFile) {
using (var rsa = new RSACryptoServiceProvider(2048)) {
rsa.PersistKeyInCsp = false;
if (File.Exists(privateKeyFile))
File.Delete(privateKeyFile);
//how to get the private key
var privKey = rsa.ExportParameters(true);
//converting the public key into a string representation
string privKeyString;
{
//we need some buffer
var sw = new System.IO.StringWriter();
//we need a serializer
var xs = new System.Xml.Serialization.XmlSerializer(typeof(RSAParameters));
//serialize the key into the stream
xs.Serialize(sw, privKey);
//get the string from the stream
privKeyString = sw.ToString();
}
File.WriteAllText(privateKeyFile, privKeyString);
}
}
//Si las llaves ya existen entonces NO Generar, solo leer la llave indicada (leer el texto de la ruta)
public RSAParameters ReadPublicKey(string publicKeyFile) {
//Leer
string pubKeyString = File.ReadAllText(publicKeyFile);
//Reconvertir
var sr = new System.IO.StringReader(pubKeyString);
//we need a deserializer
var xs = new System.Xml.Serialization.XmlSerializer(typeof(RSAParameters));
//get the object back from the stream
return (RSAParameters)xs.Deserialize(sr);
}
public RSAParameters ReadPrivateKey(string privateKeyFile) {
//Leer
string privKeyString = File.ReadAllText(privateKeyFile);
//Reconvertir
var sr = new System.IO.StringReader(privKeyString);
//we need a deserializer
var xs = new System.Xml.Serialization.XmlSerializer(typeof(RSAParameters));
//get the object back from the stream
return (RSAParameters)xs.Deserialize(sr);
}
//Con la llave publica se encripta el texto
public string Encrypt(string publicKeyFile, string textToEncrypt) {
var csp = new RSACryptoServiceProvider();
csp.ImportParameters(ReadPublicKey(publicKeyFile));
//for encryption, always handle bytes...
var bytesPlainTextData = System.Text.Encoding.Unicode.GetBytes(textToEncrypt);
//apply pkcs#1.5 padding and encrypt our data
var bytesCypherText = csp.Encrypt(bytesPlainTextData, true);
//we might want a string representation of our cypher text... base64 will do
Debug.WriteLine("Texto Encriptado: "+Convert.ToBase64String(bytesCypherText));
return Convert.ToBase64String(bytesCypherText);
}
/// <summary>
/// Con la llave Privada se Desencripta
/// </summary>
/// <param name="privateKeyFile"></param>
/// <param name="textToDecrypt"></param>
/// <returns></returns>
public string Decrypt(string privateKeyFile, string textToDecrypt) {
//first, get our bytes back from the base64 string ...
var bytesCypherText = Convert.FromBase64String(textToDecrypt);
//we want to decrypt, therefore we need a csp and load our private key
var csp = new RSACryptoServiceProvider();
csp.ImportParameters(ReadPrivateKey(privateKeyFile));
//decrypt and strip pkcs#1.5 padding
var bytesPlainTextData = csp.Decrypt(bytesCypherText, true);
Debug.WriteLine("Desencriptado: "+
System.Text.Encoding.Unicode.GetString(bytesPlainTextData));
//get our original plainText back...
return System.Text.Encoding.Unicode.GetString(bytesPlainTextData);
}
MainWindow.cs
public partial class MainWindow : Window {
readonly RsaEnc _rs = new RsaEnc();
private string _publicKeyFile = "./public.cert";
private string _privateKeyFile = "./private.key";
public MainWindow() {
InitializeComponent();
}
private void GenerateBtn_Click(object sender, RoutedEventArgs e) {
_rs.GeneratePublicKey(_publicKeyFile);
_rs.GeneratePrivateKey(_privateKeyFile);
}
private void OpenPublic_Click(object sender, RoutedEventArgs e) {
OpenFileDialog fileDialog = new OpenFileDialog {
Multiselect = false, Filter = "Public |*.cert", DefaultExt = ".cert"
};
bool? dialogOk = fileDialog.ShowDialog();
if (dialogOk == true) {
string sFilenames = "";
foreach (string sFileName in fileDialog.FileNames) {
sFilenames += ";" + sFileName;
}
sFilenames = sFilenames.Substring(1);
_publicKeyFile = sFilenames;//Esto solo da la RUTA
Debug.WriteLine("public Cert: " + _publicKeyFile);
}
}
private void OpenPrivate_Click(object sender, RoutedEventArgs e) {
OpenFileDialog fileDialog = new OpenFileDialog
{
Multiselect = false, Filter = "Certificates |*.key", DefaultExt = ".key"
};
bool? dialogOk = fileDialog.ShowDialog();
if (dialogOk == true) {
string sFilenames = "";
foreach (string sFileName in fileDialog.FileNames) {
sFilenames += ";" + sFileName;
}
sFilenames = sFilenames.Substring(1);
_privateKeyFile = sFilenames; //Esto solo da la RUTA
Debug.WriteLine("private Key: " + _privateKeyFile);
}
}
private void EncryptBtn_Click(object sender, RoutedEventArgs e) {
_rs.Encrypt(_publicKeyFile, BoxToEncrypt.Text);
}
private void DecryptBtn_Click(object sender, RoutedEventArgs e) {
_rs.Decrypt(_privateKeyFile, BoxToDecrypt.Text);
}
}
First thing, WPF and Decryption are two different things. So, we have to concentrate only on the RSA encrypt/decrypt part.
In your code, you are trying to generate public/private keys using different methods. One single rsaprovider should export both parameters. I have provided a sample code for using RSA encryption/decryption. Please check below.
internal sealed class RSA
{
public static (string public_key, string private_key) getKeyPair()
{
try
{
var rsa_provider = new RSACryptoServiceProvider(1024);
return (rsa_provider.ToXmlString(false), rsa_provider.ToXmlString(true));
}
catch (Exception ex)
{
throw ex;
}
}
public static byte[] shroud_divulge(byte[] input_byte, string _key,bool is_shroud)
{
try
{
var rsa_provider = new RSACryptoServiceProvider();
rsa_provider.FromXmlString(_key);
var padding = RSAEncryptionPadding.OaepSHA256;
switch(is_shroud)
{
case true:
return rsa_provider.Encrypt(input_byte, padding);
case false:
return rsa_provider.Decrypt(input_byte, padding);
}
return null;
}
catch (Exception)
{
throw;
}
}
}
It has two simple methods and returns output using valuetuple(So add Valutuple nuget package).
The file saving and processing are done outside of the RSA logic. Shroud- Encryption, Divulge-Decryption. (Please excuse the naming conventions used, its as per our company standards).
We usually ship our publickey along with our WPF application.
For encryption (Shroud), we pass in the private key..
For decryption (Divulge) , we pass in the publich key..
RSA keys are always generated in pairs, a public and a private. These keys are created in the constructor of RSACryptoServiceProvider. Your code calls this constructor in both GeneratePublicKey and GeneratePrivateKey, therefore there will be two unrelated key-pairs generated.
You should be able to check this. When exporting parameters the public component is always included. If they are not identical that that would serve as confirmation.
The fix would be to generate the public and private key at the same time.

How do I detect if AsymmetricAlgorithm is a private key or a public key

Is there a simple way to check if a given AsymmetricAlgorithm is a private or a public key? Consider the following example:
private void SavePrivateKey(AsymmetricAlgorithm asymmetricAlgorithm)
{
// if (asymmetricAlgorithm.IsPrivateKey == false)
// throw new ArgumentException();
}
private void SavePrivateKeys()
{
var certificate = CreateCertificate();
var privateKey = RSACertificateExtensions.GetRSAPrivateKey(certificate);
var publicKey = RSACertificateExtensions.GetRSAPublicKey(certificate);
SavePrivateKey(privateKey);
SavePrivateKey(publicKey); // this should throw an exception
}
private X509Certificate2 CreateCertificate()
{
CngKeyCreationParameters keyParams = new CngKeyCreationParameters();
keyParams.KeyUsage = CngKeyUsages.Signing;
keyParams.Provider = CngProvider.MicrosoftSoftwareKeyStorageProvider;
keyParams.ExportPolicy = CngExportPolicies.AllowExport | CngExportPolicies.AllowPlaintextExport;
keyParams.Parameters.Add(new CngProperty("Length", BitConverter.GetBytes(2048), CngPropertyOptions.None));
var cngKey = CngKey.Create(CngAlgorithm.Rsa, Guid.NewGuid().ToString(), keyParams);
var rsaKey = new RSACng(cngKey);
var req = new CertificateRequest("cn=mycert", rsaKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
var cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(5));
return cert;
}
Here, both private and public keys have the same type - RSACng. In theory I can try to export key parameters and see if the public key fails to export the private key params. But then I would not know if the export failed because it is a public key, or because it is missing export policies or something else went wrong. Also the underlying key type may be different, it could be RSACng, RSACryptoServiceProvider, DSA etc.
The ToXmlString() method takes in the includePrivateParameters parameter. If that parameter is set, and the AsymmetricAlgorithm object does not include information about the private key, ToXmlString() will throw a CryptographicException exception:
private void SavePrivateKey(AsymmetricAlgorithm aa)
{
System.Console.Write("This is ");
try
{
aa.ToXmlString(true);
}
catch(CryptographicException ce)
{
System.Console.Write("not ");
}
System.Console.WriteLine("a private key");
}

The 'await' operator can only be used within an async > method

I have the following controller:
[Authorize]
public class SetupController : ApiController
{
[HttpPost]
public Task async SetupPartnerPackAsync(SetupInformation info)
{
if (info.SslCertificateGenerate)
{
SetupManager.CreateX509Certificate(info);
}
else
{
SetupManager.LoadX509Certificate(info);
}
info.SslCertificateThumbprint = SetupManager.GetX509CertificateThumbprint(info);
info.AzureAppKeyCredential = SetupManager.GetX509CertificateInformation(info);
await SetupManager.RegisterAzureADApplication(info);
}
}
But I have the following 2 error which seems simple:
Severity Code Description Project File Line Suppression State
Error CS1520 Method must have a return
type InnovationInABoxWebApi H:\InnovationInABoxWebApi\InnovationInABoxWebApi\Controllers\SetupController.cs 24 Active
Severity Code Description Project File Line Suppression State
Error CS4033 The 'await' operator can only be used within an async
method. Consider marking this method with the 'async' modifier and
changing its return type to
'Task'. InnovationInABoxWebApi H:\InnovationInABoxWebApi\InnovationInABoxWebApi\Controllers\SetupController.cs 39 Active
However I am not sure how to fix this, as the operation can take some time to complete, it really needs to be asybnc
and the setupmanager
using CERTENROLLLib;
using Microsoft.Identity.Client;
using Microsoft.Online.SharePoint.TenantAdministration;
using Microsoft.SharePoint.Client;
using Newtonsoft.Json;
using OfficeDevPnP.Core;
using OfficeDevPnP.Core.Entities;
using OfficeDevPnP.Core.Framework.Provisioning.Model;
using OfficeDevPnP.Core.Framework.Provisioning.ObjectHandlers;
using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Resources;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Xml.Linq;
namespace InnovationInABoxWebApi.Components
{
public static class SetupManager
{
public static String GetX509CertificateThumbprint(SetupInformation info)
{
var certificate = info.AuthenticationCertificate;
return (certificate.Thumbprint.ToUpper());
}
public static String GetX509CertificateInformation(SetupInformation info)
{
// var basePath = String.Format(#"{0}..\..\..\..\Scripts\", AppDomain.CurrentDomain.BaseDirectory);
var certificate = info.AuthenticationCertificate;
//var certificate = new X509Certificate2();
//if (info.SslCertificateGenerate)
//{
// certificate.Import($#"{basePath}{info.SslCertificateCommonName}.cer");
//}
//else
//{
// certificate = new X509Certificate2(info.SslCertificateFile, info.SslCertificatePassword);
//}
var rawCert = certificate.GetRawCertData();
var base64Cert = System.Convert.ToBase64String(rawCert);
var rawCertHash = certificate.GetCertHash();
var base64CertHash = System.Convert.ToBase64String(rawCertHash);
var KeyId = System.Guid.NewGuid().ToString();
var keyCredential =
"{" +
"\"customKeyIdentifier\": \"" + base64CertHash + "\"," +
"\"keyId\": \"" + KeyId + "\"," +
"\"type\": \"AsymmetricX509Cert\"," +
"\"usage\": \"Verify\"," +
"\"key\": \"" + base64Cert + "\"" +
"}";
return (keyCredential);
}
public static void CreateX509Certificate(SetupInformation info)
{
var certificate = CreateSelfSignedCertificate(info.SslCertificateCommonName.ToLower(),
info.SslCertificateStartDate, info.SslCertificateEndDate, info.SslCertificatePassword);
SaveCertificateFiles(info, certificate);
}
public static void LoadX509Certificate(SetupInformation info)
{
var certificate = new X509Certificate2(info.SslCertificateFile, info.SslCertificatePassword);
info.AuthenticationCertificate = certificate;
info.SslCertificateCommonName = certificate.SubjectName.Name;
}
public static void SaveCertificateFiles(SetupInformation info, X509Certificate2 certificate)
{
info.AuthenticationCertificate = certificate;
//var basePath = String.Format(#"{0}..\..\..\..\Scripts\", AppDomain.CurrentDomain.BaseDirectory);
//info.SslCertificateFile = $#"{basePath}{info.SslCertificateCommonName}.pfx";
//var pfx = certificate.Export(X509ContentType.Pfx, info.SslCertificatePassword);
//System.IO.File.WriteAllBytes(info.SslCertificateFile, pfx);
//var cer = certificate.Export(X509ContentType.Cert);
//System.IO.File.WriteAllBytes($#"{basePath}{info.SslCertificateCommonName}.cer", cer);
}
public static X509Certificate2 CreateSelfSignedCertificate(string subjectName, DateTime startDate, DateTime endDate, String password)
{
// Create DistinguishedName for subject and issuer
var name = new CX500DistinguishedName();
name.Encode("CN=" + subjectName, X500NameFlags.XCN_CERT_NAME_STR_NONE);
// Create a new Private Key for the certificate
CX509PrivateKey privateKey = new CX509PrivateKey();
privateKey.ProviderName = "Microsoft RSA SChannel Cryptographic Provider";
privateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE;
privateKey.Length = 2048;
privateKey.SecurityDescriptor = "D:PAI(A;;0xd01f01ff;;;SY)(A;;0xd01f01ff;;;BA)(A;;0x80120089;;;NS)";
privateKey.MachineContext = true;
privateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG;
privateKey.Create();
// Define the hashing algorithm
var serverauthoid = new CObjectId();
serverauthoid.InitializeFromValue("1.3.6.1.5.5.7.3.1"); // Server Authentication
var ekuoids = new CObjectIds();
ekuoids.Add(serverauthoid);
var ekuext = new CX509ExtensionEnhancedKeyUsage();
ekuext.InitializeEncode(ekuoids);
// Create the self signing request
var cert = new CX509CertificateRequestCertificate();
cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextMachine, privateKey, String.Empty);
cert.Subject = name;
cert.Issuer = cert.Subject;
cert.NotBefore = startDate;
cert.NotAfter = endDate;
cert.X509Extensions.Add((CX509Extension)ekuext);
cert.Encode();
// Enroll the certificate
var enroll = new CX509Enrollment();
enroll.InitializeFromRequest(cert);
string certData = enroll.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64HEADER);
enroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate,
certData, EncodingType.XCN_CRYPT_STRING_BASE64HEADER, String.Empty);
var base64encoded = enroll.CreatePFX(password, PFXExportOptions.PFXExportChainWithRoot);
// Instantiate the target class with the PKCS#12 data
return new X509Certificate2(
System.Convert.FromBase64String(base64encoded), password,
System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable);
}
public async static Task RegisterAzureADApplication(SetupInformation info)
{
// Fix the App URL
if (!info.AzureWebAppUrl.EndsWith("/"))
{
info.AzureWebAppUrl = info.AzureWebAppUrl + "/";
}
// Load the App Manifest template
//Stream stream = typeof(SetupManager)
// .Assembly
// .GetManifestResourceStream("OfficeDevPnP.PartnerPack.Setup.Resources.azure-ad-app-manifest.json");
using (StreamReader sr = new StreamReader("Resources\azure-ad-app-manifest.json"))
{
// Get the JSON manifest
var jsonApplication = sr.ReadToEnd();
var application = JsonConvert.DeserializeObject<AzureAdApplication>(jsonApplication);
var keyCredential = JsonConvert.DeserializeObject<KeyCredential>(info.AzureAppKeyCredential);
application.displayName = info.ApplicationName;
application.homepage = info.AzureWebAppUrl;
application.identifierUris = new List<String>();
application.identifierUris.Add(info.ApplicationUniqueUri);
application.keyCredentials = new List<KeyCredential>();
application.keyCredentials.Add(keyCredential);
application.replyUrls = new List<String>();
application.replyUrls.Add(info.AzureWebAppUrl);
// Generate the Application Shared Secret
var startDate = DateTime.Now;
Byte[] bytes = new Byte[32];
using (var rand = System.Security.Cryptography.RandomNumberGenerator.Create())
{
rand.GetBytes(bytes);
}
info.AzureAppSharedSecret = System.Convert.ToBase64String(bytes);
application.passwordCredentials = new List<object>();
application.passwordCredentials.Add(new AzureAdApplicationPasswordCredential
{
CustomKeyIdentifier = null,
StartDate = startDate.ToString("o"),
EndDate = startDate.AddYears(2).ToString("o"),
KeyId = Guid.NewGuid().ToString(),
Value = info.AzureAppSharedSecret,
});
// Get an Access Token to create the application via Microsoft Graph
var office365AzureADAccessToken = await AzureManagementUtility.GetAccessTokenSilentAsync(
AzureManagementUtility.MicrosoftGraphResourceId,
ConfigurationManager.AppSettings["O365:ClientId"]);
var azureAdApplicationCreated = false;
// Create the Azure AD Application
try
{
await CreateAzureADApplication(info, application, office365AzureADAccessToken);
azureAdApplicationCreated = true;
}
catch (ApplicationException ex)
{
var graphError = JsonConvert.DeserializeObject<GraphError>(((HttpException)ex.InnerException).Message);
if (graphError != null && graphError.error.code == "Request_BadRequest" &&
graphError.error.message.Contains("identifierUris already exists"))
{
// We need to remove the existing application
// Thus, retrieve it
String jsonApplications = await HttpHelper.MakeGetRequestForStringAsync(
String.Format("{0}applications?$filter=identifierUris/any(c:c+eq+'{1}')",
AzureManagementUtility.MicrosoftGraphBetaBaseUri,
HttpUtility.UrlEncode(info.ApplicationUniqueUri)),
office365AzureADAccessToken);
var applications = JsonConvert.DeserializeObject<AzureAdApplications>(jsonApplications);
var applicationToUpdate = applications.Applications.FirstOrDefault();
if (applicationToUpdate != null)
{
// Remove it
await HttpHelper.MakeDeleteRequestAsync(
String.Format("{0}applications/{1}",
AzureManagementUtility.MicrosoftGraphBetaBaseUri,
applicationToUpdate.Id),
office365AzureADAccessToken);
// And add it again
await CreateAzureADApplication(info, application, office365AzureADAccessToken);
azureAdApplicationCreated = true;
}
}
}
if (azureAdApplicationCreated)
{
// TODO: We should upload the logo
// property mainLogo: stream of the application via PATCH
}
}
}
public static async Task CreateAzureADApplication(SetupInformation info, AzureAdApplication application, string office365AzureADAccessToken)
{
String jsonResponse = await HttpHelper.MakePostRequestForStringAsync(
String.Format("{0}applications",
AzureManagementUtility.MicrosoftGraphBetaBaseUri),
application,
"application/json", office365AzureADAccessToken);
var azureAdApplication = JsonConvert.DeserializeObject<AzureAdApplication>(jsonResponse);
info.AzureAppClientId = azureAdApplication.AppId.HasValue ? azureAdApplication.AppId.Value : Guid.Empty;
}
}
}
You are defining the method with async word after the return type Task, async must be before Task.
public async Task SetupPartnerPackAsync(SetupInformation info)
{
.
.
.

Net Core JWT Signing Credentials

I am following this tutorial:
https://jonhilton.net/2017/10/11/secure-your-asp.net-core-2.0-api-part-1---issuing-a-jwt/
Here is the main code:
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["SecurityKey"]));
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
issuer: "yourdomain.com",
audience: "yourdomain.com",
claims: user.Claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: creds);
The part I don't understand is where it looks for a key in the configuration file, but it gives no indicator what this key is/should be?
One option is to keep a repository of symmetric signing keys that are associated with the "kid" claim in the JWT header. For example, keep a file of keys in an encrypted AWS S3 bucket. The .NET Core 2 web service pulls the file of keys from the S3 bucket every X minutes.
When a request arrives, the "kid" claim value is used to look up the associated symmetric key from a collection that was built from the pulled file.
EXAMPLE:
Configure an IssuerSigningKeyResolver function.
public static IServiceCollection AddJwtValidation(this IServiceCollection services)
{
IServiceProvider sp = services.BuildServiceProvider();
ConfigRoot = sp.GetRequiredService<IConfigurationRoot>();
tokenAudience = ConfigRoot["JwtToken:Audience"];
tokenIssuer = ConfigRoot["JwtToken:Issuer"];
SecurityKeyManager.Start();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.Audience = tokenAudience;
options.ClaimsIssuer = tokenIssuer;
options.TokenValidationParameters = new TokenValidationParameters
{
// The signing key must match!
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
IssuerSigningKeyResolver = MyIssuerSigningKeyResolver,
....
The IssuerSigningKeyResolver is defined as:
public static List<SecurityKey> MyIssuerSigningKeyResolver(string token, SecurityToken jwtToken, string kid, TokenValidationParameters validationParameters)
{
List<SecurityKey> keys = new List<SecurityKey>();
if (validationParameters == null)
{
throw new ArgumentNullException("validationParameters");
}
if (jwtToken == null)
{
throw new ArgumentNullException("securityToken");
}
if (!string.IsNullOrEmpty(kid))
{
SymmetricSecurityKey key = SecurityKeyManager.GetSecurityKey(kid);
keys.Add(key);
}
return keys;
}
The SecurityKeyManager periodically pulls the key file from AWS S3 and stores the keys in a collection.
// Example approved-clients.txt file in the approved-clients S3 bucket (us-east-1).
// //kid,key,active
// customer1,AAAAAAAAAAAAAAAA,true
// customer2,BBBBBBBBBBBBBBBB,true
// customer3,CCCCCCCCCCCCCCCC,true
// customer4,DDDDDDDDDDDDDDDD,true
namespace My.CoreServices.Security.Jwt
{
public class SecurityKeyManager
{
private const int RELOAD_TIMER_DELAY_SECONDS = 3 * 1000;
private const int RELOAD_TIMER_PERIOD_MINUTES = 60 * 60 * 1000;
[DebuggerDisplay("{Kid} {SymmetricKey} {Active}")]
internal class ApprovedClient
{
public string Kid { get; set; }
public bool Active { get; set; }
public string SymmetricKey { get; set; }
};
private static List<SymmetricSecurityKey> securityKeys = new List<SymmetricSecurityKey>();
private static Timer reloadTimer = null;
private static object keySync = new object();
public static void Start()
{
// Start a new timer to reload all the security keys every RELOAD_TIMER_PERIOD_MINUTES.
if (reloadTimer == null)
{
reloadTimer = new Timer(async (t) =>
{
try
{
List<ApprovedClient> approvedClients = new List<ApprovedClient>();
Log.Debug("Pulling latest approved client symmetric keys for JWT signature validation");
string awsAccessKeyId = JwtConfigure.ConfigRoot["AWS:KeyManagement:AccessKeyId"];
string awsSecretAccessKey = fromBase64(JwtConfigure.ConfigRoot["AWS:KeyManagement:SecretAccessKey"]);
string awsRegion = JwtConfigure.ConfigRoot["AWS:KeyManagement:Region"];
using (var client = new AmazonS3Client(awsAccessKeyId, awsSecretAccessKey, RegionEndpoint.GetBySystemName(awsRegion)))
{
var request = new GetObjectRequest();
request.BucketName = JwtConfigure.ConfigRoot["AWS:KeyManagement:Bucket"];
request.Key = JwtConfigure.ConfigRoot["AWS:KeyManagement:Key"];
var response = await client.GetObjectAsync(request);
using (StreamReader sr = new StreamReader(response.ResponseStream))
{
while (sr.Peek() > 0)
{
string line = await sr.ReadLineAsync();
// Ignore comment lines in the approved-client file
if (!line.StartsWith("//") && !string.IsNullOrEmpty(line))
{
// Each line of the file should only have 3 items:
// kid, key, active
string[] items = line.Split(',');
approvedClients.Add(new ApprovedClient()
{
Kid = items[0],
SymmetricKey = items[1],
Active = Boolean.Parse(items[2])
});
}
}
}
}
lock (keySync)
{
if (approvedClients.Count > 0)
{
// Clear the security key list and repopulate
securityKeys.Clear();
foreach (var approvedClient in approvedClients)
{
if (approvedClient.Active)
{
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(approvedClient.SymmetricKey));
key.KeyId = approvedClient.Kid;
securityKeys.Add(key);
}
}
}
}
Log.Information($"Reloaded security keys");
}
catch (Exception ex)
{
Log.Warning($"Error getting current security keys - {ex.Message}");
}
}, null, RELOAD_TIMER_DELAY_SECONDS, RELOAD_TIMER_PERIOD_MINUTES);
}
}
public static void Stop()
{
if (reloadTimer != null)
{
reloadTimer.Dispose();
reloadTimer = null;
}
}
public static SymmetricSecurityKey GetSecurityKey(string kid)
{
SymmetricSecurityKey securityKey = null;
lock (keySync)
{
byte[] keyData = securityKeys.Where(k => k.KeyId == kid).Select(x => x.Key).FirstOrDefault();
if (keyData != null)
{
securityKey = new SymmetricSecurityKey(keyData);
securityKey.KeyId = kid;
}
}
return securityKey;
}
private static string fromBase64(string encodedValue)
{
byte[] decodedBytes = Convert.FromBase64String(encodedValue);
return Encoding.UTF8.GetString(decodedBytes);
}
}
}
Ensure that when a JWT is created for a specific user/customer/etc, the "kid" claim is set in the JWT header.
{
"alg": "HS256",
"kid": "customer2",
"typ": "JWT"
}
The value of "kid" will be passed as the third parameter to the IssuerSigningKeyResolver method. That kid will then be used to lookup the associated symmetric key that is used to validate the JWT signature.
This key can be any string: it's the secret key used to both encrypt and decrypt your secure payload.
From Wikipedia:
Symmetric-key algorithms are algorithms for cryptography that use the same cryptographic keys for both encryption of plaintext and decryption of ciphertext. The keys, in practice, represent a shared secret between two or more parties that can be used to maintain a private information link.
As for how to generate an efficient key, you can refer to this question on crypto.stackexchange.

Pushsharp support of Apple Push Notification Authentication Key

Does pushsharp supports a new Apple approach for sending APN using Apple Push Notification Authentication Key (which never expires) instead of using Certificates? Is any way to use it with pushsharp? If not, is there any other C# library to support it?
here you are:
private string GetToken()
{
var algorithm = "ES256";
var teamID = "teamID";
var apnsKeyID = "apnsKeyID";
var apnsAuthKeyPath = #"apnsAuthKeyPath";
var epochNow = DateTimeOffset.Now.ToUnixTimeSeconds();
var header = new Dictionary<string, object>()
{
{ "alg", algorithm },
{ "kid" , apnsKeyID }
};
var payload = new Dictionary<string, object>()
{
{ "iss", teamID },
{ "iat", epochNow }
};
var privateKey = GetPrivateKey(apnsAuthKeyPath);
var token = Jose.JWT.Encode(payload, privateKey, algorithm: Jose.JwsAlgorithm.ES256, extraHeaders: header);
return token;
}
private CngKey GetPrivateKey(string apnsAuthKey)
{
using (var reader = File.OpenText(apnsAuthKey))
{
var ecPrivateKeyParameters = (ECPrivateKeyParameters)new PemReader(reader).ReadObject();
var x = ecPrivateKeyParameters.Parameters.G.AffineXCoord.GetEncoded();
var y = ecPrivateKeyParameters.Parameters.G.AffineYCoord.GetEncoded();
var d = ecPrivateKeyParameters.D.ToByteArrayUnsigned();
return EccKey.New(x, y, d);
}
}
then you can simply call send method in apns.

Categories