Skip to content

Commit a97480c

Browse files
bryancalistot1000
andauthored
Banco Central .p12 support (#47)
* Extract the banco central private key with its own method * updates --------- Co-authored-by: t1000 <t1000@t1000s-MacBook-Pro.local>
1 parent ee9cfc6 commit a97480c

File tree

5 files changed

+103
-12
lines changed

5 files changed

+103
-12
lines changed

ec-sri-invoice-signer/src/utils/cryptography.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@ const getHash = (data: string) => {
99
return forge.util.encode64(forge.sha1.create().update(data, 'utf8').digest().bytes());
1010
}
1111

12+
const getBancoCentralPkcs12PrivateKey = (pkcs8ShroudedKeyBags: forge.pkcs12.Bag[]) => {
13+
const privateKeyBag = pkcs8ShroudedKeyBags.find((bag) => {
14+
const name = bag?.attributes?.friendlyName?.[0];
15+
return /signing key/i.test(name);
16+
});
17+
18+
if (!privateKeyBag) {
19+
throw new UnsuportedPkcs12Error("No key bag with friendly name 'Signing Key' found in the key bags of 'Banco Central del Ecuador' .p12");
20+
}
21+
22+
const privateKey = privateKeyBag.key as forge.pki.rsa.PrivateKey;
23+
24+
if (!privateKey) {
25+
throw new UnsuportedPkcs12Error("No valid key found in 'Banco Central del Ecuador' .p12");
26+
}
27+
28+
return privateKey;
29+
}
30+
1231
const extractPrivateKeyAndCertificateFromPkcs12 = (pkcs12RawData: string | Buffer, password: string = '') => {
1332
const pkcs12InBase64 = typeof pkcs12RawData === 'string' ? pkcs12RawData : pkcs12RawData.toString('base64');
1433
const pkcs12InDer = forge.util.decode64(pkcs12InBase64);
@@ -17,17 +36,31 @@ const extractPrivateKeyAndCertificateFromPkcs12 = (pkcs12RawData: string | Buffe
1736
const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
1837
const certBag = certBags[forge.pki.oids.certBag]?.[0];
1938
const pkcs8ShroudedKeyBags = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag });
20-
const pkcs8ShroudedKeyBag = pkcs8ShroudedKeyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];
2139

