Skip to content

Commit

Permalink
crypto: add CFRG curves to Web Crypto API
Browse files Browse the repository at this point in the history
PR-URL: nodejs/node#42507
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
  • Loading branch information
panva authored and guangwong committed Oct 10, 2022
1 parent 8adaf64 commit a0bc437
Show file tree
Hide file tree
Showing 24 changed files with 1,898 additions and 1,338 deletions.
341 changes: 129 additions & 212 deletions doc/api/webcrypto.md

Large diffs are not rendered by default.

369 changes: 369 additions & 0 deletions lib/internal/crypto/cfrg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
'use strict';

const {
Promise,
SafeSet,
} = primordials;

const { Buffer } = require('buffer');

const {
ECKeyExportJob,
KeyObjectHandle,
SignJob,
kCryptoJobAsync,
kKeyTypePrivate,
kKeyTypePublic,
kSignJobModeSign,
kSignJobModeVerify,
} = internalBinding('crypto');

const {
codes: {
ERR_INVALID_ARG_TYPE,
},
} = require('internal/errors');

const {
getArrayBufferOrView,
getUsagesUnion,
hasAnyNotIn,
jobPromise,
validateKeyOps,
kHandle,
kKeyObject,
} = require('internal/crypto/util');

const {
emitExperimentalWarning,
lazyDOMException,
} = require('internal/util');

const {
generateKeyPair,
} = require('internal/crypto/keygen');

const {
InternalCryptoKey,
PrivateKeyObject,
PublicKeyObject,
createPrivateKey,
createPublicKey,
isKeyObject,
} = require('internal/crypto/keys');

function verifyAcceptableCfrgKeyUse(name, type, usages) {
let checkSet;
switch (name) {
case 'X25519':
// Fall through
case 'X448':
checkSet = ['deriveKey', 'deriveBits'];
break;
case 'Ed25519':
// Fall through
case 'Ed448':
switch (type) {
case 'private':
checkSet = ['sign'];
break;
case 'public':
checkSet = ['verify'];
break;
}
}
if (hasAnyNotIn(usages, checkSet)) {
throw lazyDOMException(
`Unsupported key usage for a ${name} key`,
'SyntaxError');
}
}

function createECPublicKeyRaw(name, keyData) {
const handle = new KeyObjectHandle();
keyData = getArrayBufferOrView(keyData, 'keyData');
if (handle.initECRaw(name.toLowerCase(), keyData))
return new PublicKeyObject(handle);
}

function createCFRGRawKey(name, keyData, isPublic) {
const handle = new KeyObjectHandle();
keyData = getArrayBufferOrView(keyData, 'keyData');

switch (name) {
case 'Ed25519':
case 'X25519':
if (keyData.byteLength !== 32) {
throw lazyDOMException(
`${name} raw keys must be exactly 32-bytes`);
}
break;
case 'Ed448':
if (keyData.byteLength !== 57) {
throw lazyDOMException(
`${name} raw keys must be exactly 57-bytes`);
}
break;
case 'X448':
if (keyData.byteLength !== 56) {
throw lazyDOMException(
`${name} raw keys must be exactly 56-bytes`);
}
break;
}

const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initEDRaw(name, keyData, keyType)) {
throw lazyDOMException('Failure to generate key object');
}

return isPublic ? new PublicKeyObject(handle) : new PrivateKeyObject(handle);
}

async function cfrgGenerateKey(algorithm, extractable, keyUsages) {
const { name } = algorithm;
emitExperimentalWarning(`The ${name} Web Crypto API algorithm`);

const usageSet = new SafeSet(keyUsages);
switch (name) {
case 'Ed25519':
// Fall through
case 'Ed448':
if (hasAnyNotIn(usageSet, ['sign', 'verify'])) {
throw lazyDOMException(
`Unsupported key usage for an ${name} key`,
'SyntaxError');
}
break;
case 'X25519':
// Fall through
case 'X448':
if (hasAnyNotIn(usageSet, ['deriveKey', 'deriveBits'])) {
throw lazyDOMException(
`Unsupported key usage for an ${name} key`,
'SyntaxError');
}
break;
}
return new Promise((resolve, reject) => {
let genKeyType;
switch (name) {
case 'Ed25519':
genKeyType = 'ed25519';
break;
case 'Ed448':
genKeyType = 'ed448';
break;
case 'X25519':
genKeyType = 'x25519';
break;
case 'X448':
genKeyType = 'x448';
break;
}
generateKeyPair(genKeyType, undefined, (err, pubKey, privKey) => {
if (err) {
return reject(lazyDOMException(
'The operation failed for an operation-specific reason',
'OperationError'));
}

const algorithm = { name };

let publicUsages;
let privateUsages;
switch (name) {
case 'Ed25519':
// Fall through
case 'Ed448':
publicUsages = getUsagesUnion(usageSet, 'verify');
privateUsages = getUsagesUnion(usageSet, 'sign');
break;
case 'X25519':
// Fall through
case 'X448':
publicUsages = [];
privateUsages = getUsagesUnion(usageSet, 'deriveKey', 'deriveBits');
break;
}

const publicKey =
new InternalCryptoKey(
pubKey,
algorithm,
publicUsages,
true);

const privateKey =
new InternalCryptoKey(
privKey,
algorithm,
privateUsages,
extractable);

resolve({ publicKey, privateKey });
});
});
}

