Convert from PKCS1 to PKCS8 in C# - c#

I am using a C# google SDK to access Google Drive and GMail. For authentication, sometimes I'm provided a PKCS8 private key and other times it is a PKCS1 private key. I can make the PKCS8 authentication work, since that is what Google SDK expects. However, when I attempt to convert the PKCS1 into a PKCS8...it seems to work at first, until I actually start to make invocations against google's API.
Based on this advice, here is the code I'm using that is designed to receive PKCS1 or PKCS8:
string pkcs8Key = null;
if (creds.PrivateKey.Contains("BEGIN RSA"))
{
// Handle PKCS1
var keyParts = creds.PrivateKey.Split("-----", StringSplitOptions.RemoveEmptyEntries);
var rawKey = keyParts.OrderByDescending(s => s.Length).First();
var base64Key = rawKey.Trim();
var privateKeyBytes = Convert.FromBase64String(base64Key);
using var privateKey = RSA.Create();
privateKey.ImportRSAPrivateKey(privateKeyBytes, out _);
var keyBytes = privateKey.ExportPkcs8PrivateKey();
pkcs8Key = $"-----BEGIN PRIVATE KEY-----{Convert.ToBase64String(keyBytes)}-----END PRIVATE KEY-----\n";
}
else
{
// Handle PKCS8
pkcs8Key = creds.PrivateKey;
}
initializer = new BaseClientService.Initializer
{
HttpClientInitializer = new ServiceAccountCredential(
new ServiceAccountCredential.Initializer(creds.ID)
{
Scopes = new[] { DirectoryService.Scope.AdminDirectoryUserReadonly, DriveService.Scope.Drive },
User = email ?? creds.AdminAccount,
KeyId = creds.PrivateKeyId,
ProjectId = creds.ProjectId
}.FromPrivateKey(pkcs8Key))
};
Although the code above runs successfully, the error I receive later when I make an SDK request is this:
Error:"invalid_grant", Description:"java.security.SignatureException: Invalid signature for token: **JWT Suppressed**", Uri:""
I found this article which talks about causes of google's "invalid grant," and "reason #9" concerns me...because it seems to suggest that my token is indeed malformed, which would correspond with the "invalid signature" the error mentioned. Still, I'm not sure if I'm comparing apples to apples.
Any ideas?
Perhaps the problem is simply that the PKCS1 private key I'm using...doesn't have sufficient privileges?

I figured it out.
I had two sets of credentials to experiment with:
PKCS#1 private RSA key
PKCS#8 private key: Associated with this, I also had a "Private key id" and "project id"
Since both private keys pertained to the same service client email, I had been supplying the (otherwise optional) "private key id" and "project id" values for either private key above.
All I had to do was intentionally omit the "private key id" and "project id", when I was processing the PKCS#1, and then it worked. This leaves me still wondering what purpose those optional values serve...though I suspect it pertains to security audits (e.g. auth logs).

Related

When attempting to decrypt a key retrieved through getkeyasync I recieve errors. What am I missing?

