使用Azure证书保管库非导出私钥实现PDF数字签名遇异常
Azure密钥保管库非导出私钥证书实现PDF签名问题排查
问题描述
我在用Azure密钥保管库中带非导出私钥的证书实现PDF数字签名时遇到问题:生成的PDF仅显示签名控件但无签名内容,Adobe验证证书时提示错误。
核心代码
主签名函数
async signDocument(fileBuffer: Buffer, certName: string) { try { Logger.log(`Signing document with certificate ${certName}...`); const certificate = await this.azureService.getCertificate(certName); Logger.log('Certificate:', certificate.keyId); // Load the original PDF const pdfDocument = await PDFDocument.load(fileBuffer); // Save the original PDF to verify it's being loaded correctly const originalPdf = await pdfDocument.save(); fs.writeFileSync('original_document.pdf', originalPdf); // Save for debugging // Placeholder widget coordinates (make sure they are within the page size) const widget = [50, 50, 200, 100]; // Add signature placeholder pdflibAddPlaceholder({ pdfDoc: pdfDocument, reason: 'The user is declaring consent through JavaScript.', contactInfo: 'signpdf@example.com', name: 'John Doe', location: 'Free Text Str., Free World', widgetRect: widget, }); // Save the modified PDF with the placeholder const pdfWithPlaceholder = await pdfDocument.save(); const pdfDigest = createHash(Buffer.from(pdfWithPlaceholder)) const signature = await this.azureService.signWithAzureVault(certificate.keyId, pdfDigest) const AzureSigner = new AzureCustomSigner(); AzureSigner.setAzureSignature(signature); const signedPdf = await AzureSigner.sign(Buffer.from(pdfWithPlaceholder)); fs.writeFileSync('pdf_with_placeholder.pdf', signedPdf); Logger.log('Document signed successfully!'); return signedPdf; } catch (error) { Logger.error('Error signing document:', error.message); return error; // Return the error object } }
Azure签名服务方法
async signWithAzureVault(keyId: string, digest: Buffer) { try { Logger.log(`Signing data using Azure Key Vault with keyId: ${keyId}`); const { keyName } = parseKeyId(keyId); const key = await this.keyClient.getKey(keyName); const cryptographyClient = new CryptographyClient(key, this.credential); const signatureResult = await cryptographyClient.sign('RS256', digest); return Buffer.from(signatureResult.result); } catch (error) { Logger.error('Error signing with Azure Vault:', error.message); throw error; } }
自定义AzureSigner实现
import { Signer, SignPdfError } from '@signpdf/signpdf'; export class AzureCustomSigner extends Signer { private signatureFromAzure: Buffer | null = null; constructor() { super(); } setAzureSignature(signature: Buffer): void { this.signatureFromAzure = signature; } async sign(pdfBuffer: Buffer, signingTime?: Date): Promise<Buffer> { // Step 1: Ensure the Azure signature is set if (!this.signatureFromAzure) { throw new SignPdfError('Azure signature must be set before signing.', SignPdfError.TYPE_INPUT); } const pdfBytes = this._getPdfBytes(pdfBuffer); const signedPdfBytes = this._applyAzureSignature(pdfBytes, this.signatureFromAzure); return Buffer.from(signedPdfBytes); } private _getPdfBytes(pdfBuffer: Buffer): Uint8Array { return new Uint8Array(pdfBuffer); } private _applyAzureSignature(pdfBytes: Uint8Array, signatureFromAzure: Buffer): Uint8Array { const signatureHex = signatureFromAzure.toString('hex'); const signaturePlaceholder = this._findSignaturePlaceholder(pdfBytes); return this._replacePlaceholderWithSignature(pdfBytes, signaturePlaceholder, signatureHex); } private _findSignaturePlaceholder(pdfBytes: Uint8Array): { position: number; length: number } { return { position: 12345, // This should be the byte position of the signature placeholder in the PDF. length: 256, // The expected length of the signature. }; } private _replacePlaceholderWithSignature( pdfBytes: Uint8Array, placeholder: { position: number; length: number }, signatureHex: string ): Uint8Array { const signatureBytes = Buffer.from(signatureHex, 'hex'); if (signatureBytes.length > placeholder.length) { throw new SignPdfError('The signature is too large for the placeholder.'); } const updatedPdfBytes = new Uint8Array(pdfBytes.length); updatedPdfBytes.set(pdfBytes); for (let i = 0; i < signatureBytes.length; i++) { updatedPdfBytes[placeholder.position + i] = signatureBytes[i]; } if (signatureBytes.length < placeholder.length) { for (let i = signatureBytes.length; i < placeholder.length; i++) { updatedPdfBytes[placeholder.position + i] = 0x00; } } return updatedPdfBytes; } } export default AzureCustomSigner;
问题分析
直接指出几个致命错误:
- 摘要计算错误:直接对带占位符的整个PDF文件哈希,而PDF签名要求的是对签名域中指定的待签字节范围计算哈希,不是整个文件
- 占位符定位错误:硬编码的
position:12345和length:256完全不符合实际PDF中的签名占位符位置,导致签名注入到错误位置 - 签名格式错误:Azure返回的是原始RSA签名(PKCS#1格式),但PDF签名需要的是PKCS#7/CMS格式的签名数据,需要包含证书链和签名属性
- 缺少证书嵌入:没有将Azure证书保管库中的证书(及链)嵌入到PDF签名域中,Adobe无法验证证书有效性
修复方案
步骤1:修正签名摘要提取逻辑
使用@signpdf/signpdf提供的工具函数提取待签字节范围,计算正确的哈希:
import { getSignatureContent } from '@signpdf/signpdf'; // 在生成pdfWithPlaceholder后: const { data, signatureByteRange } = getSignatureContent(pdfWithPlaceholder); const sha256Digest = createHash('sha256').update(data).digest();
步骤2:Azure服务补充证书链获取方法
// 在azureService中添加获取证书链的方法 async getCertificateChain(certName: string) { const cert = await this.certificateClient.getCertificate(certName); return cert.certificate; // 返回DER格式的证书链 }
步骤3:实现正确的自定义Signer
生成PKCS#7格式签名并正确定位占位符:
import { Signer, SignPdfError, getSignatureContent } from '@signpdf/signpdf'; import * as pkijs from 'pkijs'; export class AzureCustomSigner extends Signer { private signature: Buffer | null = null; private certChain: Buffer | null = null; setAzureSignature(signature: Buffer): void { this.signature = signature; } setCertificateChain(certChain: Buffer): void { this.certChain = certChain; } async sign(pdfBuffer: Buffer, signingTime = new Date()): Promise<Buffer> { if (!this.signature || !this.certChain) { throw new SignPdfError('Signature and certificate chain must be set.', SignPdfError.TYPE_INPUT); } const { data, placeholder } = getSignatureContent(pdfBuffer); const sha256Digest = createHash('sha256').update(data).digest(); // 构建PKCS#7/CMS签名 const cmsSignedData = await this.buildCMSSignedData(sha256Digest, signingTime); // 转换为DER格式并编码为Base64 const cmsDer = cmsSignedData.toSchema().toBER(false); const cmsBase64 = Buffer.from(cmsDer).toString('base64'); // 替换占位符,用空格填充到占位符长度 const paddedSignature = cmsBase64.padEnd(placeholder.length, ' '); return Buffer.from(pdfBuffer.toString().replace(placeholder, paddedSignature)); } private async buildCMSSignedData(digest: Buffer, signingTime: Date): Promise<pkijs.SignedData> { const certSchema = pkijs.fromBER(this.certChain); const cert = new pkijs.Certificate({ schema: certSchema }); const signedData = new pkijs.SignedData({ version: 1, digestAlgorithms: [new pkijs.AlgorithmIdentifier({ algorithmId: '1.2.840.113549.1.1.11' })], // SHA-256 encapContentInfo: new pkijs.EncapsulatedContentInfo({ eContentType: '1.2.840.113549.1.7.1', // data eContent: new pkijs.OCTET_STRING({ valueHex: digest }), }), certificates: [cert], signerInfos: [ new pkijs.SignerInfo({ version: 1, sid: new pkijs.IssuerAndSerialNumber({ issuer: cert.issuer, serialNumber: cert.serialNumber, }), digestAlgorithm: new pkijs.AlgorithmIdentifier({ algorithmId: '1.2.840.113549.1.1.11' }), signatureAlgorithm: new pkijs.AlgorithmIdentifier({ algorithmId: '1.2.840.113549.1.1.1' }), // RSA signature: new pkijs.OCTET_STRING({ valueHex: this.signature }), signedAttrs: [ new pkijs.Attribute({ type: '1.2.840.113549.1.9.3', // contentType values: ['1.2.840.113549.1.7.1'], }), new pkijs.Attribute({ type: '1.2.840.113549.1.9.5', // signingTime values: [signingTime], }), new pkijs.Attribute({ type: '1.2.840.113549.1.9.4', // messageDigest values: [digest], }), ], }), ], }); return signedData; } }
步骤4:更新主签名函数
async signDocument(fileBuffer: Buffer, certName: string) { try { Logger.log(`Signing document with certificate ${certName}...`); const certificate = await this.azureService.getCertificate(certName); const certChain = await this.azureService.getCertificateChain(certName); Logger.log('Certificate:', certificate.keyId); // Load the original PDF const pdfDocument = await PDFDocument.load(fileBuffer); // Add signature placeholder const widget = [50, 50, 200, 100]; pdflibAddPlaceholder({ pdfDoc: pdfDocument, reason: 'The user is declaring consent through JavaScript.', contactInfo: 'signpdf@example.com', name: 'John Doe', location: 'Free Text Str., Free World', widgetRect: widget, }); // Save the modified PDF with the placeholder const pdfWithPlaceholder = await pdfDocument.save(); // 提取待签内容并计算哈希 const { data } = getSignatureContent(pdfWithPlaceholder); const sha256Digest = createHash('sha256').update(data).digest(); // 调用Azure签名 const signature = await this.azureService.signWithAzureVault(certificate.keyId, sha256Digest); // 初始化自定义Signer并签名 const azureSigner = new AzureCustomSigner(); azureSigner.setAzureSignature(signature); azureSigner.setCertificateChain(certChain); const signedPdf = await azureSigner.sign(pdfWithPlaceholder); fs.writeFileSync('signed_document.pdf', signedPdf); Logger.log('Document signed successfully!'); return signedPdf; } catch (error) { Logger.error('Error signing document:', error.message); throw error; } }
关键注意事项
- 确保
pdflibAddPlaceholder生成的占位符长度足够容纳PKCS#7签名(建议至少设置为8192字节) - Azure密钥保管库中的证书必须包含完整的链(根证书+中间证书)
- 签名算法必须与证书的密钥算法匹配(RS256对应RSA密钥)
内容的提问来源于stack exchange,提问作者Arvydas Kezunas
相关产品推荐
相关产品推荐

