Skip to content

Chữ ký Số

Tổng quan

Hóa đơn điện tử Việt Nam yêu cầu chữ ký số sử dụng định dạng PKCS#7 với chứng thư được cấp bởi các Tổ chức Chứng thực Chữ ký số (CA) được phê duyệt. Chữ ký đảm bảo tính xác thực và không thể chối bỏ của hóa đơn.

Yêu cầu Chữ ký

Yêu cầuMô tả
Thuật toánRSA với SHA-256
Định dạngPKCS#7 (CMS SignedData)
Chứng thưDo CA cấp, hợp lệ cho hóa đơn điện tử
Lưu trữUSB Token hoặc HSM
Độ dài KhóaTối thiểu 2048 bits

Quy trình Ký

Luồng Chữ ký

DigitalSignatureService

typescript
interface DigitalSignatureService {
  /**
   * Ký tài liệu XML với chứng thư của merchant
   */
  signXml(xml: string, certificateId: string): Promise<string>;

  /**
   * Xác minh chữ ký trên XML đã ký
   */
  verifySignature(signedXml: string): Promise<VerificationResult>;

  /**
   * Lấy các chứng thư có sẵn để ký
   */
  getCertificates(): Promise<Certificate[]>;

  /**
   * Xác thực chứng thư (hết hạn, thu hồi)
   */
  validateCertificate(certId: string): Promise<CertificateStatus>;

  /**
   * Lấy chuỗi chứng thư
   */
  getCertificateChain(certId: string): Promise<Certificate[]>;
}