I have an RSA key stored in the KeyVault (not a secret, a key). Using the .net KeyVaultClient I can retrieve the key.
However, when I try to decrypt the key so it can be used inside one of our applications, I am receiving an error (I have tried several methods of decrypting the key each is highlighted below, and each generates a different error message / exception).
I have tried a several methods. I will outline each, hopefully without disclosing any of our sensitive information.
KeyBundle keyBundle = await keyClient.GetKeyAsync(fullUri);
Where
(i) keyClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessToken)));
(ii) fullUri is "https://[mycompany].vault.azure.net/keys/[keyidentifier]"
I receive a KeyBundle object and inside that KeyBundle object I have the JsonWebKey which contains the Modulus and Exponent both as byte arrays.
Once I have these values I have tried decrypting the key using the following methods.
Using the keyClient.DecryptAsync(keyBundle.KeyIdentifier.ToString(), JsonWebEncryptionAlgorithm.RSA15, keyBundel.Key.N).GetAwaiter().GetResult(); method.
I have tried changing the algorithm in the above call and receive the same error (KeyVaultErrorException): BadRequest,
Also, I have tried replacing keyBundle.KeyIdentifier.ToString() with keyBundle.Key.Kid (which seems to hold the same value).
Using the KeyVaultClientExtensions.DecryptAsync(keyClient, keyBundle.Key.Kid, "RSAOAEP", keyBundle.Key.N);
I also receive a KeyVaultErrorException - BadRequest.
Using an HttpClient PostAsync event (code below) I have tried calling the Rest APi directly and receive the following exception:
{"error":{"code":"BadParameter","message":"The parameter is
incorrect.\r\n"}}
Code for the Rest Api Call:
keyClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessToken));
string fullUri = $"{baseUrl}keys/{keyIdentifier}";
KeyBundle keyBundle = await keyClient.GetKeyAsync(fullUri);
JsonWebKey key = keyBundle.Key;
string decryptUri = $"{keyBundle.Key.Kid}/decrypt?api-version=7.0";
HttpResponseMessage response = null;
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
try
{
var uri = new Uri(decryptUri, UriKind.Absolute);
string encryptedText = Convert.ToBase64String(key.N); //Encoding.UTF8.GetString(key.N, 0, key.N.Length);
DecryptionRequestInformation requestInformation = new DecryptionRequestInformation();
requestInformation.alg = "RSA1_5";
requestInformation.value = encryptedText;
var stringContent = new StringContent(JsonConvert.SerializeObject(requestInformation), Encoding.UTF8, "application/json");
response = await client.PostAsync(uri, stringContent);
}
catch (Exception ex)
{
throw ex;
}
}
string encryptionResult = string.Empty;
if (response != null)
{
encryptionResult = response.Content.ReadAsStringAsync().Result;
}
AccessToken is the result from the call to GetAccessToken in the KeyVaultClient callback
I would like to be able to use the decrypted key in one of our applications but am unable to decrypt it.
Could someone please explain what I am missing. I have a feeling that the byte array that is returned from the GetKeyAsync call is not what I think it is (is the RSA Modulus value (key.N) the encrypted key ?) or that I am not encoding the bytes (key.N) correctly to pass to the DecryptAsync method.
Unfortunately, many of the examples online refer to KeyVault secrets and not to keys and I cannot find any examples that seem to work when dealing with Keyvault keys.
What am I missing her please ?
Thanks in advance
Julian
Based on your issue description and the comments, I think you may use the Azure Key Vault in a wrong way.
You can only get the public part of the key which can be used to generate the public key.
The private key will be stored in Key Vault, and will not be able to be retrieved.
So, you can use KeyVaultClient to:
Sign digest with private key
Verify signature with public key
Encrypt data with public key
Decrypt data with private key
The Sign and Decrypt operations can only be performed with KeyVaultClient. The Verify and Encrypt operations can be performed locally, as you can generate the public key with modulus and exponent from the key's public part.
If you do want to get the private key and use it in your application, I think you have to use secret. Secret is just the thing you want: you can store it on Azure safely, and retrieve it when needed.

apiClient.RequestJWTUserToken in docusign doesn't work

I'm using DocuSign C# SDK to create a JWT token to impersonate as a user using an Integrator key but it throws an error saying:
Error while requesting server, received a non-successful HTTP code
I'm simply calling the UpdateToken() method which is like this:
private static void UpdateToken()
{
var apiClient = new ApiClient();
OAuth.OAuthToken authToken = apiClient.RequestJWTUserToken(ClientID, ImpersonatedUserGuid, AuthServer, Encoding.ASCII.GetBytes(PrivateKey), TokenExpiryInHours, new List<string> { "signature", "impersonation" });
AccessToken = authToken.access_token;
TokenExpiryTime = DateTime.Now.AddSeconds(authToken.expires_in.Value);
}
values for all the parameters passed in RequestJWTUserTokenare:
"DocuSign": {
"ClientID": "aff67220-XXXX-XXXX-XXXX-426b6575c3bd",
"ImpersonatedUserGuid": "f9a0f822-XXXX-XXXX-XXXX-7a576f06df81",
"AuthServer": "https://account-d.docusign.com",
"TokenReplacementTimeInSeconds": "600",
"TokenExpiryInHours": "1",
"PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\r\XXXXXXX\r\n-----END RSA PRIVATE KEY-----"
}
I have taken the consent from the user by using this endpoint:
https://account-d.docusign.com/oauth/auth?
response_type=token&scope=signature%20impersonation&client_id=aff67220-3ca7-4de7-8556-426b6575c3bd
&redirect_uri=https://www.vava.cars/tr/admin
and the user has successfully allowed this clientId/integrator key to have the permissions.
I don't know what I am doing wrong here.
For the Audience value (AuthServer in your code), you need to drop the https:// prefix and set that to account-d.docusign.com