function cfrgExportKey(key, format) {
emitExperimentalWarning(`The ${key.algorithm.name} Web Crypto API algorithm`);
return jobPromise(new ECKeyExportJob(
kCryptoJobAsync,
format,
key[kKeyObject][kHandle]));
}

async function cfrgImportKey(
format,
keyData,
algorithm,
extractable,
keyUsages) {

const { name } = algorithm;
emitExperimentalWarning(`The ${name} Web Crypto API algorithm`);
let keyObject;
const usagesSet = new SafeSet(keyUsages);
switch (format) {
case 'node.keyObject': {
if (!isKeyObject(keyData))
throw new ERR_INVALID_ARG_TYPE('keyData', 'KeyObject', keyData);
if (keyData.type === 'secret')
throw lazyDOMException('Invalid key type', 'InvalidAccessException');
verifyAcceptableCfrgKeyUse(name, keyData.type, usagesSet);
keyObject = keyData;
break;
}
case 'spki': {
verifyAcceptableCfrgKeyUse(name, 'public', usagesSet);
keyObject = createPublicKey({
key: keyData,
format: 'der',
type: 'spki'
});
break;
}
case 'pkcs8': {
verifyAcceptableCfrgKeyUse(name, 'private', usagesSet);
keyObject = createPrivateKey({
key: keyData,
format: 'der',
type: 'pkcs8'
});
break;
}
case 'jwk': {
if (keyData == null || typeof keyData !== 'object')
throw lazyDOMException('Invalid JWK keyData', 'DataError');
if (keyData.kty !== 'OKP')
throw lazyDOMException('Invalid key type', 'DataError');
const isPublic = keyData.d === undefined;

if (usagesSet.size > 0 && keyData.use !== undefined) {
let checkUse;
switch (name) {
case 'Ed25519':
// Fall through
case 'Ed448':
checkUse = 'sig';
break;
case 'X25519':
// Fall through
case 'X448':
checkUse = 'enc';
break;
}
if (keyData.use !== checkUse)
throw lazyDOMException('Invalid use type', 'DataError');
}

validateKeyOps(keyData.key_ops, usagesSet);

if (keyData.ext !== undefined &&
keyData.ext === false &&
extractable === true) {
throw lazyDOMException('JWK is not extractable', 'DataError');
}

if (keyData.alg !== undefined) {
if (typeof keyData.alg !== 'string')
throw lazyDOMException('Invalid alg', 'DataError');
if (
(name === 'Ed25519' || name === 'Ed448') &&
keyData.alg !== 'EdDSA'
) {
throw lazyDOMException('Invalid alg', 'DataError');
}
}

verifyAcceptableCfrgKeyUse(
name,
isPublic ? 'public' : 'private',
usagesSet);
keyObject = createCFRGRawKey(
name,
Buffer.from(
isPublic ? keyData.x : keyData.d,
'base64'),
isPublic);
break;
}
case 'raw': {
verifyAcceptableCfrgKeyUse(name, 'public', usagesSet);
keyObject = createECPublicKeyRaw(name, keyData);
if (keyObject === undefined)
throw lazyDOMException('Unable to import CFRG key', 'OperationError');
break;
}
}

if (keyObject.asymmetricKeyType !== name.toLowerCase()) {
throw lazyDOMException('Invalid key type', 'DataError');
}

return new InternalCryptoKey(
keyObject,
{ name },
keyUsages,
extractable);
}

function eddsaSignVerify(key, data, { name, context }, signature) {
emitExperimentalWarning(`The ${name} Web Crypto API algorithm`);
const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify;
const type = mode === kSignJobModeSign ? 'private' : 'public';

if (key.type !== type)
throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError');

if (name === 'Ed448' && context !== undefined) {
context =
getArrayBufferOrView(context, 'algorithm.context');
if (context.byteLength !== 0) {
throw lazyDOMException(
'Non zero-length context is not yet supported.', 'NotSupportedError');
}
}

return jobPromise(new SignJob(
kCryptoJobAsync,
mode,
key[kKeyObject][kHandle],
undefined,
undefined,
undefined,
data,
undefined,
undefined,
undefined,
undefined,
signature));
}

module.exports = {
cfrgExportKey,
cfrgImportKey,
cfrgGenerateKey,
eddsaSignVerify,
};
Loading

0 comments on commit a0bc437

Please sign in to comment.