Digital Signature
Overview
Vietnamese e-invoices require digital signatures using PKCS#7 format with certificates issued by approved Certificate Authorities (CAs). The signature ensures invoice authenticity and non-repudiation.
Signature Requirements
| Requirement | Description |
|---|---|
| Algorithm | RSA with SHA-256 |
| Format | PKCS#7 (CMS SignedData) |
| Certificate | CA-issued, valid for e-invoice |
| Storage | USB Token or HSM |
| Key Length | Minimum 2048 bits |
Signing Process
Signature Flow
DigitalSignatureService
typescript
interface DigitalSignatureService {
/**
* Sign XML document with merchant's certificate
*/
signXml(xml: string, certificateId: string): Promise<string>;
/**
* Verify signature on signed XML
*/
verifySignature(signedXml: string): Promise<VerificationResult>;
/**
* Get available certificates for signing
*/
getCertificates(): Promise<Certificate[]>;
/**
* Validate certificate (expiry, revocation)
*/
validateCertificate(certId: string): Promise<CertificateStatus>;
/**
* Get certificate chain
*/
getCertificateChain(certId: string): Promise<Certificate[]>;
}Implementation
typescript
import * as forge from 'node-forge';
import { PKCS11 } from 'pkcs11js';
@injectable()
class DigitalSignatureServiceImpl implements DigitalSignatureService {
private pkcs11: PKCS11;
constructor(private config: SignatureConfig) {
this.pkcs11 = new PKCS11();
this.pkcs11.load(config.pkcs11Library);
this.pkcs11.C_Initialize();
}
async signXml(xml: string, certificateId: string): Promise<string> {
// 1. Canonicalize XML
const canonicalXml = this.canonicalize(xml);
// 2. Calculate hash
const hash = this.calculateHash(canonicalXml);
// 3. Sign with USB Token
const signature = await this.signWithToken(hash, certificateId);
// 4. Build PKCS#7 structure
const pkcs7 = await this.buildPkcs7(signature, certificateId);
// 5. Embed in XML
return this.embedSignature(xml, pkcs7);
}
private canonicalize(xml: string): string {
// XML Canonicalization (C14N)
const doc = new DOMParser().parseFromString(xml, 'text/xml');
return new XMLSerializer().serializeToString(doc);
}
private calculateHash(data: string): Buffer {
const md = forge.md.sha256.create();
md.update(data, 'utf8');
return Buffer.from(md.digest().bytes(), 'binary');
}
private async signWithToken(
hash: Buffer,
certificateId: string,
): Promise<Buffer> {
// Open session with USB Token
const slots = this.pkcs11.C_GetSlotList(true);
const session = this.pkcs11.C_OpenSession(
slots[0],
PKCS11.CKF_RW_SESSION | PKCS11.CKF_SERIAL_SESSION,
);
// Login with PIN
this.pkcs11.C_Login(session, PKCS11.CKU_USER, this.config.tokenPin);
try {
// Find private key
const privateKey = this.findPrivateKey(session, certificateId);
// Initialize signature operation
this.pkcs11.C_SignInit(session, {
mechanism: PKCS11.CKM_SHA256_RSA_PKCS,
}, privateKey);
// Sign
const signature = this.pkcs11.C_Sign(session, hash);
return Buffer.from(signature);
} finally {
this.pkcs11.C_Logout(session);
this.pkcs11.C_CloseSession(session);
}
}
private async buildPkcs7(
signature: Buffer,
certificateId: string,
): Promise<string> {
const cert = await this.getCertificate(certificateId);
const chain = await this.getCertificateChain(certificateId);
const p7 = forge.pkcs7.createSignedData();
// Add certificates
p7.addCertificate(forge.pki.certificateFromPem(cert.pem));
for (const chainCert of chain) {
p7.addCertificate(forge.pki.certificateFromPem(chainCert.pem));
}
// Add signer info
p7.addSigner({
key: null, // Key is on token
certificate: forge.pki.certificateFromPem(cert.pem),
digestAlgorithm: forge.pki.oids.sha256,
authenticatedAttributes: [
{
type: forge.pki.oids.contentType,
value: forge.pki.oids.data,
},
{
type: forge.pki.oids.messageDigest,
},
{
type: forge.pki.oids.signingTime,
value: new Date(),
},
],
});
// Set signature
p7.signerInfos[0].signature = forge.util.createBuffer(signature);
return forge.util.encode64(forge.asn1.toDer(p7.toAsn1()).bytes());
}
private embedSignature(xml: string, signature: string): string {
// Create XML Signature element
const signatureXml = `
<DSCKS>
<NBan>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>${this.calculateDigestValue(xml)}</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>${signature}</SignatureValue>
</Signature>
</NBan>
</DSCKS>
`;
// Insert before closing </HDon> tag
return xml.replace('</HDon>', `${signatureXml}</HDon>`);
}
async verifySignature(signedXml: string): Promise<VerificationResult> {
try {
// Extract signature from XML
const signature = this.extractSignature(signedXml);
// Extract certificate
const cert = this.extractCertificate(signature);
// Verify certificate chain
const chainValid = await this.verifyCertificateChain(cert);
if (!chainValid) {
return { valid: false, error: 'Invalid certificate chain' };
}
// Check certificate revocation
const revoked = await this.checkRevocation(cert);
if (revoked) {
return { valid: false, error: 'Certificate revoked' };
}
// Verify signature value
const signatureValid = this.verifySignatureValue(signedXml, signature);
if (!signatureValid) {
return { valid: false, error: 'Invalid signature' };
}
return {
valid: true,
signer: {
subject: cert.subject,
issuer: cert.issuer,
serialNumber: cert.serialNumber,
validFrom: cert.validFrom,
validTo: cert.validTo,
},
};
} catch (error) {
return { valid: false, error: error.message };
}
}
}USB Token Integration
PKCS#11 Configuration
typescript
interface PKCS11Config {
library: string; // Path to PKCS#11 library (.so/.dll)
slot: number; // Token slot number
pin: string; // Token PIN (should be encrypted)
label?: string; // Optional token label
}
// Common USB Token libraries
const TOKEN_LIBRARIES = {
// SafeNet eToken
safenet: '/usr/lib/libeToken.so',
// Gemalto/Thales
thales: '/usr/lib/libIDPrimePKCS11.so',
// VNPT-CA Token
vnpt: '/usr/local/lib/libvnpt-pkcs11.so',
// Viettel-CA Token
viettel: '/opt/viettel-ca/lib/libvtca-pkcs11.so',
// FPT-CA Token
fpt: '/opt/fpt-ca/lib/libfptca-pkcs11.so',
};Token Operations
typescript
class PKCS11TokenService {
/**
* Initialize token connection
*/
async initialize(): Promise<void>;
/**
* List available tokens
*/
async listTokens(): Promise<TokenInfo[]>;
/**
* Login to token with PIN
*/
async login(slot: number, pin: string): Promise<SessionHandle>;
/**
* Logout from token
*/
async logout(session: SessionHandle): Promise<void>;
/**
* List certificates on token
*/
async listCertificates(session: SessionHandle): Promise<Certificate[]>;
/**
* Sign data with private key
*/
async sign(
session: SessionHandle,
keyId: string,
data: Buffer,
): Promise<Buffer>;
/**
* Verify signature with certificate
*/
async verify(
certificate: Certificate,
data: Buffer,
signature: Buffer,
): Promise<boolean>;
}Certificate Management
Certificate Entity
typescript
interface Certificate {
id: string;
serialNumber: string;
subject: {
commonName: string;
organization: string;
country: string;
};
issuer: {
commonName: string;
organization: string;
};
validFrom: Date;
validTo: Date;
keyUsage: string[];
status: 'active' | 'expired' | 'revoked';
pem: string;
}Certificate Validation
Revocation Checking
typescript
async function checkCertificateRevocation(cert: Certificate): Promise<boolean> {
// Method 1: Check CRL (Certificate Revocation List)
const crlUrl = extractCrlUrl(cert);
if (crlUrl) {
const crl = await fetchCrl(crlUrl);
if (crl.isRevoked(cert.serialNumber)) {
return true; // Revoked
}
}
// Method 2: Check OCSP (Online Certificate Status Protocol)
const ocspUrl = extractOcspUrl(cert);
if (ocspUrl) {
const status = await checkOcsp(ocspUrl, cert);
if (status === 'revoked') {
return true; // Revoked
}
}
return false; // Not revoked
}Approved Certificate Authorities
| CA | Description | Website |
|---|---|---|
| VNPT-CA | Vietnam Posts and Telecommunications | vnpt-ca.vn |
| Viettel-CA | Viettel Group | viettel-ca.vn |
| FPT-CA | FPT Corporation | fpt-ca.com |
| BKAV-CA | BKAV Corporation | bkav-ca.vn |
| MISA-CA | MISA JSC | misa-ca.vn |
| CA2 (NEAC) | National Electronic Authentication Centre | ca2.gov.vn |
Security Considerations
PIN Protection
typescript
// Store PIN encrypted, never in plain text
const encryptedPin = await encrypt(pin, masterKey);
// Use secure memory for PIN during operations
const securePin = new SecureString(pin);
try {
await token.login(slot, securePin);
} finally {
securePin.clear(); // Wipe from memory
}Key Protection
- Private keys must never leave the USB Token/HSM
- All signing operations happen on the token hardware
- Token should lock after failed PIN attempts
Audit Logging
typescript
// Log all signature operations
logger.info('Signature operation', {
certificateId: cert.id,
certificateSerial: cert.serialNumber,
invoiceId: invoice.id,
invoiceNumber: invoice.invoiceNumber,
timestamp: new Date().toISOString(),
ipAddress: request.ip,
userId: user.id,
});Error Handling
| Error | Code | Description |
|---|---|---|
TOKEN_NOT_FOUND | E001 | USB Token not connected |
PIN_INCORRECT | E002 | Wrong PIN entered |
PIN_LOCKED | E003 | Token locked (too many attempts) |
CERT_NOT_FOUND | E004 | Certificate not found on token |
CERT_EXPIRED | E005 | Certificate has expired |
CERT_REVOKED | E006 | Certificate has been revoked |
SIGN_FAILED | E007 | Signature operation failed |
VERIFY_FAILED | E008 | Signature verification failed |