-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PKCE code verifier doesn't meet rfc7636 spec #1501
Comments
I would be happy to take this one on and submit a PR if you want me to? It's just a matter of implementing a binary encoder for the character set in the spec: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" |
@schalk-b Sorry for missing this post. I would be happy to review a PR that you have submitted. Please give me some time today to review the PKCE generator code and I can get back to you with a more informed answer to your original question. |
@schalk-b Thanks again for opening this issue. As far as I can see, the lengths of the challenge and verifier that get generated are 43 characters. I used tests in the browser package as well as reviewed the network traces in different browsers. it.only("generatePkceCode()", async () => {
sinon.stub(BrowserCrypto.prototype, <any>"getSubtleCryptoDigest").callsFake(async (algorithm: string, data: Uint8Array): Promise<ArrayBuffer> => {
expect(algorithm).to.be.eq("SHA-256");
return crypto.createHash("SHA256").update(Buffer.from(data)).digest();
});
/**
* Contains alphanumeric, dash '-', underscore '_', plus '+', or slash '/' with length of 43.
*/
const regExp = new RegExp("[A-Za-z0-9-_+/]{43}");
const generatedCodes: PkceCodes = await cryptoObj.generatePkceCodes();
console.log(generatedCodes);
console.log(generatedCodes.challenge.length);
console.log(generatedCodes.verifier.length);
expect(regExp.test(generatedCodes.challenge)).to.be.true;
expect(regExp.test(generatedCodes.verifier)).to.be.true;
}); Could you let me know what browser and environment you are using? The randomness is something I am open to reviewing further. Could you explain how you arrived at the values of 4/256 and 3/256? |
@pkanher617 Thanks for spending your time on this issue. You are right that when calling generatePkceCodes(); the verifier is actually 43, as I only just noticed it encodes the verifier in base64. However, it does the base64 encoding, after running the random octet sequence through bufferToCVString. So this is currently what happens:
I believe that the bufferToCVString is unnecessary and basically takes some of the random, out of the random buffer (I'll show a test to demonstrate this at the end of the comment). We can change: /**
* Generates a random 32 byte buffer and returns the base64
* encoded string to be used as a PKCE Code Verifier
*/
private generateCodeVerifier(): string {
try {
// Generate random values as utf-8
const buffer: Uint8Array = new Uint8Array(RANDOM_BYTE_ARR_LENGTH);
this.cryptoObj.getRandomValues(buffer);
// verifier as string
const pkceCodeVerifierString = this.bufferToCVString(buffer);
// encode verifier as base64
const pkceCodeVerifierB64: string = this.base64Encode.urlEncode(pkceCodeVerifierString );
return pkceCodeVerifierB64;
} catch (e) {
throw BrowserAuthError.createPkceNotGeneratedError(e);
}
} to /**
* Generates a random 32 byte buffer and returns the base64
* encoded string to be used as a PKCE Code Verifier
*/
private generateCodeVerifier(): string {
try {
// Generate random values as utf-8
const buffer: Uint8Array = new Uint8Array(RANDOM_BYTE_ARR_LENGTH);
this.cryptoObj.getRandomValues(buffer);
// encode verifier as base64
const pkceCodeVerifierB64: string = this.base64Encode.urlEncodeArr(buffer);
return pkceCodeVerifierB64;
} catch (e) {
throw BrowserAuthError.createPkceNotGeneratedError(e);
}
} And then we can completely remove the bufferToCVString method. |
Sorry for this very long an ugly test, but this shows that the current bufferToCVString method that gets called removes some of the random, by not encoding correctly. import { expect } from "chai";
import { BrowserCrypto } from "../../src/crypto/BrowserCrypto";
import { PkceGenerator } from "../../src/crypto/PkceGenerator";
describe("PkceVerifier.spec.ts Unit Tests", () => {
it("bufferToCVString() evenly distributed encoding", async () => {
// This only works with a very large number of tests as we are testing distribution from random numbers
// Added here because they aren't exported from PkceGenerator
const CV_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
// Arrange
const browserCrypto = new BrowserCrypto();
const pkceGenerator = new PkceGenerator(browserCrypto);
// We want to count the amount of times each character is encoded.
const characterCounts = {};
[...CV_CHARSET].forEach(char => characterCounts[char] = 0);
let totalCharacters = 0;
// Act
for (let i = 0; i < 256; i++) {
const buffer: Uint8Array = new Uint8Array(2);
buffer[0] = i;
for (let j = 0; j < 256; j++) {
buffer[1] = j;
const pkceCodeVerifierString = pkceGenerator['bufferToCVString'](buffer);
totalCharacters += pkceCodeVerifierString.length;
[...pkceCodeVerifierString].forEach(char => characterCounts[char]++);
}
}
// Logging
const expectedDistribution = Math.round(1 / CV_CHARSET.length * 10000) / 10000;
console.log(`Expected distribution: ${expectedDistribution}`);
[...CV_CHARSET].forEach(char => {
const distribution = Math.round(characterCounts[char] / totalCharacters * 10000) / 10000;
console.log(`${char}: ${distribution}`);
});
// Assert
[...CV_CHARSET].forEach(char => {
const distribution = Math.round(characterCounts[char] / totalCharacters * 10000) / 10000;
expect(distribution).to.equal(expectedDistribution);
});
});
}); Example output:
|
You will see the distribution all the way through to 5 is about 0.0156 However, we expect the distribution to be 0.0152 for every character But to answer your question directly, I arrived at the values of 3/256 and 4/256 by: Math.floor(256/66) = 3 66 being the length of the character set. |
This issue has not seen activity in 14 days. It may be closed if it remains stale. |
Are you open to accepting a PR for this one? Or does it need further review? |
Hi @schalk-b, please go ahead and submit a PR! would be happy to review it. |
Merged in #1872 |
Library
msal@1.2.1
or@azure/msal@1.2.1
@azure/msal-browser@2.x.x
@azure/msal-angular@0.x.x
@azure/msal-angular@1.x.x
@azure/msal-angularjs@1.x.x
Description
Shouldn't the PKCE code verifier meet the rfc7636 spec?
Specifically in the spec it mentions
However, reviewing the code #1141 for the PkceGenerator I noticed a few things.
Because we are using modulus
buffer[i] % CV_CHARSET.length
it means from our character set, all characters before '6' have a 4/256 chance of showing, '6' and after, have a 3/256 change of showing.I know that the current implementation doesn't pose a 'serious' security risk. But I think it's worth pointing out that there is room for improvement.
The text was updated successfully, but these errors were encountered: