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ầu | Mô tả |
|---|---|
| Thuật toán | RSA với SHA-256 |
| Định dạng | PKCS#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óa | Tố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
| CA | Mô tả | Website |
|---|---|---|
| VNPT-CA | Tập đoàn Bưu chính Viễn thông Việt Nam | vnpt-ca.vn |
| Viettel-CA | Tập đoàn Viettel | viettel-ca.vn |
| FPT-CA | Tập đoàn FPT | fpt-ca.com |
| BKAV-CA | Tập đoàn BKAV | bkav-ca.vn |
| MISA-CA | Công ty Cổ phần MISA | misa-ca.vn |
| CA2 (NEAC) | Trung tâm Chứng thực Điện tử Quốc gia | ca2.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ỗi | Mã | Mô tả |
|---|---|---|
TOKEN_NOT_FOUND | E001 | Không tìm thấy USB Token |
PIN_INCORRECT | E002 | Nhập sai PIN |
PIN_LOCKED | E003 | Token bị khóa (quá nhiều lần thử) |
CERT_NOT_FOUND | E004 | Không tìm thấy chứng thư trên token |
CERT_EXPIRED | E005 | Chứng thư đã hết hạn |
CERT_REVOKED | E006 | Chứng thư đã bị thu hồi |
SIGN_FAILED | E007 | Thao tác ký thất bại |
VERIFY_FAILED | E008 | Xác minh chữ ký thất bại |