Skip to content

Commit

Permalink
Merge pull request #4 from fireblocks/code_review_fixes
Browse files Browse the repository at this point in the history
Code review fixes
  • Loading branch information
ekjnk authored Apr 20, 2024
2 parents 496266a + 122287f commit 637b2d9
Show file tree
Hide file tree
Showing 4 changed files with 6,636 additions and 58 deletions.
5 changes: 5 additions & 0 deletions examples/server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Customer server example
This server implement an example integration with HSM module assuming SoftHSM.
- Hardcoded PIN used in the example is "1234"
- The example uses first available slot which is usually slot 0
- Current example supports only ECDSA secp256k1 curve.
171 changes: 113 additions & 58 deletions examples/server/src/services/hsm-facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ export interface HSMFacade {
const LIBRARY = '/usr/local/lib/softhsm/libsofthsm2.so';
const PIN = '1234';

// Define an ASN.1 structure using asn1.js that can handle both bit string and octet string
const GenericASN1Data = asn1.define('GenericASN1Data', function () {
this.choice({
bitString: this.bitstr(),
octetString: this.octstr()
});
});


class HSM implements HSMFacade {

private pkcs11: pkcs11js.PKCS11;
Expand All @@ -50,27 +59,48 @@ class HSM implements HSMFacade {

}

private destructor() {
// can be called to cleanup pkcs11 library resources
public dispose() {

this.pkcs11.C_Logout(this.session);
this.pkcs11.C_CloseSession(this.session);
this.session = null;
this.pkcs11.C_Finalize();
}


private _fixEcpt(ecpt: Buffer): Buffer {
if ((ecpt.length & 1) === 0 &&
(ecpt[0] === 0x04) && (ecpt[ecpt.length - 1] === 0x04)) {
ecpt = ecpt.slice(0, ecpt.length - 1);
} else if (ecpt[0] === 0x04 && ecpt[2] === 0x04) {
ecpt = ecpt.slice(2);
// the public key value which is received from HSM is DER encoded (octet string)
// this is a dirty fix to decoder der
// private fixEcpt(ecpt: Buffer): Buffer {
// if ((ecpt.length & 1) === 0 &&
// (ecpt[0] === 0x04) && (ecpt[ecpt.length - 1] === 0x04)) {
// ecpt = ecpt.slice(0, ecpt.length - 1);
// } else if (ecpt[0] === 0x04 && ecpt[2] === 0x04) {
// ecpt = ecpt.slice(2);
// }
// return ecpt;
// }

// And this is a more elegant way, but requires use of the ASN1 external library
private decodeASN1Data(buffer: Buffer): Buffer {

// Attempt to decode the buffer using the defined ASN.1 structure
try {
const decoded = GenericASN1Data.decode(buffer, 'der');
// Return the data based on the type that was successfully decoded
if (decoded.type === 'bitString') {
return decoded.value.data; // For bitString, return the data portion
} else if (decoded.type === 'octetString') {
return decoded.value; // For octetString, return the Buffer directly
}
} catch (error) {
console.error('Failed to decode ASN.1 data:', error);
throw error;
}
return ecpt;
}


private _pemEncode(publicPoint: Buffer) {
// this function DER encodes public key and than encodes it again as PEM
// This works only for ECDSA, but a simple change can be made to support other protocols
private pemEncode(publicPoint: Buffer, _algorithm: string) {
// Define an ASN.1 structure for the public key
const ECPrivateKeyASN = asn1.define('ECPublicKey', function () {
this.seq().obj(
Expand Down Expand Up @@ -104,114 +134,138 @@ class HSM implements HSMFacade {
return pemFormattedKey;
}

// generates ECDSA secp256k1 keypair. Should be adjusted to generate other key pairs
async generateKeyPair(algorithm: string): Promise<{ keyId: string; pem: string }> {
//const secp256r1 = Buffer.from("06082A8648CE3D030107", "hex") //der encoded OID 1.2.840.10045.3.1.7
const secp256k1 = Buffer.from("06052b8104000A", "hex"); //der encoded OID 1.3.132.0.10

try {
const publicKeyTemplate =
[
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_PUBLIC_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_EC },
{ type: pkcs11js.CKA_TOKEN, value: true },
{ type: pkcs11js.CKA_PRIVATE, value: false },
{ type: pkcs11js.CKA_VERIFY, value: true },
{ type: pkcs11js.CKA_EC_PARAMS, value: secp256k1 },
];
const privateKeyTemplate =
[
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_PRIVATE_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_EC },
{ type: pkcs11js.CKA_PRIVATE, value: true },
{ type: pkcs11js.CKA_TOKEN, value: true },
{ type: pkcs11js.CKA_SIGN, value: true },
{ type: pkcs11js.CKA_DERIVE, value: true },
];

var keys = this.pkcs11.C_GenerateKeyPair(this.session,
const publicKeyTemplate = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_PUBLIC_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_EC },
{ type: pkcs11js.CKA_TOKEN, value: true }, //controls if the key is session scope or global
{ type: pkcs11js.CKA_PRIVATE, value: false },
{ type: pkcs11js.CKA_VERIFY, value: true },
{ type: pkcs11js.CKA_EC_PARAMS, value: secp256k1 },
];

const privateKeyTemplate = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_PRIVATE_KEY },
{ type: pkcs11js.CKA_KEY_TYPE, value: pkcs11js.CKK_EC },
{ type: pkcs11js.CKA_PRIVATE, value: true },
{ type: pkcs11js.CKA_TOKEN, value: true },//controls if the key is session scope or global
{ type: pkcs11js.CKA_SIGN, value: true },
{ type: pkcs11js.CKA_DERIVE, value: true },
];

//create a new key pair object
const keys = this.pkcs11.C_GenerateKeyPair(
this.session,
{ mechanism: pkcs11js.CKM_EC_KEY_PAIR_GEN },
publicKeyTemplate,
privateKeyTemplate);
privateKeyTemplate
);

//retrieve public key
const attrs = this.pkcs11.C_GetAttributeValue(this.session, keys.publicKey, [{ type: pkcs11js.CKA_EC_POINT }])

if (!(attrs[0].value instanceof Buffer)) {
throw new Error("ec is not buffer");
}

//convert der encoded public key into hex buffer containing EC (X, Y) coordinates
const ec = attrs[0].value;
const ecpt = this._fixEcpt(ec);
logger.debug('ec=' + ec.toString('hex') + ', ecpt=' + ecpt.toString('hex'));
const ecpt = this.decodeASN1Data(ec);

logger.info(`ec=${ec.toString('hex')}, ecpt=${ecpt.toString('hex')}}`);

//generate key id based on public key
const keyId: string = hashSha256(ecpt);
const ski = Buffer.from(keyId, 'hex');
logger.debug('ski=' + ski.toString('hex'));

this.pkcs11.C_SetAttributeValue(this.session, keys.publicKey,
[{ type: pkcs11js.CKA_ID, value: ski },
{ type: pkcs11js.CKA_LABEL, value: ski.toString('hex') }]);
this.pkcs11.C_SetAttributeValue(this.session, keys.privateKey,
[{ type: pkcs11js.CKA_ID, value: ski },
{ type: pkcs11js.CKA_LABEL, value: ski.toString('hex') }]);
//define key id and label based on the public key
this.pkcs11.C_SetAttributeValue(
this.session,
keys.publicKey,
[
{ type: pkcs11js.CKA_ID, value: ski }, //this is bytes buffer
{ type: pkcs11js.CKA_LABEL, value: ski.toString('hex') } //and this is a hex string
]
);

// same id and label for private key as for public key
this.pkcs11.C_SetAttributeValue(
this.session,
keys.privateKey,
[
{ type: pkcs11js.CKA_ID, value: ski },
{ type: pkcs11js.CKA_LABEL, value: ski.toString('hex') }
]
);

logger.debug('pub CKA_ID: ' + JSON.stringify(
(this.pkcs11.C_GetAttributeValue(this.session, keys.publicKey,
[{ type: pkcs11js.CKA_ID }]))[0].value));
(this.pkcs11.C_GetAttributeValue(
this.session,
keys.publicKey,
[{ type: pkcs11js.CKA_ID }]))[0].value)
);

logger.debug('pub CKA_LABEL: ' + JSON.stringify(
(this.pkcs11.C_GetAttributeValue(this.session, keys.publicKey,
[{ type: pkcs11js.CKA_LABEL }]))[0].value));
(this.pkcs11.C_GetAttributeValue(
this.session,
keys.publicKey,
[{ type: pkcs11js.CKA_LABEL }]))[0].value)
);


//const pem = await this.exportPublicKeyAsPem(keyId);
const pem = this._pemEncode(ecpt);
//PEM encode pure public key. The result contains key and the curve type.
const pem = this.pemEncode(ecpt, algorithm);

logger.info('Generated Key ' + keyId + ', pub key: ' + ec.toString('hex') + ', Pem encoded: ' + pem);

return { keyId, pem }
}
catch (error) {
//consider how to handle error in the client
console.error("An error occurred:", error);

return { keyId: "", pem: "" };
}
}