C# sending data using JWT in MVC when private key is from a pfx

This is my first implementation of using JWT in MVC. I have created the following token in my controller to pass to the front end when they hit my endpoint . I have used the tool on JWT.io but I am uncertain if it's worked and what I put in the signature section 'secret'. It says invalid token signature but I can see the payload. Here is the code:
Any help or pointing in the right direction to see how it works would be great.
var payload = new Dictionary<string, object>()
{
{ "id", "example123" },
{ "Name", "John Doe" }
};
X509Certificate2 certX509 = new X509Certificate2(pfxFile, password);
byte[] privateKey = certX509.Export(X509ContentType.Cert, password);
string token = Jose.JWT.Encode(payload, privateKey, JwsAlgorithm.HS256);
//string[] getSecret = token.Split(".".ToCharArray());
//secret = getSecret[2];
return token;
The result, which seems to look like a JWT token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImV4YW1wbGUxMjMiLCJOYW1lIjoiSm9obiBEb2UifQ.j4Ub0iWU-6xTbc3pvwfBy0v0o-Y2Ds6C5_ez3NIRnIk
but it doesn't validate on https://jwt.io/. Thank you in advance :)
Edit
After playing around more I found if I create a string then convert it to a byte array then use the text in the signature box it verifies!!. Because my current byte array is populated from a pfx, the value to enter is unknown.
Put private key on secret textbox. correct secret validate token.
see snap

How can the upnName (user principal name) be set with BouncyCastle X509V3CertificateGenerator

I'm writing some unit tests for a pre-existing / functioning method in an application that gathers the user principal name from a given certificate via
var upnName = currentUserCert.GetNameInfo(X509NameType.UpnName, false);
This works at run time however, I'm so far unable to retrieve this value from a BouncyCastle X509V3CertificateGenerator generated certificate. While I can get most of the way there I've been unable to generate a certificate that contains a upnName. I'm simply unsure how to set the user principal such that it may be retrieved via GetNameInfo when testing.
The test certificate itself is being generated as such
private X509Certificate2 GenerateCert(KeyPurposeID keyPurposeId, string subjectPrefix = "OID.1.2.3.4=", string validCertOriginFragment = "OU=SOME ORG")
{
const int keyStrength = 2048;
const string subject = "98876543210123";
// Generating Random Numbers
var randomGenerator = new CryptoApiRandomGenerator();
var random = new SecureRandom(randomGenerator);
var kpgen = new RsaKeyPairGenerator();
kpgen.Init(new KeyGenerationParameters(new SecureRandom(new CryptoApiRandomGenerator()), keyStrength));
var certificateGenerator = new X509V3CertificateGenerator();
var certName = new X509Name($"{subjectPrefix}{subject} + CN=JAMES BOND (Affiliate), {validCertOriginFragment}, O=SOME ORG, C=US");
var issuer = new X509Name("OU=CERT AUTH CA, OU=Certification Authorities, O=MYCERTAUTH, C=US");
var serialNo = BigInteger.ProbablePrime(120, new Random());
certificateGenerator.SetSerialNumber(serialNo);
certificateGenerator.SetSubjectDN(certName);
certificateGenerator.SetIssuerDN(issuer);
certificateGenerator.SetNotAfter(DateTime.Now.AddYears(50));
certificateGenerator.SetNotBefore(DateTime.Now);
// TODO setup upn name
var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);
var subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
if (keyPurposeId != null)
{
certificateGenerator.AddExtension(
X509Extensions.ExtendedKeyUsage.Id,
false,
new ExtendedKeyUsage(keyPurposeId));
}
// Generating the Certificate
var issuerKeyPair = subjectKeyPair;
ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA256WITHRSA", issuerKeyPair.Private, random);
// selfsign certificate
var certificate = certificateGenerator.Generate(signatureFactory);
var x509 = new X509Certificate2(certificate.GetEncoded());
return x509;
}
As per the suggestion by #peop adding the following to the TODO of setup upn name
certificateGenerator.AddExtension("2.5.29.17", true,
new GeneralNames(
new GeneralName(GeneralName.OtherName,
new DerSequence(new DerObjectIdentifier("1.3.6.1.4.1.311.20.2.3"), new DerUtf8String($"{subject}#SOMEWHERE.COM")))
));
Has gotten me closer, I think, upon investigating the certificate I have an extra extension which is good, however the expected information is still not exposed via the GetNameInfo method. Is there something more that must happen to the certificate in order for the magical Microsoft GetNameInfo method to light up and return the expected value?
How do I create a certificate via the BouncyCastle X509V3CertificateGenerator such that I can retrieve the upnName for the sake of testing?
The following worked for me and produced a certificate with a Subject Alternative Name attribute that was in the same format as a certificate produced by Microsoft Certificate Services when used for user/client authentication. That is, when viewing the Subject Alternative Name attribute of the .cer file in Windows, you see:
Other Name:
Principal Name=john.doe#contoso.com
RFC822 Name=john.doe#contoso.com
certificateGenerator.AddExtension(X509Extensions.SubjectAlternativeName.Id, false, new DerSequence(
new GeneralName(GeneralName.OtherName,
new DerSequence(new DerObjectIdentifier("1.3.6.1.4.1.311.20.2.3"),
new DerTaggedObject(0, new DerUtf8String("john.doe#contoso.com")))),
new GeneralName(GeneralName.Rfc822Name, "john.doe#costoso.com")));
The key was to wrap the UPN string in a DerTaggedObject. As suggested by pepo, I used the ASN.1 Editor to view an existing certificate. It had a bunch of Context Specific nodes around these values. So doing research on that, I found Tutorial for ASN1 DER Primitive Encoder, which led me to the DerTaggedObject. From there, I just started trying some code until the output of my generated certificate matched that from a certificate created by Microsoft Certificate Services.
Note also that the extension for Subject Alternative Name is not marked as critical (the second parameter to the AddExtension method is false). This again matches what is produced by Microsoft Certificate Services.
Microsoft provides Guidelines for enabling smart card logon with third-party certification authorities. In that document the specific format requirements for the certificate are enumerated:
The CRL Distribution Point (CDP) location (where CRL is the
Certification Revocation List) must be populated, online, and
available. For example:
[1]CRL Distribution Point
Distribution Point
Name:
Full Name: URL=http://server1.name.com/CertEnroll/caname.crl
Key Usage = Digital Signature
Basic Constraints [Subject Type=End Entity, Path Length Constraint=None (Optional)
Enhanced Key Usage =
Client Authentication (1.3.6.1.5.5.7.3.2)
(The client authentication OID) is only required if a certificate is used
for SSL authentication.)
Smart Card Logon (1.3.6.1.4.1.311.20.2.2)
Subject Alternative Name = Other Name: Principal Name= (UPN). For example:
UPN = user1#name.com
The UPN OtherName OID is : "1.3.6.1.4.1.311.20.2.3"
The UPN OtherName value: Must be ASN1-encoded UTF8 string
Subject = Distinguished name of user. This field is a mandatory extension, but the population of this field is optional.
Given the format requirements and your certificate generation code (including the UPN you've added based on pepo's answer) you need to modify the section where you are adding extensions to the cert:
if (keyPurposeId != null)
{
certificateGenerator.AddExtension(
X509Extensions.ExtendedKeyUsage.Id,
false,
new ExtendedKeyUsage(keyPurposeId));
// new extension not present in your question's code
certificateGenerator.AddExtension(
"1.3.6.1.5.5.7.3.2",
false,
new ExtendedKeyUsage(KeyPurposeID.IdKPClientAuth));
}
Doing this should create a cert that can be used as an X509Certificate2 instance to GetNameInfo(X509NameType.UpnName, false);.
According to this page UPN should be part of SubjectAlternativeName (SAN) extension.
Subject Alternative Name = Other Name: Principal Name= (UPN). For example:
UPN = user1#name.com
The UPN OtherName OID is : "1.3.6.1.4.1.311.20.2.3"
The UPN OtherName value: Must be ASN1-encoded UTF8 string
Example of generating SAN extension using X509V3CertificateGenerator can be found here.
// example of adding email address to SAN
certGen.AddExtension("2.5.29.17", true,
new GeneralNames(new GeneralName(GeneralName.Rfc822Name, "test#test.test")));
So close, but we need GeneralName.OtherName which is defined here.
So my pseudo code (written in notepad) that should do the trick could look like this:
certificateGenerator.AddExtension("2.5.29.17", true,
new GeneralNames(
new GeneralName(GeneralName.OtherName,
new DerSequence(new DerObjectIdentifier("1.3.6.1.4.1.311.20.2.3"),
new DerUtf8String(upnName)))
));
Hope I did the ASN.1 structure right - Sequence->(oid, utf8string). The best way would be to parse an existing certificate with i.e. ASN.1 editor to check the ASN.1 structure of the extension.