22-
if (!certBag || !pkcs8ShroudedKeyBag) {
40+
if (!certBag || !pkcs8ShroudedKeyBags) {
2341
throw new UnsuportedPkcs12Error();
2442
}
2543

26-
const privateKey = pkcs8ShroudedKeyBag.key as forge.pki.rsa.PrivateKey;
44+
const friendlyName = certBag?.attributes?.friendlyName?.[0];
45+
2746
const certificate = certBag.cert;
2847

29-
if (!privateKey || !certificate) {
30-
throw new UnsuportedPkcs12Error();
48+
if (!certificate) {
49+
throw new UnsuportedPkcs12Error("Couldn't find its certificate");
50+
}
51+
52+
let privateKey: forge.pki.rsa.PrivateKey | null = null;
53+
54+
if (/banco central/i.test(friendlyName)) {
55+
privateKey = getBancoCentralPkcs12PrivateKey(pkcs8ShroudedKeyBags[forge.pki.oids.pkcs8ShroudedKeyBag] ?? []);
56+
}
57+
else {
58+
const firstPkcs8ShroudedKeyBag = pkcs8ShroudedKeyBags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];
59+
privateKey = firstPkcs8ShroudedKeyBag?.key ? firstPkcs8ShroudedKeyBag.key as forge.pki.rsa.PrivateKey : null;
60+
}
61+
62+
if (!privateKey) {
63+
throw new UnsuportedPkcs12Error("Couldn't find its private key");
3164
}
3265

3366
return {

ec-sri-invoice-signer/src/utils/errors.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ class XmlFormatError extends Error {
1010
class UnsuportedPkcs12Error extends Error {
1111
name: string = 'UnsuportedPkcs12Error';
1212

13-
constructor() {
14-
const message = "The used pkcs12 file is not supported";
13+
constructor(extraMessage?: string) {
14+
let message = "The used .p12 file is not supported";
15+
16+
if (extraMessage) {
17+
message += `: ${extraMessage}`;
18+
}
19+
1520
super(message);
1621
}
1722
}

ec-sri-invoice-signer/test/test-utils/cryptography.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,36 @@ export const verifySignature = (preSignData: string, publicKey: forge.pki.rsa.Pu
44
const digest = forge.md.sha1.create().update(preSignData, 'utf8');
55
return publicKey.verify(digest.digest().bytes(), forge.util.decode64(signature));
66
}
7+
8+
/**
9+
* NOT WORKING as cannot set the 'Signing Key' friendly name in the pkcs12's cert bag
10+
*/
11+
export const createBancoCentralCertificateKeyAndP12 = () => {
12+
const keys = forge.pki.rsa.generateKeyPair(4096);
13+
const cert = forge.pki.createCertificate();
14+
cert.publicKey = keys.publicKey;
15+
cert.serialNumber = '01';
16+
cert.validity.notBefore = new Date();
17+
cert.validity.notAfter = new Date();
18+
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
19+
const attrs = [{ name: 'friendlyName', value: 'Signing Key' }];
20+
cert.setSubject(attrs);
21+
cert.setIssuer(attrs);
22+
23+
cert.sign(keys.privateKey, forge.md.sha256.create());
24+
25+
// Create a PKCS12 structure
26+
let p12Asn1 = forge.pkcs12.toPkcs12Asn1(
27+
keys.privateKey,
28+
[cert],
29+
'', // No password
30+
{ friendlyName: 'Banco Central Del Ecuador' }
31+
);
32+
33+
const certPem = forge.pki.certificateToPem(cert);
34+
const keyPem = forge.pki.privateKeyToPem(keys.privateKey);
35+
const p12Buffer = Buffer.from(forge.asn1.toDer(p12Asn1).getBytes(), 'binary');
36+
37+
return { certPem, keyPem, p12Buffer };
38+
}
39+

ec-sri-invoice-signer/test/utils/cryptography.test.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { expect } from 'chai';
22
import { extractIssuerData, extractPrivateKeyAndCertificateFromPkcs12, getHash, sign } from '../../src/utils/cryptography';
3-
import { verifySignature } from '../test-utils/cryptography';
3+
import { createBancoCentralCertificateKeyAndP12, verifySignature } from '../test-utils/cryptography';
44
import fs from 'fs';
55
import path from 'path';
66
import * as forge from 'node-forge';
7-
const signatureP12 = fs.readFileSync(path.resolve('test/test-data/pkcs12/signature.p12'));
87

98
describe('Given the sign function', () => {
109
it('should return the signature for the input data', () => {
1110
const data = 'something';
12-
const { privateKey, certificate } = extractPrivateKeyAndCertificateFromPkcs12(signatureP12);
11+
const p12 = fs.readFileSync(path.resolve('test/test-data/pkcs12/signature.p12'));
12+
const { privateKey, certificate } = extractPrivateKeyAndCertificateFromPkcs12(p12);
1313

1414
const resultSignature = sign(data, privateKey);
1515
const verifiedSuccessfully = verifySignature(data, certificate.publicKey as forge.pki.rsa.PublicKey, resultSignature);
@@ -29,15 +29,31 @@ describe('Given the extractPrivateKeyAndCertificateFromPkcs12 function', () => {
2929
it('should return an object with the private key and certificate contained in the pkcs12 file', () => {
3030
const privateKeyPem = fs.readFileSync(path.resolve('test/test-data/pkcs12/privateKey.pem')).toString('utf-8');
3131
const certificatePem = fs.readFileSync(path.resolve('test/test-data/pkcs12/certificate.pem')).toString('utf-8');
32+
const p12 = fs.readFileSync(path.resolve('test/test-data/pkcs12/signature.p12'));
3233
const password = '';
3334

34-
const result = extractPrivateKeyAndCertificateFromPkcs12(signatureP12, password);
35+
const result = extractPrivateKeyAndCertificateFromPkcs12(p12, password);
3536

3637
// Here we convert from fromPem and toPem to overcome format inconsistencies due to new line encoding and pkcs8 shrouding of private key.
3738
// This way the comparison is delegated to node-forge functions only becoming abstracted and consistent.
3839
expect(forge.pki.privateKeyToPem(result.privateKey)).to.equal(forge.pki.privateKeyToPem(forge.pki.privateKeyFromPem(privateKeyPem)));
3940
expect(forge.pki.certificateToPem(result.certificate)).to.equal(forge.pki.certificateToPem(forge.pki.certificateFromPem(certificatePem)));
4041
});
42+
43+
/**
44+
* Unskip when createBancoCentralCertificateKeyAndP12 is fixed
45+
*/
46+
it.skip("should return the correct private key for a 'Banco Central del Ecuador' .p12", () => {
47+
const { keyPem, certPem, p12Buffer } = createBancoCentralCertificateKeyAndP12();
48+
const password = '';
49+
50+
const result = extractPrivateKeyAndCertificateFromPkcs12(p12Buffer, password);
51+
52+
// Here we convert from fromPem and toPem to overcome format inconsistencies due to new line encoding and pkcs8 shrouding of private key.
53+
// This way the comparison is delegated to node-forge functions only becoming abstracted and consistent.
54+
expect(forge.pki.privateKeyToPem(result.privateKey)).to.equal(forge.pki.privateKeyToPem(forge.pki.privateKeyFromPem(keyPem)));
55+
expect(forge.pki.certificateToPem(result.certificate)).to.equal(forge.pki.certificateToPem(forge.pki.certificateFromPem(certPem)));
56+
});
4157
});
4258

4359
describe('Give the extractIssuerData function', () => {

ec-sri-invoice-signer/test/utils/errors.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ describe('XmlFormatError', () => {
99

1010
describe('UnsuportedPkcs12Error', () => {
1111
it('Show expected message when thrown', () => {
12-
expect(() => { throw new UnsuportedPkcs12Error(); }).throws("The used pkcs12 file is not supported");
12+
expect(() => { throw new UnsuportedPkcs12Error(); }).throws("The used .p12 file is not supported");
13+
});
14+
15+
it('Show expected message with additional details if available when thrown', () => {
16+
expect(() => { throw new UnsuportedPkcs12Error("Extra details"); }).throws("The used .p12 file is not supported: Extra details");
1317
});
1418
});

0 commit comments

Comments
 (0)