Skip to content

Commit

Permalink
Crypto key refactor
Browse files Browse the repository at this point in the history
Replace key handling with a JS implementation that converts keys to standard
JWK Key format.  Streamlines the Crypto.ts API and removes the various formats
and hacks we previously used to pass keys to Node's OpenSSL wrapper.

fixes project-chip#212
could be considered phase 1 of project-chip#122
  • Loading branch information
lauckhart committed Aug 6, 2023
1 parent 109ae31 commit 6c4e685
Show file tree
Hide file tree
Showing 24 changed files with 1,037 additions and 127 deletions.
60 changes: 20 additions & 40 deletions packages/matter-node.js/src/crypto/CryptoNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
import * as crypto from "crypto";
import { ByteArray } from "@project-chip/matter.js/util";
import {
CRYPTO_AUTH_TAG_LENGTH, CRYPTO_EC_CURVE, CRYPTO_ENCRYPT_ALGORITHM, CRYPTO_HASH_ALGORITHM, CRYPTO_SYMMETRIC_KEY_LENGTH,
CryptoDsaEncoding, Crypto, CryptoError
CRYPTO_AUTH_TAG_LENGTH,
CRYPTO_EC_CURVE,
CRYPTO_ENCRYPT_ALGORITHM,
CRYPTO_HASH_ALGORITHM,
CRYPTO_SYMMETRIC_KEY_LENGTH,
CryptoDsaEncoding,
Crypto,
PrivateKey,
CryptoError
} from "@project-chip/matter.js/crypto";

const EC_PRIVATE_KEY_PKCS8_HEADER = ByteArray.fromHex("308141020100301306072a8648ce3d020106082a8648ce3d030107042730250201010420");
const EC_PUBLIC_KEY_SPKI_HEADER = ByteArray.fromHex("3059301306072a8648ce3d020106082a8648ce3d030107034200");

