I'm trying to create S/MIME email with MimeKit in the .NET Core App. It's working fine with local certificate like this:
var signer = new CmsSigner("cert.pfx", "password");
message.Body = MultipartSigned.Create(ctx, signer, message.Body);
To make app more secure, I've uploaded my certificate to the Azure KeyVault. And here's my problems has begun.
The idea is to use only certificate from Azure KeyVault without any passwords etc (so only store reference to the KeyVault instead of storing two links (KeyVault cert + KeyVault certPass)).
Here's how I'm trying to get certificate from the Azure KeyVault.
private static async Task<X509Certificate2> GetCertificate()
{
var tokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));
var azureKeyVaultCert = await keyVaultClient.GetSecretAsync("https://<myapp>.vault.azure.net/secrets/<secretName>/<secretId>");
var certBytes = Convert.FromBase64String(azureKeyVaultCert.Value);
var cert = new X509Certificate2(certBytes);
return cert;
}
And then:
var cert = new GetCertificate();
var signer = new CmsSigner(cert);
Here I got error like this: MimeKit: 'RSACng' is currently not supported.
After that I tried another approach. It's working fine, however, I need to use my password. I don't think this is good idea (despite the fact I can also use Azure KeyVault for cert password).
private static async Task<byte[]> GetCertificateBytes()
{
var tokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(tokenProvider.KeyVaultTokenCallback));
var azureKeyVaultCert = await keyVaultClient.GetSecretAsync("https://<myapp>.vault.azure.net/secrets/<secretName>/<secretId>");
var certBytes = Convert.FromBase64String(azureKeyVaultCert.Value);
var certCollection = new X509Certificate2Collection();
certCollection.Import(certBytes, null, X509KeyStorageFlags.Exportable);
return certCollection.Export(X509ContentType.Pkcs12, "password");
}
var certBytes = await GetCertificateBytes();
using(var certStream = new System.IO.MemoryStream(certBytes))
{
var signer = new CmsSigner(certStream, "password");
}
In case when I use await keyVaultClient.GetCertificateAsync(), I got an error that there's no private key.
Also, there's difference between certificates created via new X509Certificate2() and X509Certificate2Collection.Export().
Maybe I miss something?
Related
I am currently working on an authentication server developed in C #, this one is hosted on an azure function app, and I use a KeyVault where my secrets are stored. My problem is the following, in my keyvault, I store a certificate (certificate + private key)
and when I retrieve it in my code, but the private key is not returned. if I test the following method: HasPrivateKey the code returns false ...
but if i use the same .pfx in localy the code return me true ...
my code:
var client = new CertificateClient(vaultUri: new Uri("https://diiage2p1g3chest.vault.azure.net/"),credential: new DefaultAzureCredential());
KeyVaultCertificate kcertificate = client.GetCertificate("try");
var cert_content = kcertificate.Cer;
X509Certificate2 certificate = new X509Certificate2(cert_content, "password", X509KeyStorageFlags.EphemeralKeySet);
any idea where the problem comes from?
CertificateClient has a method that returns a certificate with private key, but it's not obvious that's what it does.
From CertificateClient.DownloadCertificate:
Because Cer contains only the public key, this method attempts to download the managed secret that contains the full certificate. If you do not have permissions to get the secret, RequestFailedException will be thrown with an appropriate error response. If you want an X509Certificate2 with only the public key, instantiate it passing only the Cer property. This operation requires the certificates/get and secrets/get permissions.
So just refactor your code to use DownloadCertificate to get a cert with the private key.
var client = new CertificateClient(new Uri("https://diiage2p1g3chest.vault.azure.net/"), new DefaultAzureCredential());
X509Certificate2 certificate = client.DownloadCertificate("try");
The simplest way to get the full bytes of a certificate with its private information from keyvault is this. Please note you need permission to get secrets in your client id.
You need the following packages:
Azure.Identity
Azure.Security.KeyVault.Secrets
The following are deprecated:
Microsoft.IdentityModel.Clients.ActiveDirectory
Microsoft.Azure.KeyVault
Code:
using System;
using System.Threading.Tasks;
using Azure;
using Azure.Identity;
using System.Security.Cryptography.X509Certificates;
using Azure.Security.KeyVault.Secrets;
...
public async Task<X509Certificate2> GetCertificate(string certificateName,string clientId, string clientSecret, string keyVaultAddress, string tenantId)
{
ClientSecretCredential clientCredential = new ClientSecretCredential(tenantId, clientId, clientSecret);
var secretClient = new SecretClient(new Uri(keyVaultAddress), clientCredential);
var response = await secretClient.GetSecretAsync(certificateName);
var keyVaultSecret = response?.Value;
if(keyVaultSecret != null)
{
var privateKeyBytes = Convert.FromBase64String(keyVaultSecret.Value);
return new X509Certificate2(privateKeyBytes);
}
return null;
}
var _keyVaultName = $"VAULTURL";
var secretName = "CERTIFICATENAME";
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var _client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
var secret = _client.GetSecretAsync(_keyVaultName, secretName);
var privateKeyBytes = Convert.FromBase64String(secret.Result.Value);
certificate = new X509Certificate2(privateKeyBytes, string.Empty);
i solve my probleme like that :)
X509Certificate2 certificate;
ClientSecretCredential clientCredential = new ClientSecretCredential(TenantId,ClientId,ClientSecret);
var secretClient = new SecretClient(new Uri(KeyVaultUrl), clientCredential);
var response = await secretClient.GetSecretAsync(individualEnrollment.DeviceId.Replace("-", ""));
var keyVaultSecret = response?.Value;
var privateKeyBytes = Convert.FromBase64String(keyVaultSecret.Value);
certificate = new X509Certificate2(privateKeyBytes);
using (var security = new SecurityProviderX509Certificate(certificate))
using (var transport = new ProvisioningTransportHandlerAmqp(TransportFallbackType.TcpOnly))
{
ProvisioningDeviceClient provClient =
ProvisioningDeviceClient.Create(GlobalDeviceEndpoint, Scope, security, transport);
var deviceClient = await ProvisionX509Device(provClient, security);
}
I have a RSA Private key with me and I have to generate a JWT token using RS256 algorithm.
I started with the below code which was working for "HmacSha256" algorithm
but when i change it to RS256 it throws errors like " IDX10634: Unable to create the SignatureProvider.Algorithm: 'System.String',SecurityKey:'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey'"
After that I modified the code with RSACryptoServiceProvider() class. But i didnt get a solution.
Please anyone can help with a sample code using RSACryptoServiceProvider class with a private key.
public static string CreateToken()//Dictionary<string, object> payload
{
string key = GetConfiguration["privateKey"].Tostring();
var securityKey =
new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(
Encoding.UTF8.GetBytes(key));
var credentials =
new Microsoft.IdentityModel.Tokens.SigningCredentials(securityKey, "RS256");
var header = new JwtHeader(credentials);
JwtPayload payload = new JwtPayload();
payload.AddClaim(
new System.Security.Claims.Claim(
"context", "{'user': { 'name': 'username', 'email': 'email' }}",
JsonClaimValueTypes.Json));
payload.AddClaim(new System.Security.Claims.Claim("iss", #"app-key"));
payload.AddClaim(new System.Security.Claims.Claim("aud", "meet.jitsi.com"));
payload.AddClaim(new System.Security.Claims.Claim("sub", "meet.jitsi.com"));
payload.AddClaim(new System.Security.Claims.Claim("room", "TestRoom"));
var secToken = new JwtSecurityToken(header, payload);
var handler = new JwtSecurityTokenHandler();
var tokenString = handler.WriteToken(secToken);
return tokenString;
}
Currently creating Certificate Authorities and Issued Certificates. The Generation of the request, enrollment and validation are all functional, but when I checked my certificate store, I realized, it was placing them in my personal certificate directory. For memory, security and legal reasons, I can't have that.
The certificates are stored in a secure remote database. The certificates may be randomly accessed or generated on a random machine from a collection. If they generate certificates, it will store them on whichever machine created the certificate. Is there a way to generate a certificate enrollment (CX509Enrollment) without any trace of the certificate being left on the machine afterwards?
The portion that controls enrollment is relatively small and straight forward. It can only be ran as an administrator. I assume that's because it's adding certificates to the store.
I'm currently running a separate project file to attempt to debug this issue.
Both my certificates are constructed and kept in memory.
static void Main(string[] args)
{
X509Certificate2 rootCert = CreateSelfSignedCertificate("testRoot");
X509Certificate2 signedChild = CreateSignedCertificate("testyMcTesterson", rootCert);
X509Chain chain = new X509Chain();
chain.ChainPolicy = new X509ChainPolicy()
{
RevocationMode = X509RevocationMode.NoCheck,
VerificationFlags = X509VerificationFlags.AllFlags,
UrlRetrievalTimeout = new TimeSpan(0, 1, 0)
};
chain.ChainPolicy.ExtraStore.Add(rootCert);
bool isValid = chain.Build(signedChild); //Is True :D
}
The certificates end up in my personal certificate store
My enrollment occurs in this method. It takes a fully contructed and encoded certificate request.
public static CX509Enrollment EnrollCertificateRequest(CX509CertificateRequestCertificate certRequest)
{
var enroll = new CX509Enrollment();
enroll.InitializeFromRequest(certRequest);
string csr = enroll.CreateRequest();
enroll.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate,
csr, EncodingType.XCN_CRYPT_STRING_BASE64, "");
return enroll;
}
EDIT
I'm currently limited to .NET 4.5.x.
Another problem I'm running into, is that trying to sign a certificate with a root will throw a CRYPT_E_NOT_FOUND exception.
There's probably not a way to do it with CX509Enroll. But you can possibly accomplish your goals with .NET Framework 4.7.2 and the CertificateRequest class.
using (RSA rsa = RSA.Create(2048))
{
CertificateRequest request = new CertificateRequest(
"CN=Your Name Here",
rsa,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pkcs1);
SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder();
builder.AddDnsName("your.name.here");
builder.AddDnsName("your.other.name.here");
request.CertificateExtensions.Add(builder.Build());
// Any other extensions you're supposed to request, like not being a CA.
request.CertificateExtensions.Add(
new X509BasicConstraintsExtension(false, false, 0, false));
// TLS Server?
request.CertificateExtensions.Add(
new X509EnhancedKeyUsageExtension(
new OidCollection
{
new Oid("1.3.6.1.5.5.7.3.1")
},
false));
byte[] derEncodedRequest = request.CreateSigningRequest();
X509Certificate2 responseWithPrivateKey;
using (X509Certificate2 response = SendRequestToServerAndGetResponse(derEncodedRequest))
{
responseWithPrivateKey = response.CopyWithPrivateKey(rsa);
}
// Use it, save it to a PFX, whatever.
// At this point, nothing has touched the hard drive.
}
I figured it out without using enrollment and sticking to .NET 4.5.x.
I first create a RSACrytpoServiceProvider using CspParameters.
I then construct a private and public key from the RSACryptoServiceProvider. I create a certificate request and initiate from the private key. The certificate request is encoded and converted to raw data, then to bytes. The bytes are then used to create an X509Certificate2.
public static X509Certificate2 GenerateCertificate(string subjectName)
{
var dn = new CX500DistinguishedName();
dn.Encode("CN=" + subjectName, X500NameFlags.XCN_CERT_NAME_STR_COMMA_FLAG);
//Create crytpo provider to generate an assymetric key
int KeyType = (int)X509ProviderType.XCN_PROV_RSA_SCHANNEL;
CspParameters cspParams = new CspParameters(KeyType);
cspParams.Flags = CspProviderFlags.UseMachineKeyStore;
cspParams.KeyContainerName = Guid.NewGuid().ToString();
var rsa = new RSACryptoServiceProvider(2048, cspParams);
var CryptoProvider = rsa.CspKeyContainerInfo.ProviderName;
var keyContainerName = rsa.CspKeyContainerInfo.KeyContainerName;
CX509PrivateKey privateKey = new CX509PrivateKey();
privateKey.MachineContext = true;
privateKey.ProviderName = CryptoProvider;
privateKey.ContainerName = keyContainerName;
privateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES;
privateKey.Open();
keyContainerName = privateKey.ContainerName;
CX509PublicKey publicKey = privateKey.ExportPublicKey();
var oid = new CObjectId();
oid.InitializeFromValue("1.3.6.1.5.5.7.3.1"); // SSL server
var oidlist = new CObjectIds();
oidlist.Add(oid);
var eku = new CX509ExtensionEnhancedKeyUsage();
eku.InitializeEncode(oidlist);
var hashobj = new CObjectId();
hashobj.InitializeFromAlgorithmName(ObjectIdGroupId.XCN_CRYPT_HASH_ALG_OID_GROUP_ID,
ObjectIdPublicKeyFlags.XCN_CRYPT_OID_INFO_PUBKEY_ANY,
AlgorithmFlags.AlgorithmFlagsNone, "SHA256");
CX509CertificateRequestCertificate certRequest = new CX509CertificateRequestCertificate();
certRequest.InitializeFromPrivateKey(
X509CertificateEnrollmentContext.ContextMachine,
privateKey,
"");
certRequest.Subject = dn;
certRequest.NotBefore = DateTime.Now;
certRequest.NotAfter = DateTime.Now.AddYears(1);
certRequest.HashAlgorithm = hashobj;
certRequest.X509Extensions.Add((CX509Extension)eku);
certRequest.Encode();
return new X509Certificate2(
Convert.FromBase64String(certRequest.RawData), "",
X509KeyStorageFlags.Exportable)
{
PrivateKey = rsa,
FriendlyName = subjectName
};
}
To issue a cert. The same process is followed a CSignerCertificate is initiated and attached. But before that happens, I save the root certificate to the My, Local Machine Certificate Store. I then create a signed certificate using the root that was just added to the store. I then remove the certificate from the store.
Signing a Certificate Request
var dnSigner = new CX500DistinguishedName();
dnSigner.Encode("CN=" + signer.FriendlyName, X500NameFlags.XCN_CERT_NAME_STR_COMMA_FLAG);
string base64Root = Convert.ToBase64String(signer.RawData);
CSignerCertificate certSigner = new CSignerCertificate();
bool useMachineStore = ((ICspAsymmetricAlgorithm)signer.PrivateKey).CspKeyContainerInfo.MachineKeyStore;
certSigner.Initialize(useMachineStore, X509PrivateKeyVerify.VerifyNone, EncodingType.XCN_CRYPT_STRING_BASE64, base64Root);
certRequest.SignerCertificate = certSigner;
certRequest.Issuer = dnSigner;
static void Main(string[] args)
{
X509Certificate2 rootCert = GenerateCertificate("TEST_ROOT");
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadWrite);
store.Add(rootCert);
X509Certificate2 signedChild = GenerateUserCertificate("Testy McTesterson", rootCert);
store.Remove(rootCert);
store.Close();
}
A few important things to note:
This will only work with certain key usage and key spec flags
An X509Chain will still build and validate but it will recognize that the root is untrusted (Easy to bypass).
I want to create PKCS#7 detached signature with .Net Core (2.0).
I read all answers here more or less relevant to my issue and found this and this answers. All other were helpless. The first example do exactly what I need but it relies on .NetFramework.
The second one use Bouncy Castle library and do little different but similar thing. I found Portable.BouncyCastle project worked on .Net Core. As I can understand it is the only option for me.
This is the code from the first example with some modifications:
string s = "data string";
byte[] data = Encoding.UTF8.GetBytes(s);
X509Certificate2 certificate = null;
X509Store my = new X509Store(StoreName.My,StoreLocation.CurrentUser);
my.Open(OpenFlags.ReadOnly);
certificate = my.Certificates.Find(X509FindType.FindByThumbprint, "my thumbprint", false)[0];
if (certificate == null) throw new Exception("No certificates found.");
ContentInfo content = new ContentInfo(new Oid("1.2.840.113549.1.7.1"),data);
SignedCms signedCms = new SignedCms(content, true);
CmsSigner signer = new CmsSigner(certificate);
signer.DigestAlgorithm = new Oid("SHA256");
// create the signature
signedCms.ComputeSignature(signer);
return signedCms.Encode();
It works fine in my case. signedCms.Encode() returns 1835 bytes and this value pass validation.
But if I use BounceCastle I get another result. This is the code:
X509Certificate2 certificate = null;
X509Store my = new X509Store(StoreName.My, StoreLocation.CurrentUser);
my.Open(OpenFlags.ReadOnly);
certificate = my.Certificates.Find(X509FindType.FindByThumbprint, "my thumbprint", false)[0];
var privKey = DotNetUtilities.GetRsaKeyPair(certificate.GetRSAPrivateKey()).Private;
var cert = DotNetUtilities.FromX509Certificate(certificate);
var content = new CmsProcessableByteArray(data);
var generator = new CmsSignedDataGenerator();
generator.AddSigner(privKey, cert, CmsSignedGenerator.EncryptionRsa, CmsSignedGenerator.DigestSha256);
var signedContent = generator.Generate(content, false);
return signedContent.GetEncoded();
signedContent.GetEncoded() returns 502 bytes and this result can't be validated. I understand that I'm doing wrong something but I don't know what.
How should I modify the sample with Bouncy Castle that it get me the same result as the code above?
I found an another discussion that gave me a clue. There is a link to a GitHub repo with an example application. I modified it slightly and now it works as expected. Here is the code:
X509Certificate2 certificate = null;
X509Store my = new X509Store(StoreName.My, StoreLocation.CurrentUser);
my.Open(OpenFlags.ReadOnly);
certificate = my.Certificates.Find(X509FindType.FindByThumbprint, "thumbprint", false)[0];
var privKey = DotNetUtilities.GetRsaKeyPair(certificate.GetRSAPrivateKey()).Private;
var cert = DotNetUtilities.FromX509Certificate(certificate);
var content = new CmsProcessableByteArray(data);
var generator = new CmsSignedDataGenerator();
generator.AddSigner(privKey, cert, CmsSignedGenerator.EncryptionRsa, CmsSignedGenerator.DigestSha256);
var signedContent = generator.Generate(content, false);
string hashOid = OID.SHA256;
var si = signedContent.GetSignerInfos();
var signer = si.GetSigners().Cast<SignerInformation>().First();
SignerInfo signerInfo = signer.ToSignerInfo();
Asn1EncodableVector digestAlgorithmsVector = new Asn1EncodableVector();
digestAlgorithmsVector.Add(
new AlgorithmIdentifier(
algorithm: new DerObjectIdentifier(hashOid),
parameters: DerNull.Instance));
// Construct SignedData.encapContentInfo
ContentInfo encapContentInfo = new ContentInfo(
contentType: new DerObjectIdentifier(OID.PKCS7IdData),
content: null);
Asn1EncodableVector certificatesVector = new Asn1EncodableVector();
certificatesVector.Add(X509CertificateStructure.GetInstance(Asn1Object.FromByteArray(cert.GetEncoded())));
// Construct SignedData.signerInfos
Asn1EncodableVector signerInfosVector = new Asn1EncodableVector();
signerInfosVector.Add(signerInfo.ToAsn1Object());
// Construct SignedData
SignedData signedData = new SignedData(
digestAlgorithms: new DerSet(digestAlgorithmsVector),
contentInfo: encapContentInfo,
certificates: new BerSet(certificatesVector),
crls: null,
signerInfos: new DerSet(signerInfosVector));
ContentInfo contentInfo = new ContentInfo(
contentType: new DerObjectIdentifier(OID.PKCS7IdSignedData),
content: signedData);
return contentInfo.GetDerEncoded();
I'm getting the error AADSTS70002: Error validating credentials. AADSTS50012: Client assertion audience claim does not match Realm issuer
when running this code.
string[] scopes = new string[]{"https://graph.microsoft.com/.default"};
var certStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
certStore.Open(OpenFlags.ReadOnly);
var cert = certStore.Certificates.Cast<X509Certificate2>().First(c => c.Thumbprint == "XXX-XXX etc");
var cas = new ClientAssertionCertificate(cert);
var cc = new Microsoft.Identity.Client.ClientCredential(cas);
var client = new Microsoft.Identity.Client.ConfidentialClientApplication("XX-XXX etc", "http://localhost", cc, new TokenCache(), new TokenCache() );
var authResult = await client.AcquireTokenForClientAsync(scopes);
var dap = new DelegateAuthenticationProvider(rm =>
{
rm.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", authResult.AccessToken);
return Task.FromResult(0);
});
var gClient = new GraphServiceClient(dap);
gClient.Me.Dump();
Error occurrs on the call to AcquireTokenForClientAsync() method.
I can not find any online documentation for MSAL and Daemon clients where no user authentication is possible.
Suggestions ?
Found the problem. I needed to use the second overload of the ConfidentialClientApplication constructor, and supply the authorisation like this.
string authorityFormat = "https://login.microsoftonline.com/{0}/v2.0";
string tennantId = "xxx-xx-xx";
then
var client = new Microsoft.Identity.Client.ConfidentialClientApplication("xxx-x-xx etc", string.Format(authorityFormat, tennantId), "http://localhost", cc, new TokenCache(), new TokenCache() );
The code Here pointed me in the right direction.