I am using the following code, which I borrowed originally from the jwt-dotnet github page
private static string CreateToken(UserPrincipal principal)
{
/*
* https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
* http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
*/
var key = ConfigurationManager.AppSettings["jwt-key"];
var claims = new Dictionary<string, string>()
{
{ClaimTypes.Name, "Rainbow Dash" },
{ClaimTypes.WindowsAccountName, "RDash"}
};
var algorithm = new HMACSHA256Algorithm();
var serializer = new JsonNetSerializer();
var urlEncoder = new JwtBase64UrlEncoder();
var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
var token = encoder.Encode(claims, key);
return token;
}
The above code generates the following token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiUmFpbmJvdyBEYXNoIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy93aW5kb3dzYWNjb3VudG5hbWUiOiJSRGFzaCJ9.5WZWDJ0pvTe6QLjVNUeTfZicX_wSsk1dtYvXUbpiOiw
So, I hopped over to jwt.io to test my token. I'm told I have an invalid signature.
How do I give it a valid 'signature'? I don't understand what my JWT is missing.
The tool over JWT.io can verify the digital signature of your token if you give it the secret signing key you used while creating a token:
And from looking at your code it's the value contained in your:
ConfigurationManager.AppSettings["jwt-key"];
Just input the value inside the "secret" text box and if the signature of the token matches the one calculated by JWT.io then you'll get a message saying that the signature is valid.
Related
I want to use yubico OTP as a second factor in my application.
Yubico OTP documentation: https://developers.yubico.com/OTP/
The following is a c#(.net 6) example which reads the OTP via console (You need to press the button on the usbstick, then the otp is used as parameter for the rest service request). This sample is based on version 2.0 or the verify service (https://api.yubico.com/wsapi/2.0/verify)
using System.Security.Cryptography;
//Sample for validating OTP based on https://developers.yubico.com/OTP/OTPs_Explained.html
//Sample request: https://api.yubico.com/wsapi/2.0/verify?otp=vvvvvvcucrlcietctckflvnncdgckubflugerlnr&id=87&timeout=8&sl=50&nonce=askjdnkajsndjkasndkjsnad
// The yubico api clientid.
// You can open an api key here: https://upgrade.yubico.com/getapikey/
string yubicoCredentialClientId = "87";
// This is currently not required. Should be used to verify the response but its unclear whether this is possible or not.
// string yubicoCredentionPrivateKey = "";
string yubikeyValidationUrl = $"https://api.yubico.com/wsapi/2.0/verify?";
string nonce = "";
//Create a nonce
using (var random = RandomNumberGenerator.Create())
{
var tmpNonce = new byte[16];
random.GetBytes(tmpNonce);
nonce = BitConverter.ToString(tmpNonce).Replace("-", "");
}
//Get the OTP from yubikey
System.Console.WriteLine("Press yubikey button and then enter");
var otp = Console.ReadLine();
System.Console.WriteLine(otp);
string validationParameter = $"otp={otp}&id={yubicoCredentialClientId}&nonce={nonce}";
HttpClient client = new HttpClient();
var url = $"{yubikeyValidationUrl}{validationParameter}";
System.Console.WriteLine(url);
var result = client.GetAsync(url).Result;
System.Console.WriteLine(result.StatusCode);
string respnse = result.Content.ReadAsStringAsync().Result;
System.Console.WriteLine(respnse);
if (respnse.ToLower().Contains("status=ok"))
System.Console.WriteLine("OTP succsessful validated");
else
System.Console.WriteLine("OTP invalid");
This all works fine and even returns status=OK as part of the response when i use a valid OTP generated by the yubikey.
Question: Can i somehow validate the response using my yubico api private key? If not, it seems this authentication would be vulnerable to a man in the middle attack.
Side question: The request requires an api id and i even created one via https://upgrade.yubico.com/getapikey/ but i can just use any id and the request works all the same. Is this by design? If yes, what it the point of this id parameter in the first place?
There is actually documentation for this: https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
A hmac-sha1 must be created for the parameters and then this signature must be added as an additional parameter.
//Create the signature based on https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
//Prepare the parameters to be signed (Ordered alphabetically)
string signatureParameters = $"id={yubicoCredentialClientId}&nonce={nonce}&otp={otp}";
//Create the key based on the api key string
byte[] base64AsByte = Convert.FromBase64String(yubicoCredentionPrivateKey);
string signature = "";
using (var hmac = new HMACSHA1(base64AsByte))
{
//Create the hmacsha1
var signatureAsByte = hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureParameters));
signature = Convert.ToBase64String(signatureAsByte);
}
//Add the signature
signatureParameters+=$"&h={signature}";
Such an url then looks like this(The signature is part of the h parameter):
https://api.yubico.com/wsapi/2.0/verify?id=42&nonce=5FB3D5377640BA3FB8955AF98D6B71EC&otp=foobar&h=XXVw+vqc3k//qFGG6+WbP96xXis=
Complete example
The following is a complete self-contained example howto use the Yubikey OTP in a .net application (Including validation of the signatures)
The followings steps are performed:
Create the parameters for the request
Create a nonce
Get OTP from yubikey
Sign the parameter using the API key
Call the verify service from yubico
Check otp
Check return status
Compare returned signature with built signature
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
//Sample for validating OTP based on https://developers.yubico.com/OTP/OTPs_Explained.html
//Sample request: "https://api.yubico.com/wsapi/2.0/verify?id=87&nonce=44D4185490BA8E77E58A38A98CF501E9&otp=cccccxxxvulhlletkijhrtifrintlerfbnbhtdnikl&h=f9Ht4a08iaFQYQBI5E0XUni3Pss="
//Sample response: h=TC/RXXcVqPWkFr4JPlf29nWEnig=\r\nt=2022-04-09T18:58:34Z0336\r\notp=ccxxxxxtbbvulhlletkijhrtifrintlerfbnbhtdnikl\r\nnonce=44D41854DDDA8E77E58A38A98CF501E9\r\nsl=100\r\nstatus=OK\r\n\r\n"
// The yubico api clientid. You can open an api key here: https://upgrade.yubico.com/getapikey/
string yubicoApiClientId = "REPLACEWITHCLIENTID";
// This is currently not required.
string yubicoApiPrivateKey = "REPLACEWITHAPIKEY";
string yubikeyValidationUrl = $"https://api.yubico.com/wsapi/2.0/verify?";
string nonce = "";
//Create the key based on the api key string
byte[] privateKey = Convert.FromBase64String(yubicoApiPrivateKey);
//Create a nonce
using (var random = RandomNumberGenerator.Create())
{
var tmpNonce = new byte[16];
random.GetBytes(tmpNonce);
nonce = BitConverter.ToString(tmpNonce).Replace("-", "");
}
//Get the OTP from yubikey (usb stick)
System.Console.WriteLine("Press yubikey button");
var otp = Console.ReadLine();
//Create the signature based on https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
//Prepare the parameters to be signed (Ordered alphabetically)
string verifyParameters = $"id={yubicoApiClientId}&nonce={nonce}&otp={otp}";
string signature = "";
using (var hmac = new HMACSHA1(privateKey))
{
//Create the hmacsha1
var signatureAsByte = hmac.ComputeHash(Encoding.UTF8.GetBytes(verifyParameters));
signature = Convert.ToBase64String(signatureAsByte);
}
//Add the signature
verifyParameters += $"&h={signature}";
HttpClient client = new HttpClient();
var url = $"{yubikeyValidationUrl}{verifyParameters}";
System.Console.WriteLine(url);
var result = client.GetAsync(url).Result;
System.Console.WriteLine($"http statuscode: {result.StatusCode}");
string response = result.Content.ReadAsStringAsync().Result;
System.Console.WriteLine(response);
Match m = Regex.Match(response, "status=\\w*", RegexOptions.IgnoreCase);
if (m.Success)
Console.WriteLine($"OTP Status: {m.Value}");
//Verify signature based on https://developers.yubico.com/OTP/Specifications/OTP_validation_protocol.html
//The response contains a signature (h parameter) which was signed with the same private key
//This means we can just calculate the hmacsha1 again (Without the h parameter and with ordering of the parameter)
//and then compare the returned signature with the created siganture
var lines = response.Split(new string[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).ToList();
var returnedSignature = String.Empty;
string returnParameterToCheck = String.Empty;
foreach (var item in lines.OrderBy(x => x))
{
if (!string.IsNullOrEmpty(item) && !item.StartsWith("h="))
returnParameterToCheck += $"&{item}";
if (!string.IsNullOrEmpty(item) && item.StartsWith("h="))
returnedSignature = item.Replace("h=", "");
}
//Remove the first unnecessary '&' character
returnParameterToCheck = returnParameterToCheck.Remove(0, 1);
var signatureToCompare = String.Empty;
using (var hmac1 = new HMACSHA1(privateKey))
{
signatureToCompare = Convert.ToBase64String(hmac1.ComputeHash(Encoding.UTF8.GetBytes(returnParameterToCheck)));
}
if (returnedSignature == signatureToCompare)
System.Console.WriteLine("Signatures are equal");
else
System.Console.WriteLine("Signatures are not equal");
(I apparently don't have enough reputation, so I'm only allow to post 'answers')
#Manuel
I see this example all over the web in various languages, but none of them work correctly for me.
The return status is always status=OK regardless of what physical key I'm using.
I have access to a box of 50 yubikeys 5 nfc and if I use your example, status will be OK.
If I tamper with the ID, I will get responses like NO_SUCH_CLIENT or BAD_SIGNATURE etc.
So it is kind of important that certain parameters match, but the actual OTP isn't part of that.
I can register an ID and Secret at https://upgrade.yubico.com/getapikey
Do the verification in your code and it will be status=OK.
Then grab a brand new yubikey and try it and status will still be OK.
I tried verifying with my ID and Secret using a yubikey from a colleague and, you can guess it, status=OK.
So the only thing I'm really proving is that I possess 'a' yubikey.
My sample project
Summary
I have created a sample project to test the issuing of JWT tokens in an ASP.Net Core applications.
Refer to the Github repo above for the full sample.
In the JwtService.cs class, there is a method called GenerateSecurityToken, which generates the Jwt token.
The Login method of the AccountController calls the JwtService class to generate the token, saves the token in a cookie and also sets another cookie for the user id.
Note: The token is signed with a secret, which is also appended with a user specific salt. The user salt can be invalidated at any point, making the user's token invalid.
The token is also encrypted with an encryption key. This makes the body of the token illegible when inspected in JWT.io. I believe it is called a JWE.
This question discusses the signing and encrypting order.
Issue
I do not want the bearer or any third party to inspect the content of the token. The token's purpose in authentication.
My sample code intercepts the authentication pipeline, and uses the email from the cookie to pull user roles out of the database and creates role claims out of the roles.
A few of you may have figured out the information leak caused by issuing a separate cookie with the email.
Ideally, I want to ONLY issue the JWE token in the cookie.
I want to intercept the authentication pipeline, decrypt the token, use the email claim to get the user salt (from the db) and validate the token.
I have read the documentation, but cannot figure out a way to decrypt the token.
To be honest, I am not even sure of the order of operation (sign then encrypt or encrypt then sign), when the token is issued.
If anyone could even point me to the source code for JwtSecurityTokenHandler that might be a good start.
TIA
public string GenerateSecurityToken(string email, byte[] salt, string[] roles)
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_secret).Concat(salt).ToArray();
byte[] ecKey = new byte[256 / 8];
Array.Copy(Encoding.ASCII.GetBytes(_ecKey), ecKey, 256 / 8);
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = _issuer,
Audience = _audience,
Subject = new ClaimsIdentity(
new List<Claim>
{
new Claim(ClaimTypes.Email, email)
}),
// .Concat(roles.Select(r => new Claim(ClaimTypes.Role, r))).ToArray()),
Expires = DateTime.UtcNow.AddMinutes(double.Parse(_expDate)),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
EncryptingCredentials = new EncryptingCredentials(
new SymmetricSecurityKey(
ecKey),
SecurityAlgorithms.Aes256KW,
SecurityAlgorithms.Aes256CbcHmacSha512)
};
var token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
While you are very interested in hashing the JWT, I recommend you have four methods:
To generates the token (Sequence is important):
GenerateToken
HashToken
Then when you receive the bearer token with the request (Sequence is important):
DeHashToken
ValidateToken
As both processes, JWT generating and hashing are very straightforward:
To get the best results, forget about the hashing and focus on Generate Token and Validate Token.
After you get success with JWT, in a separate project, focus on Hashing and DeHashing any text (JWT or something else).
When you succeed with both, don't call each other from each other.
Build new Methods like
GenerateAndHash (It calls both GenerateToken and HashToken)
VerfyHashAndValidateToken (It calls both DeHashToken and ValidateToken)
To Generate and Validate JWT step by step see this (The title of the article could be confusing but it talks about JWT in .NET Core in a very simple way especially in part 2 I think)
https://www.codemag.com/Article/1805021/Security-in-Angular-Part-1
https://www.codemag.com/Article/1809031/Security-in-Angular-Part-2
https://www.codemag.com/Article/1811031/Security-in-Angular-Part-3
To Hash
public static string ToCustomHash(this string text)
{
byte[] salt;
new RNGCryptoServiceProvider().GetBytes(salt = new byte[16]);
var pbkdf2 = new Rfc2898DeriveBytes(text, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
byte[] hashBytes = new byte[36];
Array.Copy(salt, 0, hashBytes, 0, 16);
Array.Copy(hash, 0, hashBytes, 16, 20);
var hashedToBase64 = Convert.ToBase64String(hashBytes);
return hashedToBase64;
}
To Verify The hash
public static bool VerifyHashWith(this string storedPassword, string loginPassword)
{
byte[] hashBytes = Convert.FromBase64String(storedPassword);
byte[] salt = new byte[16];
Array.Copy(hashBytes, 0, salt, 0, 16);
var pbkdf2 = new Rfc2898DeriveBytes(loginPassword, salt, 100000);
byte[] hash = pbkdf2.GetBytes(20);
for (int i = 0; i < 20; i++)
{
if (hashBytes[i + 16] != hash[i]) { return false; }
}
return true;
}
I am using the following code, which I borrowed originally from the jwt-dotnet github page
private static string CreateToken(UserPrincipal principal)
{
/*
* https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
* http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
*/
var key = ConfigurationManager.AppSettings["jwt-key"];
var claims = new Dictionary<string, string>()
{
{ClaimTypes.Name, "Rainbow Dash" },
{ClaimTypes.WindowsAccountName, "RDash"}
};
var algorithm = new HMACSHA256Algorithm();
var serializer = new JsonNetSerializer();
var urlEncoder = new JwtBase64UrlEncoder();
var encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
var token = encoder.Encode(claims, key);
return token;
}
The above code generates the following token:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiUmFpbmJvdyBEYXNoIiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy93aW5kb3dzYWNjb3VudG5hbWUiOiJSRGFzaCJ9.5WZWDJ0pvTe6QLjVNUeTfZicX_wSsk1dtYvXUbpiOiw
So, I hopped over to jwt.io to test my token. I'm told I have an invalid signature.
How do I give it a valid 'signature'? I don't understand what my JWT is missing.
The tool over JWT.io can verify the digital signature of your token if you give it the secret signing key you used while creating a token:
And from looking at your code it's the value contained in your:
ConfigurationManager.AppSettings["jwt-key"];
Just input the value inside the "secret" text box and if the signature of the token matches the one calculated by JWT.io then you'll get a message saying that the signature is valid.
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" ;-)
I am trying to validate a JWT token. The code is within an OWIN OAuth handler, however I have taken the various pieces out into a small console application and it would appear to be a problem with how the JwtHeader method SigningKeyIdentifier creates a X509ThumbprintKeyIdentifierClause object.
My JWT has a header value of x5t = [base64urlencodedvalue], and I have confirmed that when this string is decoded it is indeed the thumbprint for my certificate. However, in the SigningKeyIdentifier class the following code seems to create a incorrect clause, e.g. the hash of the clause doesnt match the certificate.
identifier.Add(new X509ThumbprintKeyIdentifierClause(Base64UrlEncoder.DecodeBytes(this.GetStandardClaim("x5t"))));
Below is a snippet of the console app that tries to demostrate the issue:
// http://kjur.github.io/jsjws/tool_b64udec.html
const string X5T = "NmJmOGUxMzZlYjM2ZDRhNTZlYTA1YzdhZTRiOWE0NWI2M2JmOTc1ZA"; // value inside the JWT (x5t)
const string thumbPrint = "6bf8e136eb36d4a56ea05c7ae4b9a45b63bf975d"; // correct thumbprint of certificate
string thumbPrintBase64 = Base64UrlEncoder.Encode(thumbPrint); // <--- value in x5t of JWT
// finds correct certificate
var cert1 = X509CertificateHelper.FindByThumbprint(StoreName.My, StoreLocation.LocalMachine, thumbPrint).First();
var certHash = cert1.GetCertHash();
string hexa = BitConverter.ToString(certHash).Replace("-", string.Empty);
Console.WriteLine(hexa.ToLowerInvariant());
// TokenValidationParameters.IssuerSigningKey
var clause1 = new X509ThumbprintKeyIdentifierClause(cert1);
string hex1 = BitConverter.ToString(clause1.GetX509Thumbprint()).Replace("-", string.Empty);
Console.WriteLine(clause1.ToString());
Console.WriteLine(hex1.ToLowerInvariant());
// this is how JwtHeader.SigningKeyIdentifier method creates SecurityKeyIdentifier
var hash = Base64UrlEncoder.DecodeBytes(thumbPrintBase64);
var clause2 = new X509ThumbprintKeyIdentifierClause(hash); // <----- broken
string hexb = BitConverter.ToString(hash).Replace("-", string.Empty);
Console.WriteLine(hexb.ToLowerInvariant());
Console.WriteLine(clause2.ToString());
string hex2 = BitConverter.ToString(clause2.GetX509Thumbprint()).Replace("-", string.Empty);
Console.WriteLine(hex2.ToLowerInvariant());
// clause1 and clause2 should be the same, but they arent!?
The problem seems to be that the various consructors for X509ThumbprintKeyIdentifierClause end up with different hash values which when compared later dont match.
In my OWIN project one piece creates a X509ThumbprintKeyIdentifierClause from a certificate (TokenValidationParameters.IssuerSigningKey). e.g.
IssuerSigningKey = new X509SecurityKey(X509CertificateHelper.FindByThumbprint(StoreName.My, StoreLocation.LocalMachine, thumbPrint).First()),
and the IssuerSigningKeyResolver method called to match the JWT with the issue certificate using the thumbnail from the x5t field.
identifier.Add(new X509ThumbprintKeyIdentifierClause(Base64UrlEncoder.DecodeBytes(this.GetStandardClaim("x5t"))));
but they dont match.
What am I missing? Something feels wrong with the encoding/decoding of the thumbnail.
We had a great discussion about this in GitHub, should have posted here before.
https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/issues/100
The input that is encoded and creates the x5t is not the 'thumbprint', which are encoded octets. but the octets from the certhash.
The JwtHeader creates the clause as:
ski.Add(new X509ThumbprintKeyIdentifierClause(Base64UrlEncoder.DecodeBytes(GetStandardClaim(JwtHeaderParameterNames.X5t))));
Not:
var hash = Base64UrlEncoder.DecodeBytes(thumbPrintBase64);