export class CryptoNode extends Crypto {
encrypt(key: ByteArray, data: ByteArray, nonce: ByteArray, aad?: ByteArray): ByteArray {
const cipher = crypto.createCipheriv(CRYPTO_ENCRYPT_ALGORITHM, key, nonce, { authTagLength: CRYPTO_AUTH_TAG_LENGTH });
Expand Down Expand Up @@ -91,63 +95,39 @@ export class CryptoNode extends Crypto {
return new ByteArray(hmac.digest());
}

signPkcs8(privateKey: ByteArray, data: ByteArray | ByteArray[], dsaEncoding: CryptoDsaEncoding = "ieee-p1363"): ByteArray {
sign(privateKey: JsonWebKey, data: ByteArray | ByteArray[], dsaEncoding: CryptoDsaEncoding = "ieee-p1363"): ByteArray {
const signer = crypto.createSign(CRYPTO_HASH_ALGORITHM);
if (Array.isArray(data)) {
data.forEach(chunk => signer.update(chunk));
} else {
signer.update(data);
}
return new ByteArray(signer.sign({
key: Buffer.concat([EC_PRIVATE_KEY_PKCS8_HEADER, privateKey]), // key has to be a node.js Buffer object
format: "der",
key: privateKey as any,
format: "jwk",
type: "pkcs8",
dsaEncoding,
}));
}

signSec1(privateKey: ByteArray, data: ByteArray | ByteArray[], dsaEncoding: CryptoDsaEncoding = "ieee-p1363"): ByteArray {
const signer = crypto.createSign(CRYPTO_HASH_ALGORITHM);
if (Array.isArray(data)) {
data.forEach(chunk => signer.update(chunk));
} else {
signer.update(data);
}
return new ByteArray(signer.sign({
key: Buffer.from(privateKey), // key has to be a node.js Buffer object
format: "der",
type: "sec1",
dsaEncoding,
}));
}

verifySpkiEc(publicKey: ByteArray, data: ByteArray, signature: ByteArray, dsaEncoding: CryptoDsaEncoding = "ieee-p1363") {
const verifier = crypto.createVerify(CRYPTO_HASH_ALGORITHM);
verifier.update(data);
const success = verifier.verify({
key: Buffer.from(publicKey), // key has to be a node.js Buffer object
format: "der",
type: "spki",
dsaEncoding,
}, signature);
if (!success) throw new CryptoError("Signature verification failed");
}

verifySpki(publicKey: ByteArray, data: ByteArray, signature: ByteArray, dsaEncoding: CryptoDsaEncoding = "ieee-p1363") {
verify(publicKey: JsonWebKey, data: ByteArray, signature: ByteArray, dsaEncoding: CryptoDsaEncoding = "ieee-p1363") {
const verifier = crypto.createVerify(CRYPTO_HASH_ALGORITHM);
verifier.update(data);
const success = verifier.verify({
key: Buffer.concat([EC_PUBLIC_KEY_SPKI_HEADER, publicKey]), // key has to be a node.js Buffer object
format: "der",
key: publicKey as any,
format: "jwk",
type: "spki",
dsaEncoding,
}, signature);
if (!success) throw new CryptoError("Signature verification failed");
}

createKeyPair(): { publicKey: ByteArray, privateKey: ByteArray } {
createKeyPair() {
const ecdh = crypto.createECDH(CRYPTO_EC_CURVE);
ecdh.generateKeys();
return { publicKey: new ByteArray(ecdh.getPublicKey()), privateKey: new ByteArray(ecdh.getPrivateKey()) };
return PrivateKey(
ecdh.getPrivateKey(),
{ publicKey: ecdh.getPublicKey() }
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { Crypto } from "@project-chip/matter.js/crypto";
import { Crypto, Key, PrivateKey, PublicKey } from "@project-chip/matter.js/crypto";
import { CryptoNode } from "../../src/crypto/CryptoNode";

Crypto.get = () => new CryptoNode();
Expand Down Expand Up @@ -55,21 +55,21 @@ describe("CertificateManager", () => {

describe("createCertificateSigningRequest", () => {
it("generates a valid CSR", () => {
const result = CertificateManager.createCertificateSigningRequest({ publicKey: PUBLIC_KEY, privateKey: PRIVATE_KEY });
const result = CertificateManager.createCertificateSigningRequest(PrivateKey(PRIVATE_KEY, { publicKey: PUBLIC_KEY }));

const derNode = DerCodec.decode(result);
assert.equal(derNode[ELEMENTS_KEY]?.length, 3);
const [requestNode, signatureAlgorithmNode, signatureNode] = derNode[ELEMENTS_KEY] as DerNode[];
assert.deepEqual(DerCodec.encode(signatureAlgorithmNode), DerCodec.encode(EcdsaWithSHA256_X962));
const requestBytes = DerCodec.encode(requestNode);
assert.deepEqual(requestBytes, CSR_REQUEST_ASN1);
Crypto.verifySpki(PUBLIC_KEY, DerCodec.encode(requestNode), signatureNode[BYTES_KEY], "der");
Crypto.verify(PublicKey(PUBLIC_KEY), DerCodec.encode(requestNode), signatureNode[BYTES_KEY], "der");
});
});

describe("getPublicKeyFromCsr", () => {
it("get the public key from the CSR", () => {
const csr = CertificateManager.createCertificateSigningRequest({ publicKey: PUBLIC_KEY, privateKey: PRIVATE_KEY });
const csr = CertificateManager.createCertificateSigningRequest(PrivateKey(PRIVATE_KEY, { publicKey: PUBLIC_KEY }));

const result = CertificateManager.getPublicKeyFromCsr(csr);

Expand Down
10 changes: 8 additions & 2 deletions packages/matter-node.js/test/cluster/ClusterServerTestingUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ import { FabricIndex, VendorId, FabricId, NodeId } from "@project-chip/matter.js
import { ByteArray } from "@project-chip/matter.js/util";
import { Attributes, ClusterServerObj, Commands, Events } from "@project-chip/matter.js/cluster";
import { Endpoint } from "@project-chip/matter.js/device";
import { PrivateKey } from "../../src/crypto/export";

export const ZERO = new ByteArray(1);
const PRIVATE_KEY = new ByteArray(32);
PRIVATE_KEY[31] = 1; // EC doesn't like all-zero private key
export const KEY = PrivateKey(PRIVATE_KEY);

// TODO make that nicer
export async function callCommandOnClusterServer<A extends Attributes, C extends Commands, E extends Events>(clusterServer: ClusterServerObj<A, C, E>, commandName: string, args: any, endpoint: Endpoint, session?: SecureSession<any>, message?: Message): Promise<{ code: StatusCode, responseId: number, response: any }> {
Expand All @@ -22,6 +28,6 @@ export async function callCommandOnClusterServer<A extends Attributes, C extends
}

export async function createTestSessionWithFabric() {
const testFabric = new Fabric(new FabricIndex(1), new FabricId(BigInt(1)), new NodeId(BigInt(1)), new NodeId(BigInt(2)), ByteArray.fromHex("00"), ByteArray.fromHex("00"), { privateKey: ByteArray.fromHex("00"), publicKey: ByteArray.fromHex("00") }, new VendorId(1), ByteArray.fromHex("00"), ByteArray.fromHex("00"), ByteArray.fromHex("00"), ByteArray.fromHex("00"), ByteArray.fromHex("00"), "");
return await SecureSession.create({} as any, 1, testFabric, new NodeId(BigInt(1)), 1, ByteArray.fromHex("00"), ByteArray.fromHex("00"), false, false, () => { /* */ }, 1, 2);
const testFabric = new Fabric(new FabricIndex(1), new FabricId(BigInt(1)), new NodeId(BigInt(1)), new NodeId(BigInt(2)), ZERO, ZERO, KEY, new VendorId(1), ZERO, ZERO, ZERO, ZERO, ZERO, "");
return await SecureSession.create({} as any, 1, testFabric, new NodeId(BigInt(1)), 1, ZERO, ZERO, false, false, () => { /* */ }, 1, 2);
}
21 changes: 11 additions & 10 deletions packages/matter-node.js/test/crypto/CryptoTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as assert from "assert";
import { CryptoNode } from "../../src/crypto/CryptoNode";
import * as crypto from "crypto";
import { ByteArray } from "@project-chip/matter.js/util";
import { Key, PrivateKey, PublicKey } from "../../src/crypto/export";

const KEY = ByteArray.fromHex("abf227feffea8c38e688ddcbffc459f1");
const ENCRYPTED_DATA = ByteArray.fromHex("c4527bd6965518e8382edbbd28f27f42492d0766124f9961a772");
Expand Down Expand Up @@ -47,25 +48,25 @@ describe("Crypto", () => {
});
});

describe("signPkcs8 / verifySpki", () => {
describe("sign & verify with raw keys", () => {
it("signs data with known private key", () => {
const result = cryptoNode.signPkcs8(PRIVATE_KEY, ENCRYPTED_DATA);
const result = cryptoNode.sign(PrivateKey(PRIVATE_KEY), ENCRYPTED_DATA);

cryptoNode.verifySpki(PUBLIC_KEY, ENCRYPTED_DATA, result);
cryptoNode.verify(PublicKey(PUBLIC_KEY), ENCRYPTED_DATA, result);
});

it("signs data with generated private key", () => {
const ecdh = crypto.createECDH("prime256v1");
ecdh.generateKeys();
const result = cryptoNode.signPkcs8(ecdh.getPrivateKey(), ENCRYPTED_DATA);
const result = cryptoNode.sign(PrivateKey(ecdh.getPrivateKey()), ENCRYPTED_DATA);

cryptoNode.verifySpki(ecdh.getPublicKey(), ENCRYPTED_DATA, result);
cryptoNode.verify(PublicKey(ecdh.getPublicKey()), ENCRYPTED_DATA, result);
});
});

describe("signSec1 / verifySpki", () => {
describe("sign & verify with SEC1 private and SPKI public keys", () => {
it("signs data with known sec1 key", () => {
const result = cryptoNode.signSec1(SEC1_KEY, ENCRYPTED_DATA, "der");
const result = cryptoNode.sign(Key({ sec1: SEC1_KEY }), ENCRYPTED_DATA, "der");

const privateKeyObject = crypto.createPrivateKey({
key: Buffer.from(SEC1_KEY),
Expand All @@ -74,15 +75,15 @@ describe("Crypto", () => {
});
const publicKey = crypto.createPublicKey(privateKeyObject).export({ format: "der", type: "spki" });

cryptoNode.verifySpkiEc(publicKey, ENCRYPTED_DATA, result, "der");
cryptoNode.verify(Key({ spki: publicKey }), ENCRYPTED_DATA, result, "der");
});
});

describe("createKeyPair", () => {
it("generates a working key pair", () => {
const { privateKey, publicKey } = cryptoNode.createKeyPair();
const key = cryptoNode.createKeyPair();

cryptoNode.verifySpki(publicKey, ENCRYPTED_DATA, cryptoNode.signPkcs8(privateKey, ENCRYPTED_DATA));
cryptoNode.verify(key, ENCRYPTED_DATA, cryptoNode.sign(key, ENCRYPTED_DATA));
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Time.get = () => new TimeFake(0);
import { Crypto } from "@project-chip/matter.js/crypto";
import { CryptoNode } from "../../src/crypto/CryptoNode";

import { KEY } from "../cluster/ClusterServerTestingUtil.js";

Crypto.get = () => new CryptoNode();

import * as assert from "assert";
Expand Down Expand Up @@ -461,7 +463,7 @@ describe("InteractionProtocol", () => {
const interactionProtocol = new InteractionServer(storageContext);
interactionProtocol.setRootEndpoint(endpoint);

const testFabric = new Fabric(new FabricIndex(1), new FabricId(BigInt(1)), new NodeId(BigInt(1)), new NodeId(BigInt(2)), ByteArray.fromHex("00"), ByteArray.fromHex("00"), { privateKey: ByteArray.fromHex("00"), publicKey: ByteArray.fromHex("00") }, new VendorId(1), ByteArray.fromHex("00"), ByteArray.fromHex("00"), ByteArray.fromHex("00"), ByteArray.fromHex("00"), ByteArray.fromHex("00"), "");
const testFabric = new Fabric(new FabricIndex(1), new FabricId(BigInt(1)), new NodeId(BigInt(1)), new NodeId(BigInt(2)), ByteArray.fromHex("00"), ByteArray.fromHex("00"), KEY, new VendorId(1), ByteArray.fromHex("00"), ByteArray.fromHex("00"), ByteArray.fromHex("00"), ByteArray.fromHex("00"), ByteArray.fromHex("00"), "");
const testSession = await SecureSession.create({ getFabrics: () => [] } as any, 1, testFabric, new NodeId(BigInt(1)), 1, ByteArray.fromHex("00"), ByteArray.fromHex("00"), false, false, () => {/* nothing */ }, 1000, 1000);
const result = interactionProtocol.handleWriteRequest(({ channel: { name: "test" }, session: testSession }) as unknown as MessageExchange<any>, CHUNKED_ARRAY_WRITE_REQUEST);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as assert from "assert";
import { TlvEncryptedDataSigma2, TlvEncryptedDataSigma3, TlvSignedData } from "@project-chip/matter.js/session";
import { ByteArray } from "@project-chip/matter.js/util";
import { CryptoNode } from "../../../src/crypto/CryptoNode";
import { PublicKey } from "../../../src/crypto/export";

const cryptoNode = new CryptoNode();

Expand Down Expand Up @@ -38,7 +39,7 @@ describe("CasePairing", () => {

const signature = ByteArray.fromHex("1736972364d84c4ae069f642f491256c6e74c86eda9f5ed4d89dfd7cadb68b67574f032afa2764fcc890e9218eaedcc484576d2d65e4df1ae22dd916f12ab59e");

cryptoNode.verifySpki(fabricPubKey, signatureData, signature);
cryptoNode.verify(PublicKey(fabricPubKey), signatureData, signature);

const encryptedDataPlain = TlvEncryptedDataSigma2.encode({ nodeOpCert: noc, signature, resumptionId: ByteArray.fromHex("8731f8cec507136df7558fca9360e9fc") });

Expand All @@ -54,7 +55,7 @@ describe("CasePairing", () => {
const signatureData = ByteArray.fromHex("153001f1153001010124020137032414001826048012542826058015203b370624150124115d18240701240801300941049bfa105c3d209ff226c31da689dafb297b73499ec1844bba89c60ce65938b722300dd0abbb201e9451c6ab284ec99b8d90c5dd892388c59fde30c299c64af8c4370a3501280118240201360304020401183004142c3494bc756f4b58a73bde64a3141285a3efd5c9300514e766069362d7e35b79687161644d222bdde93a6818300b406772b5445ef466a669d9e5e5663238b817511e73ce992937ddd975690abda8b86b0a79f2fd49bae78c653fad9bd3d53463d4abd7f996964988a7644c4cc1d0321830020030034104473bd04e2a9c4e6a12b9008739c64a16d0113295822faf17e3d2ffcb77b0cae437701b0f0525ddcc6139da5a56dfda2af2b86a2836ef6b03f8f5c231dbaf950b3004410441838848a7e58ab46a1a71a539c474780002bf22adccbeaa43ee07f5176c61aaa3d718102333fc856595ea3a6a5bfd37d2890049acb82c49440e1f490cd970e018");
const signature = ByteArray.fromHex("75e35c22a5da60805d65772b3d4decc8c6eabe30bd2925608524ea12b729efd00a12faeb5757cdfc65aaefddd01c57be9f14d37e2c0beca43434f8ebdd81d635");

cryptoNode.verifySpki(fabricPubKey, signatureData, signature);
cryptoNode.verify(PublicKey(fabricPubKey), signatureData, signature);
});

it("generates the right bytes for sigma 3", async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/matter.js/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const config: Config = {
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.[jt]s$': '$1',
},
maxWorkers: "50%", // to make sure jest is not using all available resources
maxWorkers: "25%", // to make sure jest is not using all available resources
};

export default config;
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Crypto } from "../crypto/Crypto.js";
import { Time } from "../time/Time.js";
import { ByteArray } from "../util/ByteArray.js";
import { VendorId } from "../datatype/VendorId.js";
import { PrivateKey } from "../crypto/Key.js";

function getPaiCommonName(vendorId: VendorId, productId?: number) {
return `node-matter Dev PAI 0x${vendorId.id.toString(16).toUpperCase()} ${productId === undefined ? 'no PID' : `0x${productId.toString(16).toUpperCase()}`}`;
Expand All @@ -29,10 +30,10 @@ export class AttestationCertificateManager {

// We use the official PAA cert for now because else pairing with Chip tool do not work because
// only this one is the Certificate store
private readonly paaKeyPair = {
privateKey: TestCert_PAA_NoVID_PrivateKey,
publicKey: TestCert_PAA_NoVID_PublicKey
};
private readonly paaKeyPair = PrivateKey(
TestCert_PAA_NoVID_PrivateKey,
{ publicKey: TestCert_PAA_NoVID_PublicKey }
);
private readonly paaKeyIdentifier = TestCert_PAA_NoVID_SKID;
private readonly paiCertId = BigInt(1);
private readonly paiKeyPair = Crypto.createKeyPair();
Expand Down

0 comments on commit 6c4e685

Please sign in to comment.