Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"typescript": "5.6.3",
"vitest": "3.2.4"
},
"optionalDependencies": {
"@xmldom/xmldom": "0.8.11"
},
"dependencies": {
"@keetanetwork/currency-info": "1.2.5",
"@keetanetwork/keetanet-client": "0.14.8",
Expand Down
41 changes: 41 additions & 0 deletions src/lib/certificates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,44 @@ test('Rust Certificate Interoperability', async function() {
const contactProof = await contactAttr.value.getProof();
expect(await contactAttr.value.validateProof(contactProof)).toBe(true);
});

test('Cetificate ISO20022 Serialization', async function() {
/*
* Create account for subject
*/
const subjectAccount = KeetaNetClient.lib.Account.fromSeed(
testSeed,
0,
KeetaNetClient.lib.Account.AccountKeyAlgorithm.ECDSA_SECP256R1
);

/* Create a Certificate Builder */
const builder1 = new Certificates.Certificate.Builder({
issuer: subjectAccount,
subject: subjectAccount,
serial: 5,
validFrom: new Date(),
validTo: new Date(Date.now() + 1000 * 60 * 60 * 24)
});

/*
* Create a User Certificate
*/
builder1.setAttribute('fullName', true, 'Test User');
builder1.setAttribute('email', true, 'user@example.com');
builder1.setAttribute('dateOfBirth', true, new Date('1990-05-15'));
builder1.setAttribute('dateAndPlaceOfBirth', true, {
birthDate: new Date('1990-05-15'),
cityOfBirth: 'Springfield',
provinceOfBirth: 'IL',
countryOfBirth: 'US'
});
builder1.setAttribute('contactDetails', true, {
phoneNumber: '+1-555-000-1111'
});
const certificate = new Certificates.Certificate(await builder1.buildDER(), {
subjectKey: subjectAccount
});

console.log(await certificate.toISO20022());
});
161 changes: 161 additions & 0 deletions src/lib/certificates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,167 @@ export class Certificate extends KeetaNetClient.lib.Utils.Certificate.Certificat

return(false);
}

private static _XMLDOM: Promise<typeof import('@xmldom/xmldom')> | undefined;
private static async getXMLDOM(): Promise<typeof import('@xmldom/xmldom')> {
if (!Certificate._XMLDOM) {
Certificate._XMLDOM = import('@xmldom/xmldom');
}
return(await Certificate._XMLDOM);
}