Triển khai

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. Tính hash
    const hash = this.calculateHash(canonicalXml);

    // 3. Ký với USB Token
    const signature = await this.signWithToken(hash, certificateId);

    // 4. Xây dựng cấu trúc PKCS#7
    const pkcs7 = await this.buildPkcs7(signature, certificateId);

    // 5. Nhúng vào 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> {
    // Mở phiên với 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,
    );

    // Đăng nhập với PIN
    this.pkcs11.C_Login(session, PKCS11.CKU_USER, this.config.tokenPin);

    try {
      // Tìm khóa riêng
      const privateKey = this.findPrivateKey(session, certificateId);

      // Khởi tạo thao tác ký
      this.pkcs11.C_SignInit(session, {
        mechanism: PKCS11.CKM_SHA256_RSA_PKCS,
      }, privateKey);

      // Ký
      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();

    // Thêm chứng thư
    p7.addCertificate(forge.pki.certificateFromPem(cert.pem));
    for (const chainCert of chain) {
      p7.addCertificate(forge.pki.certificateFromPem(chainCert.pem));
    }

    // Thêm thông tin người ký
    p7.addSigner({
      key: null, // Khóa nằm trên 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(),
        },
      ],
    });

    // Đặt chữ ký
    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 {
    // Tạo phần tử XML Signature
    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>
    `;

    // Chèn trước thẻ đóng </HDon>
    return xml.replace('</HDon>', `${signatureXml}</HDon>`);
  }

  async verifySignature(signedXml: string): Promise<VerificationResult> {
    try {
      // Trích xuất chữ ký từ XML
      const signature = this.extractSignature(signedXml);

      // Trích xuất chứng thư
      const cert = this.extractCertificate(signature);

      // Xác minh chuỗi chứng thư
      const chainValid = await this.verifyCertificateChain(cert);
      if (!chainValid) {
        return { valid: false, error: 'Chuỗi chứng thư không hợp lệ' };
      }

      // Kiểm tra thu hồi chứng thư
      const revoked = await this.checkRevocation(cert);
      if (revoked) {
        return { valid: false, error: 'Chứng thư đã bị thu hồi' };
      }

      // Xác minh giá trị chữ ký
      const signatureValid = this.verifySignatureValue(signedXml, signature);
      if (!signatureValid) {
        return { valid: false, error: 'Chữ ký không hợp lệ' };
      }

      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 };
    }
  }
}

Tích hợp USB Token

Cấu hình PKCS#11

typescript
interface PKCS11Config {
  library: string;      // Đường dẫn đến thư viện PKCS#11 (.so/.dll)
  slot: number;         // Số khe token
  pin: string;          // PIN token (nên được mã hóa)
  label?: string;       // Nhãn token tùy chọn
}

// Các thư viện USB Token phổ biến
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',
};

Các Thao tác Token

typescript
class PKCS11TokenService {
  /**
   * Khởi tạo kết nối token
   */
  async initialize(): Promise<void>;

  /**
   * Liệt kê các token có sẵn
   */
  async listTokens(): Promise<TokenInfo[]>;

  /**
   * Đăng nhập vào token bằng PIN
   */
  async login(slot: number, pin: string): Promise<SessionHandle>;

  /**
   * Đăng xuất khỏi token
   */
  async logout(session: SessionHandle): Promise<void>;

  /**
   * Liệt kê các chứng thư trên token
   */
  async listCertificates(session: SessionHandle): Promise<Certificate[]>;

  /**
   * Ký dữ liệu bằng khóa riêng
   */
  async sign(
    session: SessionHandle,
    keyId: string,
    data: Buffer,
  ): Promise<Buffer>;

  /**
   * Xác minh chữ ký bằng chứng thư
   */
  async verify(
    certificate: Certificate,
    data: Buffer,
    signature: Buffer,
  ): Promise<boolean>;
}

Quản lý Chứng thư

Thực thể Chứng thư

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;
}

Xác thực Chứng thư

Kiểm tra Thu hồi

typescript
async function checkCertificateRevocation(cert: Certificate): Promise<boolean> {
  // Phương pháp 1: Kiểm tra CRL (Danh sách Thu hồi Chứng thư)
  const crlUrl = extractCrlUrl(cert);
  if (crlUrl) {
    const crl = await fetchCrl(crlUrl);
    if (crl.isRevoked(cert.serialNumber)) {
      return true; // Đã thu hồi
    }
  }

  // Phương pháp 2: Kiểm tra OCSP (Giao thức Trạng thái Chứng thư Trực tuyến)
  const ocspUrl = extractOcspUrl(cert);
  if (ocspUrl) {
    const status = await checkOcsp(ocspUrl, cert);
    if (status === 'revoked') {
      return true; // Đã thu hồi
    }
  }

  return false; // Chưa thu hồi
}

Các Nhà cung cấp CA Được phê duyệt

CAMô tảWebsite
VNPT-CATập đoàn Bưu chính Viễn thông Việt Namvnpt-ca.vn
Viettel-CATập đoàn Viettelviettel-ca.vn
FPT-CATập đoàn FPTfpt-ca.com
BKAV-CATập đoàn BKAVbkav-ca.vn
MISA-CACông ty Cổ phần MISAmisa-ca.vn
CA2 (NEAC)Trung tâm Chứng thực Điện tử Quốc giaca2.gov.vn

Cân nhắc Bảo mật

Bảo vệ PIN

typescript
// Lưu trữ PIN đã mã hóa, không bao giờ ở dạng văn bản thuần túy
const encryptedPin = await encrypt(pin, masterKey);

// Sử dụng bộ nhớ an toàn cho PIN trong các thao tác
const securePin = new SecureString(pin);
try {
  await token.login(slot, securePin);
} finally {
  securePin.clear(); // Xóa khỏi bộ nhớ
}

Bảo vệ Khóa

  • Khóa riêng không bao giờ được rời khỏi USB Token/HSM
  • Tất cả các thao tác ký diễn ra trên phần cứng token
  • Token sẽ khóa sau các lần nhập PIN sai

Ghi nhật ký Kiểm toán

typescript
// Ghi nhật ký tất cả các thao tác ký
logger.info('Thao tác ký', {
  certificateId: cert.id,
  certificateSerial: cert.serialNumber,
  invoiceId: invoice.id,
  invoiceNumber: invoice.invoiceNumber,
  timestamp: new Date().toISOString(),
  ipAddress: request.ip,
  userId: user.id,
});

Xử lý Lỗi

LỗiMô tả
TOKEN_NOT_FOUNDE001Không tìm thấy USB Token
PIN_INCORRECTE002Nhập sai PIN
PIN_LOCKEDE003Token bị khóa (quá nhiều lần thử)
CERT_NOT_FOUNDE004Không tìm thấy chứng thư trên token
CERT_EXPIREDE005Chứng thư đã hết hạn
CERT_REVOKEDE006Chứng thư đã bị thu hồi
SIGN_FAILEDE007Thao tác ký thất bại
VERIFY_FAILEDE008Xác minh chữ ký thất bại

Tài liệu Liên quan

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