private _getPrivateKey(keyId: string) {
private getPrivateKeyObject(keyId: string) {
// Find the private key by ID
const template = [
{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_PRIVATE_KEY },
{ type: pkcs11js.CKA_ID, value: Buffer.from(keyId, 'hex') }
];

//please check that in your library this does not free hObject underlying structure.
//consider moving C_FindObjectsFinal after hObject is not used anymore.
this.pkcs11.C_FindObjectsInit(this.session, template);
let hObject = this.pkcs11.C_FindObjects(this.session);
this.pkcs11.C_FindObjectsFinal(this.session);

if (hObject == null) {
logger.error(`Key not found : ${keyId}`);
return null;
throw new Error(`Key not found : ${keyId}`)
}

logger.info(`Private key found ${keyId}`);
return hObject;

}

//implemented only for ECDSA
async sign(keyId: string, payload: string, algorithm: string): Promise<string> {
try {

logger.info(`Request to sign with key ${keyId} payload: ${payload} algo: ${algorithm}`);

// Find the private key by ID
let provKeyObj = this._getPrivateKey(keyId);

if (null != provKeyObj) {
logger.info(`Private key found ${keyId}`);
}

let provKeyObj = this.getPrivateKeyObject(keyId);

// Sign the payload
// Sign the payload. Algorithm is provided here and curve is defined on the private key attributes
this.pkcs11.C_SignInit(this.session, { mechanism: pkcs11js.CKM_ECDSA }, provKeyObj);
//EC signatures are represented as a 32-bit R followed by a 32-bit S value, and not ASN.1 encoded.
const signature: Buffer = this.pkcs11.C_Sign(this.session, Buffer.from(payload, 'hex'), Buffer.alloc(64));
Expand All @@ -229,9 +283,10 @@ class HSM implements HSMFacade {

}

//implemented only for ECDSA
async verify(keyId: string, signature: string, payload: string, algorithm: string): Promise<boolean> {
try {
let provKeyObj = this._getPrivateKey(keyId);
let provKeyObj = this.getPrivateKeyObject(keyId);

this.pkcs11.C_VerifyInit(this.session, { mechanism: pkcs11js.CKM_ECDSA }, provKeyObj);
const ok = this.pkcs11.C_Verify(this.session, Buffer.from(payload, 'hex'), Buffer.from(signature, 'hex'));
Expand Down
Loading

0 comments on commit 637b2d9

Please sign in to comment.