/**
* Emit an XML fragment representing the certificate's attributes
*/
private static async iso200022PartyIdentification135(certificate: Certificate): Promise<string> {
const XMLDOM = await Certificate.getXMLDOM();
const get = certificate.getAttributeValue.bind(certificate);
const has = function(name: CertificateAttributeNames): name is CertificateAttributeNames {
return(name in certificate.attributes);
}

const doc = new XMLDOM.DOMImplementation().createDocument(null, 'Fragment', null);
const root = doc.documentElement;
if (has('fullName')) {
const fullName = await get('fullName');
const nameElement = doc.createElement('Nm');
nameElement.appendChild(doc.createTextNode(fullName));
root.appendChild(nameElement);
}

if (has('address')) {
const address = await get('address');
const addressElement = doc.createElement('PstlAdr');

if (address.country) {
const countryElement = doc.createElement('Ctry');
countryElement.appendChild(doc.createTextNode(address.country));
addressElement.appendChild(countryElement);
}
if (address.townName) {
const townElement = doc.createElement('TwnNm');
townElement.appendChild(doc.createTextNode(address.townName));
addressElement.appendChild(townElement);
}

root.appendChild(addressElement);
}

if (has('dateOfBirth') || has('dateAndPlaceOfBirth') || has('id')) {
const idElement = doc.createElement('Id');
const privateIdElement = doc.createElement('PrvtId');

if (has('dateOfBirth') || has('dateAndPlaceOfBirth')) {
const dob = has('dateOfBirth') ? await get('dateOfBirth') : undefined;
const pob = has('dateAndPlaceOfBirth') ? await get('dateAndPlaceOfBirth') : undefined;

const dobElement = doc.createElement('DtAndPlcOfBirth');
if (pob?.birthDate) {
const birthDateElement = doc.createElement('BirthDt');
birthDateElement.appendChild(doc.createTextNode(pob.birthDate.toISOString().split('T').at(0)!));
dobElement.appendChild(birthDateElement);
} else if (dob) {
const birthDateElement = doc.createElement('BirthDt');
birthDateElement.appendChild(doc.createTextNode(dob.toISOString().split('T').at(0)!));
dobElement.appendChild(birthDateElement);
}
if (pob?.provinceOfBirth) {
const provinceElement = doc.createElement('PrvcOfBirth');
provinceElement.appendChild(doc.createTextNode(pob.provinceOfBirth));
dobElement.appendChild(provinceElement);
}
if (pob?.cityOfBirth) {
const cityElement = doc.createElement('CityOfBirth');
cityElement.appendChild(doc.createTextNode(pob.cityOfBirth));
dobElement.appendChild(cityElement);
}
if (pob?.countryOfBirth) {
const countryElement = doc.createElement('CtryOfBirth');
countryElement.appendChild(doc.createTextNode(pob.countryOfBirth));
dobElement.appendChild(countryElement);
}

privateIdElement.appendChild(dobElement);
}

if (has('id')) {
/* XXX:TODO */
}

idElement.appendChild(privateIdElement);
root.appendChild(idElement);
}

if (has('countryOfResidence') || has('address')) {
let cor: string | undefined;
if (has('address')) {
const address = await get('address');
cor = address.country;
} else if (has('countryOfResidence')) {
cor = await get('countryOfResidence');
}

if (cor) {
const corElement = doc.createElement('CtryOfRes');
corElement.appendChild(doc.createTextNode(cor));
root.appendChild(corElement);
}
}

if (has('contactDetails') || has('phoneNumber') || has('email')) {
let contactDetails: Awaited<ReturnType<typeof get<'contactDetails'>>> | undefined;
if (has('contactDetails')) {
contactDetails = await get('contactDetails');
}
const contactElement = doc.createElement('CtctDtls');

if (contactDetails?.fullName) {
const nameElement = doc.createElement('Nm');
nameElement.appendChild(doc.createTextNode(contactDetails.fullName));
contactElement.appendChild(nameElement);
}

if (contactDetails?.phoneNumber) {
const phoneElement = doc.createElement('PhneNb');
phoneElement.appendChild(doc.createTextNode(contactDetails.phoneNumber));
contactElement.appendChild(phoneElement);
} else if (has('phoneNumber')) {
const phoneNumber = await get('phoneNumber');
const phoneElement = doc.createElement('PhneNb');
phoneElement.appendChild(doc.createTextNode(phoneNumber));
contactElement.appendChild(phoneElement);
}

if (contactDetails?.mobileNumber) {
const mobileElement = doc.createElement('MobNb');
mobileElement.appendChild(doc.createTextNode(contactDetails.mobileNumber));
contactElement.appendChild(mobileElement);
}
if (contactDetails?.emailAddress) {
const emailElement = doc.createElement('EmailAdr');
emailElement.appendChild(doc.createTextNode(contactDetails.emailAddress));
contactElement.appendChild(emailElement);
} else if (has('email')) {
const email = await get('email');
const emailElement = doc.createElement('EmailAdr');
emailElement.appendChild(doc.createTextNode(email));
contactElement.appendChild(emailElement);
}

root.appendChild(contactElement);
}

const output = new XMLDOM.XMLSerializer();
return('<?xml version="1.0"?>\n' + output.serializeToString(root));
}
/**
* Emit an ISO20022 Party Report message (XXX: which one ?)
* XXX: TODO
*/
async toISO20022(): Promise<string> {
return(Certificate.iso200022PartyIdentification135(this));
}

}

/** @internal */
Expand Down
7 changes: 7 additions & 0 deletions src/services/kyc/utils/oids.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@
"description": "Date of birth",
"reference": "https://oid-base.com/get/1.3.6.1.4.1.62675.1.1"
},
"countryOfResidence": {
"oid": [ 1, 3, 6, 1, 4, 1, 62675, 1, 2, 8 ],
"token": "CountryOfResidence",
"type": "UTF8String",
"description": "Country of residence (ISO 3166-1 alpha-2 or alpha-3)",
"reference": "https://oid-base.com/get/1.3.6.1.4.1.62675.1.2.8"
},
"address": {
"oid": [ 1, 3, 6, 1, 4, 1, 62675, 1, 2 ],
"token": "Address",
Expand Down