Skip to content

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

RequirementDescription
AlgorithmRSA with SHA-256
FormatPKCS#7 (CMS SignedData)
CertificateCA-issued, valid for e-invoice
StorageUSB Token or HSM
Key LengthMinimum 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

CADescriptionWebsite
VNPT-CAVietnam Posts and Telecommunicationsvnpt-ca.vn
Viettel-CAViettel Groupviettel-ca.vn
FPT-CAFPT Corporationfpt-ca.com
BKAV-CABKAV Corporationbkav-ca.vn
MISA-CAMISA JSCmisa-ca.vn
CA2 (NEAC)National Electronic Authentication Centreca2.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

ErrorCodeDescription
TOKEN_NOT_FOUNDE001USB Token not connected
PIN_INCORRECTE002Wrong PIN entered
PIN_LOCKEDE003Token locked (too many attempts)
CERT_NOT_FOUNDE004Certificate not found on token
CERT_EXPIREDE005Certificate has expired
CERT_REVOKEDE006Certificate has been revoked
SIGN_FAILEDE007Signature operation failed
VERIFY_FAILEDE008Signature verification failed

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.