From e445fb39dc6c78d056ac87631fb4181e4aebf18e Mon Sep 17 00:00:00 2001 From: Alexander Fenster Date: Wed, 5 Jun 2019 08:53:48 -0700 Subject: [PATCH] feat: make both crypto implementations support sign (#727) * fix: pad base64 strings for base64js * really test signer for Node * feat: implement sign for browser * fix test * gts fix * revert webpack.config.js --- browser-test/fixtures/keys.ts | 47 +++++++++++++++++++++++++++++++++ browser-test/test.crypto.ts | 49 +++++++++++++++++++++++------------ browser-test/test.oauth2.ts | 33 +---------------------- src/auth/googleauth.ts | 7 +++-- src/crypto/browser/crypto.ts | 20 ++++++++++++-- src/crypto/crypto.ts | 5 +++- src/crypto/node/crypto.ts | 8 ++++-- test/test.crypto.ts | 7 ++--- 8 files changed, 114 insertions(+), 62 deletions(-) create mode 100644 browser-test/fixtures/keys.ts diff --git a/browser-test/fixtures/keys.ts b/browser-test/fixtures/keys.ts new file mode 100644 index 00000000..f5785867 --- /dev/null +++ b/browser-test/fixtures/keys.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2019 Google LLC. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// The following private and public keys were copied from JWK RFC 7517: +// https://tools.ietf.org/html/rfc7517 +export const privateKey = { + kty: 'RSA', + n: + '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', + e: 'AQAB', + d: + 'X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q', + p: + '83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs', + q: + '3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk', + dp: + 'G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0', + dq: + 's9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk', + qi: + 'GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU', + alg: 'RS256', + kid: '2011-04-29', +}; + +export const publicKey = { + kty: 'RSA', + n: + '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', + e: 'AQAB', + alg: 'RS256', + kid: '2011-04-29', +}; diff --git a/browser-test/test.crypto.ts b/browser-test/test.crypto.ts index 949577b6..5c34b8be 100644 --- a/browser-test/test.crypto.ts +++ b/browser-test/test.crypto.ts @@ -1,19 +1,24 @@ +/** + * Copyright 2019 Google LLC. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import * as base64js from 'base64-js'; import {assert} from 'chai'; import {createCrypto} from '../src/crypto/crypto'; import {BrowserCrypto} from '../src/crypto/browser/crypto'; - -// The following public key was copied from JWK RFC 7517: -// https://tools.ietf.org/html/rfc7517 -// The private key used for signing the test message below was taken from the same RFC. -const publicKey = { - kty: 'RSA', - n: - '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', - e: 'AQAB', - alg: 'RS256', - kid: '2011-04-29', -}; +import {privateKey, publicKey} from './fixtures/keys'; // Not all browsers support `TextEncoder`. The following `require` will // provide a fast UTF8-only replacement for those browsers that don't support @@ -64,10 +69,22 @@ describe('Browser crypto tests', () => { assert(verified); }); - it('should not createSign', () => { - assert.throws(() => { - crypto.createSign('never worked'); - }); + it('should sign a message', async () => { + const message = 'This message is signed'; + const expectedSignatureBase64 = [ + 'BE1qD48LdssePdMmOhcanOd8V+i4yLSOL0H2EXNyy', + 'lCePnldIsLVqrOJnVkd0MUKxS/Y9B0te2tqlS8psP', + 'j9IWjcpiQeT9wUDRadxHIX26W6JHgSCOzOavpJCbh', + 'M3Kez7QEwbkrI54rYu7qgx/mmckxkC0vhg0Z5OQbO', + 'IXfILVs1ztNNdt9r/ZzNVxTMKhL3nHLfjVqG/LUGy', + 'RhFhjzLvIJAfL0CSEfycUvm6t5NVzF4SkZ8KKQ7wJ', + 'vLw492bRB/633GJOZ1prVjAUQUI64BXFrvRgWsxLK', + 'M0XtF5tNbC+eIDrH0LiMraAhcZwj1iWofH1h/dg3E', + 'xtU9UWfbed/yfw==', + ].join(''); + + const signatureBase64 = await crypto.sign(privateKey, message); + assert.strictEqual(signatureBase64, expectedSignatureBase64); }); it('should decode unpadded base64', () => { diff --git a/browser-test/test.oauth2.ts b/browser-test/test.oauth2.ts index f35bf517..8a1313d0 100644 --- a/browser-test/test.oauth2.ts +++ b/browser-test/test.oauth2.ts @@ -14,11 +14,10 @@ * limitations under the License. */ -/// - import * as base64js from 'base64-js'; import {assert} from 'chai'; import * as sinon from 'sinon'; +import {privateKey, publicKey} from './fixtures/keys'; // Not all browsers support `TextEncoder`. The following `require` will // provide a fast UTF8-only replacement for those browsers that don't support @@ -61,36 +60,6 @@ const FEDERATED_SIGNON_JWK_CERTS_AXIOS_RESPONSE = { }, data: {keys: FEDERATED_SIGNON_JWK_CERTS}, }; -// The following private and public keys were copied from JWK RFC 7517: -// https://tools.ietf.org/html/rfc7517 -const privateKey = { - kty: 'RSA', - n: - '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', - e: 'AQAB', - d: - 'X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q', - p: - '83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs', - q: - '3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk', - dp: - 'G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0', - dq: - 's9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk', - qi: - 'GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU', - alg: 'RS256', - kid: '2011-04-29', -}; -const publicKey = { - kty: 'RSA', - n: - '0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw', - e: 'AQAB', - alg: 'RS256', - kid: '2011-04-29', -}; describe('Browser OAuth2 tests', () => { let client: OAuth2Client; diff --git a/src/auth/googleauth.ts b/src/auth/googleauth.ts index 9d561a8c..bbf3a97b 100644 --- a/src/auth/googleauth.ts +++ b/src/auth/googleauth.ts @@ -779,10 +779,9 @@ export class GoogleAuth { async sign(data: string): Promise { const client = await this.getClient(); const crypto = createCrypto(); - if (client instanceof JWT && client.key && !isBrowser()) { - const sign = crypto.createSign('RSA-SHA256'); - sign.update(data); - return sign.sign(client.key, 'base64'); + if (client instanceof JWT && client.key) { + const sign = await crypto.sign(client.key, data); + return sign; } const projectId = await this.getProjectId(); diff --git a/src/crypto/browser/crypto.ts b/src/crypto/browser/crypto.ts index 3c96f39c..1b679c62 100644 --- a/src/crypto/browser/crypto.ts +++ b/src/crypto/browser/crypto.ts @@ -107,8 +107,24 @@ export class BrowserCrypto implements Crypto { return result; } - createSign(algorithm: string): CryptoSigner { - throw new Error('createSign is not implemented in BrowserCrypto'); + async sign(privateKey: JwkCertificate, data: string): Promise { + const algo = { + name: 'RSASSA-PKCS1-v1_5', + hash: {name: 'SHA-256'}, + }; + const dataArray = new TextEncoder().encode(data); + const cryptoKey = await window.crypto.subtle.importKey( + 'jwk', + privateKey, + algo, + true, + ['sign'] + ); + + // SubtleCrypto's sign method is async so we must make + // this method async as well. + const result = await window.crypto.subtle.sign(algo, cryptoKey, dataArray); + return base64js.fromByteArray(new Uint8Array(result)); } decodeBase64StringUtf8(base64: string): string { diff --git a/src/crypto/crypto.ts b/src/crypto/crypto.ts index b9f46f23..a08678c9 100644 --- a/src/crypto/crypto.ts +++ b/src/crypto/crypto.ts @@ -47,7 +47,10 @@ export interface Crypto { data: string | Buffer, signature: string ): Promise; - createSign(algorithm: string): CryptoSigner; + sign( + privateKey: string | JwkCertificate, + data: string | Buffer + ): Promise; decodeBase64StringUtf8(base64: string): string; encodeBase64StringUtf8(text: string): string; } diff --git a/src/crypto/node/crypto.ts b/src/crypto/node/crypto.ts index 65419e21..4c4fa0c5 100644 --- a/src/crypto/node/crypto.ts +++ b/src/crypto/node/crypto.ts @@ -36,11 +36,15 @@ export class NodeCrypto implements Crypto { ): Promise { const verifier = crypto.createVerify('sha256'); verifier.update(data); + verifier.end(); return verifier.verify(pubkey, signature, 'base64'); } - createSign(algorithm: string): CryptoSigner { - return crypto.createSign(algorithm); + async sign(privateKey: string, data: string | Buffer): Promise { + const signer = crypto.createSign('RSA-SHA256'); + signer.update(data); + signer.end(); + return signer.sign(privateKey, 'base64'); } decodeBase64StringUtf8(base64: string): string { diff --git a/test/test.crypto.ts b/test/test.crypto.ts index 112a81d6..09eb874b 100644 --- a/test/test.crypto.ts +++ b/test/test.crypto.ts @@ -47,7 +47,7 @@ describe('Node.js crypto tests', () => { assert(verified); }); - it('should create a signer that works', () => { + it('should sign a message', async () => { const message = 'This message is signed'; const expectedSignatureBase64 = [ 'ufyKBV+Ar7Yq8CSmSIN9m38ch4xnWBz8CP4qHh6V+', @@ -57,10 +57,7 @@ describe('Node.js crypto tests', () => { 'bP28XNU=', ].join(''); - const signer = crypto.createSign('SHA256'); - assert(signer); - signer.update(message); - const signatureBase64 = signer.sign(privateKey, 'base64'); + const signatureBase64 = await crypto.sign(privateKey, message); assert.strictEqual(signatureBase64, expectedSignatureBase64); });