I have an application (dotnet core with Bouncy Castle) that generates X509 certificates. Essential part of certificate management is also revoking them. I do support revoking by CRL currently, but there was a requirement from our vendor to implement OCSP as well. And here the problems started to appear.
I've implemented something that looks compatible with the RFC 6960 and with some random Java pieces that are doing something very similar.
However I am stuck with the following error that I could not figure out:
OCSP Request Data:
Version: 1 (0x0)
Requestor List:
Certificate ID:
Hash Algorithm: sha1
Issuer Name Hash: 5ED580C3E05BAB929E3B5FE31AF1FD2D63DA2B66
Issuer Key Hash: 0BA5610F200458E9183155354D0956598285F833
Serial Number: 2D9E89A741BE3BED
Request Extensions:
OCSP Nonce:
0410D596BF4A21EA59007CAD5AE306DCF349
Error querying OCSP responder
27384:error:0D0680A8:asn1 encoding routines:asn1_check_tlen:wrong tag:../openssl-1.1.1c/crypto/asn1/tasn_dec.c:1130:
27384:error:0D06C03A:asn1 encoding routines:asn1_d2i_ex_primitive:nested asn1 error:../openssl-1.1.1c/crypto/asn1/tasn_dec.c:694:
27384:error:0D08303A:asn1 encoding routines:asn1_template_noexp_d2i:nested asn1 error:../openssl-1.1.1c/crypto/asn1/tasn_dec.c:627:Field=responseStatus, Type=OCSP_RESPONSE
The configuration I am following is:
#1 root certificate - used to create all client certificates that can be revoked
#2 ocsp root certificate - generated separately used to sign ocsp responses
I am testing my code using the following command (from the command line):
$ openssl ocsp -issuer certs/root-ocsp.crt -cert client-cert.crt -text -url http://localhost:12345/certificate/ocsp
To be honest I have no more idea what may be wrong... Basing on the error, I suspect that something is wrong with Asn1 signatures, but after checking few options (including rootCert in chain, etc) result is the same all the time.
The crucial part of the code that generates OCSP response looks like this:
public byte[] GenerateOcsp(OcspReq ocspReq, IList<Certificate> certificates)
{
//Should be used at all for OCSP?
var rootCert = _rootCertificateProvider.GetBouncyRootCert("development");
var ocspCert = _rootCertificateProvider.GetBouncyRootCert("ocsp");
var respGen = new BasicOcspRespGenerator(ocspCert.Certificate.GetPublicKey());
var nonceExt = ocspReq.RequestExtensions.GetExtension(OcspObjectIdentifiers.PkixOcspNonce);
if (nonceExt != null)
respGen.SetResponseExtensions(new X509Extensions(new[] { OcspObjectIdentifiers.PkixOcspNonce }, new[] { nonceExt }));
foreach (var req in ocspReq.GetRequestList())
{
var serialNo = req.GetCertID().SerialNumber;
var dbCert = certificates.FirstOrDefault(c => BigInteger.ValueOf(c.SerialNumber).Equals(serialNo));
if (dbCert == null)
{
respGen.AddResponse(req.GetCertID(), new UnknownStatus());
}
else if (dbCert.Revocated)
{
respGen.AddResponse(req.GetCertID(), new RevokedStatus(dbCert.RevocationDate ?? DateTime.Now, 0));
}
else
{
respGen.AddResponse(req.GetCertID(), CertificateStatus.Good);
}
}
var random = new SecureRandom();
var signFactory = new Asn1SignatureFactory("SHA256WithRSA", ocspCert.PrivKey, random);
var ocspResponse = respGen.Generate(signFactory, new[] { ocspCert.Certificate }, DateTime.UtcNow);
return ocspResponse.GetEncoded();
}
Result is returned through the Controller like this:
return new FileContentResult(result, "application/ocsp-response");
Any ideas what may be wrong?
Thanks!
To all future people arriving to this question:
My overall approach in the question was good, the only thing missing was piping a result through an additional response generator.
So instead of returning ocspResponse.GetEncoded() you should do this and you are done:
var builder = new OCSPRespGenerator();
var response = builder.Generate(OCSPRespGenerator.Successful, ocspResponse);
return response.GetEncoded();
Related
Update
It turned out, that we have a problem with the SslStream (on which the HttpClient is based on). In the current version (7.xx) the certificate chain is not submitted to the server, when sent via the client. This is a known issue and discussed here and here.
I will leave this post online, since the code below might be helpful to others (it does not cause problems if you want to use the client certificate only in your requests).
I have spent a lot of time trying to find our what's wrong with the Client Certificate authentication using ECDsa based certificates with the native HttpClient of .Net Core (version 7.0.100 but also tried v.6xxx) but never got this thing running. (Btw. I used the same approach for RSA based Client Certificates without any problems).
Due to security reasons, I MUST use ECDsa client certificate + chain.
I can not understand or find information why this is not working / supported and the results are confusing to me.
When loading the certificate and the key and using them to sign and verify some data, all tests pass (see code).
In the end, the required Client Certificates are not sent to the server, resulting in an SSL exception (tested the same certificates with a Python script to verify that they are correct, there I had zero problems).
I can not imagine (at least I hope not) that these kind of Client Certificates are not supported. I would greatly appreciate any help or hints for alternative workarounds. It would be quite terrible to switch to some different language at this point :/
Reference Code
Contains a test certificate and key to play around with. The chain is missing in this example and can simply be attached to the certificate string
Part of the tests are taken from: Use X509Certificate2 to sign and validate ECDSA-SHA256 signatures
[Test]
// Just some test to better understand whether the certificate and key is working and belong together
public void TestEcdsaFunctionality()
{
var ecdsaCertificate = #"-----BEGIN CERTIFICATE-----
MIIBgzCCASoCCQD+iUUbrH+BOzAKBggqhkjOPQQDAjBKMQswCQYDVQQGEwJQRDEL
MAkGA1UECAwCQlcxEDAOBgNVBAcMB1BhbmRvcmExDzANBgNVBAoMBkFueU9yZzEL
MAkGA1UEAwwCNDIwHhcNMjIxMTIxMTE0MjExWhcNMjMxMTIxMTE0MjExWjBKMQsw
CQYDVQQGEwJQRDELMAkGA1UECAwCQlcxEDAOBgNVBAcMB1BhbmRvcmExDzANBgNV
BAoMBkFueU9yZzELMAkGA1UEAwwCNDIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
AAT6vBU2iIcESep8UeQhfNFgfTArFYvtb2Pmlbk1+R9gdNaWEg1UK7dlt3/mH/X3
Mrg80JaTY3OPM92MY9e9gs7ZMAoGCCqGSM49BAMCA0cAMEQCIA3p2mMOYqGEzReY
br7nYLsLdF0+dV6iZSZaG1iMHwblAiA5UaJlVr5CsCkG+j1ZJEICSOnVMyx4DjA5
oZuoMYa42w==
-----END CERTIFICATE-----";
var ecdsaPrivateKey = #"MDECAQEEIM6BExC2G7P1KpViQmZ/Z65nukv8yQmvw6PqGGQcKn9boAoGCCqGSM49
AwEH";
var cert = X509Certificate2.CreateFromPem(ecdsaCertificate.ToCharArray());
var key = ECDsa.Create("ECDsa");
var keybytes = Convert.FromBase64String(ecdsaPrivateKey);
key.ImportECPrivateKey(keybytes, out _);
var helloBytes = Encoding.UTF8.GetBytes("Hello World");
// Sign data with the ECDsa key
var signed = key.SignData(helloBytes, 0, helloBytes.Count(), HashAlgorithmName.SHA256);
// Verify the data signature with the certificates public key
var verified = cert.GetECDsaPublicKey().VerifyData(helloBytes, signed, HashAlgorithmName.SHA256);
// Assume that everything went well and the data signature is valid
Assert.That(verified, Is.EqualTo(true));
// Additional tests with the X509Certificate2 object type
X509Certificate2 keyCert = ECDsaCertificateExtensions.CopyWithPrivateKey(cert, key);
// Sing using the certificate that contains the private key
using (ECDsa ecdsa = keyCert.GetECDsaPrivateKey())
{
if (ecdsa == null)
throw new ArgumentException("Cert must have an ECDSA private key", nameof(cert));
signed = ecdsa.SignData(helloBytes, HashAlgorithmName.SHA256);
}
// Verify signed data using the certificate that contains the private key
using (ECDsa ecdsa = keyCert.GetECDsaPublicKey())
{
if (ecdsa == null)
throw new ArgumentException("Cert must be an ECDSA cert", nameof(cert));
Assert.That(ecdsa.VerifyData(helloBytes, signed, HashAlgorithmName.SHA256), Is.EqualTo(true));
}
WorkshopRegistration(keyCert);
}
// This would be what I really want to use the client certificate for
private void WorkshopRegistration(X509Certificate2 clientCert)
{
var payload = "{somepayload}";
var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
handler.ClientCertificates.Add(clientCert);
handler.ServerCertificateCustomValidationCallback =
(httpRequestMessage, cert, cetChain, policyErrors) =>
{
return true;
};
var content = new StringContent(payload, System.Text.Encoding.UTF8, "application/json");
var client = new HttpClient(handler);
client.DefaultRequestHeaders.Add("Accept", "application/json");
var result = client.PutAsync("https://someHostname.com/registration",
content).GetAwaiter().GetResult();
if (result.StatusCode != HttpStatusCode.OK)
throw new SuccessException("Registration failed with conent: " + result.Content.ToString());
}
I am currently using Forge Webhooks API to handle different events that might occur on a project. Everything works fine, except the payload signature check.
The reason why I want to check the payload is because the callback will end up on my API and I want to reject all requests that do not come from Forge's webhook service.
Steps I followed:
Add (register) secret key (token) on Forge. API Reference
Trigger an event that will eventually call my API for handling it.
Validating signature header. Followed this tutorial.
PROBLEM!!! My computedSignature is different from the signature received from Forge.
My C# code looks like this:
private const string SHA_HASH = "sha1hash";
var secretKeyBytes = Encoding.UTF8.GetBytes(ForgeAuthConfiguration.AntiForgeryToken);
using var hmac = new HMACSHA1(secretKeyBytes);
var computedHash = hmac.ComputeHash(request.Body.ReadAsBytes());
var computedSignature = $"{SHA_HASH}={computedHash.Aggregate("", (s, e) => s + $"{e:x2}", s => s)}";
For one example, Forge's request has this signature header: sha1hash=303c4e7d2a94ccfa559560dc2421cee8496d2d83
My C# code computes this signature: sha1hash=3bb8d41c3c1cb6c9652745f5996b4e7f832ca8d5
The same AntiForgeryToken was sent to Forge at step 1
Ok, I thought my C# code is broken, then I tried this online HMAC generator and for the given input, result is: 3bb8d41c3c1cb6c9652745f5996b4e7f832ca8d5 (same as C#)
Ok, maybe the online generator is broken, I tried their own code in node js and this is the result:
I have 3 ways of encrypting the SAME body using the SAME key and I get the SAME result every time. BUT those results are DIFFERENT from the signature provided by Forge, resulting in failing the check and rejecting a valid request...
Does anyone know what is happening with that signature?
Why is it different from my result if I follow their tutorial?
How are you validating your requests?
The code below is working at my side. Could you give it a try if it helps?
[HttpPost]
[Route("api/forge/callback/webhookbysig")]
public async Task<IActionResult> WebhookCallbackBySig()
{
try
{
var encoding = Encoding.UTF8;
byte[] rawBody = null;
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
rawBody = encoding.GetBytes(reader.ReadToEnd());
}
var requestSignature = Request.Headers["x-adsk-signature"];
string myPrivateToken = Credentials.GetAppSetting("FORGE_WEBHOOK_PRIVATE_TOKEN");
var tokenBytes = encoding.GetBytes(myPrivateToken);
var hmacSha1 = new HMACSHA1(tokenBytes);
byte[] hashmessage = hmacSha1.ComputeHash(rawBody);
var calculatedSignature = "sha1hash=" + BitConverter.ToString(hashmessage).ToLower().Replace("-", "");
if (requestSignature.Equals(calculatedSignature))
{
System.Diagnostics.Debug.Write("Same!");
}
else
{
System.Diagnostics.Debug.Write("diff!");
}
}
catch(Exception ex)
{
}
// ALWAYS return ok (200)
return Ok();
}
If this does not help, please share with your webhook ID (better send email at forge.help#autodesk.com). We will ask engineer team to check it.
I've attempted to check if the JWT Token Bearer support in WebAPI for .NET Framework 4.5 had the vulnerability as reported by https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/.
So I wrote the following code that takes an existing, valid JWT token, manipulates the header, regenerates the signature using the certificate public key and resend it to the server.
private static string TestSecurityVulnerability(string originalToken)
{
var parts = originalToken.Split(new[] { '.' }, StringSplitOptions.RemoveEmptyEntries);
var headerPart = Encoding.UTF8.GetString(Convert.FromBase64String(parts.First()));
var payloadPart = Encoding.UTF8.GetString(Convert.FromBase64String(parts.Skip(1).First()));
dynamic headerJson = JsonConvert.DeserializeObject(headerPart);
headerJson["alg"] = "HS256";
headerJson["kid"] = 0;
var tamperedHeaderEncoded = Base64UrlEncode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(headerJson)));
var cert = Certificate.Load();
var publicKey = cert.PublicKey.EncodedKeyValue.RawData;
var base64PublicKey = Convert.ToBase64String(publicKey); //Not using it but great for verifying if token is "valid"
var sha = new HMACSHA256(publicKey);
var newSigBinary = sha.ComputeHash(Encoding.UTF8.GetBytes(tamperedHeaderEncoded + "." + parts.Skip(1).First()));
var newSigEncoded = Base64UrlEncode(newSigBinary);
return string.Join(".", tamperedHeaderEncoded,
parts.Skip(1).First(),
newSigEncoded);
}
// from JWT spec
private static string Base64UrlEncode(byte[] input)
{
var output = Convert.ToBase64String(input);
output = output.Split('=')[0]; // Remove any trailing '='s
output = output.Replace('+', '-'); // 62nd char of encoding
output = output.Replace('/', '_'); // 63rd char of encoding
return output;
}
As for Certificate.Load(), all it does is look up the certificate that I'm using on IdentityServer to sign the token and it returns with a System.Security.Cryptography.X509Certificates.X509Certificate2 certificate (PFX with public and private key).
I also inspected the value of base64PublicKey to see what the base64'd version is of the public key, to use that as the 256-bit-secret on jwt.io.
My question is, did I do everything correctly? I've checked my solution on https://jwt.io/ and it seems legit from the point of view that I'm signing the JWT correctly but I don't know if I am extracting the certificate correctly.
Would be great just to have a pair of eyes go through this and let me know.
P.S. with my current test the server came back saying "Unauthorized" ;-)
For various reasons I need to be able to make Chef API requests in C#. I've followed the guides here (Header specification) and here (Bash example) but have reached a dead end. Every request I send comes back 401 Unauthorized with the content
{"error":["Invalid signature for user or client 'myuser'"]}
I have also configured Knife locally to use Fiddler as a HTTP proxy so I can inspect Knife HTTP requests and have copied the visible headers as accurately as possible, but naturally I cannot see the canonical header it generated nor the one the server expects.
However, I have confirmed in this way that the hashes I'm generating for the content (empty) and path are the same as Knife is generating.
Here's my code. Loading the RSA private key from PEM format is using an extension method taken from Christian Etter's blog - run out of links sorry.
const string path = "/cookbooks"
const string basePath = "https://chefserver.internal:443";
var timestamp = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ssZ");
var method = "GET";
var clientName = "myuser";
var hashedPath = ToBase64EncodedSha1String(path);
var hashedBody = ToBase64EncodedSha1String(String.Empty);
var canonicalHeader = String.Format("Method:{0}\nHashed Path:{1}\nX-Ops-Content-Hash:{2}\nX-Ops-Timestamp:{3}\nX-Ops-UserId:{4}",
method, hashedPath, hashedBody, timestamp, clientName);
var privateKey = File.ReadAllText("C:\\chef\\myuser.private.pem");
string signature;
byte[] rawData;
using (var rsa = new RSACryptoServiceProvider())
{
rsa.PersistKeyInCsp = false;
rsa.LoadPrivateKeyPEM(privateKey);
using (var sha1 = new SHA1CryptoServiceProvider())
{
rawData = rsa.SignData(Encoding.UTF8.GetBytes(canonicalHeader), sha1);
signature = Convert.ToBase64String(rawData);
}
}
var client = new HttpClient();
var message = new HttpRequestMessage();
message.Method = HttpMethod.Get;
message.RequestUri = new Uri(basePath + path);
message.Headers.Add("Accept", "application/json");
message.Headers.Add("Host", "chefserver.internal:443");
message.Headers.Add("X-Chef-Version", "11.12.4");
message.Headers.Add("X-Ops-Timestamp", timestamp);
message.Headers.Add("X-Ops-Sign", "version=1.0;");
message.Headers.Add("X-Ops-Userid", clientName);
message.Headers.Add("X-Ops-Content-Hash", hashedBody);
message.Headers.Add("User-Agent", "Chef Knife/11.4.0 (ruby-1.9.2-p320; ohai-6.16.0; x86_64-darwin11.3.0; +http://opscode.com)");
var currentItem = new StringBuilder();
var i = 1;
foreach (var l in signature)
{
currentItem.Append(l);
if (currentItem.Length == 60)
{
message.Headers.Add("X-Ops-Authorization-" + i, currentItem.ToString());
i++;
currentItem = new StringBuilder();
}
}
message.Headers.Add("X-Ops-Authorization-" + i, currentItem.ToString());
var response = await client.SendAsync(message);
var content = await response.Content.ReadAsStringAsync();
And the helper
private string ToBase64EncodedSha1String(string input)
{
return
Convert.ToBase64String(new SHA1CryptoServiceProvider().ComputeHash(Encoding.UTF8.GetBytes(input)));
}
I had a similar problem and found that the .NET crypto libraries did not support what was needed to make this work. I ended up using the BouncyCastle crypto libraries to accomplish this.
I made a simple C# class library that wraps this up. Take a look at that here: https://github.com/mattberther/dotnet-chef-api
I'm not super familiar with C# crypto, but the docs on SHA1CryptoServiceProvider seem to show that is is signing a SHA1 hash. Chef doesn't use signed hashes, you actually need to do an RSA signature on canonicalHeader itself.
SignData(Byte[], Object): Computes the hash value of the specified byte array using the specified hash algorithm, and signs the resulting hash value.
Is there a null or pass-through hash you can use?
This question already has an answer here:
Closed 11 years ago.
Possible Duplicate:
Generated signed X.509 client certificate is invalid (no certificate chain to its CA)
I followed the example at:
http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation
But the resulting signed client certificate has the following error when opened in windows:
"This file is invalid for use as the following: Security Certificate"
If I install it anyway and view it with certmgr, the certification path looks OK - I see my self-signed Certificate Authority (which is fine, no problems there) but the client cert has the following status:
"This certificate has an invalid digital signature."
If I call X509Certificate.Verify() it throws the following exception:
"Public key presented not for certificate signature"
Yet I'm using the same exact public key extracted from the Pkcs10CertificationRequest and when I called Verify() on that it's fine.
Any ideas? After days of struggling through this, I've got all the pieces working except this last one - and what's really confusing is that my self-signed CA cert is fine. There's just something going on with the client cert. Here's the entire block of code:
TextReader textReader = new StreamReader("certificaterequest.pkcs10");
PemReader pemReader = new PemReader(textReader);
Pkcs10CertificationRequest certificationRequest = (Pkcs10CertificationRequest)pemReader.ReadObject();
CertificationRequestInfo certificationRequestInfo = certificationRequest.GetCertificationRequestInfo();
SubjectPublicKeyInfo publicKeyInfo = certificationRequestInfo.SubjectPublicKeyInfo;
RsaPublicKeyStructure publicKeyStructure = RsaPublicKeyStructure.GetInstance(publicKeyInfo.GetPublicKey());
RsaKeyParameters publicKey = new RsaKeyParameters(false, publicKeyStructure.Modulus, publicKeyStructure.PublicExponent);
bool certIsOK = certificationRequest.Verify(publicKey);
// public key is OK here...
// get the server certificate
Org.BouncyCastle.X509.X509Certificate serverCertificate = DotNetUtilities.FromX509Certificate(System.Security.Cryptography.X509Certificates.X509Certificate.CreateFromCertFile("servermastercertificate.cer"));
// get the server private key
byte[] privateKeyBytes = File.ReadAllBytes("serverprivate.key");
AsymmetricKeyParameter serverPrivateKey = PrivateKeyFactory.CreateKey(privateKeyBytes);
// generate the client certificate
X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
generator.SetSerialNumber(BigInteger.ProbablePrime(120, new Random()));
generator.SetIssuerDN(serverCertificate.SubjectDN);
generator.SetNotBefore(DateTime.Now);
generator.SetNotAfter(DateTime.Now.AddYears(5));
generator.SetSubjectDN(certificationRequestInfo.Subject);
generator.SetPublicKey(publicKey);
generator.SetSignatureAlgorithm("SHA512withRSA");
generator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifierStructure(serverCertificate));
generator.AddExtension(X509Extensions.SubjectKeyIdentifier, false, new SubjectKeyIdentifierStructure(publicKey));
var newClientCert = generator.Generate(serverPrivateKey);
newClientCert.Verify(publicKey); // <-- this blows up
return DotNetUtilities.ToX509Certificate(newClientCert).Export(X509ContentType.Pkcs12, "user password");
I figured this out. If you call X509Certificate.Verify(publicKey) you have to pass the CA's public key, not the client's public key from the Pkcs10CertificationRequest.