mirror of
https://github.com/danbulant/jose
synced 2026-05-19 04:18:52 +00:00
feat(WebAPI runtime): Add CFRG key and operations supports
[skip ci]
This commit is contained in:
parent
263cc0cf58
commit
e03a96218b
12 changed files with 102 additions and 95 deletions
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
export function isCloudflareWorkers() {
|
||||
return false
|
||||
}
|
||||
export function isNodeJs() {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' })
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue