feat(WebAPI runtime): Add CFRG key and operations supports

[skip ci]
This commit is contained in:
Filip Skokan 2021-12-06 22:06:01 +01:00
parent 263cc0cf58
commit e03a96218b
12 changed files with 102 additions and 95 deletions

View file

@ -1,5 +1,3 @@
import { isCloudflareWorkers, isNodeJs } from '../runtime/env.js'
function unusable(name: string | number, prop = 'algorithm.name') {
return new TypeError(`CryptoKey does not support this operation, its ${prop} must be ${name}`)
}
@ -71,13 +69,10 @@ export function checkSigCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsa
if (actual !== expected) throw unusable(`SHA-${expected}`, 'algorithm.hash')
break
}
case isNodeJs() && 'EdDSA': {
if (key.algorithm.name !== 'NODE-ED25519' && key.algorithm.name !== 'NODE-ED448')
throw unusable('NODE-ED25519 or NODE-ED448')
break
}
case isCloudflareWorkers() && 'EdDSA': {
if (!isAlgorithm(key.algorithm, 'NODE-ED25519')) throw unusable('NODE-ED25519')
case 'EdDSA': {
if (key.algorithm.name !== 'Ed25519' && key.algorithm.name !== 'Ed448') {
throw unusable('Ed25519 or Ed448')
}
break
}
case 'ES256':
@ -116,9 +111,17 @@ export function checkEncCryptoKey(key: CryptoKey, alg: string, ...usages: KeyUsa
if (actual !== expected) throw unusable(expected, 'algorithm.length')
break
}
case 'ECDH':
if (!isAlgorithm(key.algorithm, 'ECDH')) throw unusable('ECDH')
case 'ECDH': {
switch (key.algorithm.name) {
case 'ECDH':
case 'X25519':
case 'X448':
break
default:
throw unusable('ECDH, X25519, or X448')
}
break
}
case 'PBES2-HS256+A128KW':
case 'PBES2-HS384+A192KW':
case 'PBES2-HS512+A256KW':

View file

@ -1,4 +1,3 @@
import { isCloudflareWorkers, isNodeJs } from './env.js'
import crypto, { isCryptoKey } from './webcrypto.js'
import type { PEMExportFunction, PEMImportFunction } from '../interfaces.d'
import invalidKeyInput from '../../lib/invalid_key_input.js'
@ -60,9 +59,13 @@ const getNamedCurve = (keyData: Uint8Array): string => {
return 'P-384'
case findOid(keyData, [0x2b, 0x81, 0x04, 0x00, 0x23]):
return 'P-521'
case (isCloudflareWorkers() || isNodeJs()) && findOid(keyData, [0x2b, 0x65, 0x70]):
case findOid(keyData, [0x2b, 0x65, 0x6e]):
return 'X25519'
case findOid(keyData, [0x2b, 0x65, 0x6f]):
return 'X448'
case findOid(keyData, [0x2b, 0x65, 0x70]):
return 'Ed25519'
case isNodeJs() && findOid(keyData, [0x2b, 0x65, 0x71]):
case findOid(keyData, [0x2b, 0x65, 0x71]):
return 'Ed448'
default:
throw new JOSENotSupported('Invalid or unsupported EC Key Curve or OKP Key Sub Type')
@ -126,12 +129,12 @@ const genericImport = async (
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW':
algorithm = { name: 'ECDH', namedCurve: getNamedCurve(keyData) }
const namedCurve = getNamedCurve(keyData)
algorithm = namedCurve.startsWith('P-') ? { name: 'ECDH', namedCurve } : { name: namedCurve }
keyUsages = isPublic ? [] : ['deriveBits']
break
case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA':
const namedCurve = getNamedCurve(keyData).toUpperCase()
algorithm = { name: `NODE-${namedCurve}`, namedCurve: `NODE-${namedCurve}` }
case 'EdDSA':
algorithm = { name: getNamedCurve(keyData) }
keyUsages = isPublic ? ['verify'] : ['sign']
break
default:

View file

@ -28,14 +28,24 @@ export async function deriveKey(
uint32be(keyLength),
)
let length: number
if (publicKey.algorithm.name === 'X25519') {
length = 256
} else if (publicKey.algorithm.name === 'X448') {
length = 448
} else {
length =
Math.ceil(parseInt((<EcKeyAlgorithm>publicKey.algorithm).namedCurve.substr(-3), 10) / 8) << 3
}
const sharedSecret = new Uint8Array(
await crypto.subtle.deriveBits(
{
name: 'ECDH',
name: publicKey.algorithm.name,
public: publicKey,
},
privateKey,
Math.ceil(parseInt((<EcKeyAlgorithm>privateKey.algorithm).namedCurve.slice(-3), 10) / 8) << 3,
length,
),
)
@ -54,5 +64,9 @@ export function ecdhAllowed(key: unknown) {
if (!isCryptoKey(key)) {
throw new TypeError(invalidKeyInput(key, ...types))
}
return ['P-256', 'P-384', 'P-521'].includes((<EcKeyAlgorithm>key.algorithm).namedCurve)
return (
['P-256', 'P-384', 'P-521'].includes((<EcKeyAlgorithm>key.algorithm).namedCurve) ||
key.algorithm.name === 'X25519' ||
key.algorithm.name === 'X448'
)
}

View file

@ -1,13 +1,4 @@
export function isCloudflareWorkers(): boolean {
export function isCloudflareWorkers() {
// @ts-expect-error
return typeof WebSocketPair === 'function'
}
export function isNodeJs(): boolean {
try {
// @deno-expect-error
return process.versions.node !== undefined
} catch {
return false
}
}

View file

@ -1,4 +1,3 @@
import { isCloudflareWorkers, isNodeJs } from './env.js'
import crypto from './webcrypto.js'
import { JOSENotSupported } from '../../util/errors.js'
import random from './random.js'
@ -59,7 +58,7 @@ function getModulusLengthOption(options?: GenerateKeyPairOptions) {
}
export async function generateKeyPair(alg: string, options?: GenerateKeyPairOptions) {
let algorithm: RsaHashedKeyGenParams | EcKeyGenParams
let algorithm: RsaHashedKeyGenParams | EcKeyGenParams | KeyAlgorithm
let keyUsages: KeyUsage[]
switch (alg) {
@ -109,16 +108,13 @@ export async function generateKeyPair(alg: string, options?: GenerateKeyPairOpti
algorithm = { name: 'ECDSA', namedCurve: 'P-521' }
keyUsages = ['sign', 'verify']
break
case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA':
switch (options?.crv) {
case undefined:
case 'EdDSA':
keyUsages = ['sign', 'verify']
const crv = options?.crv ?? 'Ed25519'
switch (crv) {
case 'Ed25519':
algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' }
keyUsages = ['sign', 'verify']
break
case isNodeJs() && 'Ed448':
algorithm = { name: 'NODE-ED448', namedCurve: 'NODE-ED448' }
keyUsages = ['sign', 'verify']
case 'Ed448':
algorithm = { name: crv }
break
default:
throw new JOSENotSupported(
@ -129,10 +125,27 @@ export async function generateKeyPair(alg: string, options?: GenerateKeyPairOpti
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW':
algorithm = { name: 'ECDH', namedCurve: options?.crv ?? 'P-256' }
case 'ECDH-ES+A256KW': {
keyUsages = ['deriveKey', 'deriveBits']
const crv = options?.crv ?? 'P-256'
switch (crv) {
case 'P-256':
case 'P-384':
case 'P-521': {
algorithm = { name: 'ECDH', namedCurve: crv }
break
}
case 'X25519':
case 'X448':
algorithm = { name: crv }
break
default:
throw new JOSENotSupported(
'Invalid or unsupported crv option provided, supported values are P-256, P-384, P-521, X25519, and X448',
)
}
break
}
default:
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value')
}

View file

@ -1,4 +1,3 @@
import { isCloudflareWorkers, isNodeJs } from './env.js'
import crypto from './webcrypto.js'
import type { JWKImportFunction } from '../interfaces.d'
import { JOSENotSupported } from '../../util/errors.js'
@ -106,25 +105,24 @@ function subtleMapping(jwk: JWK): {
}
break
}
case (isCloudflareWorkers() || isNodeJs()) && 'OKP':
if (jwk.alg !== 'EdDSA') {
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value')
}
switch (jwk.crv) {
case 'Ed25519':
algorithm = { name: 'NODE-ED25519', namedCurve: 'NODE-ED25519' }
case 'OKP': {
switch (jwk.alg) {
case 'EdDSA':
algorithm = { name: jwk.crv! }
keyUsages = jwk.d ? ['sign'] : ['verify']
break
case isNodeJs() && 'Ed448':
algorithm = { name: 'NODE-ED448', namedCurve: 'NODE-ED448' }
keyUsages = jwk.d ? ['sign'] : ['verify']
case 'ECDH-ES':
case 'ECDH-ES+A128KW':
case 'ECDH-ES+A192KW':
case 'ECDH-ES+A256KW':
algorithm = { name: jwk.crv! }
keyUsages = jwk.d ? ['deriveBits'] : []
break
default:
throw new JOSENotSupported(
'Invalid or unsupported JWK "crv" (Subtype of Key Pair) Parameter value',
)
throw new JOSENotSupported('Invalid or unsupported JWK "alg" (Algorithm) Parameter value')
}
break
}
default:
throw new JOSENotSupported('Invalid or unsupported JWK "kty" (Key Type) Parameter value')
}

View file

@ -1,4 +1,3 @@
import { isCloudflareWorkers, isNodeJs } from './env.js'
import { JOSENotSupported } from '../../util/errors.js'
export default function subtleDsa(alg: string, algorithm: KeyAlgorithm | EcKeyAlgorithm) {
@ -21,9 +20,8 @@ export default function subtleDsa(alg: string, algorithm: KeyAlgorithm | EcKeyAl
case 'ES384':
case 'ES512':
return { hash, name: 'ECDSA', namedCurve: (<EcKeyAlgorithm>algorithm).namedCurve }
case (isCloudflareWorkers() || isNodeJs()) && 'EdDSA':
const { namedCurve } = <EcKeyAlgorithm>algorithm
return <EcKeyAlgorithm>{ name: namedCurve, namedCurve }
case 'EdDSA':
return { name: algorithm.name }
default:
throw new JOSENotSupported(
`alg ${alg} is not supported either by JOSE or your javascript runtime`,

View file

@ -1,6 +1,3 @@
export function isCloudflareWorkers() {
return false
}
export function isNodeJs() {
return true
}

View file

@ -299,12 +299,12 @@ test('as keyobject', smoke, 'oct256gcm', ['encrypt'], ['decrypt'], true)
test(smoke, 'oct256c')
test(smoke, 'oct384c')
test(smoke, 'oct512c')
test(smoke, 'x25519dir')
conditional({ webcrypto: 0 })(smoke, 'rsa1_5')
conditional({ webcrypto: 0, electron: 0 })(smoke, 'x25519kw')
conditional({ webcrypto: 0 })(smoke, 'x25519dir')
conditional({ webcrypto: 0, electron: 0 })(smoke, 'x448kw')
conditional({ webcrypto: 0, electron: 0 })(smoke, 'x448dir')
conditional({ electron: 0 })(smoke, 'x25519kw')
conditional({ electron: 0 })(smoke, 'x448kw')
conditional({ electron: 0 })(smoke, 'x448dir')
conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct256c', undefined, undefined, true)
conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct384c', undefined, undefined, true)
conditional({ webcrypto: 0 })('as keyobject', smoke, 'oct512c', undefined, undefined, true)

View file

@ -173,10 +173,17 @@ const rsa = {
qi: 'htPHLViOVG6QrldfuHn9evfdlD-UEuViOWNx8aKR3IBv0qegpJ78vYB4hdAcJZtBslKI97En5rzOAN3Y6Y8MbI4oN77WeiePJl2cMrS64evmlERvjJ6ZTs8jK0iV5q_gIZ9Qg9drmolUgb_CccQOBFbqSL6YkXwCBxlkCrzTlhc',
kty: 'RSA',
}
const x25519 = {
crv: 'X25519',
x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4',
d: 'aCvvb3jEBnxJJBjCIN2a9ZDTL-HG6LVgBbij4m8-d3Y',
kty: 'OKP',
}
test(testKeyImportExport, { ...rsa, alg: 'RS256' })
test(testKeyImportExport, { ...rsa, alg: 'PS256' })
test(testKeyImportExport, { ...rsa, alg: 'RSA-OAEP' })
test(testKeyImportExport, { ...rsa, alg: 'RSA-OAEP-256' })
test(testKeyImportExport, { ...x25519, alg: 'ECDH-ES' })
test('Uin8tArray can be transformed to a JWK', async (t) => {
t.deepEqual(
@ -229,17 +236,10 @@ const ed448 = {
kty: 'OKP',
}
conditional({ webcrypto: 1, electron: 0 })(testKeyImportExport, { ...ed448, alg: 'EdDSA' })
const x25519 = {
crv: 'X25519',
x: 'axR8Q7PEd74nY9nWaAoAYpMe3gp5sWbau6V6X1inPw4',
d: 'aCvvb3jEBnxJJBjCIN2a9ZDTL-HG6LVgBbij4m8-d3Y',
kty: 'OKP',
}
conditional({ webcrypto: 0 })(testKeyImportExport, { ...x25519, alg: 'ECDH-ES' })
const x448 = {
crv: 'X448',
x: 'z8s0Ej7D4pgIDu233UHoDW48EbiEm5eFv8_LuFwRr0xVREHhCtdxH75x6J8egZbjDGweOSbeHbY',
d: 'xBrCwLlrHa1ov2cbmD4eMw4t6DoN_MWsBT_mxcA_QWsCS_9sKMRyFpphNN9_2iKrGPTC9pWCS5w',
kty: 'OKP',
}
conditional({ webcrypto: 0, electron: 0 })(testKeyImportExport, { ...x448, alg: 'ECDH-ES' })
conditional({ electron: 0 })(testKeyImportExport, { ...x448, alg: 'ECDH-ES' })

View file

@ -174,25 +174,15 @@ for (const alg of ['EdDSA']) {
}
for (const alg of ['ECDH-ES', 'ECDH-ES+A128KW', 'ECDH-ES+A192KW', 'ECDH-ES+A256KW']) {
conditional({ webcrypto: 0, electron: 1 })(
`import SPKI x25519 for ${alg}`,
testSPKI,
keys.x25519.publicKey,
alg,
)
conditional({ webcrypto: 0, electron: 1 })(
`import PKCS8 x25519 for ${alg}`,
testPKCS8,
keys.x25519.privateKey,
alg,
)
conditional({ webcrypto: 0, electron: 0 })(
test(`import SPKI x25519 for ${alg}`, testSPKI, keys.x25519.publicKey, alg)
test(`import PKCS8 x25519 for ${alg}`, testPKCS8, keys.x25519.privateKey, alg)
conditional({ electron: 0 })(
`import SPKI x448 for ${alg}`,
testSPKI,
keys.x448.publicKey,
alg,
)
conditional({ webcrypto: 0, electron: 0 })(
conditional({ electron: 0 })(
`import PKCS8 x448 for ${alg}`,
testPKCS8,
keys.x448.privateKey,

View file

@ -143,22 +143,22 @@ conditional({ webcrypto: 0 })('with modulusLength', testKeyPair, 'RSA1_5', {
modulusLength: 4096,
})
for (const crv of ['X25519', 'X448']) {
conditional({ webcrypto: 0, electron: crv === 'X25519' })(`crv: ${crv}`, testKeyPair, 'ECDH-ES', {
conditional({ electron: crv === 'X25519' })(`crv: ${crv}`, testKeyPair, 'ECDH-ES', {
crv,
})
conditional({ webcrypto: 0, electron: crv === 'X25519' })(
conditional({ electron: crv === 'X25519' })(
`crv: ${crv}`,
testKeyPair,
'ECDH-ES+A128KW',
{ crv },
)
conditional({ webcrypto: 0, electron: crv === 'X25519' })(
conditional({ electron: crv === 'X25519' })(
`crv: ${crv}`,
testKeyPair,
'ECDH-ES+A192KW',
{ crv },
)
conditional({ webcrypto: 0, electron: crv === 'X25519' })(
conditional({ electron: crv === 'X25519' })(
`crv: ${crv}`,
testKeyPair,
'ECDH-ES+A256KW',