@road-labs/ocmf-crypto-web 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/cjs/crypto.js +25 -0
- package/build/cjs/curve.js +25 -0
- package/build/cjs/index.js +28 -0
- package/build/cjs/private-key.js +61 -0
- package/build/cjs/public-key.js +46 -0
- package/build/cjs/sign.js +28 -0
- package/build/cjs/verify.js +41 -0
- package/build/es2022/crypto.js +18 -0
- package/build/es2022/curve.js +21 -0
- package/build/es2022/index.js +6 -0
- package/build/es2022/private-key.js +57 -0
- package/build/es2022/public-key.js +42 -0
- package/build/es2022/sign.js +25 -0
- package/build/es2022/verify.js +38 -0
- package/build/types/crypto.d.ts +9 -0
- package/build/types/curve.d.ts +3 -0
- package/build/types/index.d.ts +6 -0
- package/build/types/private-key.d.ts +17 -0
- package/build/types/public-key.d.ts +17 -0
- package/build/types/sign.d.ts +10 -0
- package/build/types/verify.d.ts +10 -0
- package/jest.config.js +11 -0
- package/package.json +37 -0
- package/src/crypto.ts +46 -0
- package/src/curve.ts +27 -0
- package/src/index.ts +6 -0
- package/src/private-key.ts +101 -0
- package/src/public-key.ts +75 -0
- package/src/sign.ts +56 -0
- package/src/verify.ts +79 -0
- package/test/curve.spec.ts +41 -0
- package/test/private-key.spec.ts +47 -0
- package/test/public-key.spec.ts +47 -0
- package/test/sign.spec.ts +18 -0
- package/test/verify.spec.ts +18 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Crypto = void 0;
|
|
7
|
+
const private_key_1 = require("./private-key");
|
|
8
|
+
const public_key_1 = require("./public-key");
|
|
9
|
+
const sign_1 = __importDefault(require("./sign"));
|
|
10
|
+
const verify_1 = __importDefault(require("./verify"));
|
|
11
|
+
class Crypto {
|
|
12
|
+
async decodeEcPrivateKey(value, format) {
|
|
13
|
+
return await private_key_1.EcPrivateKey.fromEncoded(value, format);
|
|
14
|
+
}
|
|
15
|
+
async decodeEcPublicKey(value, format) {
|
|
16
|
+
return await public_key_1.EcPublicKey.fromEncoded(value, format);
|
|
17
|
+
}
|
|
18
|
+
async sign(data, privateKey, hash, format) {
|
|
19
|
+
return (0, sign_1.default)(data, privateKey, hash, format);
|
|
20
|
+
}
|
|
21
|
+
async verify(signature, data, publicKey, hash, format) {
|
|
22
|
+
return (0, verify_1.default)(signature, data, publicKey, hash, format);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.Crypto = Crypto;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapCurveToWebCryptoCurve = mapCurveToWebCryptoCurve;
|
|
4
|
+
exports.getGroupOrderSize = getGroupOrderSize;
|
|
5
|
+
const ocmf_crypto_1 = require("@road-labs/ocmf-crypto");
|
|
6
|
+
function mapCurveToWebCryptoCurve(curve) {
|
|
7
|
+
switch (curve) {
|
|
8
|
+
case 'secp256r1':
|
|
9
|
+
return 'P-256';
|
|
10
|
+
case 'secp384r1':
|
|
11
|
+
return 'P-384';
|
|
12
|
+
default:
|
|
13
|
+
throw new ocmf_crypto_1.UnsupportedCurveError(`Curve ${curve} is not supported by webcrypto`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function getGroupOrderSize(curve) {
|
|
17
|
+
switch (curve) {
|
|
18
|
+
case 'secp256r1':
|
|
19
|
+
return 32;
|
|
20
|
+
case 'secp384r1':
|
|
21
|
+
return 48;
|
|
22
|
+
default:
|
|
23
|
+
throw new ocmf_crypto_1.UnsupportedCurveError(`Curve ${curve} is not supported by webcrypto`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.verify = exports.sign = void 0;
|
|
21
|
+
__exportStar(require("./crypto"), exports);
|
|
22
|
+
__exportStar(require("./curve"), exports);
|
|
23
|
+
__exportStar(require("./private-key"), exports);
|
|
24
|
+
__exportStar(require("./public-key"), exports);
|
|
25
|
+
var sign_1 = require("./sign");
|
|
26
|
+
Object.defineProperty(exports, "sign", { enumerable: true, get: function () { return __importDefault(sign_1).default; } });
|
|
27
|
+
var verify_1 = require("./verify");
|
|
28
|
+
Object.defineProperty(exports, "verify", { enumerable: true, get: function () { return __importDefault(verify_1).default; } });
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EcPrivateKey = void 0;
|
|
4
|
+
const public_key_1 = require("./public-key");
|
|
5
|
+
const ocmf_crypto_1 = require("@road-labs/ocmf-crypto");
|
|
6
|
+
const curve_1 = require("./curve");
|
|
7
|
+
class EcPrivateKey {
|
|
8
|
+
cryptoKey;
|
|
9
|
+
curve;
|
|
10
|
+
publicKey;
|
|
11
|
+
constructor(cryptoKey, curve, publicKey) {
|
|
12
|
+
this.cryptoKey = cryptoKey;
|
|
13
|
+
this.curve = curve;
|
|
14
|
+
this.publicKey = publicKey;
|
|
15
|
+
}
|
|
16
|
+
getCryptoKey() {
|
|
17
|
+
return this.cryptoKey;
|
|
18
|
+
}
|
|
19
|
+
getCurve() {
|
|
20
|
+
return this.curve;
|
|
21
|
+
}
|
|
22
|
+
getPublicKey() {
|
|
23
|
+
return this.publicKey;
|
|
24
|
+
}
|
|
25
|
+
static async fromEncoded(value, format) {
|
|
26
|
+
if (format !== 'pkcs8-der') {
|
|
27
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
28
|
+
}
|
|
29
|
+
const keyInfo = (0, ocmf_crypto_1.decodePkcs8PrivateKeyInfo)(value);
|
|
30
|
+
const namedCurve = keyInfo?.privateKey?.parameters?.namedCurve;
|
|
31
|
+
if (!namedCurve) {
|
|
32
|
+
throw new Error(`Named curve not specified`);
|
|
33
|
+
}
|
|
34
|
+
const curve = ocmf_crypto_1.oidToCurve.get(namedCurve);
|
|
35
|
+
if (!curve) {
|
|
36
|
+
throw new ocmf_crypto_1.UnsupportedCurveError(`Unknown curve: oid=${namedCurve}`);
|
|
37
|
+
}
|
|
38
|
+
const webCryptoCurve = (0, curve_1.mapCurveToWebCryptoCurve)(curve);
|
|
39
|
+
const privateCryptoKey = await crypto.subtle.importKey('pkcs8', value, {
|
|
40
|
+
name: 'ECDSA',
|
|
41
|
+
namedCurve: webCryptoCurve,
|
|
42
|
+
}, true, ['sign']);
|
|
43
|
+
const publicCryptoKey = await EcPrivateKey.derivePublicKey(privateCryptoKey, webCryptoCurve);
|
|
44
|
+
let publicKey = null;
|
|
45
|
+
if (publicCryptoKey) {
|
|
46
|
+
publicKey = new public_key_1.EcPublicKey(publicCryptoKey, curve);
|
|
47
|
+
}
|
|
48
|
+
return new EcPrivateKey(privateCryptoKey, curve, publicKey);
|
|
49
|
+
}
|
|
50
|
+
static async derivePublicKey(privateCryptoKey, webCryptoCurve) {
|
|
51
|
+
const privateKeyInfo = (0, ocmf_crypto_1.decodePkcs8PrivateKeyInfo)(new Uint8Array(await crypto.subtle.exportKey('pkcs8', privateCryptoKey)));
|
|
52
|
+
if (!privateKeyInfo.privateKey.publicKey) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return crypto.subtle.importKey('raw', privateKeyInfo.privateKey.publicKey, {
|
|
56
|
+
name: 'ECDSA',
|
|
57
|
+
namedCurve: webCryptoCurve,
|
|
58
|
+
}, true, ['verify']);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.EcPrivateKey = EcPrivateKey;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EcPublicKey = void 0;
|
|
4
|
+
const ocmf_crypto_1 = require("@road-labs/ocmf-crypto");
|
|
5
|
+
const curve_1 = require("./curve");
|
|
6
|
+
class EcPublicKey {
|
|
7
|
+
cryptoKey;
|
|
8
|
+
curve;
|
|
9
|
+
constructor(cryptoKey, curve) {
|
|
10
|
+
this.cryptoKey = cryptoKey;
|
|
11
|
+
this.curve = curve;
|
|
12
|
+
}
|
|
13
|
+
getCryptoKey() {
|
|
14
|
+
return this.cryptoKey;
|
|
15
|
+
}
|
|
16
|
+
getCurve() {
|
|
17
|
+
return this.curve;
|
|
18
|
+
}
|
|
19
|
+
static async fromEncoded(value, format) {
|
|
20
|
+
if (format !== 'spki-der') {
|
|
21
|
+
throw new ocmf_crypto_1.UnsupportedPublicKeyFormatError(`Unknown format: ${format}`);
|
|
22
|
+
}
|
|
23
|
+
const keyInfo = (0, ocmf_crypto_1.decodePkixSubjectPublicKeyInfo)(value);
|
|
24
|
+
const namedCurve = keyInfo.algorithm.parameters?.namedCurve;
|
|
25
|
+
if (!namedCurve) {
|
|
26
|
+
throw new Error(`Named curve not specified`);
|
|
27
|
+
}
|
|
28
|
+
const curve = ocmf_crypto_1.oidToCurve.get(namedCurve);
|
|
29
|
+
if (!curve) {
|
|
30
|
+
throw new ocmf_crypto_1.UnsupportedCurveError(`Unknown curve: oid=${namedCurve}`);
|
|
31
|
+
}
|
|
32
|
+
const cryptoKey = await crypto.subtle.importKey('spki', value, {
|
|
33
|
+
name: 'ECDSA',
|
|
34
|
+
namedCurve: (0, curve_1.mapCurveToWebCryptoCurve)(curve),
|
|
35
|
+
}, true, ['verify']);
|
|
36
|
+
return new EcPublicKey(cryptoKey, curve);
|
|
37
|
+
}
|
|
38
|
+
async encode(format) {
|
|
39
|
+
if (format !== 'spki-der') {
|
|
40
|
+
throw new ocmf_crypto_1.UnsupportedPublicKeyFormatError(`Unknown format: ${format}`);
|
|
41
|
+
}
|
|
42
|
+
const exportedKey = await crypto.subtle.exportKey('spki', this.cryptoKey);
|
|
43
|
+
return new Uint8Array(exportedKey);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.EcPublicKey = EcPublicKey;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = sign;
|
|
4
|
+
const ocmf_crypto_1 = require("@road-labs/ocmf-crypto");
|
|
5
|
+
async function sign(data, privateKey, hash, format) {
|
|
6
|
+
if (hash !== 'SHA-256') {
|
|
7
|
+
throw new ocmf_crypto_1.UnsupportedHashError(`Invalid hash: ${hash}`);
|
|
8
|
+
}
|
|
9
|
+
if (format !== 'sigvalue-der') {
|
|
10
|
+
throw new ocmf_crypto_1.UnsupportedSignatureFormatError(`Invalid signature format: ${format}`);
|
|
11
|
+
}
|
|
12
|
+
const result = await crypto.subtle.sign({ name: 'ECDSA', hash }, privateKey.getCryptoKey(), data);
|
|
13
|
+
if (result.byteLength === 0 || result.byteLength % 2 !== 0) {
|
|
14
|
+
throw new Error(`Unexpected signature length: ${result.byteLength}`);
|
|
15
|
+
}
|
|
16
|
+
const mid = result.byteLength / 2;
|
|
17
|
+
const r = addSignByte(new Uint8Array(result.slice(0, mid)));
|
|
18
|
+
const s = addSignByte(new Uint8Array(result.slice(mid, result.byteLength)));
|
|
19
|
+
return (0, ocmf_crypto_1.encodePkixEcdsaSigValue)({ r, s });
|
|
20
|
+
}
|
|
21
|
+
function addSignByte(bytes) {
|
|
22
|
+
if (bytes[0] < 0x80) {
|
|
23
|
+
return bytes;
|
|
24
|
+
}
|
|
25
|
+
const signed = new Uint8Array(bytes.length + 1);
|
|
26
|
+
signed.set(bytes, 1);
|
|
27
|
+
return signed;
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = verify;
|
|
4
|
+
const ocmf_crypto_1 = require("@road-labs/ocmf-crypto");
|
|
5
|
+
const curve_1 = require("./curve");
|
|
6
|
+
async function verify(signature, data, key, hash, format) {
|
|
7
|
+
if (hash !== 'SHA-256') {
|
|
8
|
+
throw new ocmf_crypto_1.UnsupportedHashError(`Invalid hash: ${hash}`);
|
|
9
|
+
}
|
|
10
|
+
if (format !== 'sigvalue-der') {
|
|
11
|
+
throw new ocmf_crypto_1.UnsupportedSignatureFormatError(`Invalid signature format: ${format}`);
|
|
12
|
+
}
|
|
13
|
+
const sigValue = (0, ocmf_crypto_1.decodePkixEcdsaSigValue)(signature);
|
|
14
|
+
const size = (0, curve_1.getGroupOrderSize)(key.getCurve());
|
|
15
|
+
const r = padZeros(trimSignByte(sigValue.r), size);
|
|
16
|
+
const s = padZeros(trimSignByte(sigValue.s), size);
|
|
17
|
+
const rs = new Uint8Array(size * 2);
|
|
18
|
+
rs.set(r, 0);
|
|
19
|
+
rs.set(s, size);
|
|
20
|
+
return crypto.subtle.verify({ name: 'ECDSA', hash }, key.getCryptoKey(), rs, data);
|
|
21
|
+
}
|
|
22
|
+
function trimSignByte(bytes) {
|
|
23
|
+
if (bytes.length > 1 &&
|
|
24
|
+
bytes.length % 16 === 1 &&
|
|
25
|
+
bytes[0] === 0x00 &&
|
|
26
|
+
bytes[1] >= 0x80) {
|
|
27
|
+
return bytes.slice(1);
|
|
28
|
+
}
|
|
29
|
+
return bytes;
|
|
30
|
+
}
|
|
31
|
+
function padZeros(bytes, len) {
|
|
32
|
+
if (bytes.length > len) {
|
|
33
|
+
throw new Error(`Invalid signature size, expected <= ${len}, received ${bytes.length}`);
|
|
34
|
+
}
|
|
35
|
+
if (bytes.length === len) {
|
|
36
|
+
return bytes;
|
|
37
|
+
}
|
|
38
|
+
const padded = new Uint8Array(len);
|
|
39
|
+
padded.set(bytes, len - bytes.length);
|
|
40
|
+
return padded;
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { EcPrivateKey } from './private-key';
|
|
2
|
+
import { EcPublicKey } from './public-key';
|
|
3
|
+
import sign from './sign';
|
|
4
|
+
import verify from './verify';
|
|
5
|
+
export class Crypto {
|
|
6
|
+
async decodeEcPrivateKey(value, format) {
|
|
7
|
+
return await EcPrivateKey.fromEncoded(value, format);
|
|
8
|
+
}
|
|
9
|
+
async decodeEcPublicKey(value, format) {
|
|
10
|
+
return await EcPublicKey.fromEncoded(value, format);
|
|
11
|
+
}
|
|
12
|
+
async sign(data, privateKey, hash, format) {
|
|
13
|
+
return sign(data, privateKey, hash, format);
|
|
14
|
+
}
|
|
15
|
+
async verify(signature, data, publicKey, hash, format) {
|
|
16
|
+
return verify(signature, data, publicKey, hash, format);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { UnsupportedCurveError } from '@road-labs/ocmf-crypto';
|
|
2
|
+
export function mapCurveToWebCryptoCurve(curve) {
|
|
3
|
+
switch (curve) {
|
|
4
|
+
case 'secp256r1':
|
|
5
|
+
return 'P-256';
|
|
6
|
+
case 'secp384r1':
|
|
7
|
+
return 'P-384';
|
|
8
|
+
default:
|
|
9
|
+
throw new UnsupportedCurveError(`Curve ${curve} is not supported by webcrypto`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function getGroupOrderSize(curve) {
|
|
13
|
+
switch (curve) {
|
|
14
|
+
case 'secp256r1':
|
|
15
|
+
return 32;
|
|
16
|
+
case 'secp384r1':
|
|
17
|
+
return 48;
|
|
18
|
+
default:
|
|
19
|
+
throw new UnsupportedCurveError(`Curve ${curve} is not supported by webcrypto`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { EcPublicKey } from './public-key';
|
|
2
|
+
import { decodePkcs8PrivateKeyInfo, oidToCurve, UnsupportedCurveError, } from '@road-labs/ocmf-crypto';
|
|
3
|
+
import { mapCurveToWebCryptoCurve } from './curve';
|
|
4
|
+
export class EcPrivateKey {
|
|
5
|
+
cryptoKey;
|
|
6
|
+
curve;
|
|
7
|
+
publicKey;
|
|
8
|
+
constructor(cryptoKey, curve, publicKey) {
|
|
9
|
+
this.cryptoKey = cryptoKey;
|
|
10
|
+
this.curve = curve;
|
|
11
|
+
this.publicKey = publicKey;
|
|
12
|
+
}
|
|
13
|
+
getCryptoKey() {
|
|
14
|
+
return this.cryptoKey;
|
|
15
|
+
}
|
|
16
|
+
getCurve() {
|
|
17
|
+
return this.curve;
|
|
18
|
+
}
|
|
19
|
+
getPublicKey() {
|
|
20
|
+
return this.publicKey;
|
|
21
|
+
}
|
|
22
|
+
static async fromEncoded(value, format) {
|
|
23
|
+
if (format !== 'pkcs8-der') {
|
|
24
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
25
|
+
}
|
|
26
|
+
const keyInfo = decodePkcs8PrivateKeyInfo(value);
|
|
27
|
+
const namedCurve = keyInfo?.privateKey?.parameters?.namedCurve;
|
|
28
|
+
if (!namedCurve) {
|
|
29
|
+
throw new Error(`Named curve not specified`);
|
|
30
|
+
}
|
|
31
|
+
const curve = oidToCurve.get(namedCurve);
|
|
32
|
+
if (!curve) {
|
|
33
|
+
throw new UnsupportedCurveError(`Unknown curve: oid=${namedCurve}`);
|
|
34
|
+
}
|
|
35
|
+
const webCryptoCurve = mapCurveToWebCryptoCurve(curve);
|
|
36
|
+
const privateCryptoKey = await crypto.subtle.importKey('pkcs8', value, {
|
|
37
|
+
name: 'ECDSA',
|
|
38
|
+
namedCurve: webCryptoCurve,
|
|
39
|
+
}, true, ['sign']);
|
|
40
|
+
const publicCryptoKey = await EcPrivateKey.derivePublicKey(privateCryptoKey, webCryptoCurve);
|
|
41
|
+
let publicKey = null;
|
|
42
|
+
if (publicCryptoKey) {
|
|
43
|
+
publicKey = new EcPublicKey(publicCryptoKey, curve);
|
|
44
|
+
}
|
|
45
|
+
return new EcPrivateKey(privateCryptoKey, curve, publicKey);
|
|
46
|
+
}
|
|
47
|
+
static async derivePublicKey(privateCryptoKey, webCryptoCurve) {
|
|
48
|
+
const privateKeyInfo = decodePkcs8PrivateKeyInfo(new Uint8Array(await crypto.subtle.exportKey('pkcs8', privateCryptoKey)));
|
|
49
|
+
if (!privateKeyInfo.privateKey.publicKey) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return crypto.subtle.importKey('raw', privateKeyInfo.privateKey.publicKey, {
|
|
53
|
+
name: 'ECDSA',
|
|
54
|
+
namedCurve: webCryptoCurve,
|
|
55
|
+
}, true, ['verify']);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { decodePkixSubjectPublicKeyInfo, oidToCurve, UnsupportedCurveError, UnsupportedPublicKeyFormatError, } from '@road-labs/ocmf-crypto';
|
|
2
|
+
import { mapCurveToWebCryptoCurve } from './curve';
|
|
3
|
+
export class EcPublicKey {
|
|
4
|
+
cryptoKey;
|
|
5
|
+
curve;
|
|
6
|
+
constructor(cryptoKey, curve) {
|
|
7
|
+
this.cryptoKey = cryptoKey;
|
|
8
|
+
this.curve = curve;
|
|
9
|
+
}
|
|
10
|
+
getCryptoKey() {
|
|
11
|
+
return this.cryptoKey;
|
|
12
|
+
}
|
|
13
|
+
getCurve() {
|
|
14
|
+
return this.curve;
|
|
15
|
+
}
|
|
16
|
+
static async fromEncoded(value, format) {
|
|
17
|
+
if (format !== 'spki-der') {
|
|
18
|
+
throw new UnsupportedPublicKeyFormatError(`Unknown format: ${format}`);
|
|
19
|
+
}
|
|
20
|
+
const keyInfo = decodePkixSubjectPublicKeyInfo(value);
|
|
21
|
+
const namedCurve = keyInfo.algorithm.parameters?.namedCurve;
|
|
22
|
+
if (!namedCurve) {
|
|
23
|
+
throw new Error(`Named curve not specified`);
|
|
24
|
+
}
|
|
25
|
+
const curve = oidToCurve.get(namedCurve);
|
|
26
|
+
if (!curve) {
|
|
27
|
+
throw new UnsupportedCurveError(`Unknown curve: oid=${namedCurve}`);
|
|
28
|
+
}
|
|
29
|
+
const cryptoKey = await crypto.subtle.importKey('spki', value, {
|
|
30
|
+
name: 'ECDSA',
|
|
31
|
+
namedCurve: mapCurveToWebCryptoCurve(curve),
|
|
32
|
+
}, true, ['verify']);
|
|
33
|
+
return new EcPublicKey(cryptoKey, curve);
|
|
34
|
+
}
|
|
35
|
+
async encode(format) {
|
|
36
|
+
if (format !== 'spki-der') {
|
|
37
|
+
throw new UnsupportedPublicKeyFormatError(`Unknown format: ${format}`);
|
|
38
|
+
}
|
|
39
|
+
const exportedKey = await crypto.subtle.exportKey('spki', this.cryptoKey);
|
|
40
|
+
return new Uint8Array(exportedKey);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { encodePkixEcdsaSigValue, UnsupportedHashError, UnsupportedSignatureFormatError, } from '@road-labs/ocmf-crypto';
|
|
2
|
+
export default async function sign(data, privateKey, hash, format) {
|
|
3
|
+
if (hash !== 'SHA-256') {
|
|
4
|
+
throw new UnsupportedHashError(`Invalid hash: ${hash}`);
|
|
5
|
+
}
|
|
6
|
+
if (format !== 'sigvalue-der') {
|
|
7
|
+
throw new UnsupportedSignatureFormatError(`Invalid signature format: ${format}`);
|
|
8
|
+
}
|
|
9
|
+
const result = await crypto.subtle.sign({ name: 'ECDSA', hash }, privateKey.getCryptoKey(), data);
|
|
10
|
+
if (result.byteLength === 0 || result.byteLength % 2 !== 0) {
|
|
11
|
+
throw new Error(`Unexpected signature length: ${result.byteLength}`);
|
|
12
|
+
}
|
|
13
|
+
const mid = result.byteLength / 2;
|
|
14
|
+
const r = addSignByte(new Uint8Array(result.slice(0, mid)));
|
|
15
|
+
const s = addSignByte(new Uint8Array(result.slice(mid, result.byteLength)));
|
|
16
|
+
return encodePkixEcdsaSigValue({ r, s });
|
|
17
|
+
}
|
|
18
|
+
function addSignByte(bytes) {
|
|
19
|
+
if (bytes[0] < 0x80) {
|
|
20
|
+
return bytes;
|
|
21
|
+
}
|
|
22
|
+
const signed = new Uint8Array(bytes.length + 1);
|
|
23
|
+
signed.set(bytes, 1);
|
|
24
|
+
return signed;
|
|
25
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { decodePkixEcdsaSigValue, UnsupportedHashError, UnsupportedSignatureFormatError, } from '@road-labs/ocmf-crypto';
|
|
2
|
+
import { getGroupOrderSize } from './curve';
|
|
3
|
+
export default async function verify(signature, data, key, hash, format) {
|
|
4
|
+
if (hash !== 'SHA-256') {
|
|
5
|
+
throw new UnsupportedHashError(`Invalid hash: ${hash}`);
|
|
6
|
+
}
|
|
7
|
+
if (format !== 'sigvalue-der') {
|
|
8
|
+
throw new UnsupportedSignatureFormatError(`Invalid signature format: ${format}`);
|
|
9
|
+
}
|
|
10
|
+
const sigValue = decodePkixEcdsaSigValue(signature);
|
|
11
|
+
const size = getGroupOrderSize(key.getCurve());
|
|
12
|
+
const r = padZeros(trimSignByte(sigValue.r), size);
|
|
13
|
+
const s = padZeros(trimSignByte(sigValue.s), size);
|
|
14
|
+
const rs = new Uint8Array(size * 2);
|
|
15
|
+
rs.set(r, 0);
|
|
16
|
+
rs.set(s, size);
|
|
17
|
+
return crypto.subtle.verify({ name: 'ECDSA', hash }, key.getCryptoKey(), rs, data);
|
|
18
|
+
}
|
|
19
|
+
function trimSignByte(bytes) {
|
|
20
|
+
if (bytes.length > 1 &&
|
|
21
|
+
bytes.length % 16 === 1 &&
|
|
22
|
+
bytes[0] === 0x00 &&
|
|
23
|
+
bytes[1] >= 0x80) {
|
|
24
|
+
return bytes.slice(1);
|
|
25
|
+
}
|
|
26
|
+
return bytes;
|
|
27
|
+
}
|
|
28
|
+
function padZeros(bytes, len) {
|
|
29
|
+
if (bytes.length > len) {
|
|
30
|
+
throw new Error(`Invalid signature size, expected <= ${len}, received ${bytes.length}`);
|
|
31
|
+
}
|
|
32
|
+
if (bytes.length === len) {
|
|
33
|
+
return bytes;
|
|
34
|
+
}
|
|
35
|
+
const padded = new Uint8Array(len);
|
|
36
|
+
padded.set(bytes, len - bytes.length);
|
|
37
|
+
return padded;
|
|
38
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CryptoAdapter, Hash, PrivateKeyFormat, PublicKeyFormat, SignatureFormat } from '@road-labs/ocmf-crypto';
|
|
2
|
+
import { EcPrivateKey } from './private-key';
|
|
3
|
+
import { EcPublicKey } from './public-key';
|
|
4
|
+
export declare class Crypto implements CryptoAdapter {
|
|
5
|
+
decodeEcPrivateKey(value: Uint8Array, format: PrivateKeyFormat): Promise<EcPrivateKey>;
|
|
6
|
+
decodeEcPublicKey(value: Uint8Array, format: PublicKeyFormat): Promise<EcPublicKey>;
|
|
7
|
+
sign(data: Uint8Array, privateKey: EcPrivateKey, hash: Hash, format: SignatureFormat): Promise<Uint8Array>;
|
|
8
|
+
verify(signature: Uint8Array, data: Uint8Array, publicKey: EcPublicKey, hash: Hash, format: SignatureFormat): Promise<boolean>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EcPublicKey } from './public-key';
|
|
2
|
+
import { Curve, PrivateKeyFormat } from '@road-labs/ocmf-crypto';
|
|
3
|
+
export declare class EcPrivateKey {
|
|
4
|
+
private readonly cryptoKey;
|
|
5
|
+
private readonly curve;
|
|
6
|
+
private readonly publicKey;
|
|
7
|
+
private constructor();
|
|
8
|
+
getCryptoKey(): CryptoKey;
|
|
9
|
+
getCurve(): Curve;
|
|
10
|
+
getPublicKey(): EcPublicKey | null;
|
|
11
|
+
/**
|
|
12
|
+
* @param value - Encoded private key value
|
|
13
|
+
* @param format - Encoding format
|
|
14
|
+
*/
|
|
15
|
+
static fromEncoded(value: Uint8Array, format: PrivateKeyFormat): Promise<EcPrivateKey>;
|
|
16
|
+
private static derivePublicKey;
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Curve, PublicKeyFormat } from '@road-labs/ocmf-crypto';
|
|
2
|
+
export declare class EcPublicKey {
|
|
3
|
+
private readonly cryptoKey;
|
|
4
|
+
private readonly curve;
|
|
5
|
+
constructor(cryptoKey: CryptoKey, curve: Curve);
|
|
6
|
+
getCryptoKey(): CryptoKey;
|
|
7
|
+
getCurve(): Curve;
|
|
8
|
+
/**
|
|
9
|
+
* @param value - The encoded value
|
|
10
|
+
* @param format - The format the value is encoded in
|
|
11
|
+
*/
|
|
12
|
+
static fromEncoded(value: Uint8Array, format: PublicKeyFormat): Promise<EcPublicKey>;
|
|
13
|
+
/**
|
|
14
|
+
* @param format - Format for the public key to be encoded in
|
|
15
|
+
*/
|
|
16
|
+
encode(format: PublicKeyFormat): Promise<Uint8Array>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EcPrivateKey } from './private-key';
|
|
2
|
+
import { Hash, SignatureFormat } from '@road-labs/ocmf-crypto';
|
|
3
|
+
/**
|
|
4
|
+
* @param data - Data to be signed
|
|
5
|
+
* @param privateKey - Private key to use for signing
|
|
6
|
+
* @param hash - Hash to apply
|
|
7
|
+
* @param format - Signature format
|
|
8
|
+
* @return X.509 ECDSASigValue ASN.1 type DER encoded
|
|
9
|
+
*/
|
|
10
|
+
export default function sign(data: Uint8Array, privateKey: EcPrivateKey, hash: Hash, format: SignatureFormat): Promise<Uint8Array>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EcPublicKey } from './public-key';
|
|
2
|
+
import { Hash, SignatureFormat } from '@road-labs/ocmf-crypto';
|
|
3
|
+
/**
|
|
4
|
+
* @param signature - X.509 ECDSASigValue ASN.1 type DER encoded
|
|
5
|
+
* @param data - Raw value
|
|
6
|
+
* @param key - The public key to verify against
|
|
7
|
+
* @param hash - Hash to apply
|
|
8
|
+
* @param format - Signature format
|
|
9
|
+
*/
|
|
10
|
+
export default function verify(signature: Uint8Array, data: Uint8Array, key: EcPublicKey, hash: Hash, format: SignatureFormat): Promise<boolean>;
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@road-labs/ocmf-crypto-web",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "build/cjs/index.js",
|
|
5
|
+
"module": "build/es2022/index.js",
|
|
6
|
+
"types": "build/types/index.d.ts",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"author": "",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"description": "",
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@jest/globals": "^30.0.0",
|
|
13
|
+
"@types/node": "^22.15.30",
|
|
14
|
+
"jest": "^30.0.0",
|
|
15
|
+
"rimraf": "^6.0.1",
|
|
16
|
+
"ts-jest": "^29.4.0",
|
|
17
|
+
"ts-node": "^10.9.2",
|
|
18
|
+
"typescript": "^5.8.3",
|
|
19
|
+
"test-commons": "0.0.1"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@noble/curves": "^1.9.2",
|
|
23
|
+
"@noble/hashes": "^1.8.0",
|
|
24
|
+
"@road-labs/ocmf-crypto": "0.0.1"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"clear": "rimraf build",
|
|
28
|
+
"build": "pnpm run build:module && pnpm run build:types",
|
|
29
|
+
"build:module": "pnpm run build:cjs && pnpm run build:es2022",
|
|
30
|
+
"build:cjs": "tsc -p tsconfig.json --removeComments --module commonjs --outDir build/cjs",
|
|
31
|
+
"build:es2022": "tsc -p tsconfig.json --removeComments --module es2022 --outDir build/es2022",
|
|
32
|
+
"prebuild:types": "rimraf build/types",
|
|
33
|
+
"build:types": "tsc -p tsconfig.json --outDir build/types --declaration --emitDeclarationOnly",
|
|
34
|
+
"rebuild": "pnpm run clear && pnpm run build",
|
|
35
|
+
"test": "jest"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CryptoAdapter,
|
|
3
|
+
Hash,
|
|
4
|
+
PrivateKeyFormat,
|
|
5
|
+
PublicKeyFormat,
|
|
6
|
+
SignatureFormat,
|
|
7
|
+
} from '@road-labs/ocmf-crypto';
|
|
8
|
+
import { EcPrivateKey } from './private-key';
|
|
9
|
+
import { EcPublicKey } from './public-key';
|
|
10
|
+
import sign from './sign';
|
|
11
|
+
import verify from './verify';
|
|
12
|
+
|
|
13
|
+
export class Crypto implements CryptoAdapter {
|
|
14
|
+
async decodeEcPrivateKey(
|
|
15
|
+
value: Uint8Array,
|
|
16
|
+
format: PrivateKeyFormat
|
|
17
|
+
): Promise<EcPrivateKey> {
|
|
18
|
+
return await EcPrivateKey.fromEncoded(value, format);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async decodeEcPublicKey(
|
|
22
|
+
value: Uint8Array,
|
|
23
|
+
format: PublicKeyFormat
|
|
24
|
+
): Promise<EcPublicKey> {
|
|
25
|
+
return await EcPublicKey.fromEncoded(value, format);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async sign(
|
|
29
|
+
data: Uint8Array,
|
|
30
|
+
privateKey: EcPrivateKey,
|
|
31
|
+
hash: Hash,
|
|
32
|
+
format: SignatureFormat
|
|
33
|
+
): Promise<Uint8Array> {
|
|
34
|
+
return sign(data, privateKey, hash, format);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async verify(
|
|
38
|
+
signature: Uint8Array,
|
|
39
|
+
data: Uint8Array,
|
|
40
|
+
publicKey: EcPublicKey,
|
|
41
|
+
hash: Hash,
|
|
42
|
+
format: SignatureFormat
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
return verify(signature, data, publicKey, hash, format);
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/curve.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Curve, UnsupportedCurveError } from '@road-labs/ocmf-crypto';
|
|
2
|
+
|
|
3
|
+
export function mapCurveToWebCryptoCurve(curve: Curve): string {
|
|
4
|
+
switch (curve) {
|
|
5
|
+
case 'secp256r1':
|
|
6
|
+
return 'P-256';
|
|
7
|
+
case 'secp384r1':
|
|
8
|
+
return 'P-384';
|
|
9
|
+
default:
|
|
10
|
+
throw new UnsupportedCurveError(
|
|
11
|
+
`Curve ${curve} is not supported by webcrypto`
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getGroupOrderSize(curve: Curve): number {
|
|
17
|
+
switch (curve) {
|
|
18
|
+
case 'secp256r1':
|
|
19
|
+
return 32;
|
|
20
|
+
case 'secp384r1':
|
|
21
|
+
return 48;
|
|
22
|
+
default:
|
|
23
|
+
throw new UnsupportedCurveError(
|
|
24
|
+
`Curve ${curve} is not supported by webcrypto`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { EcPublicKey } from './public-key';
|
|
2
|
+
import {
|
|
3
|
+
Curve,
|
|
4
|
+
decodePkcs8PrivateKeyInfo,
|
|
5
|
+
oidToCurve,
|
|
6
|
+
PrivateKeyFormat,
|
|
7
|
+
UnsupportedCurveError,
|
|
8
|
+
} from '@road-labs/ocmf-crypto';
|
|
9
|
+
import { mapCurveToWebCryptoCurve } from './curve';
|
|
10
|
+
|
|
11
|
+
export class EcPrivateKey {
|
|
12
|
+
private constructor(
|
|
13
|
+
private readonly cryptoKey: CryptoKey,
|
|
14
|
+
private readonly curve: Curve,
|
|
15
|
+
private readonly publicKey: EcPublicKey | null
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
public getCryptoKey(): CryptoKey {
|
|
19
|
+
return this.cryptoKey;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getCurve(): Curve {
|
|
23
|
+
return this.curve;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public getPublicKey(): EcPublicKey | null {
|
|
27
|
+
return this.publicKey;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param value - Encoded private key value
|
|
32
|
+
* @param format - Encoding format
|
|
33
|
+
*/
|
|
34
|
+
static async fromEncoded(
|
|
35
|
+
value: Uint8Array,
|
|
36
|
+
format: PrivateKeyFormat
|
|
37
|
+
): Promise<EcPrivateKey> {
|
|
38
|
+
if (format !== 'pkcs8-der') {
|
|
39
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const keyInfo = decodePkcs8PrivateKeyInfo(value);
|
|
43
|
+
|
|
44
|
+
const namedCurve = keyInfo?.privateKey?.parameters?.namedCurve;
|
|
45
|
+
if (!namedCurve) {
|
|
46
|
+
throw new Error(`Named curve not specified`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const curve = oidToCurve.get(namedCurve);
|
|
50
|
+
if (!curve) {
|
|
51
|
+
throw new UnsupportedCurveError(`Unknown curve: oid=${namedCurve}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const webCryptoCurve = mapCurveToWebCryptoCurve(curve);
|
|
55
|
+
|
|
56
|
+
const privateCryptoKey = await crypto.subtle.importKey(
|
|
57
|
+
'pkcs8',
|
|
58
|
+
value,
|
|
59
|
+
{
|
|
60
|
+
name: 'ECDSA',
|
|
61
|
+
namedCurve: webCryptoCurve,
|
|
62
|
+
},
|
|
63
|
+
true,
|
|
64
|
+
['sign']
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const publicCryptoKey = await EcPrivateKey.derivePublicKey(
|
|
68
|
+
privateCryptoKey,
|
|
69
|
+
webCryptoCurve
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
let publicKey: EcPublicKey | null = null;
|
|
73
|
+
if (publicCryptoKey) {
|
|
74
|
+
publicKey = new EcPublicKey(publicCryptoKey, curve);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return new EcPrivateKey(privateCryptoKey, curve, publicKey);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static async derivePublicKey(
|
|
81
|
+
privateCryptoKey: CryptoKey,
|
|
82
|
+
webCryptoCurve: string
|
|
83
|
+
): Promise<CryptoKey | null> {
|
|
84
|
+
const privateKeyInfo = decodePkcs8PrivateKeyInfo(
|
|
85
|
+
new Uint8Array(await crypto.subtle.exportKey('pkcs8', privateCryptoKey))
|
|
86
|
+
);
|
|
87
|
+
if (!privateKeyInfo.privateKey.publicKey) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return crypto.subtle.importKey(
|
|
91
|
+
'raw',
|
|
92
|
+
privateKeyInfo.privateKey.publicKey,
|
|
93
|
+
{
|
|
94
|
+
name: 'ECDSA',
|
|
95
|
+
namedCurve: webCryptoCurve,
|
|
96
|
+
},
|
|
97
|
+
true,
|
|
98
|
+
['verify']
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Curve,
|
|
3
|
+
decodePkixSubjectPublicKeyInfo,
|
|
4
|
+
oidToCurve,
|
|
5
|
+
PublicKeyFormat,
|
|
6
|
+
UnsupportedCurveError,
|
|
7
|
+
UnsupportedPublicKeyFormatError,
|
|
8
|
+
} from '@road-labs/ocmf-crypto';
|
|
9
|
+
import { mapCurveToWebCryptoCurve } from './curve';
|
|
10
|
+
|
|
11
|
+
export class EcPublicKey {
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly cryptoKey: CryptoKey,
|
|
14
|
+
private readonly curve: Curve
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
public getCryptoKey(): CryptoKey {
|
|
18
|
+
return this.cryptoKey;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public getCurve(): Curve {
|
|
22
|
+
return this.curve;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param value - The encoded value
|
|
27
|
+
* @param format - The format the value is encoded in
|
|
28
|
+
*/
|
|
29
|
+
static async fromEncoded(
|
|
30
|
+
value: Uint8Array,
|
|
31
|
+
format: PublicKeyFormat
|
|
32
|
+
): Promise<EcPublicKey> {
|
|
33
|
+
if (format !== 'spki-der') {
|
|
34
|
+
throw new UnsupportedPublicKeyFormatError(`Unknown format: ${format}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const keyInfo = decodePkixSubjectPublicKeyInfo(value);
|
|
38
|
+
|
|
39
|
+
const namedCurve = keyInfo.algorithm.parameters?.namedCurve;
|
|
40
|
+
if (!namedCurve) {
|
|
41
|
+
throw new Error(`Named curve not specified`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const curve = oidToCurve.get(namedCurve);
|
|
45
|
+
if (!curve) {
|
|
46
|
+
throw new UnsupportedCurveError(`Unknown curve: oid=${namedCurve}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
50
|
+
'spki',
|
|
51
|
+
value,
|
|
52
|
+
{
|
|
53
|
+
name: 'ECDSA',
|
|
54
|
+
namedCurve: mapCurveToWebCryptoCurve(curve),
|
|
55
|
+
},
|
|
56
|
+
true,
|
|
57
|
+
['verify']
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return new EcPublicKey(cryptoKey, curve);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param format - Format for the public key to be encoded in
|
|
65
|
+
*/
|
|
66
|
+
public async encode(format: PublicKeyFormat): Promise<Uint8Array> {
|
|
67
|
+
if (format !== 'spki-der') {
|
|
68
|
+
throw new UnsupportedPublicKeyFormatError(`Unknown format: ${format}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const exportedKey = await crypto.subtle.exportKey('spki', this.cryptoKey);
|
|
72
|
+
|
|
73
|
+
return new Uint8Array(exportedKey);
|
|
74
|
+
}
|
|
75
|
+
}
|
package/src/sign.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { EcPrivateKey } from './private-key';
|
|
2
|
+
import {
|
|
3
|
+
encodePkixEcdsaSigValue,
|
|
4
|
+
Hash,
|
|
5
|
+
SignatureFormat,
|
|
6
|
+
UnsupportedHashError,
|
|
7
|
+
UnsupportedSignatureFormatError,
|
|
8
|
+
} from '@road-labs/ocmf-crypto';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param data - Data to be signed
|
|
12
|
+
* @param privateKey - Private key to use for signing
|
|
13
|
+
* @param hash - Hash to apply
|
|
14
|
+
* @param format - Signature format
|
|
15
|
+
* @return X.509 ECDSASigValue ASN.1 type DER encoded
|
|
16
|
+
*/
|
|
17
|
+
export default async function sign(
|
|
18
|
+
data: Uint8Array,
|
|
19
|
+
privateKey: EcPrivateKey,
|
|
20
|
+
hash: Hash,
|
|
21
|
+
format: SignatureFormat
|
|
22
|
+
): Promise<Uint8Array> {
|
|
23
|
+
if (hash !== 'SHA-256') {
|
|
24
|
+
throw new UnsupportedHashError(`Invalid hash: ${hash}`);
|
|
25
|
+
}
|
|
26
|
+
if (format !== 'sigvalue-der') {
|
|
27
|
+
throw new UnsupportedSignatureFormatError(
|
|
28
|
+
`Invalid signature format: ${format}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const result = await crypto.subtle.sign(
|
|
33
|
+
{ name: 'ECDSA', hash },
|
|
34
|
+
privateKey.getCryptoKey(),
|
|
35
|
+
data
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (result.byteLength === 0 || result.byteLength % 2 !== 0) {
|
|
39
|
+
throw new Error(`Unexpected signature length: ${result.byteLength}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mid = result.byteLength / 2;
|
|
43
|
+
const r = addSignByte(new Uint8Array(result.slice(0, mid)));
|
|
44
|
+
const s = addSignByte(new Uint8Array(result.slice(mid, result.byteLength)));
|
|
45
|
+
|
|
46
|
+
return encodePkixEcdsaSigValue({ r, s });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function addSignByte(bytes: Uint8Array): Uint8Array {
|
|
50
|
+
if (bytes[0] < 0x80) {
|
|
51
|
+
return bytes;
|
|
52
|
+
}
|
|
53
|
+
const signed = new Uint8Array(bytes.length + 1);
|
|
54
|
+
signed.set(bytes, 1);
|
|
55
|
+
return signed;
|
|
56
|
+
}
|
package/src/verify.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { EcPublicKey } from './public-key';
|
|
2
|
+
import {
|
|
3
|
+
decodePkixEcdsaSigValue,
|
|
4
|
+
Hash,
|
|
5
|
+
SignatureFormat,
|
|
6
|
+
UnsupportedHashError,
|
|
7
|
+
UnsupportedSignatureFormatError,
|
|
8
|
+
} from '@road-labs/ocmf-crypto';
|
|
9
|
+
import { getGroupOrderSize } from './curve';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param signature - X.509 ECDSASigValue ASN.1 type DER encoded
|
|
13
|
+
* @param data - Raw value
|
|
14
|
+
* @param key - The public key to verify against
|
|
15
|
+
* @param hash - Hash to apply
|
|
16
|
+
* @param format - Signature format
|
|
17
|
+
*/
|
|
18
|
+
export default async function verify(
|
|
19
|
+
signature: Uint8Array,
|
|
20
|
+
data: Uint8Array,
|
|
21
|
+
key: EcPublicKey,
|
|
22
|
+
hash: Hash,
|
|
23
|
+
format: SignatureFormat
|
|
24
|
+
): Promise<boolean> {
|
|
25
|
+
if (hash !== 'SHA-256') {
|
|
26
|
+
throw new UnsupportedHashError(`Invalid hash: ${hash}`);
|
|
27
|
+
}
|
|
28
|
+
if (format !== 'sigvalue-der') {
|
|
29
|
+
throw new UnsupportedSignatureFormatError(
|
|
30
|
+
`Invalid signature format: ${format}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Convert to IEEE P1363 concatenated R|S format.
|
|
35
|
+
// Ref:
|
|
36
|
+
// - https://crypto.stackexchange.com/a/1797
|
|
37
|
+
// - https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/sun/security/util/ECUtil.java#L290-L299
|
|
38
|
+
// - https://chromium.googlesource.com/chromium/src/+/master/components/webcrypto/algorithms/ecdsa.cc#74
|
|
39
|
+
const sigValue = decodePkixEcdsaSigValue(signature);
|
|
40
|
+
const size = getGroupOrderSize(key.getCurve());
|
|
41
|
+
const r = padZeros(trimSignByte(sigValue.r), size);
|
|
42
|
+
const s = padZeros(trimSignByte(sigValue.s), size);
|
|
43
|
+
const rs = new Uint8Array(size * 2);
|
|
44
|
+
rs.set(r, 0);
|
|
45
|
+
rs.set(s, size);
|
|
46
|
+
|
|
47
|
+
return crypto.subtle.verify(
|
|
48
|
+
{ name: 'ECDSA', hash },
|
|
49
|
+
key.getCryptoKey(),
|
|
50
|
+
rs,
|
|
51
|
+
data
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function trimSignByte(bytes: Uint8Array): Uint8Array {
|
|
56
|
+
if (
|
|
57
|
+
bytes.length > 1 &&
|
|
58
|
+
bytes.length % 16 === 1 &&
|
|
59
|
+
bytes[0] === 0x00 &&
|
|
60
|
+
bytes[1] >= 0x80
|
|
61
|
+
) {
|
|
62
|
+
return bytes.slice(1);
|
|
63
|
+
}
|
|
64
|
+
return bytes;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function padZeros(bytes: Uint8Array, len: number): Uint8Array {
|
|
68
|
+
if (bytes.length > len) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`Invalid signature size, expected <= ${len}, received ${bytes.length}`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (bytes.length === len) {
|
|
74
|
+
return bytes;
|
|
75
|
+
}
|
|
76
|
+
const padded = new Uint8Array(len);
|
|
77
|
+
padded.set(bytes, len - bytes.length);
|
|
78
|
+
return padded;
|
|
79
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getGroupOrderSize, mapCurveToWebCryptoCurve } from '../src';
|
|
2
|
+
import { Curve } from '@road-labs/ocmf-crypto';
|
|
3
|
+
import { describe, expect, it } from '@jest/globals';
|
|
4
|
+
|
|
5
|
+
describe('mapCurveToWebCryptoCurve', () => {
|
|
6
|
+
const testCases: { curve: Curve; expected: string }[] = [
|
|
7
|
+
{ curve: 'secp256r1', expected: 'P-256' },
|
|
8
|
+
{ curve: 'secp384r1', expected: 'P-384' },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
it.each(testCases)(
|
|
12
|
+
'should return $expected for curve $curve',
|
|
13
|
+
({ curve, expected }) => {
|
|
14
|
+
const actual = mapCurveToWebCryptoCurve(curve);
|
|
15
|
+
expect(actual).toBe(expected);
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
it('throws if the curve is not supported', () => {
|
|
20
|
+
expect(() => mapCurveToWebCryptoCurve('brainpool384r1')).toThrow();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('getGroupOrderSize', () => {
|
|
25
|
+
const testCases: { curve: Curve; expected: number }[] = [
|
|
26
|
+
{ curve: 'secp256r1', expected: 32 },
|
|
27
|
+
{ curve: 'secp384r1', expected: 48 },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
it.each(testCases)(
|
|
31
|
+
'should return $expected for curve $curve',
|
|
32
|
+
({ curve, expected }) => {
|
|
33
|
+
const actual = getGroupOrderSize(curve);
|
|
34
|
+
expect(actual).toBe(expected);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
it('throws if the curve is not supported', () => {
|
|
39
|
+
expect(() => getGroupOrderSize('brainpool384r1')).toThrow();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { Curve, UnsupportedCurveError } from '@road-labs/ocmf-crypto';
|
|
3
|
+
import { buildPrivateKeyTestCases } from 'test-commons';
|
|
4
|
+
import { EcPrivateKey } from '../src';
|
|
5
|
+
|
|
6
|
+
const unsupported: Curve[] = [
|
|
7
|
+
'brainpool256r1',
|
|
8
|
+
'brainpool384r1',
|
|
9
|
+
'secp192r1',
|
|
10
|
+
'secp192k1',
|
|
11
|
+
'secp256k1',
|
|
12
|
+
];
|
|
13
|
+
const curves: Curve[] = ['secp256r1', 'secp384r1'];
|
|
14
|
+
|
|
15
|
+
describe('EcPrivateKey', () => {
|
|
16
|
+
describe('fromEncoded', () => {
|
|
17
|
+
it.each(buildPrivateKeyTestCases(curves))(
|
|
18
|
+
'supports $name',
|
|
19
|
+
async ({ curve, pkcs8 }) => {
|
|
20
|
+
const privateKey = await EcPrivateKey.fromEncoded(pkcs8, 'pkcs8-der');
|
|
21
|
+
expect(privateKey.getCurve()).toEqual(curve);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
it.each(buildPrivateKeyTestCases(unsupported))(
|
|
26
|
+
'does not support $name',
|
|
27
|
+
async ({ curve, pkcs8 }) => {
|
|
28
|
+
await expect(
|
|
29
|
+
EcPrivateKey.fromEncoded(pkcs8, 'pkcs8-der')
|
|
30
|
+
).rejects.toThrow(UnsupportedCurveError);
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('getPublicKey', () => {
|
|
36
|
+
it.each(buildPrivateKeyTestCases(curves))(
|
|
37
|
+
'can derive a public key for $name',
|
|
38
|
+
async ({ curve, pkcs8, spki }) => {
|
|
39
|
+
const publicKey = (
|
|
40
|
+
await EcPrivateKey.fromEncoded(pkcs8, 'pkcs8-der')
|
|
41
|
+
).getPublicKey();
|
|
42
|
+
expect(publicKey?.getCurve()).toEqual(curve);
|
|
43
|
+
expect(await publicKey?.encode('spki-der')).toEqual(spki);
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { Curve, UnsupportedCurveError } from '@road-labs/ocmf-crypto';
|
|
3
|
+
import { buildPublicKeyTestCases, isValidPublicKey } from 'test-commons';
|
|
4
|
+
import { EcPublicKey } from '../src';
|
|
5
|
+
|
|
6
|
+
const unsupported: Curve[] = [
|
|
7
|
+
'brainpool256r1',
|
|
8
|
+
'brainpool384r1',
|
|
9
|
+
'secp192r1',
|
|
10
|
+
'secp192k1',
|
|
11
|
+
'secp256k1',
|
|
12
|
+
];
|
|
13
|
+
const curves: Curve[] = ['secp256r1', 'secp384r1'];
|
|
14
|
+
|
|
15
|
+
describe('EcPublicKey', () => {
|
|
16
|
+
describe('fromEncoded', () => {
|
|
17
|
+
it.each(buildPublicKeyTestCases(curves))(
|
|
18
|
+
'supports $name',
|
|
19
|
+
async ({ curve, spki }) => {
|
|
20
|
+
const publicKey = await EcPublicKey.fromEncoded(spki, 'spki-der');
|
|
21
|
+
expect(publicKey.getCurve()).toEqual(curve);
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
it.each(buildPublicKeyTestCases(unsupported))(
|
|
26
|
+
'does not support $name',
|
|
27
|
+
async ({ curve, spki }) => {
|
|
28
|
+
await expect(EcPublicKey.fromEncoded(spki, 'spki-der')).rejects.toThrow(
|
|
29
|
+
UnsupportedCurveError
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('encode', () => {
|
|
36
|
+
it.each(buildPublicKeyTestCases(curves))(
|
|
37
|
+
'encodes $name',
|
|
38
|
+
async ({ spki, curve }) => {
|
|
39
|
+
const publicKey = await (
|
|
40
|
+
await EcPublicKey.fromEncoded(spki, 'spki-der')
|
|
41
|
+
).encode('spki-der');
|
|
42
|
+
expect(publicKey).toEqual(spki);
|
|
43
|
+
expect(isValidPublicKey(publicKey, curve)).toEqual(true);
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { buildSignTestCases, isValidSignature } from 'test-commons';
|
|
3
|
+
import { EcPrivateKey, sign } from '../src';
|
|
4
|
+
import { Curve } from '@road-labs/ocmf-crypto';
|
|
5
|
+
|
|
6
|
+
const format = 'sigvalue-der';
|
|
7
|
+
const curves: Curve[] = ['secp256r1', 'secp384r1'];
|
|
8
|
+
|
|
9
|
+
describe('verify', () => {
|
|
10
|
+
it.each(buildSignTestCases(curves))(
|
|
11
|
+
'$name',
|
|
12
|
+
async ({ curve, data, hash, pkcs8 }) => {
|
|
13
|
+
const privateKey = await EcPrivateKey.fromEncoded(pkcs8, 'pkcs8-der');
|
|
14
|
+
const signature = await sign(data, privateKey, hash, format);
|
|
15
|
+
expect(isValidSignature(signature, curve)).toEqual(true);
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from '@jest/globals';
|
|
2
|
+
import { buildVerifyTestCases } from 'test-commons';
|
|
3
|
+
import { EcPublicKey, verify } from '../src';
|
|
4
|
+
import { Curve } from '@road-labs/ocmf-crypto';
|
|
5
|
+
|
|
6
|
+
const format = 'sigvalue-der';
|
|
7
|
+
const curves: Curve[] = ['secp256r1', 'secp384r1'];
|
|
8
|
+
|
|
9
|
+
describe('verify', () => {
|
|
10
|
+
it.each(buildVerifyTestCases(curves))(
|
|
11
|
+
'$name',
|
|
12
|
+
async ({ curve, signature, data, hash, spki, expected }) => {
|
|
13
|
+
const publicKey = await EcPublicKey.fromEncoded(spki, 'spki-der');
|
|
14
|
+
const actual = await verify(signature, data, publicKey, hash, format);
|
|
15
|
+
expect(actual).toEqual(expected);
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
});
|
package/tsconfig.json
ADDED