Constructing RSACryptoServiceProvider from X509Certificate2 without LDAP request being made

When the line to extract the public key is executed, an LDAP request is sent:
this.certificate = new X509Certificate2(buffer);
System.Security.Cryptography.X509Certificates.PublicKey key = this.certificate.PublicKey;
50 0.853745000 xxx.xxx.xxx.xxx xxx.xxx.xxx.xxx LDAP 404 searchRequest(1) "" baseObject
...which I believe is authenticating the currently logged on user. I really need to avoid this call, as on a customer system this causes a long delay because of network configuration.
I'm assuming it's attempting to do some authentication around some kind of key store, but as in this case the certificate is all contained in the buffer provided, all I want is for the key to be used without this request being sent.
All I actually want is to create an RSACryptoServiceProvider from the private key in the certificate. I tried a few methods I've found on here involving GetPrivateKey, but struggled to get anything to work.
Thanks in advance!
EDIT Test program:
static void Main( string[] args )
{
var certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(#"E:\Temp\Cert.cer");
System.Security.Cryptography.X509Certificates.PublicKey key = certificate.PublicKey;
}
The certificate I tested with can be found here: Cert.cer
Yes, it's not the strongest signature or key, before I get comments!
Thanks again.
EDIT: I actually worked around this by using a suggestion to use BouncyCastle. I use this to parse the certificate:
X509CertificateParser parser = new X509CertificateParser();
Org.BouncyCastle.X509.X509Certificate cert = parser.ReadCertificate(buffer);
I then extract the modulus and exponent and push them into a Microsoft RSAParameters:
RsaKeyParameters key = (RsaKeyParameters)cert.GetPublicKey();
// Construct a microsoft RSA crypto service provider using the public key in the certificate
RSAParameters param = new RSAParameters();
param.Exponent = key.Exponent.ToByteArrayUnsigned();
param.Modulus = key.Modulus.ToByteArrayUnsigned();
I can then construct the Microsoft RSACryptoServiceProvider from this:
using (RSACryptoServiceProvider provider = new RSACryptoServiceProvider())
{
provider.ImportParameters(param);
byte[] rsaBlock = provider.Encrypt(preMasterSecret, false);
this.Client.Writer.Write(rsaBlock);
}
I never got any other response, so here's the Bouncycastle implementation I used.
X509CertificateParser parser = new X509CertificateParser();
Org.BouncyCastle.X509.X509Certificate cert = parser.ReadCertificate(buffer);
I then extract the modulus and exponent and push them into a Microsoft RSAParameters:
RsaKeyParameters key = (RsaKeyParameters)cert.GetPublicKey();
// Construct a microsoft RSA crypto service provider using the public key in the certificate
RSAParameters param = new RSAParameters();
param.Exponent = key.Exponent.ToByteArrayUnsigned();
param.Modulus = key.Modulus.ToByteArrayUnsigned();
I can then construct the Microsoft RSACryptoServiceProvider from this:
using (RSACryptoServiceProvider provider = new RSACryptoServiceProvider())
{
provider.ImportParameters(param);
byte[] rsaBlock = provider.Encrypt(preMasterSecret, false);
this.Client.Writer.Write(rsaBlock);
}

Categories