feat(签章验签): 完善OFD加密、签章与验签能力,统一国密实现,优化界面文案与交互
build.yaml / build (push) Successful in 40s

This commit is contained in:
xiaoqidun committed 2026-09-23 23:02:48 +08:00
1 parent d6f70de7be
commit 3927929822
30 files changed
+4707 -1161

No files matched your search

+55
View File
@@ -353,8 +353,59 @@
</div>
</form>
</dialog>
<dialog id="signPanel" class="form-dialog security-dialog" aria-label="签章">
<form id="signForm" autocomplete="off">
<div class="form-row"><label for="signCertificate">证书</label><input id="signCertificate" type="file" accept=".pem,.cer,.crt,.der" required></div>
<div class="form-row"><label for="signKey">私钥</label><input id="signKey" type="file" accept=".pem,.key,.der" required></div>
<div class="form-row"><label for="signKeyPassword">私钥口令</label><input id="signKeyPassword" type="password" autocomplete="off"></div>
<div class="form-row"><label for="signRoots">信任根</label><input id="signRoots" type="file" accept=".pem,.cer,.crt,.der" multiple required></div>
<div class="form-row"><label for="signIntermediates">中间证书</label><input id="signIntermediates" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
<div class="form-row"><label for="signSeal">电子印章</label><input id="signSeal" type="file" accept=".esl,.ses,.der" aria-label="SES电子印章"></div>
<div id="signPlacementFields" hidden>
<div class="form-row"><label for="signPlacement">位置</label><select id="signPlacement"><option value="normal">页面</option><option value="seam">骑缝</option></select></div>
<div class="form-row"><label for="signPages">页码</label><input id="signPages" type="text" autocomplete="off" spellcheck="false"></div>
<div class="form-row"><label for="signX">横坐标</label><div class="form-unit"><input id="signX" type="number" min="0" step="any" value="10" aria-label="横坐标(毫米)"><span>mm</span></div></div>
<div class="form-row"><label for="signY">纵坐标</label><div class="form-unit"><input id="signY" type="number" min="0" step="any" value="10" aria-label="纵坐标(毫米)"><span>mm</span></div></div>
<div class="form-row"><label for="signWidth">宽度</label><div class="form-unit"><input id="signWidth" type="number" min="0.01" step="any" value="40" aria-label="宽度(毫米)"><span>mm</span></div></div>
<div class="form-row"><label for="signHeight">高度</label><div class="form-unit"><input id="signHeight" type="number" min="0.01" step="any" value="40" aria-label="高度(毫米)"><span>mm</span></div></div>
</div>
<div class="form-row"><label for="signMode">方式</label><select id="signMode"><option value="append">追加</option><option value="replace">替换</option></select></div>
<div class="form-row"><label for="signLock">禁止追加</label><input id="signLock" type="checkbox" aria-label="禁止追加签名"></div>
<div class="form-actions"><div id="signStatus" class="form-status" role="status" aria-live="polite"></div><button id="signCancel" class="button" type="button">取消</button><button class="button" type="submit" title="签署另存">签署</button></div>
</form>
</dialog>
<dialog id="verifyPanel" class="form-dialog security-dialog" aria-label="验签">
<form id="verifyForm">
<div class="form-row"><label for="verifyRoots">信任根</label><input id="verifyRoots" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
<div class="form-row"><label for="verifyCerts">证书</label><input id="verifyCerts" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
<div class="form-row"><label for="verifyTimeRoots">时间戳根</label><input id="verifyTimeRoots" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
<div class="form-row"><label for="verifyTokens">时间戳</label><input id="verifyTokens" type="file" accept=".tsr,.tst,.der" multiple></div>
<div class="form-row"><label for="verifyCRLs">撤销表</label><input id="verifyCRLs" type="file" accept=".crl,.der" multiple></div>
<div class="form-row"><label for="verifyOCSP">OCSP响应</label><input id="verifyOCSP" type="file" accept=".ocsp,.der" multiple></div>
<div class="form-row"><span>要求</span><div class="form-options"><label><input id="verifyRequireTime" type="checkbox"><span>时间戳</span></label><label><input id="verifyRequireRevocation" type="checkbox"><span>撤销检查</span></label></div></div>
<div class="form-actions"><div id="verifyStatus" class="form-status" role="status" aria-live="polite"></div><button id="verifyCancel" class="button" type="button">取消</button><button class="button" type="submit">验签</button></div>
</form>
</dialog>
<dialog id="credentialsPanel" class="form-dialog security-dialog" aria-label="解锁文档">
<form id="credentialsForm" autocomplete="off">
<div class="form-row"><label for="credentialsType">方式</label><select id="credentialsType"><option value="password">口令</option><option value="certificate">证书</option></select></div>
<div class="form-row"><label for="credentialsUser">用户</label><input id="credentialsUser" type="text" autocomplete="off" spellcheck="false"></div>
<div id="credentialsPasswordRow" class="form-row"><label for="credentialsPassword">口令</label><input id="credentialsPassword" type="password" autocomplete="off" required autofocus></div>
<div id="credentialsCertificateRow" class="form-row" hidden><label for="credentialsCertificate">证书</label><input id="credentialsCertificate" type="file" accept=".pem,.cer,.crt,.der"></div>
<div id="credentialsKeyRow" class="form-row" hidden><label for="credentialsKey">私钥</label><input id="credentialsKey" type="file" accept=".pem,.key,.der"></div>
<div id="credentialsKeyPasswordRow" class="form-row" hidden><label for="credentialsKeyPassword">私钥口令</label><input id="credentialsKeyPassword" type="password" autocomplete="off"></div>
<div class="form-actions"><div id="credentialsStatus" class="form-status" role="status" aria-live="polite"></div><button id="credentialsCancel" class="button" type="button">取消</button><button class="button" type="submit">解锁</button></div>
</form>
</dialog>
<dialog id="exportPanel" class="form-dialog" aria-label="导出文档">
<form id="exportForm">
<div id="encryptionFields" hidden>
<div class="form-row"><label for="encryptionType">方式</label><select id="encryptionType"><option value="password">口令</option><option value="certificate">证书</option></select></div>
<div id="encryptionUserRow" class="form-row"><label for="encryptionUser">用户</label><input id="encryptionUser" type="text" autocomplete="off" spellcheck="false"></div>
<div id="encryptionPasswordRow" class="form-row"><label for="encryptionPassword">口令</label><input id="encryptionPassword" type="password" autocomplete="new-password"></div>
<div id="encryptionConfirmRow" class="form-row"><label for="encryptionConfirm">确认</label><input id="encryptionConfirm" type="password" autocomplete="new-password"></div>
<div id="encryptionCertificatesRow" class="form-row" hidden><label for="encryptionCertificates">证书</label><input id="encryptionCertificates" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
</div>
<div class="form-row">
<span id="exportScopeLabel">范围</span>
<div class="form-options" role="radiogroup" aria-labelledby="exportScopeLabel">
@@ -500,6 +551,10 @@
<dd id="metaFonts">0</dd>
</div>
</dl>
<section id="securityPanel" class="security-panel" aria-labelledby="securityPanelTitle" hidden>
<div class="panel-row"><div id="securityPanelTitle" class="panel-title compact">安全</div><div class="panel-actions"><button id="encryptSaveButton" class="small-button" type="button" title="加密另存" aria-label="加密另存" aria-haspopup="dialog" aria-controls="exportPanel" disabled>加密</button><button id="signButton" class="small-button" type="button" aria-haspopup="dialog" aria-controls="signPanel" disabled>签章</button><button id="verifyButton" class="small-button" type="button" aria-haspopup="dialog" aria-controls="verifyPanel" disabled>验签</button></div></div>
<dl class="meta-list"><div><dt>加密</dt><dd id="securityEncryption">未加密</dd></div><div id="securityMethodRow" hidden><dt>算法</dt><dd id="securityMethod"></dd></div></dl>
</section>
<section id="attachmentPanel" class="attachment-panel" aria-labelledby="attachmentPanelTitle" hidden>
<div id="attachmentPanelTitle" class="panel-title">附件</div>
<div id="attachmentList" class="attachment-list"></div>
+66 -22
View File
@@ -645,6 +645,23 @@ button:not(:disabled):hover {
padding: 12px;
}
.security-dialog {
width: 360px;
}
.security-dialog .form-row {
grid-template-columns: 4em minmax(0, 1fr);
}
#verifyForm .form-row {
grid-template-columns: 5em minmax(0, 1fr);
}
.security-dialog .form-row > label,
.security-dialog .form-row > span {
text-align-last: justify;
}
.form-row {
display: grid;
grid-template-columns: 2em minmax(0, 1fr);
@@ -711,6 +728,7 @@ button:not(:disabled):hover {
}
.form-row input[type="text"],
.form-row input[type="password"],
.form-row input[type="number"],
.form-row input[type="file"],
.form-row select,
@@ -895,6 +913,8 @@ body[data-hide-meta] .meta-panel {
border-right: 1px solid var(--line);
background: var(--panel);
overflow: auto;
font-size: 13px;
line-height: 1.5;
}
.page-list-panel {
@@ -1168,7 +1188,7 @@ body[data-hide-meta] .meta-panel {
gap: 6px;
margin: 8px 0;
color: var(--text-muted);
font-size: 11px;
font-size: 12px;
font-variant-numeric: tabular-nums;
}
@@ -1205,7 +1225,7 @@ body[data-hide-meta] .meta-panel {
display: block;
margin-bottom: 3px;
color: var(--text-muted);
font-size: 11px;
font-size: 12px;
}
.search-result mark {
@@ -1284,7 +1304,13 @@ button.outline-link:hover {
transition: border-color 0.16s ease, background-color 0.16s ease;
}
.page-list-item[aria-current] {
.page-list-item:not(:disabled):hover {
border-color: var(--line-strong);
background: #f0f5f4;
}
.page-list-item[aria-current],
.page-list-item[aria-current]:hover {
border-color: var(--accent);
color: var(--accent-strong);
font-weight: 700;
@@ -1297,7 +1323,8 @@ button.outline-link:hover {
}
.page-list-item[aria-current]:not([aria-pressed="false"]),
.page-list-item[aria-pressed="true"] {
.page-list-item[aria-pressed="true"],
.page-list-item[aria-pressed="true"]:hover {
border-color: var(--accent);
background: #e4f3ef;
color: var(--accent-strong);
@@ -1351,7 +1378,7 @@ button.outline-link:hover {
}
.thumb-size {
font-size: 11px;
font-size: 12px;
color: var(--text-muted);
font-weight: 400;
}
@@ -2037,7 +2064,8 @@ button.outline-link:hover {
.font-panel,
.doc-font-panel,
.attachment-panel,
.signature-panel {
.signature-panel,
.security-panel {
margin-top: 18px;
padding-top: 12px;
border-top: 1px solid var(--line);
@@ -2070,7 +2098,7 @@ button.outline-link:hover {
.attachment-detail {
color: var(--text-muted);
font-size: 11px;
font-size: 12px;
}
.count-pill {
@@ -2083,7 +2111,7 @@ button.outline-link:hover {
border: 1px solid var(--line);
background: #ffffff;
color: var(--text-muted);
font-size: 11px;
font-size: 12px;
text-align: center;
white-space: nowrap;
}
@@ -2104,7 +2132,6 @@ button.outline-link:hover {
transition: border-color 0.16s ease, background-color 0.16s ease;
}
.page-list-item:not(:disabled):hover,
.doc-font-row:hover,
.signature-row:hover,
.font-row:hover {
@@ -2194,10 +2221,6 @@ button.outline-link:hover {
flex-wrap: wrap;
justify-content: flex-end;
gap: 4px;
}
.doc-font-badges,
.signature-badges {
float: right;
margin-left: 8px;
}
@@ -2210,7 +2233,7 @@ button.outline-link:hover {
padding: 3px 6px;
background: #eef1f4;
color: var(--text-muted);
font-size: 11px;
font-size: 12px;
line-height: 1.35;
white-space: nowrap;
}
@@ -2235,19 +2258,40 @@ button.outline-link:hover {
.info-line {
display: grid;
grid-template-columns: 30px minmax(0, 1fr);
grid-template-columns: 2em minmax(0, 1fr);
align-items: start;
gap: 7px;
color: var(--text-muted);
font-size: 11px;
line-height: 1.35;
font-size: 12px;
line-height: 1.5;
}
.info-label {
color: #7b8791;
color: var(--text-muted);
white-space: nowrap;
}
.meta-panel .info-line {
grid-template-columns: 4em minmax(0, 1fr);
}
.meta-panel .info-label {
text-align-last: justify;
}
.signature-row details {
font-size: 12px;
}
.signature-row summary {
cursor: pointer;
color: var(--accent-strong);
}
.signature-row details[open] > .info-line {
margin-top: 6px;
}
.info-value {
min-width: 0;
overflow-wrap: anywhere;
@@ -2263,8 +2307,8 @@ button.outline-link:hover {
.doc-font-detail {
color: var(--text-muted);
font-size: 11px;
line-height: 1.35;
font-size: 12px;
line-height: 1.5;
overflow-wrap: anywhere;
}
@@ -2337,7 +2381,7 @@ button.outline-link:hover {
border: 1px solid var(--line);
background: #f8fafb;
color: var(--text-muted);
font-size: 11px;
font-size: 12px;
text-align: center;
white-space: nowrap;
}
@@ -2348,7 +2392,7 @@ button.outline-link:hover {
gap: 3px;
height: 26px;
color: var(--text-muted);
font-size: 11px;
font-size: 12px;
white-space: nowrap;
}
+498 -42
View File
@@ -24,6 +24,7 @@ let wasmWorker = null;
let wasmRequestID = 0;
let wasmRecoveryTimer = 0;
let textMeasure = null;
let credentialRequest = null;
const state = {
composite: null,
@@ -35,6 +36,8 @@ const state = {
wasmRecovering: false,
wasmRecoveries: 0,
exporting: false,
signing: false,
signCanceled: false,
exportRequestID: 0,
editing: false,
selectObjects: true,
@@ -44,6 +47,7 @@ const state = {
savedRevision: null,
insertObject: null,
importPageCount: 0,
importEncrypted: false,
importing: false,
pageSelection: new Set(),
pageSelectionAnchor: null,
@@ -102,11 +106,74 @@ const state = {
exportFormats: [],
exportPages: null,
exportBackdrop: false,
exportEncrypted: false,
showPages: !COMPACT_LAYOUT.matches,
showMeta: !COMPACT_LAYOUT.matches,
};
const el = {
signButton: document.querySelector("#signButton"),
signPanel: document.querySelector("#signPanel"),
signForm: document.querySelector("#signForm"),
signCertificate: document.querySelector("#signCertificate"),
signKey: document.querySelector("#signKey"),
signKeyPassword: document.querySelector("#signKeyPassword"),
signRoots: document.querySelector("#signRoots"),
signIntermediates: document.querySelector("#signIntermediates"),
signSeal: document.querySelector("#signSeal"),
signPlacementFields: document.querySelector("#signPlacementFields"),
signPlacement: document.querySelector("#signPlacement"),
signPages: document.querySelector("#signPages"),
signX: document.querySelector("#signX"),
signY: document.querySelector("#signY"),
signWidth: document.querySelector("#signWidth"),
signHeight: document.querySelector("#signHeight"),
signMode: document.querySelector("#signMode"),
signLock: document.querySelector("#signLock"),
signStatus: document.querySelector("#signStatus"),
signCancel: document.querySelector("#signCancel"),
verifyButton: document.querySelector("#verifyButton"),
verifyPanel: document.querySelector("#verifyPanel"),
verifyForm: document.querySelector("#verifyForm"),
verifyRoots: document.querySelector("#verifyRoots"),
verifyCerts: document.querySelector("#verifyCerts"),
verifyTimeRoots: document.querySelector("#verifyTimeRoots"),
verifyTokens: document.querySelector("#verifyTokens"),
verifyCRLs: document.querySelector("#verifyCRLs"),
verifyOCSP: document.querySelector("#verifyOCSP"),
verifyRequireTime: document.querySelector("#verifyRequireTime"),
verifyRequireRevocation: document.querySelector("#verifyRequireRevocation"),
verifyStatus: document.querySelector("#verifyStatus"),
verifyCancel: document.querySelector("#verifyCancel"),
securityPanel: document.querySelector("#securityPanel"),
securityEncryption: document.querySelector("#securityEncryption"),
securityMethod: document.querySelector("#securityMethod"),
securityMethodRow: document.querySelector("#securityMethodRow"),
encryptSaveButton: document.querySelector("#encryptSaveButton"),
credentialsPanel: document.querySelector("#credentialsPanel"),
credentialsType: document.querySelector("#credentialsType"),
credentialsPasswordRow: document.querySelector("#credentialsPasswordRow"),
credentialsCertificateRow: document.querySelector("#credentialsCertificateRow"),
credentialsKeyRow: document.querySelector("#credentialsKeyRow"),
credentialsCertificate: document.querySelector("#credentialsCertificate"),
credentialsKey: document.querySelector("#credentialsKey"),
credentialsKeyPassword: document.querySelector("#credentialsKeyPassword"),
credentialsKeyPasswordRow: document.querySelector("#credentialsKeyPasswordRow"),
credentialsForm: document.querySelector("#credentialsForm"),
credentialsUser: document.querySelector("#credentialsUser"),
credentialsPassword: document.querySelector("#credentialsPassword"),
credentialsStatus: document.querySelector("#credentialsStatus"),
credentialsCancel: document.querySelector("#credentialsCancel"),
encryptionFields: document.querySelector("#encryptionFields"),
encryptionType: document.querySelector("#encryptionType"),
encryptionUserRow: document.querySelector("#encryptionUserRow"),
encryptionPasswordRow: document.querySelector("#encryptionPasswordRow"),
encryptionConfirmRow: document.querySelector("#encryptionConfirmRow"),
encryptionCertificatesRow: document.querySelector("#encryptionCertificatesRow"),
encryptionCertificates: document.querySelector("#encryptionCertificates"),
encryptionUser: document.querySelector("#encryptionUser"),
encryptionPassword: document.querySelector("#encryptionPassword"),
encryptionConfirm: document.querySelector("#encryptionConfirm"),
ofdInput: document.querySelector("#ofdInput"),
ofdButton: document.querySelector("#ofdButton"),
newButton: document.querySelector("#newButton"),
@@ -755,6 +822,7 @@ el.importPanel.addEventListener("cancel", event => {
});
el.importPanel.addEventListener("close", () => {
state.importPageCount = 0;
state.importEncrypted = false;
el.importForm.reset();
if (state.ready) callWASM("ofdgoLoadImport", null).catch(err => showError(err, false));
});
@@ -770,7 +838,12 @@ el.importForm.addEventListener("submit", async event => {
event.preventDefault();
if (document.body.hasAttribute("aria-busy") || !state.importPageCount || !el.importForm.reportValidity()) return;
const position = { before:state.pageIndex, after:state.pageIndex+1, first:0, last:state.doc.pageCount }[el.importPosition.value];
const saved = await changeDocument("ofdgoImportPages", null, el.importRange.value === "custom" ? el.importPages.value : "", position, el.importOutlines.checked);
const options = [];
if (state.importEncrypted) {
if (!window.confirm(state.doc.encryption?.encrypted ? "按目标文档的加密策略导入,是否继续?" : "目标未加密,导入内容将按明文保存,是否继续?")) return;
options.push(true);
}
const saved = await changeDocument("ofdgoImportPages", null, el.importRange.value === "custom" ? el.importPages.value : "", position, el.importOutlines.checked, ...options);
if (saved) el.importPanel.close();
});
editorClick(el.copyPageButton, () => changeSelectedPages("copy"));
@@ -926,6 +999,60 @@ el.insertPanel.addEventListener("close", () => { state.insertObject = null; });
el.insertForm.addEventListener("submit", insertObject);
el.textFontAdd.addEventListener("click", () => openFontFile(el.fontInput));
el.saveButton.addEventListener("click", () => exportFile(true, null, "ofd"));
el.encryptSaveButton.addEventListener("click", () => openExportPanel(true));
el.signButton.addEventListener("click", () => {
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
el.signForm.reset();
el.signPages.value = state.editing && state.pageSelection.size ? selectedPageRange() : String(state.pageIndex + 1);
el.signStatus.textContent = "";
updateSignPlacement();
el.signPanel.showModal();
});
el.signSeal.addEventListener("change", updateSignPlacement);
el.signPlacement.addEventListener("change", updateSignPlacement);
el.signCancel.addEventListener("click", () => state.signing ? cancelSigning() : el.signPanel.close());
el.signPanel.addEventListener("cancel", event => {
if (state.signing) { event.preventDefault(); cancelSigning(); }
});
el.signPanel.addEventListener("close", () => { if (!el.signPanel.open) el.signForm.reset(); });
el.signForm.addEventListener("submit", signDocument);
el.verifyButton.addEventListener("click", () => {
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
el.verifyForm.reset();
el.verifyStatus.textContent = "";
el.verifyPanel.showModal();
});
el.verifyCancel.addEventListener("click", () => el.verifyPanel.close());
el.verifyPanel.addEventListener("close", () => { if (!el.verifyPanel.open) el.verifyForm.reset(); });
el.verifyForm.addEventListener("submit", verifyDocument);
el.credentialsCancel.addEventListener("click", () => finishCredentials(null));
el.credentialsPanel.addEventListener("cancel", event => { event.preventDefault(); finishCredentials(null); });
el.credentialsPanel.addEventListener("close", () => { if (!el.credentialsPanel.open) finishCredentials(null); });
el.credentialsType.addEventListener("change", updateCredentialsType);
el.credentialsForm.addEventListener("submit", async event => {
event.preventDefault();
if (!credentialRequest) return;
if (el.credentialsType.value === "certificate") {
const request = credentialRequest;
let key;
const keyPassword = new TextEncoder().encode(el.credentialsKeyPassword.value);
el.credentialsKeyPassword.value = "";
try {
key = (await securityFiles(el.credentialsKey))[0];
const certificate = (await securityFiles(el.credentialsCertificate))[0];
if (credentialRequest !== request) { key?.fill(0); keyPassword.fill(0); return; }
if (!key || !certificate) { el.credentialsStatus.textContent = "请选择证书和私钥"; key?.fill(0); keyPassword.fill(0); return; }
finishCredentials({password:new Uint8Array(), key, keyPassword, certificate, userName:el.credentialsUser.value.trim()});
} catch {
key?.fill(0);
keyPassword.fill(0);
if (credentialRequest === request) el.credentialsStatus.textContent = "文件读取失败";
}
return;
}
if (!el.credentialsPassword.value) return;
finishCredentials({ password: new TextEncoder().encode(el.credentialsPassword.value), userName: el.credentialsUser.value.trim() });
});
el.togglePagesButton.addEventListener("click", () => toggleSidebar("pages"));
el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta"));
el.pagesTab.addEventListener("click", () => showNavigation(el.pagesTab));
@@ -983,8 +1110,10 @@ el.viewerPanel.addEventListener("paste", pasteEditorContent);
document.addEventListener("selectionchange", syncSelection);
el.exportFormat.addEventListener("change", () => updateDPIControl());
el.exportPageButton.addEventListener("click", () => exportFile(false));
el.exportButton.addEventListener("click", openExportPanel);
el.exportButton.addEventListener("click", () => openExportPanel());
el.exportCancel.addEventListener("click", () => el.exportPanel.close());
el.exportPanel.addEventListener("close", () => { if (!el.exportPanel.open) clearEncryptionForm(); });
el.encryptionType.addEventListener("change", updateEncryptionType);
el.cancelExportButton.addEventListener("click", cancelExport);
el.exportPanel.addEventListener("pointerdown", (event) => {
state.exportBackdrop = event.target === el.exportPanel;
@@ -1000,9 +1129,32 @@ el.exportSpecified.addEventListener("change", () => {
el.exportRange.focus();
});
el.exportRange.addEventListener("input", updateExportRange);
el.exportForm.addEventListener("submit", (event) => {
el.exportForm.addEventListener("submit", async (event) => {
event.preventDefault();
if (!el.exportSubmit.disabled) {
if (state.exportEncrypted) {
const certificates = el.encryptionType.value === "certificate";
if (certificates && !el.encryptionCertificates.files?.length) {
el.exportRangeStatus.textContent = "请选择接收者证书";
return;
}
if (!certificates && !el.encryptionPassword.value) {
el.exportRangeStatus.textContent = "请输入口令";
el.encryptionPassword.focus();
return;
}
if (!certificates && el.encryptionPassword.value !== el.encryptionConfirm.value) {
el.exportRangeStatus.textContent = "口令不一致";
el.encryptionConfirm.focus();
return;
}
const encryption = certificates
? {password:new Uint8Array(), userName:"", recipientFiles:Array.from(el.encryptionCertificates.files)}
: { password: new TextEncoder().encode(el.encryptionPassword.value), userName: el.encryptionUser.value.trim() };
clearEncryptionForm();
try { return await exportFile(true, state.exportPages, "ofd", encryption); }
finally { encryption.password.fill(0); }
}
return exportFile(true, state.exportPages);
}
});
@@ -1317,18 +1469,20 @@ async function loadImportFile() {
const file = el.importFile.files[0];
const openSeq = state.openSeq;
state.importPageCount = 0;
state.importEncrypted = false;
el.importSubmit.disabled = true;
el.importStatus.textContent = "";
setBusy(true);
try {
const data = file ? new Uint8Array(await file.arrayBuffer()) : null;
if (openSeq !== state.openSeq) return;
const info = await callWASM("ofdgoLoadImport", data);
const info = data ? await openWithCredentials(null, openSeq, data, true) : await callWASM("ofdgoLoadImport", null);
if (openSeq !== state.openSeq || !el.importPanel.open) {
await callWASM("ofdgoLoadImport", null);
return;
}
state.importPageCount = info?.pageCount || 0;
state.importEncrypted = Boolean(info?.encrypted);
el.importPages.title = state.importPageCount ? `页码 1-${state.importPageCount}` : "页码";
el.importStatus.textContent = info?.signed ? "保留签章外观,原签名不再有效" : "";
el.importSubmit.disabled = !state.importPageCount;
@@ -1494,6 +1648,7 @@ async function createDocument(event) {
return;
}
const title = el.createName.value.trim() || "未命名";
clearSecurityForms();
const openSeq = ++state.openSeq;
setBusy(true, "正在新建文档", null, "正在新建文档");
try {
@@ -2714,10 +2869,11 @@ async function loadWASM() {
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
if (data.stage === "save") {
el.cancelExportButton.disabled = true;
if (state.signing) el.signCancel.disabled = true;
setProgress("正在保存", null);
} else if (!el.cancelExportButton.disabled) {
if (data.stage === "prepare") {
const label = { snapshot: "正在准备", ids: "正在检查", commit: "正在整理", fonts: "正在处理字体", pages: "正在处理页面", references: "正在检查引用", resources: "正在整理资源", write: "正在写入" }[data.phase];
const label = { snapshot: "正在准备", ids: "正在检查", commit: "正在整理", fonts: "正在处理字体", pages: "正在处理页面", references: "正在检查引用", resources: "正在整理资源", write: "正在写入", sign: "正在签署", encrypt: "正在加密" }[data.phase];
setProgress(label, data.total ? data.completed / data.total * 100 : null);
} else if (data.completed === data.total) {
setProgress("正在收尾", null);
@@ -2753,6 +2909,7 @@ async function loadWASM() {
request.resolve(data.data);
} else {
const err = new Error(missingGlyphMessage(data.missingGlyphs) || (data.reasonCode === "layoutRequired" ? "跨段或换行修改需先设置段落排版" : data.error));
err.code = data.code;
if (data.canceled) {
err.name = "AbortError";
}
@@ -2768,6 +2925,7 @@ function markWASMExited(wasmSeq = state.wasmSeq, err) {
return;
}
state.ready = false;
clearSecurityForms();
state.wasmExited = true;
wasmWorker?.terminate();
wasmWorker = null;
@@ -2830,6 +2988,7 @@ async function openOFD(file) {
if (!discardChanges()) {
return;
}
clearSecurityForms();
state.wasmRecoveries = 0;
const openSeq = ++state.openSeq;
setBusy(true, "正在读取文档", 10, STATUS.opening);
@@ -3306,6 +3465,86 @@ function updateFontPermissionHint() {
el.fontPermissionHint.hidden = !fontManager.canReadLocal() || fontManager.permission === "granted";
}
function clearSecurityForms() {
finishCredentials(null);
clearEncryptionForm();
el.signKey.value = "";
el.signKeyPassword.value = "";
if (el.signPanel.open) el.signPanel.close();
if (el.verifyPanel.open) el.verifyPanel.close();
}
function updateCredentialsType() {
const certificate = el.credentialsType.value === "certificate";
el.credentialsStatus.textContent = "";
el.credentialsPasswordRow.hidden = certificate;
el.credentialsCertificateRow.hidden = el.credentialsKeyRow.hidden = !certificate;
el.credentialsKeyPasswordRow.hidden = !certificate;
el.credentialsPassword.required = !certificate;
el.credentialsCertificate.required = el.credentialsKey.required = certificate;
el.credentialsPassword.value = "";
el.credentialsCertificate.value = el.credentialsKey.value = "";
el.credentialsKeyPassword.value = "";
}
function finishCredentials(credentials) {
const request = credentialRequest;
credentialRequest = null;
el.credentialsPassword.value = "";
el.credentialsUser.value = "";
el.credentialsCertificate.value = el.credentialsKey.value = "";
el.credentialsKeyPassword.value = "";
if (el.credentialsPanel.open) el.credentialsPanel.close();
if (request && request.openSeq === state.openSeq) request.resolve(credentials);
else {
credentials?.password.fill(0);
credentials?.key?.fill(0);
credentials?.keyPassword?.fill(0);
request?.resolve(null);
}
}
function requestCredentials(code, openSeq) {
finishCredentials(null);
el.credentialsStatus.textContent = code === "invalidCredentials" ? "解锁失败,请重试" : "";
el.progressPanel.hidden = true;
return new Promise(resolve => {
credentialRequest = { resolve, openSeq };
el.credentialsPanel.showModal();
(el.credentialsType.value === "certificate" ? el.credentialsCertificate : el.credentialsPassword).focus();
});
}
async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, importing = false) {
let credentials = null;
try {
while (openSeq === state.openSeq) {
try {
return importing ? await callWASM("ofdgoLoadImport", data.slice(), credentials)
: await callWASM("ofdgoOpen", data, fonts, state.renderAnnotations, credentials);
} catch (err) {
if (openSeq !== state.openSeq || !["credentialsRequired", "invalidCredentials"].includes(err.code)) throw err;
credentials?.password.fill(0);
credentials?.key?.fill(0);
credentials?.keyPassword?.fill(0);
credentials = await requestCredentials(err.code, openSeq);
if (!credentials || openSeq !== state.openSeq) {
const canceled = new Error("打开已取消");
canceled.name = "AbortError";
throw canceled;
}
}
}
const canceled = new Error("打开已取消");
canceled.name = "AbortError";
throw canceled;
} finally {
credentials?.password.fill(0);
credentials?.key?.fill(0);
credentials?.keyPassword?.fill(0);
}
}
async function openDocument(options = {}) {
if (!state.ofdBytes && !state.editorInfo) {
return;
@@ -3341,7 +3580,7 @@ async function openDocument(options = {}) {
}
const doc = options.doc || (options.reuseSession
? await callWASM("ofdgoConfigure", fonts, state.renderAnnotations)
: await callWASM("ofdgoOpen", state.ofdBytes, fonts, state.renderAnnotations));
: await openWithCredentials(fonts, openSeq));
if (openSeq !== state.openSeq) {
return;
}
@@ -3439,6 +3678,12 @@ async function openDocument(options = {}) {
return;
}
if (options.throwError) throw err;
if (!options.reuseSession && !options.doc) {
state.ofdBytes = null;
state.doc = null;
renderSecurity();
updateControls();
}
showError(err, true);
} finally {
if (openSeq === state.openSeq) {
@@ -3522,6 +3767,7 @@ async function downloadAttachment(attachment) {
if (!state.doc || document.body.hasAttribute("aria-busy")) {
return;
}
if (state.doc.encryption?.encrypted && !window.confirm("附件将按明文下载,是否继续?")) return;
const openSeq = state.openSeq;
state.exporting = true;
updateControls();
@@ -3558,11 +3804,165 @@ async function downloadAttachment(attachment) {
}
}
function openExportPanel() {
function updateEncryptionType() {
const certificates = el.encryptionType.value === "certificate";
el.encryptionUserRow.hidden = el.encryptionPasswordRow.hidden = el.encryptionConfirmRow.hidden = certificates;
el.encryptionCertificatesRow.hidden = !certificates;
el.encryptionPassword.required = el.encryptionConfirm.required = state.exportEncrypted && !certificates;
el.encryptionCertificates.required = state.exportEncrypted && certificates;
el.encryptionPassword.value = el.encryptionConfirm.value = "";
el.encryptionCertificates.value = "";
}
async function encryptionRecipients(files) {
const names = new Set();
return Promise.all(files.map(async file => {
const base = file.name.replace(/\.[^.]+$/, "") || "用户";
let name = base, index = 2;
while (names.has(name)) name = `${base} ${index++}`;
names.add(name);
return {userName:name, certificate:new Uint8Array(await file.arrayBuffer())};
}));
}
function clearEncryptionForm() {
el.encryptionPassword.value = "";
el.encryptionConfirm.value = "";
el.encryptionUser.value = "";
el.encryptionCertificates.value = "";
state.exportEncrypted = false;
}
function renderSecurity() {
const encryption = state.doc?.encryption;
el.securityPanel.hidden = !state.doc;
el.securityEncryption.textContent = encryption?.encrypted ? "已加密" : "未加密";
el.securityMethod.textContent = encryption?.method || "";
el.securityMethodRow.hidden = !encryption?.method;
el.encryptSaveButton.disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
el.verifyButton.disabled = el.encryptSaveButton.disabled;
el.signButton.disabled = el.encryptSaveButton.disabled;
}
function cancelSigning() {
if (el.signCancel.disabled) return;
state.signCanceled = true;
el.signStatus.textContent = "正在取消";
cancelExport();
}
function updateSignPlacement() {
const stamp = Boolean(el.signSeal.files?.length);
el.signPlacementFields.hidden = !stamp;
el.signPages.required = stamp;
el.signX.disabled = !stamp || el.signPlacement.value === "seam";
}
async function signDocument(event) {
event.preventDefault();
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
if (!el.signCertificate.files?.length || !el.signKey.files?.length || !el.signRoots.files?.length) {
el.signStatus.textContent = "请选择证书、私钥和信任根";
return;
}
if (el.signMode.value === "replace" && !window.confirm("替换现有签名,是否继续?")) return;
let openSeq = state.openSeq;
let key;
const keyPassword = new TextEncoder().encode(el.signKeyPassword.value);
el.signKeyPassword.value = "";
state.exporting = true;
state.signing = true;
state.signCanceled = false;
el.signCancel.disabled = false;
setBusy(true, "正在签署", null, "正在签署");
el.signForm.querySelectorAll("input, select, button[type=submit]").forEach(input => { input.disabled = true; });
try {
const name = `${baseFileName()}_签章.ofd`;
const file = window.showSaveFilePicker ? await window.showSaveFilePicker({suggestedName:name, types:[{description:"OFD",accept:{"application/ofd":[".ofd"]}}]}) : null;
if (openSeq !== state.openSeq || state.signCanceled) return;
if (canvasEditor.input || canvasEditor.crop || canvasEditor.nudge || canvasEditor.nudgeCommit) {
setBusy(false);
if (!await canvasEditor.commitNudge() || !await canvasEditor.commitText() || !await canvasEditor.commitCrop()) return;
openSeq = state.openSeq;
setBusy(true, "正在签署", null, "正在签署");
}
key = (await securityFiles(el.signKey))[0];
const options = {
key, keyPassword, certificate:(await securityFiles(el.signCertificate))[0], roots:await securityFiles(el.signRoots),
intermediates:await securityFiles(el.signIntermediates), seal:(await securityFiles(el.signSeal))[0] || null,
pages:el.signPages.value.trim(), placement:el.signPlacement.value,
x:Number(el.signX.value), y:Number(el.signY.value), width:Number(el.signWidth.value), height:Number(el.signHeight.value),
mode:el.signMode.value, lock:el.signLock.checked,
};
el.signKey.value = "";
if (openSeq !== state.openSeq || state.signCanceled || !el.signPanel.open) return;
const result = await callWASM("ofdgoSaveSigned", null, options, file);
if (openSeq !== state.openSeq) return;
if (result.blob) downloadBytes(result.blob, result.mime, name);
el.signPanel.close();
setStatus("签署已另存");
} catch (err) {
if (openSeq === state.openSeq) el.signStatus.textContent = err.name === "AbortError" ? "签署已取消" : err.message;
} finally {
key?.fill(0);
keyPassword.fill(0);
el.signKey.value = "";
el.signForm.querySelectorAll("input, select, button[type=submit]").forEach(input => { input.disabled = false; });
el.signCancel.disabled = false;
if (state.signCanceled && openSeq === state.openSeq) el.signStatus.textContent = "签署已取消";
state.signing = false;
updateSignPlacement();
state.exporting = false;
state.exportRequestID = 0;
el.cancelExportButton.hidden = true;
if (openSeq === state.openSeq) { setBusy(false); updateControls(); }
}
}
async function securityFiles(input) {
return Promise.all(Array.from(input.files || [], async file => new Uint8Array(await file.arrayBuffer())));
}
async function verifyDocument(event) {
event.preventDefault();
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
const openSeq = state.openSeq;
setBusy(true, "正在验签", null, "正在验签");
el.verifyForm.inert = true;
try {
const options = {
roots: await securityFiles(el.verifyRoots), certificates: await securityFiles(el.verifyCerts),
timestampRoots: await securityFiles(el.verifyTimeRoots), tokens: await securityFiles(el.verifyTokens),
crls: await securityFiles(el.verifyCRLs), ocsp: await securityFiles(el.verifyOCSP),
requireTimestamp: el.verifyRequireTime.checked, requireRevocation: el.verifyRequireRevocation.checked,
};
if (openSeq !== state.openSeq || !el.verifyPanel.open) return;
const info = await callWASM("ofdgoVerifySignatures", options);
if (openSeq !== state.openSeq || !el.verifyPanel.open) return;
Object.assign(state.doc, info);
renderMeta();
el.verifyPanel.close();
setStatus(info.signatureCount ? "验签完成" : "无签名");
} catch (err) {
if (openSeq === state.openSeq) el.verifyStatus.textContent = err.message;
} finally {
el.verifyForm.inert = false;
if (openSeq === state.openSeq) setBusy(false);
}
}
function openExportPanel(encrypted = false) {
if (!state.doc || document.body.hasAttribute("aria-busy")) {
return;
}
el.exportForm.reset();
clearEncryptionForm();
state.exportEncrypted = encrypted;
el.encryptionFields.hidden = !encrypted;
el.encryptionPassword.required = el.encryptionConfirm.required = encrypted;
updateEncryptionType();
el.exportPanel.setAttribute("aria-label", encrypted ? "加密另存" : "导出文档");
el.exportSubmit.textContent = encrypted ? "保存" : "导出";
if (state.editing && state.pageSelection.size) {
el.exportSpecified.checked = true;
el.exportRange.value = selectedPageRange();
@@ -3600,14 +4000,15 @@ async function updateExportRange() {
}
}
async function exportFile(whole, indices = null, value = el.exportFormat.value) {
async function exportFile(whole, indices = null, value = el.exportFormat.value, encryption = null) {
if (!state.doc || document.body.hasAttribute("aria-busy")) {
return;
}
const saving = value === "ofd";
if (saving && !state.editorInfo) {
if (saving && !state.editorInfo && !encryption) {
return;
}
if (!saving && state.doc.encryption?.encrypted && !window.confirm("此格式将输出明文,是否继续?")) return;
const format = saving ? { value: "ofd", label: "OFD", extension: "ofd", mime: "application/ofd" } : exportFormatInfo(value);
if (!format) {
return;
@@ -3640,7 +4041,12 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
openSeq = state.openSeq;
setBusy(true, `正在生成 ${label}`, null, status);
}
const result = saving ? await callWASM("ofdgoSaveDocument", indices, file) : whole
if (encryption?.recipientFiles) {
encryption.recipients = await encryptionRecipients(encryption.recipientFiles);
delete encryption.recipientFiles;
if (openSeq !== state.openSeq) return;
}
const result = encryption ? await callWASM("ofdgoSaveEncrypted", indices, encryption, file) : saving ? await callWASM("ofdgoSaveDocument", indices, file) : whole
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, file)
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, file);
if (openSeq !== state.openSeq) {
@@ -3649,7 +4055,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
if (result.blob) {
downloadBytes(result.blob, result.mime, fileName);
}
if (saving && indices === null) {
if (saving && indices === null && state.editorInfo) {
state.savedRevision = state.editorInfo.revision;
setDirty(false);
}
@@ -5344,6 +5750,7 @@ function markThumbnailError(index) {
}
function renderMeta(keepDetails = false) {
renderSecurity();
const doc = state.doc || {};
el.metaPanel.setAttribute("aria-busy", String(!!doc.detailsPending));
document.title = `OFDGo WebUI - ${state.fileName}`;
@@ -5440,41 +5847,88 @@ function renderSignatures() {
const badges = document.createElement("div");
badges.className = "signature-badges";
badges.append(fontBadge(signature.status === "valid" ? "通过" : signature.status === "invalid" ? "异常" : "未验", signature.status));
badges.append(fontBadge(signature.trustedValid ? "可信" : signature.status === "valid" ? "完整" : signature.status === "invalid" ? "异常" : "未验", signature.status));
head.append(badges, name);
row.append(head);
appendInfoLine(row, "编号", signature.id);
appendInfoLine(row, "版本", signature.version);
appendInfoLine(row, "章图", signature.sealType);
appendInfoLine(row, "章号", signature.sealId);
appendInfoLine(row, "章名", signature.sealName);
appendInfoLine(row, "厂商", signature.sealVendor);
appendInfoLine(row, "签者", signature.signer);
appendInfoLine(row, "签署人", signature.signer);
appendInfoLine(row, "时间", formatDocumentTime(signature.signatureDateTime));
appendInfoLine(row, "机构", signatureAgency(signature));
appendSignatureCheck(row, "原文", signature.dataHashChecked, signature.dataHashOK);
appendSignatureCheck(row, "签名", signature.signedValueChecked, signature.signedValueOK);
if (signature.type !== "Sign") {
appendSignatureCheck(row, "章验", signature.sealChecked, signature.sealOK);
appendSignaturePolicy(row, "一致", signature.sealMatchChecked, signature.sealMatchOK);
}
appendSignatureCheck(row, "证书", signature.certChecked, signature.certOK);
appendSignaturePolicy(row, "签期", signature.signatureTimeChecked, signature.signatureTimeOK);
appendSignaturePolicy(row, "章期", signature.sealTimeChecked, signature.sealTimeOK);
if (signature.sealCertTimeChecked) {
appendInfoLine(row, "制期", signature.sealCertTimeOK ? "有效" : "失效", signature.sealCertTimeOK ? "ok" : "");
}
appendSignaturePolicy(row, "时效", signature.certTimeChecked, signature.certTimeOK);
appendSignatureCheck(row, "信任", signature.certTrustChecked, signature.certTrustOK);
appendInfoLine(row, "保护", signatureReferenceText(signature), signatureReferenceStatus(signature));
appendInfoLine(row, "算法", signature.signatureMethod);
appendInfoLine(row, "散列", signature.digestMethod);
appendInfoLine(row, "序号", signature.signSerial);
appendInfoLine(row, "主体", signature.signSubject && signature.signSubject !== signature.signer ? signature.signSubject : "");
appendInfoLine(row, "颁发", signature.signIssuer);
appendInfoLine(row, "章证", signature.sealSubject);
appendInfoLine(row, "保护文件", signatureReferenceText(signature), signatureReferenceStatus(signature));
appendInfoLine(row, "错误", signature.error, "fail");
const checks = document.createElement("details");
const checksTitle = document.createElement("summary");
checksTitle.textContent = "校验详情";
checks.append(checksTitle);
appendSignatureCheck(checks, "原文", signature.dataHashChecked, signature.dataHashOK);
appendSignatureCheck(checks, "签名", signature.signedValueChecked, signature.signedValueOK);
if (signature.type !== "Sign") {
appendSignatureCheck(checks, "印章", signature.sealChecked, signature.sealOK);
appendSignaturePolicy(checks, "印章匹配", signature.sealMatchChecked, signature.sealMatchOK);
}
appendSignatureCheck(checks, "证书", signature.certChecked, signature.certOK);
appendSignaturePolicy(checks, "签署时间", signature.signatureTimeChecked, signature.signatureTimeOK);
appendSignaturePolicy(checks, "印章期限", signature.sealTimeChecked, signature.sealTimeOK);
if (signature.sealCertTimeChecked) {
appendInfoLine(checks, "制章证书", signature.sealCertTimeOK ? "有效" : "失效", signature.sealCertTimeOK ? "ok" : "fail");
}
appendSignaturePolicy(checks, "证书期限", signature.certTimeChecked, signature.certTimeOK);
appendSignatureCheck(checks, "策略", signature.policyChecked, signature.policyOK);
appendSignatureCheck(checks, "覆盖", signature.coverageChecked, signature.coverageOK);
appendSignatureCheck(checks, "时间戳", signature.timestampChecked, signature.timestampOK);
appendSignatureCheck(checks, "撤销", signature.revocationChecked, signature.revocationOK);
appendInfoLine(checks, "策略错误", signature.policyError, "fail");
appendInfoLine(checks, "信任错误", signature.certTrustError, "fail");
appendInfoLine(checks, "覆盖错误", signature.coverageError, "fail");
appendInfoLine(checks, "未保护", signature.uncoveredFiles?.join("、"), "fail");
row.append(checks);
for (const timestamp of signature.timestamps || []) {
const details = document.createElement("details");
const title = document.createElement("summary");
title.textContent = timestamp.valid ? "可信时间戳" : "时间戳详情";
details.append(title);
appendInfoLine(details, timestamp.valid ? "时间" : "声明时间", formatDocumentTime(timestamp.time));
appendSignatureCheck(details, "绑定", timestamp.bindingChecked, timestamp.bindingOK);
appendSignatureCheck(details, "签名", timestamp.signedValueChecked, timestamp.signedValueOK);
appendSignatureCheck(details, "信任", timestamp.certTrustChecked, timestamp.certTrustOK);
appendSignatureCheck(details, "时效", timestamp.certTimeChecked, timestamp.certTimeOK);
appendInfoLine(details, "错误", timestamp.error, "fail");
row.append(details);
}
for (const revocation of signature.revocations || []) {
const details = document.createElement("details");
const title = document.createElement("summary");
title.textContent = "撤销详情";
details.append(title);
appendInfoLine(details, "证书", revocation.subject);
appendInfoLine(details, "来源", revocation.source);
const valid = revocation.checked && revocation.ok && revocation.status === "good";
appendInfoLine(details, "状态", !revocation.checked ? "未验" : valid ? "未撤销" : revocation.status === "revoked" ? "已撤销" : "未知", valid ? "ok" : revocation.checked ? "fail" : "");
appendInfoLine(details, "更新", formatDocumentTime(revocation.thisUpdate));
appendInfoLine(details, "截止", formatDocumentTime(revocation.nextUpdate));
appendInfoLine(details, "撤销时间", formatDocumentTime(revocation.revokedAt));
appendInfoLine(details, "错误", revocation.error, "fail");
row.append(details);
}
const details = document.createElement("details");
const title = document.createElement("summary");
title.textContent = "签章信息";
details.append(title);
appendInfoLine(details, "编号", signature.id);
appendInfoLine(details, "版本", signature.version);
appendInfoLine(details, "章图", signature.sealType);
appendInfoLine(details, "章号", signature.sealId);
appendInfoLine(details, "厂商", signature.sealVendor);
appendInfoLine(details, "文档", signature.docRoot);
appendInfoLine(details, "算法", signature.signatureMethod);
appendInfoLine(details, "摘要", signature.digestMethod);
appendInfoLine(details, "序列号", signature.signSerial);
appendInfoLine(details, "证书主体", signature.signSubject);
appendInfoLine(details, "颁发者", signature.signIssuer);
appendInfoLine(details, "制章证书", signature.sealSubject);
row.append(details);
fragment.append(row);
}
el.signatureList.append(fragment);
@@ -5493,11 +5947,11 @@ function signatureSummary(signatures) {
if (unchecked) {
parts.push(`未验 ${unchecked}`);
}
return parts.length > 1 ? parts.join(" · ") : `通过 ${signatures.length}`;
return parts.length > 1 ? parts.join(" · ") : `${signatures.every(signature => signature.trustedValid) ? "可信" : "完整"} ${signatures.length}`;
}
function signatureNameNode(signature) {
const stamps = signature.stamps || [];
const stamps = signature.docIndex ? [] : signature.stamps || [];
const name = document.createElement("div");
name.className = "signature-name";
if (!stamps.length) {
@@ -6060,6 +6514,7 @@ function updateFitSpace() {
}
function updateControls() {
renderSecurity();
const hasDoc = Boolean(state.doc);
updateRenderBackend();
updateEditorTools();
@@ -6221,11 +6676,11 @@ async function callWASM(name, ...args) {
return new Promise((resolve, reject) => {
const id = ++wasmRequestID;
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
if (name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument" || name === "ofdgoImportPages") {
if (name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") {
state.exportRequestID = id;
el.cancelExportButton.hidden = false;
el.cancelExportButton.disabled = false;
el.cancelExportButton.title = { ofdgoSaveDocument: "取消保存", ofdgoImportPages: "取消导入", ofdgoExportAttachment: "取消下载" }[name] || "取消导出";
el.cancelExportButton.title = { ofdgoSaveDocument: "取消保存", ofdgoSaveEncrypted: "取消保存", ofdgoSaveSigned: "取消签署", ofdgoImportPages: "取消导入", ofdgoExportAttachment: "取消下载" }[name] || "取消导出";
el.cancelExportButton.setAttribute("aria-label", el.cancelExportButton.title);
}
try {
@@ -6253,6 +6708,7 @@ function showError(err, empty = !state.doc) {
function setBusy(busy, text = "", percent = 0, status = "") {
document.body.toggleAttribute("aria-busy", busy);
renderSecurity();
updateRenderBackend();
el.editButton.disabled = busy || !state.doc || !state.ready || state.exporting;
el.createForm.inert = busy;
+10 -3
View File
@@ -7,7 +7,7 @@ self.onmessage = ({ data }) => {
operations.get(data.id)?.abort();
return;
}
if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoImportPages") {
if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveEncrypted" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") {
operations.set(data.id, new AbortController());
}
pending = pending.then(() => handleMessage(data));
@@ -17,6 +17,10 @@ async function handleMessage({ id, name, args }) {
let channel;
const signal = operations.get(id)?.signal;
const importing = name === "ofdgoImportPages";
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned";
const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null;
const keyPassword = name === "ofdgoOpen" ? args[3]?.keyPassword : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.keyPassword : null;
const password = name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoSaveEncrypted" || name === "ofdgoLoadImport" ? args[1]?.password : null;
try {
signal?.throwIfAborted();
const chunks = [];
@@ -37,7 +41,7 @@ async function handleMessage({ id, name, args }) {
});
} else {
const file = args.pop();
const indices = name === "ofdgoSaveDocument" ? args.shift() : null;
const indices = saving ? args.shift() : null;
if (file) output = await file.createWritable();
signal.throwIfAborted();
args.push((bytes, done) => {
@@ -58,7 +62,7 @@ async function handleMessage({ id, name, args }) {
self.postMessage({ id, type: "export", stage: "pages", completed, total });
finish(done);
});
} else if (name === "ofdgoSaveDocument") {
} else if (saving) {
args.push((phase, completed, total, done) => {
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
finish(done);
@@ -94,6 +98,9 @@ async function handleMessage({ id, name, args }) {
}
self.postMessage(result);
} finally {
password?.fill(0);
key?.fill(0);
keyPassword?.fill(0);
channel?.port1.close();
channel?.port2.close();
operations.delete(id);
+3 -1
View File
@@ -3,11 +3,14 @@ module github.com/xiaoqidun/ofdgo
go 1.26.0
require (
github.com/emmansun/gmsm v0.44.1
github.com/go-fonts/latin-modern v0.3.3
github.com/go-text/typesetting v0.3.5
github.com/gogpu/gg v0.52.5
github.com/tdewolff/canvas v0.0.0-20260913163248-dd4999d1c76a
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
github.com/xiaoqidun/jbig2 v1.1.1
golang.org/x/crypto v0.57.0
golang.org/x/image v0.46.0
golang.org/x/text v0.42.0
)
@@ -22,7 +25,6 @@ require (
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/benoitkugler/textlayout v0.3.2 // indirect
github.com/benoitkugler/textprocessing v0.0.6 // indirect
github.com/go-fonts/latin-modern v0.3.3 // indirect
github.com/gogpu/gpucontext v0.28.0 // indirect
github.com/gogpu/gputypes v0.5.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
+4
View File
@@ -28,6 +28,8 @@ github.com/benoitkugler/textlayout v0.3.2/go.mod h1:o+1hFV+JSHBC9qNLIuwVoLedERU7
github.com/benoitkugler/textlayout-testdata v0.1.1/go.mod h1:i/qZl09BbUOtd7Bu/W1CAubRwTWrEXWq6JwMkw8wYxo=
github.com/benoitkugler/textprocessing v0.0.6 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g=
github.com/benoitkugler/textprocessing v0.0.6/go.mod h1:Io0gN08/PXEzrSOWFa88xHx2Xv3VjvLMY7H76YoI23A=
github.com/emmansun/gmsm v0.44.1 h1:zDTkdtLWFG0vCbhPV+k9pte14tix/eK71At9Iai9fP4=
github.com/emmansun/gmsm v0.44.1/go.mod h1:p6RIUta0/KboFHrOxr1x8q+pd8RZtdaTO7XNp0RmMQM=
github.com/go-fonts/latin-modern v0.3.3 h1:g2xNgI8yzdNzIVm+qvbMryB6yGPe0pSMss8QT3QwlJ0=
github.com/go-fonts/latin-modern v0.3.3/go.mod h1:tHaiWDGze4EPB0Go4cLT5M3QzRY3peya09Z/8KSCrpY=
github.com/go-text/typesetting v0.3.5 h1:XZPUooClHY0Vf/rFyUyuPRNEkawARaFzLMQcXLSEyPk=
@@ -76,6 +78,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
+309 -12
View File
@@ -20,6 +20,7 @@ import (
"bufio"
"bytes"
"context"
"crypto"
"encoding/hex"
"encoding/json"
"errors"
@@ -91,6 +92,7 @@ var copiedStyle *ofdgo.GraphicObject
// apiResult 浏览器接口返回结果
type apiResult struct {
OK bool `json:"ok"`
Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"`
Data any `json:"data,omitempty"`
MissingGlyphs *ofdgo.MissingGlyphError `json:"missingGlyphs,omitempty"`
@@ -102,6 +104,7 @@ func RunWASM() {
registerCallback("ofdgoOpen", openDocument)
registerCallback("ofdgoConfigure", configureDocument)
registerCallback("ofdgoDocumentInfo", documentInfo)
registerCallback("ofdgoVerifySignatures", verifySignatures)
registerCallback("ofdgoRenderPage", renderPage)
registerCallback("ofdgoRenderBackends", renderBackends)
registerCallback("ofdgoSVGFontData", svgFontData)
@@ -171,6 +174,8 @@ func RunWASM() {
registerCallback("ofdgoUndo", func([]js.Value) (any, error) { return restoreEditor(false) })
registerCallback("ofdgoRedo", func([]js.Value) (any, error) { return restoreEditor(true) })
registerAsyncCallback("ofdgoSaveDocument", saveDocument)
registerAsyncCallback("ofdgoSaveEncrypted", saveEncrypted)
registerAsyncCallback("ofdgoSaveSigned", saveSigned)
select {}
}
@@ -214,6 +219,16 @@ func callbackResult(fn func([]js.Value) (any, error), args []js.Value) any {
data, err := safeCall(fn, args)
if err != nil {
result := apiResult{Error: err.Error()}
switch {
case errors.Is(err, ofdgo.ErrCredentialsRequired):
result.Code, result.Error = "credentialsRequired", "文档已加密"
case errors.Is(err, ofdgo.ErrInvalidCredentials):
result.Code, result.Error = "invalidCredentials", "凭据无效或文件损坏"
case errors.Is(err, ofdgo.ErrUnsupportedEncryption):
result.Code, result.Error = "unsupportedEncryption", "不支持此加密方式"
case errors.Is(err, ofdgo.ErrEncryptionPolicyRequired):
result.Code, result.Error = "encryptionPolicyRequired", "请设置加密方式"
}
var failure *ofdgo.EditError
if errors.As(err, &failure) {
result.ReasonCode = failure.Code
@@ -257,6 +272,19 @@ func openDocument(args []js.Value) (any, error) {
return nil, err
}
renderAnnotations := args[2].Bool()
options := OpenOptions{Fonts: fonts, RenderAnnotations: renderAnnotations}
if len(args) > 3 && !args[3].IsNull() && !args[3].IsUndefined() {
credentials, err := credentialsFromJS(args[3])
if err != nil {
return nil, err
}
defer clear(credentials.Password)
options.ReaderOptions = append(options.ReaderOptions, ofdgo.WithCredentials(credentials))
}
session, err := Open(data, options)
if err != nil {
return nil, err
}
clearImport()
currentEditor = nil
copiedObjects = nil
@@ -265,14 +293,49 @@ func openDocument(args []js.Value) (any, error) {
_ = currentSession.Close()
currentSession = nil
}
session, err := Open(data, OpenOptions{Fonts: fonts, RenderAnnotations: renderAnnotations})
if err != nil {
return nil, err
}
currentSession = session
return currentSession.Summary(), nil
}
// credentialsFromJS 解析本次打开的口令或本机证书私钥
// 入参: value 解锁表单数据
// 返回: ofdgo.Credentials 会话凭据, error 错误信息
func credentialsFromJS(value js.Value) (ofdgo.Credentials, error) {
credentials := ofdgo.Credentials{UserName: value.Get("userName").String()}
if keyValue := value.Get("key"); !keyValue.IsNull() && !keyValue.IsUndefined() {
key, err := bytesFromJS(keyValue)
if err != nil {
return credentials, err
}
defer clear(key)
certificate, err := bytesFromJS(value.Get("certificate"))
if err != nil {
return credentials, err
}
var password []byte
if field := value.Get("keyPassword"); !field.IsUndefined() && !field.IsNull() {
password, err = bytesFromJS(field)
if err != nil {
return credentials, err
}
defer clear(password)
}
signer, certificate, err := ofdgo.ParseSignatureIdentity(certificate, key, password)
if err != nil {
return credentials, ofdgo.ErrInvalidCredentials
}
decrypter, ok := signer.(crypto.Decrypter)
if !ok {
return credentials, ofdgo.ErrInvalidCredentials
}
credentials.Certificate, credentials.Decrypter = certificate, decrypter
return credentials, nil
}
var err error
credentials.Password, err = bytesFromJS(value.Get("password"))
return credentials, err
}
// configureDocument 更新当前会话的字体和注解设置
// 入参: args 浏览器参数
// 返回: any 文档信息, error 错误信息
@@ -315,6 +378,70 @@ func documentInfo(args []js.Value) (any, error) {
return currentSession.Info(), nil
}
// securityFilesFromJS 读取会话内的本机证据文件
// 入参: value 二进制数组列表
// 返回: [][]byte 文件内容, error 错误信息
func securityFilesFromJS(value js.Value) ([][]byte, error) {
if value.IsUndefined() || value.IsNull() {
return nil, nil
}
files := make([][]byte, value.Length())
for i := range files {
var err error
files[i], err = bytesFromJS(value.Index(i))
if err != nil {
return nil, err
}
}
return files, nil
}
// verifySignatures 按本机信任锚和离线证据重新验签,不设置联网回调
// 入参: args 信任根、辅助证书、时间戳及撤销证据
// 返回: any 分项报告, error 错误信息
func verifySignatures(args []js.Value) (any, error) {
if currentSession == nil || len(args) == 0 {
return nil, fmt.Errorf("missing signature verification arguments")
}
input := args[0]
files := make(map[string][][]byte)
for _, name := range []string{"roots", "certificates", "timestampRoots", "tokens", "crls", "ocsp"} {
data, err := securityFilesFromJS(input.Get(name))
if err != nil {
return nil, err
}
files[name] = data
}
now := time.Now()
policy := ofdgo.SignaturePolicy{RejectWeakAlgorithms: true, RequireDocumentCoverage: true,
RequireTimestamp: input.Get("requireTimestamp").Bool(), RequireRevocation: input.Get("requireRevocation").Bool()}
options := []ofdgo.SignatureVerifyOption{
ofdgo.WithSignatureVerifyTime(now), ofdgo.WithSignaturePolicy(policy),
ofdgo.WithSignatureTrustCerts(files["roots"]...), ofdgo.WithSignatureCerts(files["certificates"]...),
}
if policy.RequireTimestamp || len(files["timestampRoots"])+len(files["tokens"]) > 0 {
options = append(options, ofdgo.WithSignatureTimestamp(ofdgo.SignatureTimestampOptions{
TrustCerts: files["timestampRoots"], Certificates: files["certificates"], Tokens: files["tokens"], Required: policy.RequireTimestamp, VerifyTime: &now,
}))
}
if policy.RequireRevocation || len(files["crls"])+len(files["ocsp"]) > 0 {
options = append(options, ofdgo.WithSignatureRevocation(ofdgo.SignatureRevocationOptions{
SignatureRevocationEvidence: ofdgo.SignatureRevocationEvidence{CRLs: files["crls"], OCSPResponses: files["ocsp"]},
Certificates: append(files["certificates"], files["roots"]...), Required: policy.RequireRevocation, VerifyTime: &now,
}))
}
reports, err := currentSession.Reader.VerifySignatures(options...)
if err != nil {
return nil, err
}
infos := make([]SignatureInfo, len(reports))
for i, report := range reports {
infos[i] = signatureInfo(report)
}
currentSession.signatures, currentSession.signatureError, currentSession.signaturesRead = infos, nil, true
return map[string]any{"signatures": infos, "signatureCount": len(infos), "signatureError": ""}, nil
}
// exportAttachment 分块导出附件
// 入参: args 浏览器参数
// 返回: any 附件数据, error 错误信息
@@ -2686,7 +2813,16 @@ func loadImport(args []js.Value) (any, error) {
if err != nil {
return nil, err
}
reader, err := ofdgo.NewReader(bytes.NewReader(data), int64(len(data)))
var options []ofdgo.ReaderOption
if len(args) > 1 && !args[1].IsNull() && !args[1].IsUndefined() {
credentials, err := credentialsFromJS(args[1])
if err != nil {
return nil, err
}
defer clear(credentials.Password)
options = append(options, ofdgo.WithCredentials(credentials))
}
reader, err := ofdgo.NewReader(bytes.NewReader(data), int64(len(data)), options...)
if err != nil {
return nil, err
}
@@ -2700,7 +2836,7 @@ func loadImport(args []js.Value) (any, error) {
return nil, fmt.Errorf("document has no pages")
}
pendingImport = reader
return map[string]any{"pageCount": len(doc.Pages.Page), "signed": doc.Signatures != ""}, nil
return map[string]any{"pageCount": len(doc.Pages.Page), "signed": doc.Signatures != "", "encrypted": reader.Encryption().Encrypted}, nil
}
// importPages 插入指定来源页面并保持目标文档元数据
@@ -2727,8 +2863,13 @@ func importPages(args []js.Value) (any, error) {
}
at := args[1].Int()
options := ofdgo.PageImportOptions{Outlines: len(args) > 2 && args[2].Bool()}
if len(args) > 3 {
options.OnProgress = editorOperationProgress(args[3])
progressIndex := 3
if len(args) > 3 && args[3].Type() == js.TypeBoolean {
options.AllowDecrypted = args[3].Bool()
progressIndex = 4
}
if len(args) > progressIndex {
options.OnProgress = editorOperationProgress(args[progressIndex])
}
if _, err := currentEditor.ImportPagesWithOptions(pendingImport, indexes, at, options); err != nil {
return nil, err
@@ -3454,16 +3595,172 @@ func saveDocument(args []js.Value) (any, error) {
if currentEditor == nil {
return nil, fmt.Errorf("no document is being edited")
}
if len(args) > 1 {
currentEditor.OnWriteProgress = editorOperationProgress(args[1])
return saveEditor(currentEditor, args)
}
// saveEncrypted 以独立快照加密另存,不修改当前文档的输出策略
// 入参: args 加密选项、写出回调、进度回调和可选页面索引
// 返回: any 保存结果, error 错误信息
func saveEncrypted(args []js.Value) (any, error) {
if currentSession == nil || len(args) < 3 {
return nil, fmt.Errorf("missing encrypted save arguments")
}
password, err := bytesFromJS(args[0].Get("password"))
if err != nil {
return nil, err
}
defer clear(password)
options := &ofdgo.EncryptionOptions{Password: password, UserName: args[0].Get("userName").String()}
if recipients := args[0].Get("recipients"); !recipients.IsUndefined() && !recipients.IsNull() {
for i := 0; i < recipients.Length(); i++ {
item := recipients.Index(i)
data, err := bytesFromJS(item.Get("certificate"))
if err != nil {
return nil, err
}
certificate, err := ofdgo.ParseEncryptionCertificate(data)
if err != nil {
return nil, fmt.Errorf("接收者证书无效或不支持SM2加密")
}
options.Recipients = append(options.Recipients, ofdgo.EncryptionRecipient{UserName: item.Get("userName").String(), Certificate: certificate})
}
}
if len(password) == 0 && len(options.Recipients) == 0 {
return nil, ofdgo.ErrEncryptionPolicyRequired
}
reader := currentSession.Reader
if currentEditor != nil {
reader, err = currentEditor.Reader()
if err != nil {
return nil, err
}
defer reader.Close()
}
editor, err := reader.Editor()
if err != nil {
return nil, err
}
if err := editor.SetEncryption(options); err != nil {
return nil, err
}
return saveEditor(editor, args[1:])
}
// saveSigned 调用库层最终包签署并在回验成功后交付浏览器
// 入参: args 本机证书私钥、信任根、印章位置、写出回调及进度回调
// 返回: any 保存结果, error 错误信息
func saveSigned(args []js.Value) (any, error) {
if currentSession == nil || len(args) < 3 {
return nil, fmt.Errorf("missing signature arguments")
}
input := args[0]
key, err := bytesFromJS(input.Get("key"))
if err != nil {
return nil, err
}
defer clear(key)
certificate, err := bytesFromJS(input.Get("certificate"))
if err != nil {
return nil, err
}
var password []byte
if field := input.Get("keyPassword"); !field.IsUndefined() && !field.IsNull() {
password, err = bytesFromJS(field)
if err != nil {
return nil, err
}
defer clear(password)
}
signer, certificate, err := ofdgo.ParseSignatureIdentity(certificate, key, password)
if err != nil {
return nil, fmt.Errorf("证书、私钥或口令无效")
}
roots, err := securityFilesFromJS(input.Get("roots"))
if err != nil {
return nil, err
}
intermediates, err := securityFilesFromJS(input.Get("intermediates"))
if err != nil {
return nil, err
}
options := ofdgo.SignatureWriteOptions{Signer: signer, Certificate: certificate, TrustRoots: roots,
Intermediates: intermediates, Lock: input.Get("lock").Bool()}
switch input.Get("mode").String() {
case "append":
options.Mode = ofdgo.SignatureAppend
case "replace":
options.Mode = ofdgo.SignatureReplace
default:
return nil, fmt.Errorf("invalid signature mode")
}
if seal := input.Get("seal"); !seal.IsNull() && !seal.IsUndefined() {
options.Seal, err = bytesFromJS(seal)
if err != nil {
return nil, err
}
count, err := currentSession.Reader.PageCount()
if err != nil {
return nil, err
}
indices, err := ofdgo.ParsePageRange(input.Get("pages").String(), count)
if err != nil {
return nil, err
}
box := ofdgo.Box{X: input.Get("x").Float(), Y: input.Get("y").Float(), W: input.Get("width").Float(), H: input.Get("height").Float()}
if input.Get("placement").String() == "seam" {
pages := make([]int, len(indices))
for i, index := range indices {
pages[i] = index + 1
}
options.Stamps, err = currentSession.Reader.SignatureSeamStamps(pages, box)
if err != nil {
return nil, err
}
} else {
for _, index := range indices {
options.Stamps = append(options.Stamps, ofdgo.SignatureStamp{PageRef: currentSession.doc.Pages.Page[index].ID,
Boundary: fmt.Sprintf("%g %g %g %g", box.X, box.Y, box.W, box.H)})
}
}
}
progress := editorOperationProgress(args[2])
if err := progress("sign", 0, 1); err != nil {
return nil, err
}
var output bytes.Buffer
if currentEditor != nil {
currentEditor.OnWriteProgress = progress
defer func() { currentEditor.OnWriteProgress = nil }()
_, err = ofdgo.SignEditorTo(&output, currentEditor, options)
} else {
_, err = currentSession.Reader.SignTo(&output, options)
}
if err != nil {
return nil, err
}
if err := progress("sign", 1, 1); err != nil {
return nil, err
}
if _, err := (exportWriter{write: args[1]}).Write(output.Bytes()); err != nil {
return nil, err
}
return successResult(map[string]any{"mime": "application/ofd", "label": "OFD"}), nil
}
// saveEditor 统一分块写出及进度取消,加密策略由库层执行
// 入参: editor 待写出文档, args 写出回调、进度回调和可选页面索引
// 返回: any 保存结果, error 错误信息
func saveEditor(editor *ofdgo.Editor, args []js.Value) (any, error) {
if len(args) > 1 {
editor.OnWriteProgress = editorOperationProgress(args[1])
defer func() { editor.OnWriteProgress = nil }()
}
writer := bufio.NewWriterSize(exportWriter{write: args[0]}, 1<<20)
var err error
if len(args) > 2 {
_, err = currentEditor.WritePagesTo(writer, indexesFromJS(args[2]))
_, err = editor.WritePagesTo(writer, indexesFromJS(args[2]))
} else {
_, err = currentEditor.WriteTo(writer)
_, err = editor.WriteTo(writer)
}
if err != nil {
return nil, err
+115 -7
View File
@@ -30,6 +30,7 @@ import (
type OpenOptions struct {
Fonts []FontFile
RenderAnnotations bool
ReaderOptions []ofdgo.ReaderOption
}
// FontFile 字体文件
@@ -40,6 +41,23 @@ type FontInfo = ofdgo.FontInfo
// SignatureInfo 签名验证信息
type SignatureInfo struct {
DocIndex int `json:"docIndex"`
DocRoot string `json:"docRoot,omitempty"`
IntegrityValid bool `json:"integrityValid"`
TrustedValid bool `json:"trustedValid"`
PolicyChecked bool `json:"policyChecked"`
PolicyOK bool `json:"policyOK"`
PolicyError string `json:"policyError,omitempty"`
CoverageChecked bool `json:"coverageChecked"`
CoverageOK bool `json:"coverageOK"`
CoverageError string `json:"coverageError,omitempty"`
UncoveredFiles []string `json:"uncoveredFiles,omitempty"`
TimestampChecked bool `json:"timestampChecked"`
TimestampOK bool `json:"timestampOK"`
Timestamps []TimestampInfo `json:"timestamps,omitempty"`
RevocationChecked bool `json:"revocationChecked"`
RevocationOK bool `json:"revocationOK"`
Revocations []RevocationInfo `json:"revocations,omitempty"`
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
@@ -72,6 +90,7 @@ type SignatureInfo struct {
CertTimeOK bool `json:"certTimeOK,omitempty"`
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
CertTrustOK bool `json:"certTrustOK,omitempty"`
CertTrustError string `json:"certTrustError,omitempty"`
ReferenceCount int `json:"referenceCount"`
ReferenceChecked int `json:"referenceChecked"`
ReferencePassed int `json:"referencePassed"`
@@ -85,6 +104,34 @@ type SignatureInfo struct {
Error string `json:"error,omitempty"`
}
// TimestampInfo 独立展示时间戳绑定、签名、信任和证书时效
type TimestampInfo struct {
Time string `json:"time,omitempty"`
Valid bool `json:"valid"`
BindingChecked bool `json:"bindingChecked"`
BindingOK bool `json:"bindingOK"`
SignedValueChecked bool `json:"signedValueChecked"`
SignedValueOK bool `json:"signedValueOK"`
CertTrustChecked bool `json:"certTrustChecked"`
CertTrustOK bool `json:"certTrustOK"`
CertTimeChecked bool `json:"certTimeChecked"`
CertTimeOK bool `json:"certTimeOK"`
Error string `json:"error,omitempty"`
}
// RevocationInfo 仅表示签者或制章证书的离线撤销状态
type RevocationInfo struct {
Subject string `json:"subject,omitempty"`
Source string `json:"source,omitempty"`
Status string `json:"status"`
Checked bool `json:"checked"`
OK bool `json:"ok"`
ThisUpdate string `json:"thisUpdate,omitempty"`
NextUpdate string `json:"nextUpdate,omitempty"`
RevokedAt string `json:"revokedAt,omitempty"`
Error string `json:"error,omitempty"`
}
// SignatureStampInfo 签名外观信息
type SignatureStampInfo struct {
ID string `json:"id,omitempty"`
@@ -119,6 +166,7 @@ type Session struct {
// DocumentInfo 文档信息
type DocumentInfo struct {
Encryption EncryptionInfo `json:"encryption"`
Version string `json:"version"`
DocType string `json:"docType"`
Title string `json:"title"`
@@ -139,6 +187,21 @@ type DocumentInfo struct {
DetailsPending bool `json:"detailsPending,omitempty"`
}
// EncryptionInfo 仅向界面传递非敏感加密状态
type EncryptionInfo struct {
Encrypted bool `json:"encrypted"`
Method string `json:"method,omitempty"`
Users []string `json:"users,omitempty"`
Layers int `json:"layers,omitempty"`
}
// encryptionInfo 获取当前文档的加密来源,不包含凭据
// 返回: EncryptionInfo 加密状态
func (s *Session) encryptionInfo() EncryptionInfo {
info := s.Reader.Encryption()
return EncryptionInfo{Encrypted: info.Encrypted, Method: info.Method, Users: info.Users, Layers: info.Layers}
}
// OutlineInfo 目录节点信息
type OutlineInfo = ofdgo.OutlineInfo
@@ -222,7 +285,7 @@ func Open(data []byte, opts OpenOptions) (*Session, error) {
if len(data) == 0 {
return nil, fmt.Errorf("empty ofd data")
}
reader, err := ofdgo.NewReader(bytes.NewReader(data), int64(len(data)))
reader, err := ofdgo.NewReader(bytes.NewReader(data), int64(len(data)), opts.ReaderOptions...)
if err != nil {
return nil, err
}
@@ -381,6 +444,7 @@ func (s *Session) SearchPage(index int, query string) ([]ofdgo.TextMatch, error)
// 返回: DocumentInfo 文档信息
func (s *Session) Summary() DocumentInfo {
info := DocumentInfo{
Encryption: s.encryptionInfo(),
Version: s.Reader.Version(),
DocType: s.Reader.DocType(),
PageCount: len(s.doc.Pages.Page),
@@ -425,11 +489,12 @@ func (s *Session) Summary() DocumentInfo {
// 返回: DocumentInfo 文档信息
func (s *Session) Info() DocumentInfo {
info := DocumentInfo{
Version: s.Reader.Version(),
DocType: s.Reader.DocType(),
PageCount: len(s.doc.Pages.Page),
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
Outlines: s.doc.OutlineInfos(),
Encryption: s.encryptionInfo(),
Version: s.Reader.Version(),
DocType: s.Reader.DocType(),
PageCount: len(s.doc.Pages.Page),
Pages: make([]PageInfo, 0, len(s.doc.Pages.Page)),
Outlines: s.doc.OutlineInfos(),
}
if attachments, err := s.Reader.Attachments(); err == nil {
for _, attachment := range attachments {
@@ -613,7 +678,22 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
dateTime = report.SignatureTime.Format(time.RFC3339Nano)
}
checked, passed := signatureReferenceCounts(report.References)
return SignatureInfo{
info := SignatureInfo{
DocIndex: report.DocIndex,
DocRoot: report.DocRoot,
IntegrityValid: report.IntegrityValid(),
TrustedValid: report.TrustedValid(),
PolicyChecked: report.PolicyChecked,
PolicyOK: report.PolicyOK,
PolicyError: report.PolicyError,
CoverageChecked: report.CoverageChecked,
CoverageOK: report.CoverageOK,
CoverageError: report.CoverageError,
UncoveredFiles: report.UncoveredFiles,
TimestampChecked: report.TimestampChecked,
TimestampOK: report.TimestampOK,
RevocationChecked: report.RevocationChecked,
RevocationOK: report.RevocationOK,
ID: report.ID,
Type: string(report.Type),
Status: status,
@@ -646,6 +726,7 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
CertTimeOK: report.CertTimeOK,
CertTrustChecked: report.CertTrustChecked,
CertTrustOK: report.CertTrustOK,
CertTrustError: report.CertTrustError,
ReferenceCount: len(report.References),
ReferenceChecked: checked,
ReferencePassed: passed,
@@ -658,6 +739,33 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
Stamps: signatureStampInfos(report.StampPositions),
Error: signatureReportError(report),
}
for _, stamp := range report.Timestamps {
info.Timestamps = append(info.Timestamps, TimestampInfo{
Time: securityTime(stamp.Time), Valid: stamp.Valid,
BindingChecked: stamp.BindingChecked, BindingOK: stamp.BindingOK,
SignedValueChecked: stamp.SignedValueChecked, SignedValueOK: stamp.SignedValueOK,
CertTrustChecked: stamp.CertTrustChecked, CertTrustOK: stamp.CertTrustOK,
CertTimeChecked: stamp.CertTimeChecked, CertTimeOK: stamp.CertTimeOK, Error: stamp.Error,
})
}
for _, revocation := range report.Revocations {
info.Revocations = append(info.Revocations, RevocationInfo{
Subject: revocation.Certificate.Subject, Source: revocation.Source, Status: revocation.Status,
Checked: revocation.Checked, OK: revocation.OK, ThisUpdate: securityTime(revocation.ThisUpdate),
NextUpdate: securityTime(revocation.NextUpdate), RevokedAt: securityTime(revocation.RevokedAt), Error: revocation.Error,
})
}
return info
}
// securityTime 格式化已提供的安全证据时间,零值不展示
// 入参: value 时间
// 返回: string 标准时间文本
func securityTime(value time.Time) string {
if value.IsZero() {
return ""
}
return value.Format(time.RFC3339Nano)
}
// signatureSigner 获取签名人名称
+6 -6
View File
@@ -24,9 +24,9 @@ import (
// Open 打开OFD文件
// 返回的阅读器持有文件,使用完毕后需调用Close
// 入参: path 文件路径
// 入参: path 文件路径, options 阅读选项
// 返回: *Reader 阅读器实例, error 错误信息
func Open(path string) (*Reader, error) {
func Open(path string, options ...ReaderOption) (*Reader, error) {
r, err := zip.OpenReader(path)
if err != nil {
return nil, err
@@ -36,7 +36,7 @@ func Open(path string) (*Reader, error) {
Zip: &r.Reader,
Closer: r,
}
if err := reader.initRoot(); err != nil {
if err := reader.open(options); err != nil {
reader.Close()
return nil, err
}
@@ -45,9 +45,9 @@ func Open(path string) (*Reader, error) {
// NewReader 从IO读取器创建OFD阅读器
// 读取器的生命周期由调用方管理,Reader.Close不会关闭它
// 入参: r IO读取器, size 数据大小
// 入参: r IO读取器, size 数据大小, options 阅读选项
// 返回: *Reader 阅读器实例, error 错误信息
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
func NewReader(r io.ReaderAt, size int64, options ...ReaderOption) (*Reader, error) {
zr, err := zip.NewReader(r, size)
if err != nil {
return nil, err
@@ -55,7 +55,7 @@ func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
reader := &Reader{
Zip: zr,
}
if err := reader.initRoot(); err != nil {
if err := reader.open(options); err != nil {
return nil, err
}
return reader, nil
+137
View File
@@ -0,0 +1,137 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"crypto"
"errors"
)
// 加密错误可通过errors.Is识别,不包含口令或私钥
var (
ErrCredentialsRequired = errors.New("document credentials required")
ErrInvalidCredentials = errors.New("invalid credentials or damaged encrypted document")
ErrUnsupportedEncryption = errors.New("unsupported document encryption")
ErrEncryptionPolicyRequired = errors.New("explicit encryption policy required")
)
// Credentials 解密凭据,UserName为空时尝试匹配用户;证书采用DER编码
// Password与Decrypter分别用于口令和证书方案,私钥可由外部密码设备持有
type Credentials struct {
UserName string
Password []byte
Certificate []byte
Decrypter crypto.Decrypter
}
// CryptoProvider 提供GM/T0099的密码运算,不依赖渲染后端
// SM4采用CBC模式及PKCS#7填充,PasswordKey采用GB/T32918的SM3密钥派生
// EncryptKey返回SM2密文的ASN.1编码;签名和私钥解密使用crypto.Signer与crypto.Decrypter
// 提供者不得修改输入字节,返回结果由调用方持有
type CryptoProvider interface {
EncryptSM4(key, iv, plaintext []byte) ([]byte, error)
DecryptSM4(key, iv, ciphertext []byte) ([]byte, error)
PasswordKey(password []byte) ([]byte, error)
EncryptKey(certificate, key []byte) ([]byte, error)
}
// ReaderOption 阅读器的凭据与密码提供者选项
type ReaderOption func(*readerOptions)
// readerOptions 保存打开文档时的会话参数
type readerOptions struct {
credentials []Credentials
provider CryptoProvider
}
// WithCredentials 添加解密凭据,多次调用可解开多层加密
// 入参: credentials 解密凭据
// 返回: ReaderOption 阅读选项
func WithCredentials(credentials Credentials) ReaderOption {
credentials.Password = bytes.Clone(credentials.Password)
credentials.Certificate = bytes.Clone(credentials.Certificate)
return func(options *readerOptions) {
copy := credentials
copy.Password = bytes.Clone(credentials.Password)
copy.Certificate = bytes.Clone(credentials.Certificate)
options.credentials = append(options.credentials, copy)
}
}
// WithCryptoProvider 设置文档解密的密码提供者
// 入参: provider 密码提供者,nil使用内置实现
// 返回: ReaderOption 阅读选项
func WithCryptoProvider(provider CryptoProvider) ReaderOption {
return func(options *readerOptions) { options.provider = provider }
}
// EncryptionRecipient 证书加密接收者,Certificate为SM2公钥证书的DER编码
type EncryptionRecipient struct {
UserName string
UserType string
Certificate []byte
}
// EncryptionOptions 设置GM/T0099加密,口令与证书方案二选一
// 口令方案应使用足够长的随机口令;标准密钥派生不提供慢速口令哈希
type EncryptionOptions struct {
Password []byte
UserName string
UserType string
Recipients []EncryptionRecipient
Provider CryptoProvider
}
// EncryptionInfo 不含敏感凭据的文档加密信息
type EncryptionInfo struct {
Encrypted bool
Method string
Users []string
Layers int
}
// encryptionState 保存解密来源与可继承的输出策略
type encryptionState struct {
info EncryptionInfo
options *EncryptionOptions
}
// Encryption 获取文档加密状态,不暴露口令或私钥
// 返回: EncryptionInfo 加密状态
func (r *Reader) Encryption() EncryptionInfo {
if r.encryption == nil {
return EncryptionInfo{}
}
info := r.encryption.info
info.Users = append([]string(nil), info.Users...)
return info
}
// cloneEncryptionOptions 复制可继承的加密策略,密码提供者保持共享
// 入参: options 加密策略
// 返回: *EncryptionOptions 独立策略副本
func cloneEncryptionOptions(options *EncryptionOptions) *EncryptionOptions {
if options == nil {
return nil
}
copy := *options
copy.Password = bytes.Clone(options.Password)
copy.Recipients = append([]EncryptionRecipient(nil), options.Recipients...)
for i := range copy.Recipients {
copy.Recipients[i].Certificate = bytes.Clone(copy.Recipients[i].Certificate)
}
return &copy
}
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"io"
)
// SetEncryption 设置后续保存的加密策略,nil显式选择明文保存
// 打开单用户口令或证书加密文档时默认继承原策略;多人口令和多层加密需显式选择输出策略
// 新的加密密钥与初始向量在每次保存时独立生成,设置不改变文档内容或撤销历史
// 入参: options 加密策略
// 返回: error 错误信息
func (e *Editor) SetEncryption(options *EncryptionOptions) error {
if options == nil {
e.encryption = nil
return nil
}
if err := validateEncryptionOptions(*options); err != nil {
return err
}
info := EncryptionInfo{Encrypted: true, Method: "1.1.1", Layers: 1}
if len(options.Password) != 0 {
name := options.UserName
if name == "" {
name = "User"
}
info.Users = []string{name}
} else {
info.Method = "1.1.2"
for _, recipient := range options.Recipients {
info.Users = append(info.Users, recipient.UserName)
}
}
e.encryption = &encryptionState{info: info, options: cloneEncryptionOptions(options)}
return nil
}
// writeEncrypted 在全部准备及加密成功后写出结果,取消不产生输出
// 输出流自身写入失败时调用方仍需丢弃本次输出
// 入参: writer 输出流
// 返回: int64 写入字节数, error 错误信息
func (e *Editor) writeEncrypted(writer io.Writer) (int64, error) {
if e.encryption.options == nil {
return 0, ErrEncryptionPolicyRequired
}
var plaintext bytes.Buffer
if _, err := e.writePlaintext(&plaintext); err != nil {
return 0, err
}
defer clear(plaintext.Bytes())
r, err := NewReader(bytes.NewReader(plaintext.Bytes()), int64(plaintext.Len()))
if err != nil {
return 0, err
}
defer r.Close()
parts, err := r.packageData()
if err != nil {
return 0, err
}
defer func() {
for _, data := range parts {
clear(data)
}
}()
data, err := encryptPackageParts(parts, *e.encryption.options, editorProgress(e.OnWriteProgress))
if err != nil {
return 0, err
}
return io.Copy(writer, bytes.NewReader(data))
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/rand"
"fmt"
"github.com/emmansun/gmsm/padding"
"github.com/emmansun/gmsm/sm2"
"github.com/emmansun/gmsm/sm3"
"github.com/emmansun/gmsm/sm4"
"github.com/emmansun/gmsm/smx509"
)
// GMCryptoProvider 使用纯Go国密实现,可在原生环境和WASM使用
type GMCryptoProvider struct{}
// EncryptSM4 使用SM4-CBC及PKCS#7填充加密
// 入参: key 密钥, iv 初始向量, plaintext 明文
// 返回: []byte 密文, error 错误信息
func (GMCryptoProvider) EncryptSM4(key, iv, plaintext []byte) ([]byte, error) {
block, err := sm4.NewCipher(key)
if err != nil {
return nil, err
}
if len(iv) != block.BlockSize() {
return nil, fmt.Errorf("invalid SM4 IV length")
}
out := padding.NewPKCS7Padding(sm4.BlockSize).Pad(bytes.Clone(plaintext))
cipher.NewCBCEncrypter(block, iv).CryptBlocks(out, out)
return out, nil
}
// DecryptSM4 解密SM4-CBC并校验PKCS#7填充
// 入参: key 密钥, iv 初始向量, ciphertext 密文
// 返回: []byte 明文, error 错误信息
func (GMCryptoProvider) DecryptSM4(key, iv, ciphertext []byte) ([]byte, error) {
block, err := sm4.NewCipher(key)
if err != nil {
return nil, err
}
if len(iv) != block.BlockSize() || len(ciphertext) == 0 || len(ciphertext)%block.BlockSize() != 0 {
return nil, ErrInvalidCredentials
}
out := make([]byte, len(ciphertext))
cipher.NewCBCDecrypter(block, iv).CryptBlocks(out, ciphertext)
plain, err := padding.NewPKCS7Padding(sm4.BlockSize).ConstantTimeUnpad(out)
if err != nil {
clear(out)
return nil, ErrInvalidCredentials
}
return plain, nil
}
// PasswordKey 按GB/T32918派生16字节口令密钥
// 入参: password UTF-8口令
// 返回: []byte 密钥, error 错误信息
func (GMCryptoProvider) PasswordKey(password []byte) ([]byte, error) {
if len(password) == 0 {
return nil, ErrCredentialsRequired
}
return sm3.Kdf(password, sm4.BlockSize), nil
}
// EncryptKey 使用SM2公钥证书封装文件密钥
// 入参: certificate DER证书, key 文件密钥
// 返回: []byte ASN.1密文, error 错误信息
func (GMCryptoProvider) EncryptKey(certificate, key []byte) ([]byte, error) {
cert, err := parseEncryptionCertificate(certificate)
if err != nil {
return nil, err
}
return sm2.EncryptASN1(rand.Reader, cert.PublicKey.(*ecdsa.PublicKey), key)
}
// ParseEncryptionCertificate 解析单张SM2加密证书并返回DER副本
// 仅检查格式、算法和密钥用途,接收者身份及信任应由调用方确认
// 入参: certificate PEM或DER证书
// 返回: []byte DER证书, error 错误信息
func ParseEncryptionCertificate(certificate []byte) ([]byte, error) {
cert, err := parseEncryptionCertificate(certificate)
if err != nil {
return nil, err
}
return bytes.Clone(cert.Raw), nil
}
// parseEncryptionCertificate 解析并检查加密证书
// 入参: certificate PEM或DER证书
// 返回: *smx509.Certificate 证书, error 错误信息
func parseEncryptionCertificate(certificate []byte) (*smx509.Certificate, error) {
cert, err := parseSignatureCertificate(certificate)
if err != nil {
return nil, err
}
public, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok || !sm2.IsSM2PublicKey(public) {
return nil, fmt.Errorf("SM2 encryption certificate required")
}
if cert.KeyUsage != 0 && cert.KeyUsage&(smx509.KeyUsageKeyEncipherment|smx509.KeyUsageDataEncipherment|smx509.KeyUsageKeyAgreement) == 0 {
return nil, fmt.Errorf("certificate does not permit encryption")
}
return cert, nil
}
+550
View File
@@ -0,0 +1,550 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"archive/zip"
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/xml"
"fmt"
"maps"
"slices"
"strings"
"time"
)
// encryptionList 对应GM/T0099的加密入口
type encryptionList struct {
XMLName xml.Name `xml:"http://www.ofdspec.org/2016 Encryptions"`
Records []encryptionRecord `xml:"EncryptInfo"`
}
// encryptionRecord 关联密钥描述和明密文映射
type encryptionRecord struct {
ID string `xml:"ID,attr"`
Relative string `xml:"Relative,attr,omitempty"`
Provider encryptionProvider `xml:"Provider"`
Scope string `xml:"EncryptScope"`
Date string `xml:"EncryptDate,omitempty"`
Seed string `xml:"DecryptSeedLoc"`
Entries string `xml:"EntriesMapLoc"`
}
// encryptionProvider 标识生成加密包的组件
type encryptionProvider struct {
Name string `xml:"Name,attr"`
Version string `xml:"Version,attr"`
}
// encryptionSeed 保存标准的文件密钥包装信息
type encryptionSeed struct {
XMLName xml.Name `xml:"http://www.ofdspec.org/2016 DecyptSeed"`
ID string `xml:"ID,attr"`
Method string `xml:"EncryptCaseId,attr"`
Users []encryptionUser `xml:"UserInfo"`
Params encryptionParams `xml:"ExtendParams"`
}
// encryptionParams 保留未知扩展,避免默认另存时静默丢失其语义
type encryptionParams struct {
Raw string `xml:",innerxml"`
}
// encryptionUser 保存单个接收者的公开信息及包装密钥
type encryptionUser struct {
Name string `xml:"UserName,attr"`
Role string `xml:"UserType,attr,omitempty"`
Certificate string `xml:"UserCert,omitempty"`
Key string `xml:"EncryptedWK"`
IV string `xml:"IVValue"`
}
// encryptionEntries 保存明密文映射
type encryptionEntries struct {
XMLName xml.Name
ID string `xml:"ID,attr"`
Entries []encryptionEntry `xml:"EncryptEntry"`
}
// encryptionEntry 对应一项包内文件加密映射
type encryptionEntry struct {
Path string `xml:"Path,attr"`
EncryptedPath string `xml:"EPath,attr"`
Seed string `xml:"DecryptSeedLoc,attr,omitempty"`
}
// open 初始化包索引并在解析OFD根节点前解密
// 入参: options 阅读选项
// 返回: error 错误信息
func (r *Reader) open(options []ReaderOption) error {
settings := readerOptions{provider: GMCryptoProvider{}}
for _, option := range options {
option(&settings)
}
if settings.provider == nil {
settings.provider = GMCryptoProvider{}
}
defer func() {
for i := range settings.credentials {
clear(settings.credentials[i].Password)
}
}()
if err := r.indexPackage(); err != nil {
return err
}
if _, ok := r.fileNamesFold["encryptions.xml"]; ok {
if err := r.decryptPackage(settings); err != nil {
return err
}
}
return r.initRoot()
}
// packageData 获取包内所有条目的实际字节,保留未识别文件
// 返回: map[string][]byte 包内条目, error 错误信息
func (r *Reader) packageData() (map[string][]byte, error) {
parts := make(map[string][]byte, len(r.fileNamesFold))
for _, name := range r.fileNamesFold {
data, err := r.readFile(name)
if err != nil {
return nil, err
}
parts[name] = data
}
return parts, nil
}
// decryptPackage 在独立内存包内解密,不修改输入文件
// 入参: settings 会话凭据和密码实现
// 返回: error 错误信息
func (r *Reader) decryptPackage(settings readerOptions) error {
data, err := r.readFile("Encryptions.xml")
if err != nil {
return err
}
var list encryptionList
if err := xml.Unmarshal(data, &list); err != nil || len(list.Records) == 0 {
return fmt.Errorf("%w: invalid Encryptions.xml", ErrUnsupportedEncryption)
}
if len(settings.credentials) == 0 {
return ErrCredentialsRequired
}
parts, err := r.packageData()
if err != nil {
return err
}
state := &encryptionState{info: EncryptionInfo{Encrypted: true, Layers: len(list.Records)}}
list.Records, err = orderEncryptionRecords(list.Records)
if err != nil {
return err
}
for i := len(list.Records) - 1; i >= 0; i-- {
record := list.Records[i]
seedData, seedName, err := encryptionPart(parts, record.Seed)
if err != nil {
return err
}
var seed encryptionSeed
if err := xml.Unmarshal(seedData, &seed); err != nil || seed.ID != record.ID || len(seed.Users) == 0 {
return fmt.Errorf("%w: invalid encryption seed", ErrUnsupportedEncryption)
}
key, iv, credential, err := unwrapEncryptionKey(seed, settings)
if err != nil {
return err
}
mapData, mapName, err := encryptionPart(parts, record.Entries)
var entries encryptionEntries
if err == nil {
entries, err = decodeEncryptionEntries(mapData, key, iv, settings.provider)
}
if err != nil || entries.ID != record.ID || len(entries.Entries) == 0 {
clear(key)
return ErrInvalidCredentials
}
decrypted := make(map[string][]byte, len(entries.Entries))
used := make(map[string]bool)
extraSeeds := make(map[string]bool)
for _, entry := range entries.Entries {
name := cleanPackagePath(entry.Path)
fold := strings.ToLower(name)
if !validPackagePath(name) || used[fold] {
clear(key)
return fmt.Errorf("ambiguous encrypted package path: %q", entry.Path)
}
entryKey, entryIV := key, iv
if entry.Seed != "" && cleanPackagePath(entry.Seed) != seedName {
seedData, extraSeed, seedErr := encryptionPart(parts, entry.Seed)
var entrySeed encryptionSeed
if seedErr == nil {
seedErr = xml.Unmarshal(seedData, &entrySeed)
}
if seedErr == nil {
entryKey, entryIV, _, seedErr = unwrapEncryptionKey(entrySeed, settings)
}
if seedErr != nil {
clear(key)
return seedErr
}
extraSeeds[extraSeed] = true
}
ciphertext, actual, err := encryptionPart(parts, entry.EncryptedPath)
if err == nil {
decrypted[name], err = settings.provider.DecryptSM4(entryKey, entryIV, ciphertext)
}
if entry.Seed != "" && cleanPackagePath(entry.Seed) != seedName {
clear(entryKey)
}
if err != nil {
clear(key)
return ErrInvalidCredentials
}
delete(parts, actual)
used[fold] = true
}
clear(key)
delete(parts, seedName)
delete(parts, mapName)
for name := range extraSeeds {
delete(parts, name)
}
remaining := make(map[string]string, len(parts))
for name := range parts {
remaining[strings.ToLower(name)] = name
}
for name, plain := range decrypted {
if existing, ok := remaining[strings.ToLower(name)]; ok {
delete(parts, existing)
}
parts[name] = plain
}
state.info.Method = seed.Method
for _, user := range seed.Users {
state.info.Users = append(state.info.Users, user.Name)
}
if len(list.Records) == 1 && len(extraSeeds) == 0 {
state.options = inheritedEncryptionOptions(seed, credential, settings.provider)
}
}
for name := range parts {
if strings.ToLower(name) == "encryptions.xml" {
delete(parts, name)
}
}
r.Zip, r.files, r.encryption = nil, parts, state
return nil
}
// decodeEncryptionEntries 读取标准允许的明文或密文明密文映射表
// 入参: data 映射文件, key 文件密钥, iv 初始向量, provider 密码实现
// 返回: encryptionEntries 映射表, error 错误信息
func decodeEncryptionEntries(data, key, iv []byte, provider CryptoProvider) (encryptionEntries, error) {
var entries encryptionEntries
valid := func() bool {
return entries.XMLName.Space == ofdNamespace && (entries.XMLName.Local == "EncryptEntries" || entries.XMLName.Local == "EncryptedEntries")
}
if err := xml.Unmarshal(data, &entries); err == nil && valid() {
return entries, nil
}
plain, err := provider.DecryptSM4(key, iv, data)
if err != nil {
return encryptionEntries{}, err
}
defer clear(plain)
entries = encryptionEntries{}
if err := xml.Unmarshal(plain, &entries); err != nil || !valid() {
return encryptionEntries{}, ErrInvalidCredentials
}
return entries, nil
}
// encryptionPart 按统一大小写规则读取加密元数据引用
// 入参: parts 包内条目, name 引用路径
// 返回: []byte 条目内容, string 实际路径, error 错误信息
func encryptionPart(parts map[string][]byte, name string) ([]byte, string, error) {
name = cleanPackagePath(name)
if !validPackagePath(name) {
return nil, "", fmt.Errorf("invalid encryption path: %q", name)
}
if data, ok := parts[name]; ok {
return data, name, nil
}
for actual, data := range parts {
if strings.ToLower(actual) == strings.ToLower(name) {
return data, actual, nil
}
}
return nil, "", fmt.Errorf("missing encrypted entry: %s", name)
}
// orderEncryptionRecords 按前驱关系排序加密层,拒绝环和缺失的前驱
// 入参: records 加密记录
// 返回: []encryptionRecord 从内到外的顺序, error 错误信息
func orderEncryptionRecords(records []encryptionRecord) ([]encryptionRecord, error) {
index := make(map[string]encryptionRecord, len(records))
for _, record := range records {
if _, exists := index[record.ID]; exists || record.ID == "" {
return nil, fmt.Errorf("%w: duplicate encryption operation", ErrUnsupportedEncryption)
}
index[record.ID] = record
}
states := make(map[string]uint8, len(records))
ordered := make([]encryptionRecord, 0, len(records))
for _, record := range records {
chain := []encryptionRecord{}
for record.ID != "" && states[record.ID] != 2 {
if states[record.ID] == 1 {
return nil, fmt.Errorf("%w: cyclic encryption operations", ErrUnsupportedEncryption)
}
states[record.ID] = 1
chain = append(chain, record)
if record.Relative == "" {
break
}
previous, ok := index[record.Relative]
if !ok {
return nil, fmt.Errorf("%w: missing encryption predecessor", ErrUnsupportedEncryption)
}
record = previous
}
for i := len(chain) - 1; i >= 0; i-- {
states[chain[i].ID] = 2
ordered = append(ordered, chain[i])
}
}
return ordered, nil
}
// unwrapEncryptionKey 按方案尝试会话凭据,不向错误信息写入口令
// 入参: seed 密钥描述, settings 会话凭据
// 返回: []byte 文件密钥, []byte 初始向量, Credentials 命中凭据, error 错误信息
func unwrapEncryptionKey(seed encryptionSeed, settings readerOptions) ([]byte, []byte, Credentials, error) {
if seed.Method != "1.1.1" && seed.Method != "1.1.2" {
return nil, nil, Credentials{}, ErrUnsupportedEncryption
}
for _, credential := range settings.credentials {
for _, user := range seed.Users {
if credential.UserName != "" && credential.UserName != user.Name {
continue
}
wrapped, err := base64.StdEncoding.DecodeString(user.Key)
if err != nil {
return nil, nil, Credentials{}, ErrInvalidCredentials
}
iv, err := base64.StdEncoding.DecodeString(user.IV)
if user.IV == "" {
iv = make([]byte, 16)
}
if err != nil || len(iv) != 16 {
return nil, nil, Credentials{}, ErrInvalidCredentials
}
var key []byte
if seed.Method == "1.1.1" && len(credential.Password) != 0 {
kek, keyErr := settings.provider.PasswordKey(credential.Password)
if keyErr != nil {
return nil, nil, Credentials{}, keyErr
}
key, err = settings.provider.DecryptSM4(kek, iv, wrapped)
clear(kek)
} else if seed.Method == "1.1.2" && credential.Decrypter != nil {
certificate, decodeErr := base64.StdEncoding.DecodeString(user.Certificate)
if decodeErr != nil || len(certificate) == 0 {
return nil, nil, Credentials{}, ErrInvalidCredentials
}
if len(credential.Certificate) != 0 && !bytes.Equal(credential.Certificate, certificate) {
continue
}
key, err = credential.Decrypter.Decrypt(rand.Reader, wrapped, nil)
} else {
continue
}
if err == nil && len(key) == 16 {
return key, iv, credential, nil
}
clear(key)
}
}
return nil, nil, Credentials{}, ErrInvalidCredentials
}
// inheritedEncryptionOptions 保留能够完整恢复的原接收者策略
// 入参: seed 原密钥描述, credential 命中凭据, provider 密码实现
// 返回: *EncryptionOptions 可继承策略,nil表示需调用方显式选择
func inheritedEncryptionOptions(seed encryptionSeed, credential Credentials, provider CryptoProvider) *EncryptionOptions {
if strings.TrimSpace(seed.Params.Raw) != "" {
return nil
}
options := &EncryptionOptions{Provider: provider}
if seed.Method == "1.1.1" {
if len(seed.Users) != 1 {
return nil
}
options.UserName, options.UserType, options.Password = seed.Users[0].Name, seed.Users[0].Role, bytes.Clone(credential.Password)
return options
}
for _, user := range seed.Users {
certificate, err := base64.StdEncoding.DecodeString(user.Certificate)
if err != nil {
return nil
}
options.Recipients = append(options.Recipients, EncryptionRecipient{UserName: user.Name, UserType: user.Role, Certificate: certificate})
}
return options
}
// EncryptPackage 对完整OFD包进行GM/T0099加密,保留全部条目的原始内容
// 先签后加密可保留对明文的验签能力;出错不返回部分密文,已有加密包须先解密
// 入参: data 明文OFD包, options 加密策略
// 返回: []byte 加密OFD包, error 错误信息
func EncryptPackage(data []byte, options EncryptionOptions) ([]byte, error) {
r, err := NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, err
}
defer r.Close()
parts, err := r.packageData()
if err != nil {
return nil, err
}
return encryptPackageParts(parts, options, nil)
}
// encryptPackageParts 加密所有原条目,再生成标准入口、密钥描述与映射
// 入参: parts 明文条目, options 加密策略, progress 取消与进度检查
// 返回: []byte 加密包, error 错误信息
func encryptPackageParts(parts map[string][]byte, options EncryptionOptions, progress editorProgress) ([]byte, error) {
if err := validateEncryptionOptions(options); err != nil {
return nil, err
}
provider := options.Provider
if provider == nil {
provider = GMCryptoProvider{}
}
key, iv, identifier := make([]byte, 16), make([]byte, 16), make([]byte, 16)
for _, buffer := range [][]byte{key, iv, identifier} {
if _, err := rand.Read(buffer); err != nil {
return nil, err
}
}
defer clear(key)
id := "E" + hex.EncodeToString(identifier)
seed := encryptionSeed{ID: id, Method: "1.1.1"}
if len(options.Password) != 0 {
kek, err := provider.PasswordKey(options.Password)
if err != nil {
return nil, err
}
wrapped, err := provider.EncryptSM4(kek, iv, key)
clear(kek)
if err != nil {
return nil, err
}
name := options.UserName
if name == "" {
name = "User"
}
seed.Users = []encryptionUser{{Name: name, Role: options.UserType, Key: base64.StdEncoding.EncodeToString(wrapped), IV: base64.StdEncoding.EncodeToString(iv)}}
} else {
seed.Method = "1.1.2"
for _, recipient := range options.Recipients {
certificate, err := ParseEncryptionCertificate(recipient.Certificate)
if err != nil {
return nil, err
}
wrapped, err := provider.EncryptKey(certificate, key)
if err != nil {
return nil, err
}
seed.Users = append(seed.Users, encryptionUser{Name: recipient.UserName, Role: recipient.UserType, Certificate: base64.StdEncoding.EncodeToString(certificate), Key: base64.StdEncoding.EncodeToString(wrapped), IV: base64.StdEncoding.EncodeToString(iv)})
}
}
entries := encryptionEntries{XMLName: xml.Name{Space: ofdNamespace, Local: "EncryptEntries"}, ID: id}
cipherParts := make(map[string][]byte, len(parts)+3)
for i, name := range slices.Sorted(maps.Keys(parts)) {
if err := progress.report("encrypt", i, len(parts)); err != nil {
return nil, err
}
encryptedName := fmt.Sprintf("Encrypt/Files/%d.dat", i)
data, err := provider.EncryptSM4(key, iv, parts[name])
if err != nil {
return nil, err
}
cipherParts[encryptedName] = data
entries.Entries = append(entries.Entries, encryptionEntry{Path: "/" + name, EncryptedPath: "/" + encryptedName})
}
mapData, err := xml.Marshal(entries)
if err != nil {
return nil, err
}
mapData, err = provider.EncryptSM4(key, iv, mapData)
if err != nil {
return nil, err
}
cipherParts["Encrypt/Entries.dat"] = mapData
seedData, err := xml.Marshal(seed)
if err != nil {
return nil, err
}
cipherParts["Encrypt/DecryptSeed.dat"] = seedData
list := encryptionList{Records: []encryptionRecord{{ID: id, Provider: encryptionProvider{Name: "OFDGo", Version: "1.0"}, Scope: "All", Date: time.Now().Format(time.RFC3339), Seed: "/Encrypt/DecryptSeed.dat", Entries: "/Encrypt/Entries.dat"}}}
listData, err := xml.Marshal(list)
if err != nil {
return nil, err
}
cipherParts["Encryptions.xml"] = listData
var output bytes.Buffer
archive := zip.NewWriter(&output)
for _, name := range slices.Sorted(maps.Keys(cipherParts)) {
entry, err := archive.CreateHeader(&zip.FileHeader{Name: name, Method: zip.Store})
if err != nil {
return nil, err
}
if _, err := entry.Write(cipherParts[name]); err != nil {
return nil, err
}
}
if err := archive.Close(); err != nil {
return nil, err
}
if err := progress.report("encrypt", len(parts), len(parts)); err != nil {
return nil, err
}
return output.Bytes(), nil
}
// validateEncryptionOptions 校验输出方案,禁止空口令或混用口令与证书方案
// 入参: options 加密策略
// 返回: error 错误信息
func validateEncryptionOptions(options EncryptionOptions) error {
if (len(options.Password) != 0) == (len(options.Recipients) != 0) {
return fmt.Errorf("choose password or certificate encryption")
}
if options.UserType != "" && options.UserType != "User" && options.UserType != "Owner" {
return fmt.Errorf("invalid encryption user type")
}
seen := make(map[string]bool)
for _, recipient := range options.Recipients {
if recipient.UserName == "" || seen[recipient.UserName] || len(recipient.Certificate) == 0 {
return fmt.Errorf("invalid encryption recipient")
}
if recipient.UserType != "" && recipient.UserType != "User" && recipient.UserType != "Owner" {
return fmt.Errorf("invalid encryption recipient type")
}
seen[recipient.UserName] = true
}
return nil
}
+1
View File
@@ -61,6 +61,7 @@ type Editor struct {
source *editorSource
origins map[string]*editorObjectOrigin
outlines []byte
encryption *encryptionState
}
// SetFontFS 设置编辑与几何度量使用的外部字体来源,不嵌入或替换文档字体
+7 -2
View File
@@ -62,9 +62,11 @@ type editorPageImport struct {
// PageImportOptions 页面导入选项,零值仅导入页面及其关联资源
// OnProgress按ids、pages、resources、commit阶段回报进度,total为0表示总量未知
// 回调返回错误则取消导入,commit检查点之后统一提交;回调不可重入修改编辑器
// AllowDecrypted显式允许将加密来源导入未加密目标,不影响原文件
type PageImportOptions struct {
Outlines bool
OnProgress func(stage string, completed, total int) error
Outlines bool
AllowDecrypted bool
OnProgress func(stage string, completed, total int) error
}
// ImportPages 将来源主文档中的指定页面插入目标位置,保持输入顺序并作为一次撤销操作
@@ -83,6 +85,9 @@ func (e *Editor) ImportPages(source *Reader, indexes []int, at int) ([]string, e
// 入参: source 来源阅读器, indexes 来源页面索引, at 目标插入位置, options 导入选项
// 返回: []string 新页面标识, error 错误信息
func (e *Editor) ImportPagesWithOptions(source *Reader, indexes []int, at int, options PageImportOptions) ([]string, error) {
if source.encryption != nil && e.encryption == nil && !options.AllowDecrypted {
return nil, ErrEncryptionPolicyRequired
}
return e.importPages(source, indexes, at, false, options)
}
+2 -11
View File
@@ -116,17 +116,7 @@ type PageCapabilities struct {
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader
// 返回: *Editor 编辑器, error 错误信息
func (r *Reader) Editor() (*Editor, error) {
if r.Zip != nil {
seen := make(map[string]bool)
for _, file := range r.Zip.File {
name := cleanPackagePath(file.Name)
if seen[name] {
return nil, fmt.Errorf("ambiguous package entry %q", file.Name)
}
seen[name] = true
}
}
reader := &Reader{Zip: r.Zip, files: r.files}
reader := &Reader{Zip: r.Zip, files: r.files, encryption: r.encryption}
if err := reader.initRoot(); err != nil {
return nil, err
}
@@ -143,6 +133,7 @@ func (r *Reader) Editor() (*Editor, error) {
return nil, err
}
e := NewEditor()
e.encryption = r.encryption
e.Info, e.maxID = cloneEditorData(*info), maximum
directory := cleanPackagePath(reader.RootDir)
e.source = &editorSource{reader: reader, document: doc, info: cloneEditorData(*info), directory: directory, pages: make(map[string]*editorSourcePage), origins: make(map[string]*editorObjectOrigin)}
+1 -1
View File
@@ -538,7 +538,7 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
files = make(map[string][]byte)
}
maps.Copy(files, parts)
reader := &Reader{Zip: e.source.reader.Zip, files: files}
reader := &Reader{Zip: e.source.reader.Zip, files: files, encryption: e.encryption}
if err := reader.initRoot(); err != nil {
return nil, err
}
+54 -11
View File
@@ -49,6 +49,8 @@ type Reader struct {
fileIndex map[string]*zip.File
fileIndexFold map[string]*zip.File
files map[string][]byte
fileNamesFold map[string]string
encryption *encryptionState
}
// Close 关闭阅读器
@@ -63,17 +65,8 @@ func (r *Reader) Close() error {
// initRoot 读取根节点信息
// 返回: error 错误信息
func (r *Reader) initRoot() error {
r.fileIndex = make(map[string]*zip.File)
r.fileIndexFold = make(map[string]*zip.File)
if r.Zip != nil {
for _, f := range r.Zip.File {
name := cleanPackagePath(f.Name)
r.fileIndex[name] = f
fold := strings.ToLower(name)
if _, ok := r.fileIndexFold[fold]; !ok {
r.fileIndexFold[fold] = f
}
}
if err := r.indexPackage(); err != nil {
return err
}
data, err := r.readFile("OFD.xml")
if err != nil {
@@ -96,11 +89,58 @@ func (r *Reader) initRoot() error {
return nil
}
// indexPackage 建立包路径索引,拒绝会使显示与验签产生歧义的同名条目
// 返回: error 错误信息
func (r *Reader) indexPackage() error {
r.fileIndex = make(map[string]*zip.File)
r.fileIndexFold = make(map[string]*zip.File)
r.fileNamesFold = make(map[string]string)
if r.Zip != nil {
for _, f := range r.Zip.File {
if f.FileInfo().IsDir() {
continue
}
name := cleanPackagePath(f.Name)
if !validPackagePath(name) {
return fmt.Errorf("invalid package path: %q", f.Name)
}
fold := strings.ToLower(name)
if previous, ok := r.fileIndexFold[fold]; ok {
return fmt.Errorf("ambiguous package paths: %q and %q", previous.Name, f.Name)
}
r.fileIndex[name] = f
r.fileIndexFold[fold] = f
r.fileNamesFold[fold] = name
}
}
for name := range r.files {
if name != cleanPackagePath(name) || !validPackagePath(name) {
return fmt.Errorf("invalid package path: %q", name)
}
fold := strings.ToLower(name)
if previous, ok := r.fileNamesFold[fold]; ok && previous != name {
return fmt.Errorf("ambiguous package paths: %q and %q", previous, name)
}
r.fileNamesFold[fold] = name
}
return nil
}
// validPackagePath 判断规范化路径是否位于包内
// 入参: name 规范化路径
// 返回: bool 是否有效
func validPackagePath(name string) bool {
return name != "." && name != ".." && !strings.HasPrefix(name, "../") && !strings.HasPrefix(name, "/") && !strings.ContainsAny(name, ":\x00")
}
// readFile 读取文档内的文件
// 入参: name 文件名
// 返回: []byte 文件内容, error 错误信息
func (r *Reader) readFile(name string) ([]byte, error) {
name = cleanPackagePath(name)
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
name = actual
}
if data, ok := r.files[name]; ok {
return bytes.Clone(data), nil
}
@@ -118,6 +158,9 @@ func (r *Reader) readFile(name string) ([]byte, error) {
// 返回: io.ReadCloser 文件流, error 错误信息
func (r *Reader) openFile(name string) (io.ReadCloser, error) {
name = cleanPackagePath(name)
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
name = actual
}
if data, ok := r.files[name]; ok {
return io.NopCloser(bytes.NewReader(data)), nil
}
+71 -10
View File
@@ -29,6 +29,7 @@ const (
// digitalVerifyResult 数字签名验证结果
type digitalVerifyResult struct {
Timestamps []signatureTimestampEvidence
DataHashChecked bool
DataHashOK bool
SignedChecked bool
@@ -42,6 +43,7 @@ type digitalVerifyResult struct {
// gbtSignedData GB/T 35275 SignedData结构
type gbtSignedData struct {
Version int
ContentDigest []byte
Certs []gbtCertificate
Signers []gbtSignerInfo
@@ -56,6 +58,8 @@ type gbtCertificate struct {
// gbtSignerInfo SignedData签名者信息
type gbtSignerInfo struct {
Version int
UnauthAttrs asn1.RawValue
Issuer []byte
Serial *big.Int
DigestAlg string
@@ -70,6 +74,15 @@ type gbtSignerInfo struct {
// 返回: *digitalVerifyResult 验证结果, error 错误信息
func verifyDigitalSignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
if value, ok := normalizeGBT35275SignedValue(signedValue); ok {
sd, err := parseGBT35275SignedData(value)
if err != nil {
return nil, err
}
for _, signer := range sd.Signers {
if method != "" && !signatureAlgorithmEquivalent(method, signer.SignatureAlg, signer.DigestAlg) {
return nil, fmt.Errorf("signature method does not match SignedData")
}
}
return verifyGBT35275SignedData(value, signedData, options)
}
if isSM2SignatureMethod(method) {
@@ -99,7 +112,7 @@ func verifyRawDigitalSignature(signedValue, signedData []byte, options *signatur
result.SignerCerts = [][]byte{cert}
result.CertInfo = signatureCertInfo(cert)
result.SignedChecked = true
if sm2VerifySignature(pub, nil, signedData, signedValue) {
if verifySM2Signature(pub, nil, signedData, signedValue) {
result.SignedOK = true
return result, nil
}
@@ -144,6 +157,15 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
if err != nil {
return nil, err
}
if options.Policy != nil && len(options.Policy.SignedDataVersions) != 0 {
allowed := false
for _, v := range options.Policy.SignedDataVersions {
allowed = allowed || v == sd.Version
}
if !allowed {
return nil, rejectSignaturePolicy("SignedData version rejected: %d", sd.Version)
}
}
for _, cert := range options.SignCerts {
c, err := parseGBTCertificate(cert)
if err == nil {
@@ -155,6 +177,12 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
}
result := &digitalVerifyResult{Certs: sd.rawCerts()}
for index, signer := range sd.Signers {
if err := signatureSignerDigest(signer.SignatureAlg, signer.DigestAlg); err != nil {
return result, err
}
if err := signatureAlgorithmPolicy(options.Policy, signer.SignatureAlg, signer.DigestAlg); err != nil {
return result, err
}
digest, err := signatureDigest(signer.DigestAlg, signedData)
if err != nil {
return nil, err
@@ -184,6 +212,13 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
return result, nil
}
result.SignerCerts = append(result.SignerCerts, cert.Raw)
tokens, err := signatureTimestampAttributes(signer.UnauthAttrs)
if err != nil {
return result, err
}
for _, token := range tokens {
result.Timestamps = append(result.Timestamps, signatureTimestampEvidence{Token: token, Data: signer.Signature})
}
result.CertInfo = signatureCertInfo(cert.Raw)
if isSM2SignatureMethod(signer.SignatureAlg) {
pub, err := parseSM2PublicKeyFromCert(cert.Raw)
@@ -191,7 +226,7 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
result.CertOK = false
return result, err
}
if !sm2VerifySignature(pub, nil, plain, signer.Signature) {
if !verifySM2Signature(pub, nil, plain, signer.Signature) {
result.SignedChecked = true
result.CertChecked = index == len(sd.Signers)-1
result.CertOK = true
@@ -376,7 +411,11 @@ func parseGBT35275SignedData(data []byte) (*gbtSignedData, error) {
if !ok || len(items) < 4 {
return nil, fmt.Errorf("invalid signed data")
}
sd := &gbtSignedData{}
version, err := asn1Integer(items[0])
if err != nil || version != 1 && version != 3 {
return nil, fmt.Errorf("unsupported GB/T signed data version: %d", version)
}
sd := &gbtSignedData{Version: version}
contentOID, content, hasContent, err := parseGBTContentInfo(items[2])
if err != nil {
return nil, err
@@ -431,7 +470,7 @@ func parseGBTContentInfoBytes(data []byte) (string, asn1.RawValue, bool, error)
// 返回: string 内容类型, asn1.RawValue 内容, bool 是否存在内容, error 错误信息
func parseGBTContentInfo(raw asn1.RawValue) (string, asn1.RawValue, bool, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) == 0 {
if !ok || len(items) == 0 || len(items) > 2 || raw.Class != asn1.ClassUniversal || raw.Tag != asn1.TagSequence || !raw.IsCompound {
return "", asn1.RawValue{}, false, fmt.Errorf("invalid content info")
}
oid, err := asn1OIDString(items[0])
@@ -477,8 +516,8 @@ func parseGBTCertificate(data []byte) (gbtCertificate, error) {
}
return gbtCertificate{
Raw: cert.Raw,
Issuer: cert.Issuer,
Serial: cert.Serial,
Issuer: cert.RawIssuer,
Serial: cert.SerialNumber,
}, nil
}
@@ -509,6 +548,10 @@ func parseGBTSignerInfo(raw asn1.RawValue) (gbtSignerInfo, error) {
if !ok || len(items) < 5 {
return gbtSignerInfo{}, fmt.Errorf("invalid signer info")
}
version, err := asn1Integer(items[0])
if err != nil || version != 1 {
return gbtSignerInfo{}, fmt.Errorf("unsupported signer info version")
}
issuer, serial, err := parseGBTIssuerAndSerial(items[1])
if err != nil {
return gbtSignerInfo{}, err
@@ -539,7 +582,16 @@ func parseGBTSignerInfo(raw asn1.RawValue) (gbtSignerInfo, error) {
if err != nil {
return gbtSignerInfo{}, err
}
var unauth asn1.RawValue
if len(items) > idx+2 {
unauth = items[idx+2]
if len(items) != idx+3 || unauth.Class != asn1.ClassContextSpecific || unauth.Tag != 1 || !unauth.IsCompound {
return gbtSignerInfo{}, fmt.Errorf("invalid unauthenticated attributes")
}
}
return gbtSignerInfo{
Version: version,
UnauthAttrs: unauth,
Issuer: issuer,
Serial: serial,
DigestAlg: digestAlg,
@@ -587,20 +639,29 @@ func parseGBTMessageDigestAttr(raw asn1.RawValue) ([]byte, error) {
if !ok {
return nil, fmt.Errorf("invalid authenticated attributes")
}
var digest []byte
found := false
for _, attr := range attrs {
items, ok := asn1Children(attr.Bytes)
if !ok || len(items) < 2 {
continue
if !ok || len(items) != 2 {
return nil, fmt.Errorf("invalid authenticated attribute")
}
oid, err := asn1OIDString(items[0])
if err != nil || oid != signAttrMessageDigest {
continue
}
values, ok := asn1Children(items[1].Bytes)
if !ok || len(values) == 0 {
if !ok || len(values) != 1 || found {
return nil, fmt.Errorf("invalid message digest attribute")
}
return asn1OctetString(values[0])
digest, err = asn1OctetString(values[0])
if err != nil {
return nil, err
}
found = true
}
if found {
return digest, nil
}
return nil, fmt.Errorf("message digest attribute not found")
}
+366
View File
@@ -0,0 +1,366 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"crypto"
"encoding/xml"
"errors"
"fmt"
"path"
"sort"
"strings"
)
// SignaturePolicy 签名验签策略
// 空列表不限制已支持的算法或版本, RequiredFiles使用包内绝对路径
// RequireDocumentCoverage检查OFD.xml、文档目录及已知OFD引用可达的跨目录文件
// 仅排除当前Signature.xml和SignedValue及签名列表, 外置印章也须有引用摘要
// 此范围不表示历史修订范围或整个多文档包, 自定义扩展隐含引用应使用RequiredFiles补充
type SignaturePolicy struct {
OFDVersions []string
SESVersions []int
SignedDataVersions []int
DigestMethods []string
SignatureMethods []string
RejectWeakAlgorithms bool
RequiredFiles []string
RequireDocumentCoverage bool
RequireTimestamp bool
RequireRevocation bool
}
// signaturePolicyRejection 区分策略拒绝与解析失败
type signaturePolicyRejection struct{ message string }
// Error 返回策略拒绝原因
// 返回: string 错误信息
func (e signaturePolicyRejection) Error() string { return e.message }
// rejectSignaturePolicy 构造可识别的策略拒绝错误
// 入参: format 消息模板, values 模板参数
// 返回: error 策略错误
func rejectSignaturePolicy(format string, values ...any) error {
return signaturePolicyRejection{message: fmt.Sprintf(format, values...)}
}
// applySignaturePolicyError 将实际算法策略拒绝记入报告
// 入参: err 验签错误
func (report *SignatureVerifyReport) applySignaturePolicyError(err error) {
var rejection signaturePolicyRejection
if errors.As(err, &rejection) {
report.PolicyChecked, report.PolicyOK, report.PolicyError = true, false, rejection.Error()
}
}
// WithSignaturePolicy 设置签名策略
// 入参: policy 签名策略
// 返回: SignatureVerifyOption 验签选项
func WithSignaturePolicy(policy SignaturePolicy) SignatureVerifyOption {
policy.OFDVersions = append([]string(nil), policy.OFDVersions...)
policy.SESVersions = append([]int(nil), policy.SESVersions...)
policy.SignedDataVersions = append([]int(nil), policy.SignedDataVersions...)
policy.DigestMethods = append([]string(nil), policy.DigestMethods...)
policy.SignatureMethods = append([]string(nil), policy.SignatureMethods...)
policy.RequiredFiles = append([]string(nil), policy.RequiredFiles...)
return func(o *signatureVerifyOptions) { o.Policy = &policy }
}
// applySignaturePolicy 应用版本和算法限制
// 入参: o 验签选项, ofdVersion OFD版本, sesVersion SES版本, method 签名算法, digest 摘要算法
func (report *SignatureVerifyReport) applySignaturePolicy(o *signatureVerifyOptions, ofdVersion string, sesVersion int, method, digest string) {
if o.Policy == nil {
return
}
if !report.PolicyChecked {
report.PolicyChecked, report.PolicyOK = true, true
}
p := o.Policy
var err error
if len(p.OFDVersions) != 0 && !signatureStringAllowed(ofdVersion, p.OFDVersions) {
err = fmt.Errorf("OFD version rejected: %s", ofdVersion)
}
if sesVersion != 0 && len(p.SESVersions) != 0 {
allowed := false
for _, v := range p.SESVersions {
allowed = allowed || v == sesVersion
}
if !allowed {
err = fmt.Errorf("SES version rejected: %d", sesVersion)
}
}
if e := signatureAlgorithmPolicy(p, method, digest); e != nil {
err = e
}
if err != nil {
report.PolicyOK, report.PolicyError = false, err.Error()
}
}
// signatureAlgorithmPolicy 检查实际签名和摘要算法
// 入参: p 策略, method 签名算法, digest 摘要算法
// 返回: error 策略拒绝原因
func signatureAlgorithmPolicy(p *SignaturePolicy, method, digest string) error {
if p == nil {
return nil
}
if len(p.DigestMethods) != 0 {
allowed := false
for _, v := range p.DigestMethods {
allowed = allowed || signatureDigestEquivalent(v, digest)
}
if !allowed {
return rejectSignaturePolicy("digest algorithm rejected: %s", digest)
}
}
if len(p.SignatureMethods) != 0 {
allowed := false
for _, v := range p.SignatureMethods {
allowed = allowed || signatureAlgorithmEquivalent(v, method, digest)
}
if !allowed {
return rejectSignaturePolicy("signature algorithm rejected: %s", method)
}
}
if p.RejectWeakAlgorithms {
h, ok := signatureDigestHash(digest)
if ok && (h == crypto.MD5 || h == crypto.SHA1) {
return rejectSignaturePolicy("weak digest algorithm: %s", digest)
}
h, err := signatureMethodHash(method, digest)
if !isSM2SignatureMethod(method) && err == nil && (h == crypto.MD5 || h == crypto.SHA1) {
return rejectSignaturePolicy("weak signature algorithm: %s", method)
}
}
return nil
}
// signatureStringAllowed 检查值是否在允许列表中
// 入参: value 待检查值, values 允许列表
// 返回: bool 是否允许
func signatureStringAllowed(value string, values []string) bool {
for _, v := range values {
if v == value {
return true
}
}
return false
}
// signatureDigestEquivalent 比较摘要算法别名与OID
// 入参: a 第一算法, b 第二算法
// 返回: bool 是否为相同算法
func signatureDigestEquivalent(a, b string) bool {
if isSM3DigestMethod(a) && isSM3DigestMethod(b) {
return true
}
x, ok := signatureDigestHash(a)
y, ok2 := signatureDigestHash(b)
return ok && ok2 && x == y
}
// signatureAlgorithmEquivalent 比较签名算法别名与OID
// 入参: a 第一算法, b 第二算法, digest 摘要算法
// 返回: bool 是否为相同算法
func signatureAlgorithmEquivalent(a, b, digest string) bool {
if isSM2SignatureMethod(a) && isSM2SignatureMethod(b) {
return true
}
if !(isRSASignatureMethod(a) && isRSASignatureMethod(b) || isECDSASignatureMethod(a) && isECDSASignatureMethod(b)) {
return false
}
x, err := signatureMethodHash(a, digest)
y, err2 := signatureMethodHash(b, digest)
return err == nil && err2 == nil && x == y
}
// signatureSignerDigest 验证SignerInfo中的摘要与签名算法一致
// 入参: method 签名算法, digest 摘要算法
// 返回: error 不一致时的错误
func signatureSignerDigest(method, digest string) error {
if isSM2SignatureMethod(method) {
if !isSM3DigestMethod(digest) {
return fmt.Errorf("SM2 requires SM3 signer digest")
}
return nil
}
h, err := signatureMethodHash(method, digest)
d, ok := signatureDigestHash(digest)
if err != nil || !ok || h != d {
return fmt.Errorf("signer digest and signature algorithm mismatch")
}
return nil
}
// applySignatureCoverage 区分引用摘要通过和文档覆盖完整
// 入参: report 验签报告, listPath 签名列表路径, o 验签选项
func (r *Reader) applySignatureCoverage(report *SignatureVerifyReport, listPath string, o *signatureVerifyOptions) {
covered := make(map[string]bool)
for _, ref := range report.References {
if ref.Checked && ref.OK {
covered[r.signatureCoveragePath(ref.Path)] = true
}
}
for name := range covered {
report.CoveredFiles = append(report.CoveredFiles, name)
}
sort.Strings(report.CoveredFiles)
if o.Policy == nil || !o.Policy.RequireDocumentCoverage && len(o.Policy.RequiredFiles) == 0 {
return
}
report.CoverageChecked, report.CoverageOK = true, true
required := make(map[string]bool)
for _, name := range o.Policy.RequiredFiles {
required[r.signatureCoveragePath(name)] = true
}
if o.Policy.RequireDocumentCoverage {
required[r.signatureCoveragePath("OFD.xml")] = true
excluded := map[string]bool{r.signatureCoveragePath(listPath): true}
name := r.signatureCoveragePath(signatureRefPath(listPath, report.BaseLoc))
data, err := r.readFile(name)
if err != nil {
report.CoverageOK = false
report.CoverageError = err.Error()
return
}
file, err := parseSignatureFile(data)
if err != nil {
report.CoverageOK = false
report.CoverageError = err.Error()
return
}
excluded[name] = true
if file.SignedValue != "" {
excluded[r.signatureCoveragePath(signatureRefPath(name, file.SignedValue))] = true
}
root := path.Dir(r.signatureCoveragePath(o.DocRoot))
include := func(name string) {
if (root == "." || strings.HasPrefix(name, root+"/")) && !excluded[name] && !strings.HasSuffix(name, "/") {
required[name] = true
}
}
for name, file := range r.fileIndex {
if !file.FileInfo().IsDir() {
include(name)
}
}
for name := range r.files {
include(name)
}
// 可达文件始终必需, 不允许伪装为签名载体后排除
if err := r.signatureCoverageReferences(o.DocRoot, required); err != nil {
report.CoverageOK, report.CoverageError = false, err.Error()
}
}
for name := range required {
if !covered[name] {
report.UncoveredFiles = append(report.UncoveredFiles, name)
}
}
sort.Strings(report.UncoveredFiles)
report.CoverageOK = report.CoverageOK && len(report.UncoveredFiles) == 0
}
// signatureCoveragePath 复用阅读器大小写索引获取同一文件身份
// 入参: name 包内路径
// 返回: string 实际包内路径
func (r *Reader) signatureCoveragePath(name string) string {
name = cleanPackagePath(name)
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
return actual
}
return name
}
// signatureCoverageNode 收集OFD结构中的包内路径引用
type signatureCoverageNode struct {
XMLName xml.Name
Attrs []xml.Attr `xml:",any,attr"`
Text string `xml:",chardata"`
Children []signatureCoverageNode `xml:",any"`
}
// signatureCoverageReferences 收集标准OFD引用的传递闭包且包含跨文档资源
// 入参: root 文档入口, required 待补充的必需文件集合
// 返回: error 引用文件无法读取或解析时的错误
func (r *Reader) signatureCoverageReferences(root string, required map[string]bool) error {
queue := []string{r.signatureCoveragePath(root)}
seen := make(map[string]bool)
for len(queue) != 0 {
name := queue[0]
queue = queue[1:]
if seen[name] {
continue
}
seen[name], required[name] = true, true
data, err := r.readFile(name)
if err != nil {
return err
}
if !bytes.HasPrefix(bytes.TrimSpace(data), []byte("<")) {
continue
}
var node signatureCoverageNode
if err := xml.Unmarshal(data, &node); err != nil {
return fmt.Errorf("coverage XML %s: %w", name, err)
}
base := ""
if node.XMLName.Local == "Res" {
for _, attr := range node.Attrs {
if attr.Name.Local == "BaseLoc" {
base = attr.Value
}
}
}
add := func(loc string, resource bool) {
if strings.TrimSpace(loc) == "" {
return
}
resBase := ""
if resource {
resBase = base
}
resolved := r.signatureCoveragePath(resolveResourcePath(name, resBase, loc))
required[resolved] = true
if !seen[resolved] {
queue = append(queue, resolved)
}
}
var walk func(signatureCoverageNode)
walk = func(current signatureCoverageNode) {
kind := current.XMLName.Local
if kind == "Signatures" || kind == "Signature" {
return
}
for _, attr := range current.Attrs {
if attr.Name.Local == "BaseLoc" && kind != "Res" || attr.Name.Local == "Link" && kind == "DrawParam" {
add(attr.Value, node.XMLName.Local == "Res")
}
}
switch kind {
case "PublicRes", "DocumentRes", "PageRes", "Annotations", "Attachments", "CustomTags", "Extensions", "FileLoc", "SchemaLoc":
add(current.Text, false)
case "FontFile", "MediaFile", "Profile":
add(current.Text, true)
}
for _, child := range current.Children {
walk(child)
}
}
walk(node)
}
return nil
}
+435
View File
@@ -0,0 +1,435 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"time"
"github.com/emmansun/gmsm/smx509"
"golang.org/x/crypto/ocsp"
)
// SignatureRevocationEvidence 离线吊销证据
type SignatureRevocationEvidence struct {
CRLs [][]byte
OCSPResponses [][]byte
}
// SignatureRevocationRequest 外部吊销证据请求
type SignatureRevocationRequest struct {
Certificate []byte
Issuer []byte
VerifyTime time.Time
}
// SignatureRevocationOptions 离线吊销检查选项
// 检查签署者及制章者证书, 不代表整条证书链或TSA的吊销状态
// Fetch仅提供DER证据, 本库不主动联网, 缺少NextUpdate时必须显式设置MaxAge
// VerifyTime缺省取验签指定时间或当前时间, 不采用签名自报时间
type SignatureRevocationOptions struct {
SignatureRevocationEvidence
Certificates [][]byte
Fetch func(SignatureRevocationRequest) (SignatureRevocationEvidence, error)
Required bool
VerifyTime *time.Time
MaxAge time.Duration
}
// SignatureRevocationReport 单张证书吊销检查结果
// Checked仅表示已取得经验证的状态结论, OK仅在状态为good时为true
type SignatureRevocationReport struct {
Certificate SignatureCertInfo
Source string
Status string
Checked bool
OK bool
ThisUpdate time.Time
NextUpdate time.Time
RevokedAt time.Time
Error string
}
// WithSignatureRevocation 设置离线吊销证据检查
// 入参: options 吊销选项
// 返回: SignatureVerifyOption 验签选项
func WithSignatureRevocation(options SignatureRevocationOptions) SignatureVerifyOption {
options.CRLs = cloneSignatureEvidence(options.CRLs)
options.OCSPResponses = cloneSignatureEvidence(options.OCSPResponses)
options.Certificates = appendSignatureCerts(nil, options.Certificates...)
if options.VerifyTime != nil {
t := *options.VerifyTime
options.VerifyTime = &t
}
return func(o *signatureVerifyOptions) { o.Revocation = &options }
}
// VerifySignatureRevocation 验证单张证书的离线吊销证据
// 入参: cert 证书DER, issuer 直接颁发者DER, options 吊销选项
// 返回: SignatureRevocationReport 验证结果
func VerifySignatureRevocation(cert, issuer []byte, options SignatureRevocationOptions) SignatureRevocationReport {
result := SignatureRevocationReport{Certificate: signatureCertInfo(cert), Status: "unknown"}
c, err := parseSignatureCertificate(cert)
if err != nil {
result.Error = err.Error()
return result
}
i, err := parseSignatureCertificate(issuer)
if err != nil || !i.IsCA || i.KeyUsage != 0 && i.KeyUsage&smx509.KeyUsageCertSign == 0 || !bytes.Equal(c.RawIssuer, i.RawSubject) {
result.Error = "invalid revocation issuer"
return result
}
valid, err := verifyCertificateSignature(c, issuer)
if err != nil || !valid {
result.Error = "certificate issuer signature mismatch"
return result
}
at := time.Now()
if options.VerifyTime != nil {
at = *options.VerifyTime
}
if !timeInRange(at, i.NotBefore, i.NotAfter) || len(i.UnhandledCriticalExtensions) != 0 {
result.Error = "invalid revocation issuer validity"
return result
}
evidence := options.SignatureRevocationEvidence
if options.Fetch != nil {
fetched, err := options.Fetch(SignatureRevocationRequest{Certificate: bytes.Clone(cert), Issuer: bytes.Clone(issuer), VerifyTime: at})
if err != nil {
result.Error = err.Error()
return result
}
evidence.CRLs = append(cloneSignatureEvidence(evidence.CRLs), fetched.CRLs...)
evidence.OCSPResponses = append(cloneSignatureEvidence(evidence.OCSPResponses), fetched.OCSPResponses...)
}
var good *SignatureRevocationReport
for _, raw := range evidence.CRLs {
item := verifySignatureCRL(raw, cert, issuer, at, options.MaxAge)
if item.Checked && item.Status == "revoked" {
return item
}
if item.OK {
copy := item
good = &copy
} else if item.Error != "" {
result.Error = item.Error
}
}
for _, raw := range evidence.OCSPResponses {
item := verifySignatureOCSP(raw, cert, issuer, at, options.MaxAge)
if item.Checked && item.Status == "revoked" {
return item
}
if item.OK {
copy := item
good = &copy
} else if item.Error != "" {
result.Error = item.Error
}
if item.Checked && item.Status == "unknown" && !result.Checked {
result = item
}
}
if good != nil {
return *good
}
if result.Error == "" {
result.Error = "usable revocation evidence not found"
}
return result
}
// signatureEvidenceFresh 检查证据时间窗口及最大年龄
// 入参: thisUpdate 生效时间, nextUpdate 下次更新时间, at 验证时间, maxAge 最大年龄
// 返回: bool 证据是否新鲜
func signatureEvidenceFresh(thisUpdate, nextUpdate, at time.Time, maxAge time.Duration) bool {
if thisUpdate.IsZero() || thisUpdate.After(at) || maxAge < 0 {
return false
}
if maxAge > 0 && at.Sub(thisUpdate) > maxAge {
return false
}
if nextUpdate.IsZero() {
return maxAge > 0
}
return nextUpdate.After(thisUpdate) && at.Before(nextUpdate)
}
// verifySignatureCRL 验证直接完整CRL及目标序列号
// 入参: raw CRL字节, cert 证书, issuer 颁发者, at 验证时间, maxAge 最大年龄
// 返回: SignatureRevocationReport 吊销结果
func verifySignatureCRL(raw, cert, issuer []byte, at time.Time, maxAge time.Duration) (result SignatureRevocationReport) {
result = SignatureRevocationReport{Certificate: signatureCertInfo(cert), Source: "CRL", Status: "unknown"}
err := func() error {
crl, err := smx509.ParseRevocationList(raw)
if err != nil {
return err
}
parent, err := parseSignatureCertificate(issuer)
if err != nil {
return err
}
if !bytes.Equal(crl.RawIssuer, parent.RawSubject) {
return fmt.Errorf("CRL issuer mismatch")
}
if len(crl.AuthorityKeyId) != 0 && !bytes.Equal(crl.AuthorityKeyId, parent.SubjectKeyId) {
return fmt.Errorf("CRL authority key mismatch")
}
if err := crl.CheckSignatureFrom(parent); err != nil {
return err
}
for _, ext := range crl.Extensions {
if ext.Id.String() == "2.5.29.27" || ext.Id.String() == "2.5.29.28" {
return fmt.Errorf("delta or scoped CRL not supported")
}
if ext.Critical {
return fmt.Errorf("unsupported critical CRL extension")
}
}
result.ThisUpdate, result.NextUpdate = crl.ThisUpdate, crl.NextUpdate
if !signatureEvidenceFresh(crl.ThisUpdate, crl.NextUpdate, at, maxAge) {
return fmt.Errorf("stale or future CRL")
}
c, err := parseSignatureCertificate(cert)
if err != nil {
return err
}
for _, entry := range crl.RevokedCertificateEntries {
for _, ext := range entry.Extensions {
if ext.Critical || ext.Id.String() == "2.5.29.29" {
return fmt.Errorf("unsupported CRL entry extension")
}
}
if entry.SerialNumber.Cmp(c.SerialNumber) == 0 {
if entry.ReasonCode == 8 {
return fmt.Errorf("removeFromCRL requires delta processing")
}
if entry.RevocationTime.After(crl.ThisUpdate) {
return fmt.Errorf("CRL revocation time is in the future")
}
result.Checked, result.Status, result.RevokedAt = true, "revoked", entry.RevocationTime
return nil
}
}
result.Checked, result.OK, result.Status = true, true, "good"
return nil
}()
if err != nil {
result.Error = err.Error()
}
return result
}
// verifySignatureOCSP 验证OCSP签名、授权、颁发者绑定及状态
// 入参: raw 响应DER, cert 证书, issuer 颁发者, at 验证时间, maxAge 最大年龄
// 返回: SignatureRevocationReport 吊销结果
func verifySignatureOCSP(raw, cert, issuer []byte, at time.Time, maxAge time.Duration) (result SignatureRevocationReport) {
result = SignatureRevocationReport{Certificate: signatureCertInfo(cert), Source: "OCSP", Status: "unknown"}
err := func() error {
c, err := x509.ParseCertificate(cert)
if err != nil {
return fmt.Errorf("unsupported OCSP certificate: %w", err)
}
parent, err := x509.ParseCertificate(issuer)
if err != nil {
return err
}
response, err := ocsp.ParseResponseForCert(raw, c, parent)
if err != nil {
return err
}
if err := verifyOCSPCertID(response.TBSResponseData, c, parent, response.IssuerHash); err != nil {
return err
}
responder := parent
if response.Certificate != nil && !bytes.Equal(response.Certificate.Raw, parent.Raw) {
responder = response.Certificate
if !bytes.Equal(responder.RawIssuer, parent.RawSubject) || responder.CheckSignatureFrom(parent) != nil {
return fmt.Errorf("OCSP responder is not issued by certificate issuer")
}
allowed := false
for _, usage := range responder.ExtKeyUsage {
allowed = allowed || usage == x509.ExtKeyUsageOCSPSigning
}
if !allowed || responder.KeyUsage != 0 && responder.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
return fmt.Errorf("unauthorized OCSP responder")
}
}
if len(responder.UnhandledCriticalExtensions) != 0 || !timeInRange(at, responder.NotBefore, responder.NotAfter) || !timeInRange(response.ProducedAt, responder.NotBefore, responder.NotAfter) {
return fmt.Errorf("OCSP responder certificate not valid")
}
if len(response.RawResponderName) != 0 && !bytes.Equal(response.RawResponderName, responder.RawSubject) {
return fmt.Errorf("OCSP responder name mismatch")
}
if len(response.ResponderKeyHash) != 0 {
var spki struct {
Algorithm pkix.AlgorithmIdentifier
Key asn1.BitString
}
if _, err := asn1.Unmarshal(responder.RawSubjectPublicKeyInfo, &spki); err != nil {
return err
}
if !bytes.Equal(response.ResponderKeyHash, signatureHashBytes(crypto.SHA1, spki.Key.Bytes)) {
return fmt.Errorf("OCSP responder key mismatch")
}
}
result.ThisUpdate, result.NextUpdate, result.RevokedAt = response.ThisUpdate, response.NextUpdate, response.RevokedAt
if !signatureEvidenceFresh(response.ThisUpdate, response.NextUpdate, at, maxAge) || response.ProducedAt.After(at) || response.ProducedAt.Before(response.ThisUpdate) {
return fmt.Errorf("stale or future OCSP response")
}
switch response.Status {
case ocsp.Good:
result.Checked, result.OK, result.Status = true, true, "good"
case ocsp.Revoked:
if response.RevokedAt.IsZero() || response.RevokedAt.After(response.ThisUpdate) {
return fmt.Errorf("invalid OCSP revocation time")
}
result.Checked, result.Status = true, "revoked"
case ocsp.Unknown:
result.Checked = true
default:
return fmt.Errorf("unsupported OCSP status")
}
return nil
}()
if err != nil {
result.Error = err.Error()
}
return result
}
// verifyOCSPCertID 补充校验x/crypto未验证的颁发者摘要和重复CertID
// 入参: tbs 响应待签名数据, cert 证书, issuer 颁发者, hash CertID摘要算法
// 返回: error 绑定错误
func verifyOCSPCertID(tbs []byte, cert, issuer *x509.Certificate, hash crypto.Hash) error {
var data struct {
Version int `asn1:"optional,explicit,tag:0"`
Responder asn1.RawValue
ProducedAt time.Time `asn1:"generalized"`
Responses []asn1.RawValue
Extensions []pkix.Extension `asn1:"optional,explicit,tag:1"`
}
rest, err := asn1.Unmarshal(tbs, &data)
if err != nil || len(rest) != 0 || data.Version != 0 {
return fmt.Errorf("invalid OCSP response data")
}
for _, ext := range data.Extensions {
if ext.Critical {
return fmt.Errorf("unsupported critical OCSP extension")
}
}
var spki struct {
Algorithm pkix.AlgorithmIdentifier
Key asn1.BitString
}
if _, err := asn1.Unmarshal(issuer.RawSubjectPublicKeyInfo, &spki); err != nil {
return err
}
matched := 0
for _, raw := range data.Responses {
fields, ok := asn1Children(raw.Bytes)
if !ok || len(fields) < 3 {
return fmt.Errorf("invalid OCSP single response")
}
id, ok := asn1Children(fields[0].Bytes)
if !ok || len(id) != 4 {
return fmt.Errorf("invalid OCSP CertID")
}
serial, err := asn1IntegerBig(id[3])
if err != nil {
return err
}
if serial.Cmp(cert.SerialNumber) != 0 {
continue
}
matched++
nameHash, err := asn1OctetString(id[1])
if err != nil {
return err
}
keyHash, err := asn1OctetString(id[2])
if err != nil {
return err
}
if !bytes.Equal(nameHash, signatureHashBytes(hash, issuer.RawSubject)) || !bytes.Equal(keyHash, signatureHashBytes(hash, spki.Key.Bytes)) {
return fmt.Errorf("OCSP issuer hash mismatch")
}
}
if matched != 1 {
return fmt.Errorf("ambiguous OCSP certificate status")
}
return nil
}
// applySignatureRevocation 检查签署者及制章者的离线吊销证据
// 入参: o 验签选项, certs 目标证书, extra 辅助证书
func (report *SignatureVerifyReport) applySignatureRevocation(o *signatureVerifyOptions, certs, extra [][]byte) {
var options SignatureRevocationOptions
if o.Revocation != nil {
options = *o.Revocation
}
if o.Policy != nil {
options.Required = options.Required || o.Policy.RequireRevocation
}
if o.Revocation == nil && !options.Required {
return
}
at := time.Now()
if o.VerifyTime != nil {
at = *o.VerifyTime
}
if options.VerifyTime != nil {
at = *options.VerifyTime
}
options.VerifyTime = &at
pool := appendSignatureCerts(nil, options.Certificates...)
pool = append(pool, extra...)
pool = append(pool, o.SignCerts...)
pool = append(pool, o.TrustCerts...)
pool = compactSignatureCerts(pool)
for _, raw := range compactSignatureCerts(certs) {
result := SignatureRevocationReport{Certificate: signatureCertInfo(raw), Status: "unknown", Error: "revocation issuer not found"}
cert, err := parseSignatureCertificate(raw)
if err == nil {
for _, issuerRaw := range pool {
issuer, err := parseSignatureCertificate(issuerRaw)
if err != nil || !bytes.Equal(cert.RawIssuer, issuer.RawSubject) {
continue
}
ok, err := verifyCertificateSignature(cert, issuerRaw)
if err != nil || !ok {
continue
}
result = VerifySignatureRevocation(raw, issuerRaw, options)
break
}
}
report.Revocations = append(report.Revocations, result)
}
if len(report.Revocations) != 0 {
report.RevocationChecked, report.RevocationOK = true, true
for _, result := range report.Revocations {
report.RevocationOK = report.RevocationOK && result.Checked && result.OK
}
}
if options.Required && !report.RevocationChecked {
report.PolicyChecked, report.PolicyOK, report.PolicyError = true, false, "required revocation evidence missing"
}
}
+71 -71
View File
@@ -16,11 +16,14 @@ package ofdgo
import (
"bytes"
"crypto/ecdsa"
"encoding/asn1"
"fmt"
"math/big"
"strings"
"time"
"github.com/emmansun/gmsm/sm2"
"github.com/emmansun/gmsm/sm3"
)
const (
@@ -30,14 +33,12 @@ const (
signMethodSM2SM3 = "1.2.156.10197.1.501"
signMethodSM2SM3B = "1.2.156.10197.501"
signMethodSM2Sign = "1.2.156.10197.1.301.1"
signCurveSM2P256 = "1.2.156.10197.1.301"
signECPublicKey = "1.2.840.10045.2.1"
signASN1Sequence = 16
signPublicKeySize = 65
)
// sesSignature SES签章值
type sesSignature struct {
Timestamp []byte
ToSign []byte
Cert []byte
SignAlg string
@@ -49,15 +50,16 @@ type sesSignature struct {
// sesSeal SES电子印章
type sesSeal struct {
Raw []byte
SignData []byte
Cert []byte
SignAlg string
Signature []byte
PicType string
PicData []byte
CertList sesCertList
Info SignatureSealInfo
TimestampRaw []byte
Raw []byte
SignData []byte
Cert []byte
SignAlg string
Signature []byte
PicType string
PicData []byte
CertList sesCertList
Info SignatureSealInfo
}
// sesCertList SES印章证书列表
@@ -74,6 +76,8 @@ type sesCertDigest struct {
// sesVerifyResult SES签章验证结果
type sesVerifyResult struct {
SignatureMethod string
Timestamps []signatureTimestampEvidence
DataHashChecked bool
DataHashOK bool
SignedChecked bool
@@ -147,7 +151,22 @@ func parseSESSignature(data []byte) (*sesSignature, error) {
if err != nil {
return nil, err
}
if version != 4 {
return nil, fmt.Errorf("unsupported ses version: %d", version)
}
var timestamp []byte
if len(items) == 5 {
value, parseErr := asn1Explicit(items[4])
if parseErr != nil {
return nil, fmt.Errorf("invalid ses timestamp tag: %w", parseErr)
}
timestamp, err = asn1BitStringBytes(value)
if err != nil || len(timestamp) == 0 {
return nil, fmt.Errorf("invalid ses timestamp")
}
}
return &sesSignature{
Timestamp: timestamp,
ToSign: append([]byte(nil), items[0].FullBytes...),
Cert: cert,
SignAlg: alg,
@@ -175,7 +194,7 @@ func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
return nil, err
}
version, err := asn1Integer(tbsItems[0])
if err != nil || version != seal.Info.Version {
if err != nil || version != seal.Info.Version || version != 1 {
return nil, fmt.Errorf("invalid ses version")
}
signatureTime := parseSESSignatureTimeV1(tbsItems[2])
@@ -233,7 +252,16 @@ func verifySESSignature(data, signedData []byte, options *signatureVerifyOptions
if !isSM2SignatureMethod(sig.SignAlg) || !isSM2SignatureMethod(sig.Seal.SignAlg) {
return nil, fmt.Errorf("unsupported signature method")
}
for _, method := range []string{sig.SignAlg, sig.Seal.SignAlg} {
if err := signatureAlgorithmPolicy(options.Policy, method, "SM3"); err != nil {
return nil, err
}
}
result := &sesVerifyResult{}
result.SignatureMethod = sig.SignAlg
if len(sig.Timestamp) != 0 {
result.Timestamps = append(result.Timestamps, signatureTimestampEvidence{Token: sig.Timestamp, Data: sig.Signature})
}
result.SignCert = signatureCertInfo(sig.Cert)
result.SealCert = signatureCertInfo(sig.Seal.Cert)
result.SignCertRaw = sig.Cert
@@ -255,9 +283,9 @@ func verifySESSignature(data, signedData []byte, options *signatureVerifyOptions
return result, err
}
result.SignedChecked = true
result.SignedOK = sm2VerifySignature(signPub, nil, sig.ToSign, sig.Signature)
result.SignedOK = verifySM2Signature(signPub, nil, sig.ToSign, sig.Signature)
result.SealChecked = true
result.SealOK = sm2VerifySignature(sealPub, nil, sig.Seal.SignData, sig.Seal.Signature)
result.SealOK = verifySM2Signature(sealPub, nil, sig.Seal.SignData, sig.Seal.Signature)
result.CertChecked = true
result.CertOK = sesCertInList(sig.Cert, sig.Seal.CertList)
return result, nil
@@ -297,20 +325,29 @@ func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
if err != nil {
return nil, err
}
var timestampRaw []byte
if len(items) == 5 {
if info.Version != 5 {
return nil, fmt.Errorf("unexpected timestamp in ses seal version %d", info.Version)
}
// V5仅保留解析能力, 不将未验证的制章时间戳当作可信时间
timestampRaw = append([]byte(nil), items[4].FullBytes...)
}
picType, picData, err := parseSESPicture(infoItems[3])
if err != nil {
return nil, err
}
return &sesSeal{
Raw: append([]byte(nil), raw.FullBytes...),
SignData: append([]byte(nil), items[0].FullBytes...),
Cert: cert,
SignAlg: alg,
Signature: signature,
PicType: picType,
PicData: picData,
CertList: certList,
Info: info,
TimestampRaw: timestampRaw,
Raw: append([]byte(nil), raw.FullBytes...),
SignData: append([]byte(nil), items[0].FullBytes...),
Cert: cert,
SignAlg: alg,
Signature: signature,
PicType: picType,
PicData: picData,
CertList: certList,
Info: info,
}, nil
}
@@ -531,62 +568,25 @@ func parseSESPicture(raw asn1.RawValue) (string, []byte, error) {
// parseSM2PublicKeyFromCert 从证书解析SM2公钥
// 入参: data DER编码证书
// 返回: sm2PublicKey SM2公钥, error 错误信息
func parseSM2PublicKeyFromCert(data []byte) (sm2PublicKey, error) {
// 返回: *ecdsa.PublicKey SM2公钥, error 错误信息
func parseSM2PublicKeyFromCert(data []byte) (*ecdsa.PublicKey, error) {
cert, err := parseSignatureCertificate(data)
if err != nil {
return sm2PublicKey{}, err
return nil, err
}
return parseSM2PublicKeyInfo(cert.PublicKey)
}
// parseSM2PublicKeyInfo 解析SM2公钥信息
// 入参: raw SubjectPublicKeyInfo原始值
// 返回: sm2PublicKey SM2公钥, error 错误信息
func parseSM2PublicKeyInfo(raw asn1.RawValue) (sm2PublicKey, error) {
var spki struct {
Algorithm asn1.RawValue
SubjectPublicKey asn1.BitString
pub, ok := cert.PublicKey.(*ecdsa.PublicKey)
if !ok || !sm2.IsSM2PublicKey(pub) {
return nil, fmt.Errorf("SM2 public key required")
}
rest, err := asn1.Unmarshal(raw.FullBytes, &spki)
if err != nil {
return sm2PublicKey{}, err
}
if len(rest) != 0 {
return sm2PublicKey{}, fmt.Errorf("invalid subject public key info")
}
algItems, ok := asn1Children(spki.Algorithm.Bytes)
if !ok || len(algItems) < 2 {
return sm2PublicKey{}, fmt.Errorf("invalid public key algorithm")
}
alg, err := asn1OIDString(algItems[0])
if err != nil {
return sm2PublicKey{}, err
}
curve, err := asn1OIDString(algItems[1])
if err != nil {
return sm2PublicKey{}, err
}
if alg != signECPublicKey || curve != signCurveSM2P256 {
return sm2PublicKey{}, fmt.Errorf("unsupported public key algorithm")
}
key := spki.SubjectPublicKey.Bytes
if len(key) != signPublicKeySize || key[0] != 4 {
return sm2PublicKey{}, fmt.Errorf("invalid sm2 public key")
}
return sm2PublicKey{
X: new(big.Int).SetBytes(key[1:33]),
Y: new(big.Int).SetBytes(key[33:65]),
}, nil
return pub, nil
}
// signSM3 计算SM3摘要
// 入参: data 原文数据
// 返回: []byte 摘要值
func signSM3(data []byte) []byte {
h := newSM3()
h.Write(data)
return h.Sum(nil)
digest := sm3.Sum(data)
return digest[:]
}
// isSM2SignatureMethod 判断是否为SM2签名算法
-326
View File
@@ -1,326 +0,0 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"encoding/asn1"
"encoding/binary"
"math/big"
)
const sm2DefaultUserID = "1234567812345678"
var sm2P256 = newSM2P256()
// sm2PublicKey SM2公钥
type sm2PublicKey struct {
X *big.Int
Y *big.Int
}
// sm2Curve SM2椭圆曲线
type sm2Curve struct {
P *big.Int
N *big.Int
B *big.Int
Gx *big.Int
Gy *big.Int
}
// sm2Point SM2雅可比坐标点
type sm2Point struct {
X *big.Int
Y *big.Int
Z *big.Int
}
// newSM2P256 创建SM2椭圆曲线
// 返回: *sm2Curve SM2椭圆曲线
func newSM2P256() *sm2Curve {
return &sm2Curve{
P: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF"),
N: sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123"),
B: sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93"),
Gx: sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7"),
Gy: sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0"),
}
}
// sm2Big 解析SM2大整数常量
// 入参: s 十六进制字符串
// 返回: *big.Int 大整数
func sm2Big(s string) *big.Int {
n, _ := new(big.Int).SetString(s, 16)
return n
}
// sm2VerifySignature 验证SM2签名值
// 入参: pub 公钥, userID 用户标识, msg 原文, sig 签名值
// 返回: bool 是否验证通过
func sm2VerifySignature(pub sm2PublicKey, userID, msg, sig []byte) bool {
r, s, ok := parseSM2Signature(sig)
if !ok {
return false
}
return sm2Verify(pub, userID, msg, r, s)
}
// parseSM2Signature 解析SM2签名值
// 入参: sig 签名值
// 返回: *big.Int R值, *big.Int S值, bool 是否解析成功
func parseSM2Signature(sig []byte) (*big.Int, *big.Int, bool) {
if len(sig) == 64 {
return new(big.Int).SetBytes(sig[:32]), new(big.Int).SetBytes(sig[32:]), true
}
var rs struct {
R *big.Int
S *big.Int
}
rest, err := asn1.Unmarshal(sig, &rs)
if err != nil || len(rest) != 0 || rs.R == nil || rs.S == nil {
return nil, nil, false
}
return rs.R, rs.S, true
}
// sm2Verify 验证SM2签名
// 入参: pub 公钥, userID 用户标识, msg 原文, r R值, s S值
// 返回: bool 是否验证通过
func sm2Verify(pub sm2PublicKey, userID, msg []byte, r, s *big.Int) bool {
n := sm2P256.N
if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(n) >= 0 || s.Cmp(n) >= 0 {
return false
}
if pub.X == nil || pub.Y == nil || !sm2P256.isOnCurve(pub.X, pub.Y) {
return false
}
e := new(big.Int).SetBytes(sm2MessageDigest(pub, userID, msg))
t := new(big.Int).Add(r, s)
t.Mod(t, n)
if t.Sign() == 0 {
return false
}
x, ok := sm2P256.combinedMult(pub.X, pub.Y, s, t)
if !ok {
return false
}
v := new(big.Int).Add(e, x)
v.Mod(v, n)
return v.Cmp(r) == 0
}
// sm2MessageDigest 计算SM2签名摘要
// 入参: pub 公钥, userID 用户标识, msg 原文
// 返回: []byte 摘要值
func sm2MessageDigest(pub sm2PublicKey, userID, msg []byte) []byte {
h := newSM3()
h.Write(sm2ZA(pub, userID))
h.Write(msg)
return h.Sum(nil)
}
// sm2ZA 计算SM2用户标识杂凑值
// 入参: pub 公钥, userID 用户标识
// 返回: []byte ZA值
func sm2ZA(pub sm2PublicKey, userID []byte) []byte {
if len(userID) == 0 {
userID = []byte(sm2DefaultUserID)
}
h := newSM3()
var entl [2]byte
binary.BigEndian.PutUint16(entl[:], uint16(len(userID)*8))
h.Write(entl[:])
h.Write(userID)
h.Write(sm2Fixed(sm2A()))
h.Write(sm2Fixed(sm2P256.B))
h.Write(sm2Fixed(sm2P256.Gx))
h.Write(sm2Fixed(sm2P256.Gy))
h.Write(sm2Fixed(pub.X))
h.Write(sm2Fixed(pub.Y))
return h.Sum(nil)
}
// sm2A 获取SM2曲线A参数
// 返回: *big.Int 曲线A参数
func sm2A() *big.Int {
return new(big.Int).Sub(sm2P256.P, big.NewInt(3))
}
// isOnCurve 判断点是否位于SM2曲线
// 入参: x X坐标, y Y坐标
// 返回: bool 是否位于曲线
func (c *sm2Curve) isOnCurve(x, y *big.Int) bool {
if x.Sign() < 0 || y.Sign() < 0 || x.Cmp(c.P) >= 0 || y.Cmp(c.P) >= 0 {
return false
}
left := c.fieldSquare(y)
right := c.fieldAdd(c.fieldSub(c.fieldMul(c.fieldSquare(x), x), c.fieldScale(x, 3)), c.B)
return left.Cmp(right) == 0
}
// combinedMult 计算sG+tP
// 入参: x 公钥X坐标, y 公钥Y坐标, s 标量S, t 标量T
// 返回: *big.Int 结果X坐标, bool 是否计算成功
func (c *sm2Curve) combinedMult(x, y, s, t *big.Int) (*big.Int, bool) {
base := c.scalarMult(c.Gx, c.Gy, s.Bytes())
public := c.scalarMult(x, y, t.Bytes())
result := c.add(base, public)
affineX, _, ok := c.affine(result)
return affineX, ok
}
// scalarMult 计算椭圆曲线标量乘法
// 入参: x 点X坐标, y 点Y坐标, scalar 标量
// 返回: sm2Point 雅可比坐标点
func (c *sm2Curve) scalarMult(x, y *big.Int, scalar []byte) sm2Point {
result := c.infinity()
point := sm2Point{X: new(big.Int).Set(x), Y: new(big.Int).Set(y), Z: big.NewInt(1)}
for _, value := range scalar {
for bit := 7; bit >= 0; bit-- {
result = c.double(result)
if value&(1<<uint(bit)) != 0 {
result = c.add(result, point)
}
}
}
return result
}
// add 计算椭圆曲线点加法
// 入参: p 点P, q 点Q
// 返回: sm2Point 结果点
func (c *sm2Curve) add(p, q sm2Point) sm2Point {
if p.Z.Sign() == 0 {
return q
}
if q.Z.Sign() == 0 {
return p
}
z1z1 := c.fieldSquare(p.Z)
z2z2 := c.fieldSquare(q.Z)
u1 := c.fieldMul(p.X, z2z2)
u2 := c.fieldMul(q.X, z1z1)
s1 := c.fieldMul(p.Y, c.fieldMul(q.Z, z2z2))
s2 := c.fieldMul(q.Y, c.fieldMul(p.Z, z1z1))
if u1.Cmp(u2) == 0 {
if s1.Cmp(s2) != 0 {
return c.infinity()
}
return c.double(p)
}
h := c.fieldSub(u2, u1)
i := c.fieldSquare(c.fieldScale(h, 2))
j := c.fieldMul(h, i)
r := c.fieldScale(c.fieldSub(s2, s1), 2)
v := c.fieldMul(u1, i)
x := c.fieldSub(c.fieldSub(c.fieldSquare(r), j), c.fieldScale(v, 2))
y := c.fieldSub(c.fieldMul(r, c.fieldSub(v, x)), c.fieldScale(c.fieldMul(s1, j), 2))
z := c.fieldMul(c.fieldSub(c.fieldSub(c.fieldSquare(c.fieldAdd(p.Z, q.Z)), z1z1), z2z2), h)
return sm2Point{X: x, Y: y, Z: z}
}
// double 计算椭圆曲线点倍加
// 入参: p 点P
// 返回: sm2Point 结果点
func (c *sm2Curve) double(p sm2Point) sm2Point {
if p.Z.Sign() == 0 || p.Y.Sign() == 0 {
return c.infinity()
}
delta := c.fieldSquare(p.Z)
gamma := c.fieldSquare(p.Y)
beta := c.fieldMul(p.X, gamma)
alpha := c.fieldScale(c.fieldMul(c.fieldSub(p.X, delta), c.fieldAdd(p.X, delta)), 3)
x := c.fieldSub(c.fieldSquare(alpha), c.fieldScale(beta, 8))
z := c.fieldSub(c.fieldSub(c.fieldSquare(c.fieldAdd(p.Y, p.Z)), gamma), delta)
y := c.fieldSub(c.fieldMul(alpha, c.fieldSub(c.fieldScale(beta, 4), x)), c.fieldScale(c.fieldSquare(gamma), 8))
return sm2Point{X: x, Y: y, Z: z}
}
// affine 将雅可比坐标转换为仿射坐标
// 入参: p 雅可比坐标点
// 返回: *big.Int X坐标, *big.Int Y坐标, bool 是否转换成功
func (c *sm2Curve) affine(p sm2Point) (*big.Int, *big.Int, bool) {
if p.Z.Sign() == 0 {
return nil, nil, false
}
z := new(big.Int).ModInverse(p.Z, c.P)
if z == nil {
return nil, nil, false
}
z2 := c.fieldSquare(z)
x := c.fieldMul(p.X, z2)
y := c.fieldMul(p.Y, c.fieldMul(z2, z))
return x, y, true
}
// infinity 获取无穷远点
// 返回: sm2Point 无穷远点
func (c *sm2Curve) infinity() sm2Point {
return sm2Point{X: new(big.Int), Y: new(big.Int), Z: new(big.Int)}
}
// fieldAdd 计算有限域加法
// 入参: x 左操作数, y 右操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldAdd(x, y *big.Int) *big.Int {
value := new(big.Int).Add(x, y)
return value.Mod(value, c.P)
}
// fieldSub 计算有限域减法
// 入参: x 左操作数, y 右操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldSub(x, y *big.Int) *big.Int {
value := new(big.Int).Sub(x, y)
return value.Mod(value, c.P)
}
// fieldMul 计算有限域乘法
// 入参: x 左操作数, y 右操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldMul(x, y *big.Int) *big.Int {
value := new(big.Int).Mul(x, y)
return value.Mod(value, c.P)
}
// fieldSquare 计算有限域平方
// 入参: x 操作数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldSquare(x *big.Int) *big.Int {
return c.fieldMul(x, x)
}
// fieldScale 计算有限域整数倍
// 入参: x 操作数, scale 倍数
// 返回: *big.Int 计算结果
func (c *sm2Curve) fieldScale(x *big.Int, scale int64) *big.Int {
return c.fieldMul(x, big.NewInt(scale))
}
// sm2Fixed 转换为SM2固定长度字节
// 入参: n 大整数
// 返回: []byte 固定长度字节
func sm2Fixed(n *big.Int) []byte {
out := make([]byte, 32)
if n == nil {
return out
}
b := n.Bytes()
if len(b) > len(out) {
b = b[len(b)-len(out):]
}
copy(out[len(out)-len(b):], b)
return out
}
-211
View File
@@ -1,211 +0,0 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"encoding/binary"
"hash"
"math/bits"
)
const (
sm3Size = 32
sm3BlockSize = 64
)
var sm3Init = [8]uint32{
0x7380166f,
0x4914b2b9,
0x172442d7,
0xda8a0600,
0xa96f30bc,
0x163138aa,
0xe38dee4d,
0xb0fb0e4e,
}
// sm3Digest SM3杂凑值计算器
type sm3Digest struct {
h [8]uint32
x [sm3BlockSize]byte
nx int
len uint64
}
// newSM3 创建SM3杂凑值计算器
// 返回: hash.Hash SM3杂凑值计算器
func newSM3() hash.Hash {
d := new(sm3Digest)
d.Reset()
return d
}
// Reset 重置SM3状态
func (d *sm3Digest) Reset() {
d.h = sm3Init
d.nx = 0
d.len = 0
}
// Size 获取SM3杂凑值长度
// 返回: int 杂凑值长度
func (d *sm3Digest) Size() int {
return sm3Size
}
// BlockSize 获取SM3分组长度
// 返回: int 分组长度
func (d *sm3Digest) BlockSize() int {
return sm3BlockSize
}
// Write 写入待计算数据
// 入参: p 待计算数据
// 返回: int 写入长度, error 错误信息
func (d *sm3Digest) Write(p []byte) (int, error) {
nn := len(p)
d.len += uint64(nn)
if d.nx > 0 {
n := copy(d.x[d.nx:], p)
d.nx += n
if d.nx == sm3BlockSize {
sm3Block(d, d.x[:])
d.nx = 0
}
p = p[n:]
}
if len(p) >= sm3BlockSize {
n := len(p) &^ (sm3BlockSize - 1)
sm3Block(d, p[:n])
p = p[n:]
}
if len(p) > 0 {
d.nx = copy(d.x[:], p)
}
return nn, nil
}
// Sum 将SM3杂凑值追加到前缀数据后,不改变当前状态
// 入参: in 前缀数据
// 返回: []byte 前缀数据及杂凑值
func (d *sm3Digest) Sum(in []byte) []byte {
dd := *d
hash := dd.checkSum()
return append(in, hash[:]...)
}
// checkSum 计算SM3最终杂凑值
// 返回: [sm3Size]byte 杂凑值
func (d *sm3Digest) checkSum() [sm3Size]byte {
lenBits := d.len << 3
var tmp [64]byte
tmp[0] = 0x80
if d.nx < 56 {
d.Write(tmp[:56-d.nx])
} else {
d.Write(tmp[:64+56-d.nx])
}
binary.BigEndian.PutUint64(tmp[:8], lenBits)
d.Write(tmp[:8])
var digest [sm3Size]byte
for i, v := range d.h {
binary.BigEndian.PutUint32(digest[i*4:], v)
}
return digest
}
// sm3Block 处理SM3消息分组
// 入参: d SM3杂凑值计算器, p 消息分组数据
func sm3Block(d *sm3Digest, p []byte) {
var w [68]uint32
var w1 [64]uint32
for len(p) >= sm3BlockSize {
for i := 0; i < 16; i++ {
w[i] = binary.BigEndian.Uint32(p[i*4:])
}
for i := 16; i < 68; i++ {
x := w[i-16] ^ w[i-9] ^ bits.RotateLeft32(w[i-3], 15)
w[i] = sm3P1(x) ^ bits.RotateLeft32(w[i-13], 7) ^ w[i-6]
}
for i := 0; i < 64; i++ {
w1[i] = w[i] ^ w[i+4]
}
a, b, c, e := d.h[0], d.h[1], d.h[2], d.h[4]
dd, f, g, hh := d.h[3], d.h[5], d.h[6], d.h[7]
for i := 0; i < 64; i++ {
t := uint32(0x7a879d8a)
if i < 16 {
t = 0x79cc4519
}
ss1 := bits.RotateLeft32(bits.RotateLeft32(a, 12)+e+bits.RotateLeft32(t, i), 7)
ss2 := ss1 ^ bits.RotateLeft32(a, 12)
tt1 := sm3FF(i, a, b, c) + dd + ss2 + w1[i]
tt2 := sm3GG(i, e, f, g) + hh + ss1 + w[i]
dd = c
c = bits.RotateLeft32(b, 9)
b = a
a = tt1
hh = g
g = bits.RotateLeft32(f, 19)
f = e
e = sm3P0(tt2)
}
d.h[0] ^= a
d.h[1] ^= b
d.h[2] ^= c
d.h[3] ^= dd
d.h[4] ^= e
d.h[5] ^= f
d.h[6] ^= g
d.h[7] ^= hh
p = p[sm3BlockSize:]
}
}
// sm3P0 计算SM3置换函数P0
// 入参: x 输入值
// 返回: uint32 置换结果
func sm3P0(x uint32) uint32 {
return x ^ bits.RotateLeft32(x, 9) ^ bits.RotateLeft32(x, 17)
}
// sm3P1 计算SM3置换函数P1
// 入参: x 输入值
// 返回: uint32 置换结果
func sm3P1(x uint32) uint32 {
return x ^ bits.RotateLeft32(x, 15) ^ bits.RotateLeft32(x, 23)
}
// sm3FF 计算SM3布尔函数FF
// 入参: i 轮数, x 输入值, y 输入值, z 输入值
// 返回: uint32 计算结果
func sm3FF(i int, x, y, z uint32) uint32 {
if i < 16 {
return x ^ y ^ z
}
return (x & y) | (x & z) | (y & z)
}
// sm3GG 计算SM3布尔函数GG
// 入参: i 轮数, x 输入值, y 输入值, z 输入值
// 返回: uint32 计算结果
func sm3GG(i int, x, y, z uint32) uint32 {
if i < 16 {
return x ^ y ^ z
}
return (x & y) | (^x & z)
}
var _ hash.Hash = (*sm3Digest)(nil)
+572
View File
@@ -0,0 +1,572 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"encoding/asn1"
"fmt"
"math/big"
"time"
"github.com/emmansun/gmsm/smx509"
)
const (
signTimestampContent = "1.2.840.113549.1.9.16.1.4"
signTimestampAttr = "1.2.840.113549.1.9.16.2.14"
signContentCMS = "1.2.840.113549.1.7.2"
signAttrContentType = "1.2.840.113549.1.9.3"
signAttrESS = "1.2.840.113549.1.9.16.2.12"
signAttrESSV2 = "1.2.840.113549.1.9.16.2.47"
)
// SignatureTimestampRequest 外部时间戳证据请求
// Data为完整SignedValue文件字节, 回调返回的RFC3161令牌须绑定Data
type SignatureTimestampRequest struct {
DocIndex int
SignatureID string
Data []byte
}
// SignatureTimestampOptions 离线RFC3161时间戳验证选项
// Tokens及Fetch返回的令牌绑定完整SignedValue, 内嵌令牌绑定所属签名值
// Fetch仅提供证据, 本库不主动联网, TrustCerts须由调用方显式提供
type SignatureTimestampOptions struct {
Tokens [][]byte
TrustCerts [][]byte
Certificates [][]byte
Fetch func(SignatureTimestampRequest) ([][]byte, error)
Required bool
PolicyOID string
Nonce *big.Int
VerifyTime *time.Time
}
// SignatureTimestampReport 时间戳验证报告
// Time只表示令牌声明时间, 仅Valid为true时可作为可信时间使用
type SignatureTimestampReport struct {
tsaName asn1.RawValue
Time time.Time
PolicyOID string
SerialNumber string
Nonce *big.Int
Signer SignatureCertInfo
BindingChecked bool
BindingOK bool
SignedValueChecked bool
SignedValueOK bool
CertTrustChecked bool
CertTrustOK bool
CertTimeChecked bool
CertTimeOK bool
Valid bool
Error string
}
type signatureTimestampEvidence struct {
Token []byte
Data []byte
}
// WithSignatureTimestamp 设置离线时间戳验证
// 入参: options 时间戳选项
// 返回: SignatureVerifyOption 验签选项
func WithSignatureTimestamp(options SignatureTimestampOptions) SignatureVerifyOption {
options.Tokens = cloneSignatureEvidence(options.Tokens)
options.TrustCerts = appendSignatureCerts(nil, options.TrustCerts...)
options.Certificates = appendSignatureCerts(nil, options.Certificates...)
if options.Nonce != nil {
options.Nonce = new(big.Int).Set(options.Nonce)
}
if options.VerifyTime != nil {
t := *options.VerifyTime
options.VerifyTime = &t
}
return func(o *signatureVerifyOptions) { o.Timestamp = &options }
}
// cloneSignatureEvidence 深拷贝离线证据
// 入参: values 原证据列表
// 返回: [][]byte 独立证据列表
func cloneSignatureEvidence(values [][]byte) [][]byte {
out := make([][]byte, len(values))
for i := range values {
out[i] = bytes.Clone(values[i])
}
return out
}
// VerifySignatureTimestamp 验证RFC3161令牌与原始字节的绑定
// 入参: token DER编码TimeStampToken, data 被时间戳保护的原始字节, options 验证选项
// 返回: SignatureTimestampReport 验证报告
func VerifySignatureTimestamp(token, data []byte, options SignatureTimestampOptions) (report SignatureTimestampReport) {
err := verifySignatureTimestamp(token, data, options, &report)
if err != nil {
report.Error = err.Error()
}
report.Valid = err == nil && report.BindingChecked && report.BindingOK && report.SignedValueChecked && report.SignedValueOK && report.CertTrustChecked && report.CertTrustOK && report.CertTimeChecked && report.CertTimeOK
return report
}
// verifySignatureTimestamp 检查令牌绑定、签名与TSA信任
// 入参: token 令牌DER, data 原始字节, o 验证选项, report 结果
// 返回: error 验证错误
func verifySignatureTimestamp(token, data []byte, o SignatureTimestampOptions, report *SignatureTimestampReport) error {
oid, content, present, err := parseGBTContentInfoBytes(token)
if err != nil || !present || oid != signContentCMS {
return fmt.Errorf("invalid RFC3161 CMS token")
}
items, ok := asn1Children(content.Bytes)
if !ok || len(items) < 4 {
return fmt.Errorf("invalid timestamp signed data")
}
version, err := asn1Integer(items[0])
if err != nil || version != 3 {
return fmt.Errorf("unsupported timestamp CMS version")
}
oid, value, present, err := parseGBTContentInfo(items[2])
if err != nil || !present || oid != signTimestampContent {
return fmt.Errorf("invalid TSTInfo content type")
}
tst, err := asn1OctetString(value)
if err != nil {
return err
}
if err := verifyTimestampInfo(tst, data, o, report); err != nil {
return err
}
certs := appendSignatureCerts(nil, o.Certificates...)
idx := 3
if items[idx].Class == asn1.ClassContextSpecific && items[idx].Tag == 0 {
parsed, err := parseGBTCertificates(items[idx])
if err != nil {
return err
}
for _, cert := range parsed {
certs = append(certs, cert.Raw)
}
idx++
}
if idx < len(items) && items[idx].Class == asn1.ClassContextSpecific && items[idx].Tag == 1 {
idx++
}
if idx != len(items)-1 || items[idx].Tag != asn1.TagSet || items[idx].Class != asn1.ClassUniversal {
return fmt.Errorf("invalid timestamp signer set")
}
signers, err := parseGBTSignerInfos(items[idx])
if err != nil || len(signers) != 1 {
return fmt.Errorf("timestamp requires one issuer-and-serial signer")
}
signer := signers[0]
if err := signatureSignerDigest(signer.SignatureAlg, signer.DigestAlg); err != nil {
return err
}
algorithms, ok := asn1Children(items[1].Bytes)
if !ok || items[1].Tag != asn1.TagSet || len(algorithms) != 1 {
return fmt.Errorf("invalid timestamp digest set")
}
digestAlg, err := parseGBTAlgorithm(algorithms[0])
if err != nil || digestAlg != signer.DigestAlg {
return fmt.Errorf("timestamp digest set mismatch")
}
if err := signatureAlgorithmPolicy(&SignaturePolicy{RejectWeakAlgorithms: true}, signer.SignatureAlg, signer.DigestAlg); err != nil {
return err
}
attrs, err := signatureAuthenticatedAttributes(signer.AuthAttrs)
if err != nil {
return err
}
ct, err := asn1OIDString(attrs[signAttrContentType])
if err != nil || ct != signTimestampContent {
return fmt.Errorf("timestamp content-type attribute mismatch")
}
digest, err := signatureDigest(signer.DigestAlg, tst)
if err != nil || !bytes.Equal(digest, signer.AttrDigest) {
return fmt.Errorf("timestamp content digest mismatch")
}
var cert []byte
for _, raw := range certs {
c, err := parseSignatureCertificate(raw)
if err == nil && bytes.Equal(c.RawIssuer, signer.Issuer) && c.SerialNumber.Cmp(signer.Serial) == 0 {
if cert != nil && !bytes.Equal(cert, raw) {
return fmt.Errorf("ambiguous TSA certificate")
}
cert = raw
}
}
if cert == nil {
return fmt.Errorf("TSA certificate not found")
}
if err := verifyTimestampESS(attrs, cert); err != nil {
return err
}
c, err := parseSignatureCertificate(cert)
if err != nil {
return err
}
if len(report.tsaName.FullBytes) != 0 && (report.tsaName.Class != asn1.ClassContextSpecific || report.tsaName.Tag != 4 || !bytes.Equal(report.tsaName.Bytes, c.RawSubject)) {
return fmt.Errorf("unsupported or mismatched TSTInfo TSA name")
}
critical := false
for _, extension := range c.Extensions {
if extension.Id.String() == "2.5.29.37" {
critical = extension.Critical
}
}
if !critical || len(c.UnknownExtKeyUsage) != 0 || len(c.ExtKeyUsage) != 1 || c.ExtKeyUsage[0] != smx509.ExtKeyUsageTimeStamping {
return fmt.Errorf("TSA certificate requires exclusive critical timestamping usage")
}
report.Signer = signatureCertInfo(cert)
report.SignedValueChecked = true
if isSM2SignatureMethod(signer.SignatureAlg) {
pub, err := parseSM2PublicKeyFromCert(cert)
if err != nil {
return err
}
report.SignedValueOK = verifySM2Signature(pub, nil, signer.AuthAttrs, signer.Signature)
} else {
report.SignedValueOK, err = verifyPublicKeySignature(signer.SignatureAlg, signer.DigestAlg, cert, signer.AuthAttrs, signer.Signature)
if err != nil {
return err
}
}
if !report.SignedValueOK {
return fmt.Errorf("invalid timestamp signature")
}
report.CertTimeChecked = true
report.CertTimeOK = timeInRange(report.Time, c.NotBefore, c.NotAfter)
if !report.CertTimeOK {
return fmt.Errorf("TSA certificate not valid at timestamp time")
}
trusts := appendSignatureCerts(nil, o.TrustCerts...)
if len(trusts) == 0 {
return fmt.Errorf("timestamp trust certificates not provided")
}
report.CertTrustChecked = true
report.CertTrustOK = verifySignatureCertificateChain(c, certs, trusts, report.Time, smx509.ExtKeyUsageTimeStamping) == nil
if !report.CertTrustOK {
return fmt.Errorf("untrusted TSA certificate")
}
return nil
}
// verifyTimestampInfo 验证TSTInfo及可选nonce和策略
// 入参: data TSTInfo字节, bound 被保护字节, o 验证选项, report 结果
// 返回: error 验证错误
func verifyTimestampInfo(data, bound []byte, o SignatureTimestampOptions, report *SignatureTimestampReport) error {
var raw asn1.RawValue
rest, err := asn1.Unmarshal(data, &raw)
if err != nil || len(rest) != 0 || raw.Tag != asn1.TagSequence {
return fmt.Errorf("invalid TSTInfo")
}
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) < 5 {
return fmt.Errorf("invalid TSTInfo fields")
}
v, err := asn1Integer(items[0])
if err != nil || v != 1 {
return fmt.Errorf("unsupported TSTInfo version")
}
report.PolicyOID, err = asn1OIDString(items[1])
if err != nil {
return err
}
if o.PolicyOID != "" && o.PolicyOID != report.PolicyOID {
return fmt.Errorf("timestamp policy mismatch")
}
imprint, ok := asn1Children(items[2].Bytes)
if !ok || len(imprint) != 2 {
return fmt.Errorf("invalid timestamp imprint")
}
method, err := parseGBTAlgorithm(imprint[0])
if err != nil {
return err
}
if err := signatureAlgorithmPolicy(&SignaturePolicy{RejectWeakAlgorithms: true}, "", method); err != nil {
return err
}
want, err := asn1OctetString(imprint[1])
if err != nil {
return err
}
got, err := signatureDigest(method, bound)
if err != nil {
return err
}
report.BindingChecked, report.BindingOK = true, bytes.Equal(want, got)
if !report.BindingOK {
return fmt.Errorf("timestamp message imprint mismatch")
}
serial, err := asn1IntegerBig(items[3])
if err != nil || serial.Sign() < 0 {
return fmt.Errorf("invalid timestamp serial")
}
report.SerialNumber = serial.String()
if items[4].Tag != asn1.TagGeneralizedTime {
return fmt.Errorf("invalid timestamp generalized time")
}
report.Time, err = asn1Time(items[4])
if err != nil {
return err
}
now := time.Now()
if o.VerifyTime != nil {
now = *o.VerifyTime
}
if report.Time.After(now) {
return fmt.Errorf("timestamp is in the future")
}
previous := -1
for _, item := range items[5:] {
order := -1
switch {
case item.Class == asn1.ClassUniversal && item.Tag == asn1.TagSequence:
order = 0
var accuracy struct {
Seconds int `asn1:"optional"`
Millis int `asn1:"optional,tag:0"`
Micros int `asn1:"optional,tag:1"`
}
rest, err := asn1.Unmarshal(item.FullBytes, &accuracy)
if err != nil || len(rest) != 0 || accuracy.Seconds < 0 || accuracy.Millis < 0 || accuracy.Millis > 999 || accuracy.Micros < 0 || accuracy.Micros > 999 {
return fmt.Errorf("invalid timestamp accuracy")
}
case item.Class == asn1.ClassUniversal && item.Tag == asn1.TagBoolean:
order = 1
var ordering bool
if _, err := asn1.Unmarshal(item.FullBytes, &ordering); err != nil {
return err
}
case item.Class == asn1.ClassUniversal && item.Tag == asn1.TagInteger:
order = 2
report.Nonce, err = asn1IntegerBig(item)
if err != nil || report.Nonce.Sign() < 0 {
return fmt.Errorf("invalid timestamp nonce")
}
case item.Class == asn1.ClassContextSpecific && item.Tag == 0:
order = 3
report.tsaName, err = asn1Explicit(item)
if err != nil {
return err
}
case item.Class == asn1.ClassContextSpecific && item.Tag == 1:
return fmt.Errorf("timestamp extensions not supported")
default:
return fmt.Errorf("invalid timestamp optional field")
}
if order <= previous {
return fmt.Errorf("duplicate or unordered timestamp field")
}
previous = order
}
if o.Nonce != nil && (report.Nonce == nil || report.Nonce.Cmp(o.Nonce) != 0) {
return fmt.Errorf("timestamp nonce mismatch")
}
return nil
}
// signatureAuthenticatedAttributes 拒绝重复属性和非DER排序
// 入参: data 签名属性DER集合
// 返回: map[string]asn1.RawValue 属性映射, error 解析错误
func signatureAuthenticatedAttributes(data []byte) (map[string]asn1.RawValue, error) {
var raw asn1.RawValue
rest, err := asn1.Unmarshal(data, &raw)
if err != nil || len(rest) != 0 || raw.Class != asn1.ClassUniversal || raw.Tag != asn1.TagSet {
return nil, fmt.Errorf("invalid signed attributes")
}
attrs, ok := asn1Children(raw.Bytes)
if !ok {
return nil, fmt.Errorf("invalid signed attributes")
}
out := make(map[string]asn1.RawValue)
var previous []byte
for _, attr := range attrs {
if bytes.Compare(previous, attr.FullBytes) > 0 {
return nil, fmt.Errorf("non-DER signed attributes")
}
previous = attr.FullBytes
items, ok := asn1Children(attr.Bytes)
if !ok || len(items) != 2 || items[1].Tag != asn1.TagSet {
return nil, fmt.Errorf("invalid signed attribute")
}
oid, err := asn1OIDString(items[0])
if err != nil {
return nil, err
}
if _, exists := out[oid]; exists {
return nil, fmt.Errorf("duplicate signed attribute: %s", oid)
}
values, ok := asn1Children(items[1].Bytes)
if !ok || len(values) != 1 {
return nil, fmt.Errorf("signed attribute must have one value")
}
out[oid] = values[0]
}
return out, nil
}
// verifyTimestampESS 验证ESS证书摘要及颁发者序列号绑定
// 入参: attrs 签名属性, cert TSA证书DER
// 返回: error 绑定错误
func verifyTimestampESS(attrs map[string]asn1.RawValue, cert []byte) error {
found := false
for _, oid := range []string{signAttrESS, signAttrESSV2} {
value, exists := attrs[oid]
if !exists {
continue
}
found = true
outer, ok := asn1Children(value.Bytes)
if !ok || len(outer) < 1 || len(outer) > 2 {
return fmt.Errorf("invalid ESS signing certificate")
}
certs, ok := asn1Children(outer[0].Bytes)
if !ok || len(certs) == 0 {
return fmt.Errorf("empty ESS certificate list")
}
fields, ok := asn1Children(certs[0].Bytes)
if !ok || len(fields) == 0 {
return fmt.Errorf("invalid ESS certificate identifier")
}
method := "SHA1"
idx := 0
if oid == signAttrESSV2 {
method = "SHA256"
if fields[0].Tag == asn1.TagSequence {
var err error
method, err = parseGBTAlgorithm(fields[0])
if err != nil {
return err
}
idx++
}
}
if len(fields) < idx+1 || len(fields) > idx+2 {
return fmt.Errorf("invalid ESS certificate fields")
}
want, err := asn1OctetString(fields[idx])
if err != nil {
return err
}
got, err := signatureDigest(method, cert)
if err != nil || !bytes.Equal(want, got) {
return fmt.Errorf("ESS certificate hash mismatch")
}
if len(fields) == idx+2 {
issuer, ok := asn1Children(fields[idx+1].Bytes)
if !ok || len(issuer) != 2 {
return fmt.Errorf("invalid ESS issuer serial")
}
serial, err := asn1IntegerBig(issuer[1])
if err != nil {
return err
}
c, err := parseSignatureCertificate(cert)
if err != nil {
return err
}
names, ok := asn1Children(issuer[0].Bytes)
matched := false
for _, name := range names {
matched = matched || name.Class == asn1.ClassContextSpecific && name.Tag == 4 && bytes.Equal(name.Bytes, c.RawIssuer)
}
if !ok || !matched || serial.Cmp(c.SerialNumber) != 0 {
return fmt.Errorf("ESS issuer serial mismatch")
}
}
}
if !found {
return fmt.Errorf("timestamp ESS certificate binding missing")
}
return nil
}
// signatureTimestampAttributes 提取签名值中的RFC3161未认证属性
// 入参: raw 未认证属性ASN.1值
// 返回: [][]byte 时间戳令牌, error 解析错误
func signatureTimestampAttributes(raw asn1.RawValue) ([][]byte, error) {
if len(raw.FullBytes) == 0 {
return nil, nil
}
attrs, ok := asn1Children(raw.Bytes)
if !ok {
return nil, fmt.Errorf("invalid timestamp attributes")
}
var tokens [][]byte
for _, attr := range attrs {
items, ok := asn1Children(attr.Bytes)
if !ok || len(items) != 2 {
return nil, fmt.Errorf("invalid unsigned attribute")
}
oid, err := asn1OIDString(items[0])
if err != nil {
return nil, err
}
if oid != signTimestampAttr {
continue
}
if len(tokens) != 0 {
return nil, fmt.Errorf("duplicate timestamp attribute")
}
values, ok := asn1Children(items[1].Bytes)
if !ok || items[1].Tag != asn1.TagSet || len(values) == 0 {
return nil, fmt.Errorf("empty timestamp attribute")
}
for _, value := range values {
tokens = append(tokens, bytes.Clone(value.FullBytes))
}
}
return tokens, nil
}
// applySignatureEvidence 验证内嵌及外部时间戳并应用吊销策略
// 入参: o 验签选项, signedValue 签名文件字节, embedded 内嵌令牌, certs 签署证书, extra 辅助证书
func (report *SignatureVerifyReport) applySignatureEvidence(o *signatureVerifyOptions, signedValue []byte, embedded []signatureTimestampEvidence, certs, extra [][]byte) {
var options SignatureTimestampOptions
if o.Timestamp != nil {
options = *o.Timestamp
}
if o.Policy != nil {
options.Required = options.Required || o.Policy.RequireTimestamp
}
evidence := append([]signatureTimestampEvidence(nil), embedded...)
for _, token := range options.Tokens {
evidence = append(evidence, signatureTimestampEvidence{Token: token, Data: signedValue})
}
if options.Fetch != nil {
tokens, err := options.Fetch(SignatureTimestampRequest{DocIndex: report.DocIndex, SignatureID: report.ID, Data: bytes.Clone(signedValue)})
if err != nil {
report.Timestamps = append(report.Timestamps, SignatureTimestampReport{Error: err.Error()})
}
for _, token := range tokens {
evidence = append(evidence, signatureTimestampEvidence{Token: token, Data: signedValue})
}
}
for _, item := range evidence {
report.Timestamps = append(report.Timestamps, VerifySignatureTimestamp(item.Token, item.Data, options))
}
if len(report.Timestamps) != 0 {
report.TimestampChecked, report.TimestampOK = true, true
for _, result := range report.Timestamps {
report.TimestampOK = report.TimestampOK && result.Valid
}
}
if options.Required && !report.TimestampChecked {
report.PolicyChecked, report.PolicyOK = true, false
report.PolicyError = "required timestamp evidence missing"
}
report.applySignatureRevocation(o, certs, extra)
}
+174 -424
View File
@@ -24,7 +24,6 @@ import (
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
@@ -35,13 +34,32 @@ import (
"path"
"strings"
"time"
"github.com/emmansun/gmsm/sm2"
"github.com/emmansun/gmsm/smx509"
)
// SignatureVerifyReport 签名验证报告
// Valid表示签名完整性、签名时间语义及调用方指定的证书策略均通过
// Valid表示签名完整性、签名时间语义及调用方指定的全部策略均通过
// Checked表示对应检查已得出结论, 未检查时不能将OK的零值视为失败
// SealCertTimeOK仅提供制章证书在签名时间的状态信息, 不参与Valid判断
type SignatureVerifyReport struct {
DocIndex int
DocRoot string
PolicyChecked bool
PolicyOK bool
PolicyError string
CoverageChecked bool
CoverageOK bool
CoverageError string
CoveredFiles []string
UncoveredFiles []string
TimestampChecked bool
TimestampOK bool
Timestamps []SignatureTimestampReport
RevocationChecked bool
RevocationOK bool
Revocations []SignatureRevocationReport
ID string
BaseLoc string
Type SignType
@@ -80,6 +98,7 @@ type SignatureVerifyReport struct {
CertTimeOK bool
CertTrustChecked bool
CertTrustOK bool
CertTrustError string
Valid bool
Error string
}
@@ -87,7 +106,10 @@ type SignatureVerifyReport struct {
// IntegrityValid 判断签名完整性是否有效
// 返回: bool 是否有效
func (report SignatureVerifyReport) IntegrityValid() bool {
return report.Error == "" && report.DigestOK && report.DataHashOK && report.SignedValueOK && report.SealOK && report.SealMatchOK && report.CertOK
sealOK := report.Type == SignTypeSign || report.SealChecked && report.SealOK
return report.Error == "" && report.DigestOK && referencesOK(report.References) &&
report.DataHashChecked && report.DataHashOK && report.SignedValueChecked && report.SignedValueOK &&
sealOK && report.SealMatchOK && report.CertChecked && report.CertOK
}
// TrustedValid 判断签名是否可信有效
@@ -151,6 +173,11 @@ type signatureVerifyOptions struct {
SignCerts [][]byte
TrustCerts [][]byte
VerifyTime *time.Time
Policy *SignaturePolicy
Timestamp *SignatureTimestampOptions
Revocation *SignatureRevocationOptions
DocIndex int
DocRoot string
}
var signatureMethodReplacer = strings.NewReplacer("-", "", "_", "", " ", "")
@@ -177,6 +204,7 @@ func WithSignatureCerts(certs ...[]byte) SignatureVerifyOption {
}
// WithSignatureTrustCert 添加签名信任证书
// 指定信任后同时验证证书链有效期,默认使用当前时间
// 入参: cert DER或PEM编码证书
// 返回: SignatureVerifyOption 签名验证选项
func WithSignatureTrustCert(cert []byte) SignatureVerifyOption {
@@ -186,6 +214,7 @@ func WithSignatureTrustCert(cert []byte) SignatureVerifyOption {
}
// WithSignatureTrustCerts 添加多张签名信任证书
// 指定信任后同时验证证书链有效期,默认使用当前时间
// 入参: certs DER或PEM编码证书列表
// 返回: SignatureVerifyOption 签名验证选项
func WithSignatureTrustCerts(certs ...[]byte) SignatureVerifyOption {
@@ -246,19 +275,56 @@ func VerifySignaturesReader(r io.ReaderAt, size int64, opts ...SignatureVerifyOp
// 入参: opts 签名验证选项
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
func (r *Reader) VerifySignatures(opts ...SignatureVerifyOption) ([]SignatureVerifyReport, error) {
if r.OFD == nil || len(r.OFD.DocBody) == 0 {
return nil, fmt.Errorf("no docbody found")
}
if _, err := r.Doc(); err != nil {
return nil, err
}
var reports []SignatureVerifyReport
for i := range r.OFD.DocBody {
items, err := r.VerifyDocumentSignatures(i, opts...)
reports = append(reports, items...)
if err != nil {
return reports, fmt.Errorf("document %d: %w", i, err)
}
}
return reports, nil
}
// VerifyDocumentSignatures 验证指定DocBody的签名且不切换阅读器当前文档
// 入参: index 从0开始的文档索引, opts 签名验证选项
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
func (r *Reader) VerifyDocumentSignatures(index int, opts ...SignatureVerifyOption) ([]SignatureVerifyReport, error) {
if r.OFD == nil || index < 0 || index >= len(r.OFD.DocBody) {
return nil, fmt.Errorf("document index out of range: %d", index)
}
options := signatureVerifyOptions{}
for _, opt := range opts {
opt(&options)
if opt != nil {
opt(&options)
}
}
doc, err := r.Doc()
body := r.OFD.DocBody[index]
options.DocIndex, options.DocRoot = index, r.signatureCoveragePath(body.DocRoot)
data, err := r.readFile(body.DocRoot)
if err != nil {
return nil, err
}
var doc Document
if err := xml.Unmarshal(data, &doc); err != nil {
return nil, err
}
if doc.Signatures == "" {
doc.Signatures = body.Signatures
}
if doc.Signatures == "" {
return nil, nil
}
sigListPath := r.ResPath(doc.Signatures)
data, err := r.readFileExact(sigListPath)
view := *r
view.doc, view.RootDir = &doc, path.Dir(body.DocRoot)
sigListPath := view.ResPath(doc.Signatures)
data, err = view.readFile(sigListPath)
if err != nil {
return nil, err
}
@@ -268,7 +334,7 @@ func (r *Reader) VerifySignatures(opts ...SignatureVerifyOption) ([]SignatureVer
}
reports := make([]SignatureVerifyReport, 0, len(signatures.List))
for _, sigRef := range signatures.List {
reports = append(reports, r.verifySignature(sigListPath, sigRef, &options))
reports = append(reports, view.verifySignature(sigListPath, sigRef, &options))
}
return reports, nil
}
@@ -279,6 +345,8 @@ func (r *Reader) VerifySignatures(opts ...SignatureVerifyOption) ([]SignatureVer
func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *signatureVerifyOptions) SignatureVerifyReport {
sigPath := signatureRefPath(sigListPath, sigRef.BaseLoc)
report := SignatureVerifyReport{
DocIndex: options.DocIndex,
DocRoot: options.DocRoot,
ID: sigRef.ID,
BaseLoc: sigRef.BaseLoc,
Type: sigRef.Type,
@@ -287,7 +355,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
if report.Type == "" {
report.Type = SignTypeSeal
}
sigData, err := r.readFileExact(sigPath)
sigData, err := r.readFile(sigPath)
if err != nil {
report.Error = err.Error()
return report
@@ -311,8 +379,10 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.StampPositionError = err.Error()
}
report.DigestOK = referencesOK(report.References)
r.applySignatureCoverage(&report, sigListPath, options)
report.applySignaturePolicy(options, r.OFD.Version, 0, report.SignatureMethod, report.DigestMethod)
signedValuePath := signatureRefPath(sigPath, sigFile.SignedValue)
signedValue, err := r.readFileExact(signedValuePath)
signedValue, err := r.readFile(signedValuePath)
if err != nil {
report.Error = err.Error()
return report
@@ -331,6 +401,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.Signer = result.CertInfo.CommonName
}
if err != nil {
report.applySignaturePolicyError(err)
report.Error = err.Error()
return report
}
@@ -338,6 +409,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.SignatureTime = parseSignatureDateTime(report.SignatureDateTime)
report.applySignatureTimePolicy()
report.applySignatureCertificatePolicy(options, result.SignerCerts, result.Certs)
report.applySignatureEvidence(options, signedValue, result.Timestamps, result.SignerCerts, result.Certs)
report.Valid = report.IntegrityValid() && report.certificatePolicyOK()
return report
case SignTypeSeal:
@@ -363,14 +435,21 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.Signer = sesResult.SignCert.CommonName
}
if err != nil {
report.applySignaturePolicyError(err)
report.Error = err.Error()
return report
}
report.applySignatureTimePolicy()
if report.SignatureMethod != "" && !signatureAlgorithmEquivalent(report.SignatureMethod, sesResult.SignatureMethod, "SM3") {
report.Error = "signature method does not match SES"
return report
}
report.applySignatureCertificatePolicy(options, [][]byte{sesResult.SignCertRaw, sesResult.SealCertRaw}, sesResult.Certs)
report.applySignaturePolicy(options, r.OFD.Version, sesResult.SealInfo.Version, sesResult.SignatureMethod, "SM3")
report.applySignatureEvidence(options, signedValue, sesResult.Timestamps, [][]byte{sesResult.SignCertRaw, sesResult.SealCertRaw}, sesResult.Certs)
if sigFile.SignedInfo.Seal.BaseLoc != "" {
sealPath := signatureRefPath(sigPath, sigFile.SignedInfo.Seal.BaseLoc)
sealData, err := r.readFileExact(sealPath)
sealData, err := r.readFile(sealPath)
if err != nil {
report.Error = err.Error()
return report
@@ -407,7 +486,7 @@ func (r *Reader) verifySignatureReference(sigPath, method string, ref SignatureR
result.Error = err.Error()
return result
}
data, err := r.readFileExact(refPath)
data, err := r.readFile(refPath)
if err != nil {
result.Error = err.Error()
return result
@@ -435,20 +514,6 @@ func parseSignatureFile(data []byte) (*SignatureFile, error) {
return &sigFile, nil
}
// readFileExact 读取OFD包内文件
// 入参: name 包内文件路径
// 返回: []byte 文件数据, error 错误信息
func (r *Reader) readFileExact(name string) ([]byte, error) {
name = cleanPackagePath(name)
if data, ok := r.files[name]; ok {
return bytes.Clone(data), nil
}
if f, ok := r.fileIndex[name]; ok {
return readZipFile(f)
}
return nil, fmt.Errorf("file not found: %s", name)
}
// signatureDigest 计算签名摘要
// 入参: method 摘要算法, data 原文数据
// 返回: []byte 摘要值, error 错误信息
@@ -612,7 +677,7 @@ func verifyPublicKeySignature(method, digestMethod string, cert, signedData, sig
if len(digest) == 0 {
return false, fmt.Errorf("unsupported digest method")
}
x509Cert, err := x509.ParseCertificate(cert)
x509Cert, err := parseSignatureCertificate(cert)
if err != nil {
return false, err
}
@@ -632,6 +697,23 @@ func verifyPublicKeySignature(method, digestMethod string, cert, signedData, sig
}
}
// verifySM2Signature 验证DER或定长拼接编码的SM2签名值
// 入参: pub 公钥, userID 用户标识, msg 原文, sig 签名值
// 返回: bool 是否验证通过
func verifySM2Signature(pub *ecdsa.PublicKey, userID, msg, sig []byte) bool {
if sm2.VerifyASN1WithSM2(pub, userID, msg, sig) {
return true
}
if len(sig) != 64 {
return false
}
digest, err := sm2.CalculateSM2Hash(pub, msg, userID)
if err != nil {
return false
}
return sm2.Verify(pub, digest, new(big.Int).SetBytes(sig[:32]), new(big.Int).SetBytes(sig[32:]))
}
// verifyECDSASignature 验证ECDSA签名
// 入参: pub 公钥, digest 摘要, sig 签名值
// 返回: bool 是否验证通过
@@ -668,7 +750,7 @@ func referencesOK(refs []SignatureReferenceVerify) bool {
return false
}
for _, ref := range refs {
if !ref.OK {
if !ref.Checked || !ref.OK || ref.Error != "" {
return false
}
}
@@ -679,9 +761,14 @@ func referencesOK(refs []SignatureReferenceVerify) bool {
// 入参: options 验证选项, certs 待验证证书, extraCerts 证书池
func (report *SignatureVerifyReport) applySignatureCertificatePolicy(options *signatureVerifyOptions, certs [][]byte, extraCerts [][]byte) {
certs = compactSignatureCerts(certs)
if options.VerifyTime != nil {
verifyTime := options.VerifyTime
if verifyTime == nil && len(options.TrustCerts) != 0 {
now := time.Now()
verifyTime = &now
}
if verifyTime != nil {
report.CertTimeChecked = true
report.CertTimeOK = signatureCertsValidAt(certs, *options.VerifyTime)
report.CertTimeOK = signatureCertsValidAt(certs, *verifyTime)
}
if len(options.TrustCerts) != 0 {
report.CertTrustChecked = true
@@ -691,13 +778,15 @@ func (report *SignatureVerifyReport) applySignatureCertificatePolicy(options *si
pool = append(pool, extraCerts...)
pool = compactSignatureCerts(pool)
for _, cert := range certs {
if !signatureCertTrustedBy(cert, pool, options.TrustCerts, options.VerifyTime) {
if err := verifySignatureCertificateTrust(cert, pool, options.TrustCerts, *verifyTime); err != nil {
report.CertTrustOK = false
report.CertTrustError = err.Error()
break
}
}
if len(certs) == 0 {
report.CertTrustOK = false
report.CertTrustError = "signature certificate missing"
}
}
}
@@ -728,6 +817,10 @@ func timeInRange(t, start, end time.Time) bool {
// certificatePolicyOK 判断证书策略是否通过
// 返回: bool 是否通过
func (report SignatureVerifyReport) certificatePolicyOK() bool {
if report.PolicyChecked && !report.PolicyOK || report.CoverageChecked && !report.CoverageOK ||
report.TimestampChecked && !report.TimestampOK || report.RevocationChecked && !report.RevocationOK {
return false
}
if report.SignatureTimeChecked && !report.SignatureTimeOK {
return false
}
@@ -759,112 +852,51 @@ func signatureCertsValidAt(certs [][]byte, t time.Time) bool {
return true
}
// signatureCertMaxChecks 最大证书签名验证次数
const signatureCertMaxChecks = 100
// signatureCertPathState 证书路径状态
type signatureCertPathState struct {
Visited map[string]bool
SignatureChecks int
}
// signatureCertTrustedBy 判断证书是否可链到信任证书
// 入参: cert 证书, pool 证书池, trusts 信任证书, verifyTime 中间证书验证时间
// 返回: bool 是否受信任
func signatureCertTrustedBy(cert []byte, pool, trusts [][]byte, verifyTime *time.Time) bool {
state := signatureCertPathState{Visited: make(map[string]bool)}
return signatureCertPathTrustedBy(cert, pool, trusts, verifyTime, &state, 0, true)
}
// signatureCertPathTrustedBy 验证证书路径
// 入参: cert 证书, pool 证书池, trusts 信任证书, verifyTime 中间证书验证时间, state 路径状态, caBelow 下级非自颁发中间CA数量, target 是否目标证书
// 返回: bool 是否受信任
func signatureCertPathTrustedBy(cert []byte, pool, trusts [][]byte, verifyTime *time.Time, state *signatureCertPathState, caBelow int, target bool) bool {
if len(cert) == 0 {
return false
}
trusted := false
for _, trust := range trusts {
if bytes.Equal(cert, trust) {
trusted = true
break
}
}
if trusted && !target {
return true
}
key := string(cert)
if state.Visited[key] {
return false
}
state.Visited[key] = true
defer delete(state.Visited, key)
// verifySignatureCertificateTrust 按指定时间验证签署证书到显式信任根的证书链
// 入参: cert 证书, pool 证书池, trusts 信任证书, at 验证时间
// 返回: error 验证错误
func verifySignatureCertificateTrust(cert []byte, pool, trusts [][]byte, at time.Time) error {
c, err := parseSignatureCertificate(cert)
if err != nil {
return false
return err
}
if c.UnhandledCritical {
return false
if c.KeyUsage != 0 && c.KeyUsage&(smx509.KeyUsageDigitalSignature|smx509.KeyUsageContentCommitment) == 0 {
return fmt.Errorf("certificate does not authorize digital signatures")
}
if !target && verifyTime != nil && (verifyTime.Before(c.NotBefore) || verifyTime.After(c.NotAfter)) {
return false
}
if target {
if c.KeyUsage != 0 && c.KeyUsage&(x509.KeyUsageDigitalSignature|x509.KeyUsageContentCommitment) == 0 {
return false
}
} else {
if !c.IsCA {
return false
}
if c.KeyUsage != 0 && c.KeyUsage&x509.KeyUsageCertSign == 0 {
return false
}
if c.MaxPathLen != nil && c.MaxPathLen.Cmp(big.NewInt(int64(caBelow))) < 0 {
return false
return verifySignatureCertificateChain(c, pool, trusts, at, smx509.ExtKeyUsageAny)
}
// verifySignatureCertificateChain 使用统一X.509实现验证证书链
// 入参: cert 目标证书, pool 中间证书, trusts 信任根, at 验证时间, usage 扩展用途
// 返回: error 验证错误
func verifySignatureCertificateChain(cert *smx509.Certificate, pool, trusts [][]byte, at time.Time, usage smx509.ExtKeyUsage) error {
roots, intermediates := smx509.NewCertPool(), smx509.NewCertPool()
for _, group := range []struct {
data [][]byte
pool *smx509.CertPool
}{{trusts, roots}, {pool, intermediates}} {
for _, data := range group.data {
ca, err := parseSignatureCertificate(data)
if err != nil {
return err
}
group.pool.AddCert(ca)
}
}
if trusted {
return true
}
nextCABelow := caBelow
if !target && !bytes.Equal(c.Issuer, c.Subject) {
nextCABelow++
}
for _, issuerCert := range pool {
if state.SignatureChecks >= signatureCertMaxChecks {
return false
}
if bytes.Equal(cert, issuerCert) {
continue
}
issuer, err := parseSignatureCertificate(issuerCert)
if err != nil || !bytes.Equal(c.Issuer, issuer.Subject) {
continue
}
state.SignatureChecks++
if ok, err := verifyCertificateSignature(c, issuerCert); err != nil || !ok {
continue
}
if signatureCertPathTrustedBy(issuerCert, pool, trusts, verifyTime, state, nextCABelow, false) {
return true
}
}
return false
_, err := cert.Verify(smx509.VerifyOptions{Roots: roots, Intermediates: intermediates, CurrentTime: at, KeyUsages: []smx509.ExtKeyUsage{usage}})
return err
}
// verifyCertificateSignature 验证证书签名
// 入参: cert 证书信息, issuerCert 颁发者证书
// 返回: bool 是否验证通过, error 错误信息
func verifyCertificateSignature(cert signatureCertificate, issuerCert []byte) (bool, error) {
if isSM2SignatureMethod(cert.SignatureAlg) {
pub, err := parseSM2PublicKeyFromCert(issuerCert)
if err != nil {
return false, err
}
return sm2VerifySignature(pub, nil, cert.TBS, cert.Signature), nil
func verifyCertificateSignature(cert *smx509.Certificate, issuerCert []byte) (bool, error) {
issuer, err := parseSignatureCertificate(issuerCert)
if err != nil {
return false, err
}
return verifyPublicKeySignature(cert.SignatureAlg, "", issuerCert, cert.TBS, cert.Signature)
err = issuer.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature)
return err == nil, err
}
// compactSignatureCerts 清理证书列表
@@ -895,21 +927,16 @@ func signatureCertInfo(data []byte) SignatureCertInfo {
if err != nil {
return SignatureCertInfo{}
}
subject := certificateNameValues(cert.SubjectValue)
issuer := certificateNameValues(cert.IssuerValue)
info := SignatureCertInfo{
Raw: append([]byte(nil), data...),
Subject: certificateNameString(subject),
CommonName: certificateNameFirst(subject, "2.5.4.3"),
Organization: certificateNameFirst(subject, "2.5.4.10"),
Issuer: certificateNameString(issuer),
return SignatureCertInfo{
Raw: bytes.Clone(cert.Raw),
Subject: cert.Subject.String(),
CommonName: cert.Subject.CommonName,
Organization: strings.Join(cert.Subject.Organization, ", "),
Issuer: cert.Issuer.String(),
NotBefore: cert.NotBefore,
NotAfter: cert.NotAfter,
SerialNumber: cert.SerialNumber.String(),
}
if cert.Serial != nil {
info.SerialNumber = cert.Serial.String()
}
return info
}
// parseSignatureDateTime 解析带时区的签名时间
@@ -929,227 +956,17 @@ func parseSignatureDateTime(value string) time.Time {
return time.Time{}
}
const (
signatureExtensionKeyUsage = "2.5.29.15"
signatureExtensionBasicConstraints = "2.5.29.19"
)
// signatureCertificateExtensions 签名证书扩展
type signatureCertificateExtensions struct {
IsCA bool
MaxPathLen *big.Int
KeyUsage x509.KeyUsage
UnhandledCritical bool
}
// signatureCertificate 签名证书结构
type signatureCertificate struct {
Raw []byte
TBS []byte
Issuer []byte
IssuerValue asn1.RawValue
Subject []byte
SubjectValue asn1.RawValue
PublicKey asn1.RawValue
Serial *big.Int
NotBefore time.Time
NotAfter time.Time
SignatureAlg string
Signature []byte
signatureCertificateExtensions
}
// parseSignatureCertificate 解析签名证书
// 入参: data DER编码证书
// 返回: signatureCertificate 签名证书结构, error 错误信息
func parseSignatureCertificate(data []byte) (signatureCertificate, error) {
var cert struct {
TBSCertificate asn1.RawValue
SignatureAlgorithm asn1.RawValue
SignatureValue asn1.BitString
}
rest, err := asn1.Unmarshal(data, &cert)
if err != nil || len(rest) != 0 {
return signatureCertificate{}, fmt.Errorf("invalid certificate")
}
items, ok := asn1Children(cert.TBSCertificate.Bytes)
if !ok {
return signatureCertificate{}, fmt.Errorf("invalid tbs certificate")
}
idx := 0
if len(items) > 0 && items[0].Class == asn1.ClassContextSpecific && items[0].Tag == 0 {
idx++
}
if len(items) <= idx+5 {
return signatureCertificate{}, fmt.Errorf("invalid certificate")
}
if !bytes.Equal(items[idx+1].FullBytes, cert.SignatureAlgorithm.FullBytes) {
return signatureCertificate{}, fmt.Errorf("certificate signature algorithm mismatch")
}
serial, err := asn1IntegerBig(items[idx])
if err != nil {
return signatureCertificate{}, err
}
validity, err := parseCertificateValidity(items[idx+3])
if err != nil {
return signatureCertificate{}, err
}
extensions, err := parseSignatureCertificateExtensions(items[idx+6:])
if err != nil {
return signatureCertificate{}, err
}
alg, err := parseGBTAlgorithm(cert.SignatureAlgorithm)
if err != nil {
return signatureCertificate{}, err
}
if cert.SignatureValue.BitLength%8 != 0 {
return signatureCertificate{}, fmt.Errorf("invalid certificate signature")
}
return signatureCertificate{
Raw: append([]byte(nil), data...),
TBS: append([]byte(nil), cert.TBSCertificate.FullBytes...),
Issuer: append([]byte(nil), items[idx+2].FullBytes...),
IssuerValue: items[idx+2],
Subject: append([]byte(nil), items[idx+4].FullBytes...),
SubjectValue: items[idx+4],
PublicKey: items[idx+5],
Serial: serial,
NotBefore: validity[0],
NotAfter: validity[1],
SignatureAlg: alg,
Signature: append([]byte(nil), cert.SignatureValue.Bytes...),
signatureCertificateExtensions: extensions,
}, nil
}
// parseSignatureCertificateExtensions 解析签名证书扩展
// 入参: items TBS证书剩余字段
// 返回: signatureCertificateExtensions 签名证书扩展, error 错误信息
func parseSignatureCertificateExtensions(items []asn1.RawValue) (signatureCertificateExtensions, error) {
var out signatureCertificateExtensions
var extensions []struct {
ID asn1.ObjectIdentifier
Critical bool `asn1:"optional"`
Value []byte
}
found := false
for _, item := range items {
if item.Class != asn1.ClassContextSpecific || item.Tag != 3 {
continue
}
if found || !item.IsCompound {
return out, fmt.Errorf("invalid certificate extensions")
}
found = true
rest, err := asn1.Unmarshal(item.Bytes, &extensions)
if err != nil || len(rest) != 0 {
return out, fmt.Errorf("invalid certificate extensions")
// parseSignatureCertificate 解析唯一的PEM或DER证书
// 入参: data 证书数据
// 返回: *smx509.Certificate 证书, error 错误信息
func parseSignatureCertificate(data []byte) (*smx509.Certificate, error) {
if block, rest := pem.Decode(data); block != nil {
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 || len(bytes.TrimSpace(rest)) != 0 {
return nil, fmt.Errorf("expected one certificate")
}
data = block.Bytes
}
seen := make(map[string]bool)
for _, extension := range extensions {
oid := extension.ID.String()
if seen[oid] {
return out, fmt.Errorf("duplicate certificate extension: %s", oid)
}
seen[oid] = true
switch oid {
case signatureExtensionBasicConstraints:
isCA, maxPathLen, err := parseSignatureBasicConstraints(extension.Value)
if err != nil {
return out, err
}
out.IsCA = isCA
out.MaxPathLen = maxPathLen
case signatureExtensionKeyUsage:
keyUsage, err := parseSignatureKeyUsage(extension.Value)
if err != nil {
return out, err
}
out.KeyUsage = keyUsage
default:
if extension.Critical {
out.UnhandledCritical = true
}
}
}
return out, nil
}
// parseSignatureBasicConstraints 解析证书基本约束
// 入参: data 扩展DER数据
// 返回: bool 是否为CA, *big.Int 路径长度限制, error 错误信息
func parseSignatureBasicConstraints(data []byte) (bool, *big.Int, error) {
var raw asn1.RawValue
rest, err := asn1.Unmarshal(data, &raw)
if err != nil || len(rest) != 0 || raw.Tag != asn1.TagSequence || !raw.IsCompound {
return false, nil, fmt.Errorf("invalid basic constraints")
}
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) > 2 {
return false, nil, fmt.Errorf("invalid basic constraints")
}
idx := 0
isCA := false
if len(items) > 0 && items[0].Tag == asn1.TagBoolean {
rest, err := asn1.Unmarshal(items[0].FullBytes, &isCA)
if err != nil || len(rest) != 0 {
return false, nil, fmt.Errorf("invalid basic constraints")
}
idx++
}
var maxPathLen *big.Int
if idx < len(items) {
maxPathLen, err = asn1IntegerBig(items[idx])
if err != nil || maxPathLen.Sign() < 0 {
return false, nil, fmt.Errorf("invalid basic constraints")
}
idx++
}
if idx != len(items) || (maxPathLen != nil && !isCA) {
return false, nil, fmt.Errorf("invalid basic constraints")
}
return isCA, maxPathLen, nil
}
// parseSignatureKeyUsage 解析证书密钥用途
// 入参: data 扩展DER数据
// 返回: x509.KeyUsage 密钥用途, error 错误信息
func parseSignatureKeyUsage(data []byte) (x509.KeyUsage, error) {
var bits asn1.BitString
rest, err := asn1.Unmarshal(data, &bits)
if err != nil || len(rest) != 0 || bits.BitLength == 0 || bits.BitLength > 9 {
return 0, fmt.Errorf("invalid key usage")
}
var out x509.KeyUsage
for i := 0; i < bits.BitLength; i++ {
if bits.At(i) != 0 {
out |= 1 << uint(i)
}
}
if out == 0 {
return 0, fmt.Errorf("invalid key usage")
}
return out, nil
}
// parseCertificateValidity 解析证书有效期
// 入参: raw 证书有效期ASN.1值
// 返回: [2]time.Time 生效和失效时间, error 错误信息
func parseCertificateValidity(raw asn1.RawValue) ([2]time.Time, error) {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) != 2 {
return [2]time.Time{}, fmt.Errorf("invalid certificate validity")
}
notBefore, err := asn1Time(items[0])
if err != nil {
return [2]time.Time{}, err
}
notAfter, err := asn1Time(items[1])
if err != nil {
return [2]time.Time{}, err
}
return [2]time.Time{notBefore, notAfter}, nil
return smx509.ParseCertificate(data)
}
// asn1Time 解析ASN.1时间
@@ -1164,82 +981,15 @@ func asn1Time(raw asn1.RawValue) (time.Time, error) {
return t, nil
}
// certificateNameValues 解析证书名称字段
// 入参: raw 名称原始值
// 返回: map[string][]string OID字段列表
func certificateNameValues(raw asn1.RawValue) map[string][]string {
out := make(map[string][]string)
sets, ok := asn1Children(raw.Bytes)
if !ok {
return out
}
for _, set := range sets {
attrs, ok := asn1Children(set.Bytes)
if !ok {
continue
}
for _, attr := range attrs {
items, ok := asn1Children(attr.Bytes)
if !ok || len(items) < 2 {
continue
}
oid, err := asn1OIDString(items[0])
if err != nil {
continue
}
if value := strings.TrimSpace(asn1String(items[1])); value != "" {
out[oid] = append(out[oid], value)
}
}
}
return out
}
// certificateNameFirst 获取证书名称字段首值
// 入参: values OID字段列表, oid 字段OID
// 返回: string 字段值
func certificateNameFirst(values map[string][]string, oid string) string {
if items := values[oid]; len(items) > 0 {
return items[0]
}
return ""
}
// certificateNameString 格式化证书名称
// 入参: values OID字段列表
// 返回: string 证书名称
func certificateNameString(values map[string][]string) string {
var parts []string
for _, item := range []struct {
OID string
Label string
}{
{"2.5.4.3", "CN"},
{"2.5.4.10", "O"},
{"2.5.4.11", "OU"},
{"2.5.4.6", "C"},
{"2.5.4.8", "ST"},
{"2.5.4.7", "L"},
{"2.5.4.5", "SN"},
{"1.2.840.113549.1.9.1", "E"},
} {
for _, value := range values[item.OID] {
parts = append(parts, item.Label+"="+value)
}
}
return strings.Join(parts, ", ")
}
// parseSignatureCerts 解析签名验证证书
// 入参: data DER或PEM编码证书
// 返回: [][]byte DER编码证书列表
func parseSignatureCerts(data []byte) [][]byte {
data = bytes.TrimSpace(data)
if len(data) == 0 {
return nil
}
var certs [][]byte
rest := data
rest := bytes.TrimSpace(data)
hasPEM := false
for {
block, next := pem.Decode(rest)
+503
View File
@@ -0,0 +1,503 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"archive/zip"
"bytes"
"crypto"
"encoding/base64"
"encoding/xml"
"fmt"
"io"
"path"
"slices"
"strconv"
"strings"
"time"
)
// SignatureWriteMode 签署方式
type SignatureWriteMode uint8
const (
// SignatureAppend 追加签署,要求原活动签名有效且未保护签名列表
SignatureAppend SignatureWriteMode = iota
// SignatureReplace 重新签署,替换全部活动签名,保留原签名数据文件
SignatureReplace
)
// SignatureWriteOptions 最终包签署选项,不修改Editor或调用方的原件
// SM2数字签名使用GB/T 35275-2017消息结构;RSA/ECDSA使用裸签名,验签需WithSignatureCert
// Seal仅接受已有合法SES v4电子印章,使用SM2/SM3,不声明GM/T 0031-2025兼容
// TrustRoots必须显式指定;证书吊销状态和外部业务授权由调用方在签署前检查
type SignatureWriteOptions struct {
Signer crypto.Signer
Certificate []byte
TrustRoots [][]byte
Intermediates [][]byte
Seal []byte
Stamps []SignatureStamp
Mode SignatureWriteMode
// Time是自报签署时间,不是可信时间戳;零值取当前时间
Time time.Time
Provider SignatureProvider
// Lock保护签名列表,禁止后续追加签署
Lock bool
// ExistingVerifyOptions用于追加前与写出后回验,例如提供其他旧裸签名的证书
ExistingVerifyOptions []SignatureVerifyOption
// ReaderOptions用于打开加密包;Encryption为空时继承阅读器的加密策略
ReaderOptions []ReaderOption
Encryption *EncryptionOptions
progress editorProgress
}
// SignPackage 签署最终OFD包,失败时返回nil且不修改输入
// 入参: data 最终包字节, options 签署选项
// 返回: []byte 签署后的完整包, error 错误信息
func SignPackage(data []byte, options SignatureWriteOptions) ([]byte, error) {
r, err := NewReader(bytes.NewReader(data), int64(len(data)), options.ReaderOptions...)
if err != nil {
return nil, err
}
return signatureWriteOutput(r, options)
}
// SignEditorTo 将编辑器序列化为最终文件后签署,保留或显式覆盖加密策略
// 不修改编辑器状态;OnWriteProgress可在准备、签署和加密阶段取消,准备失败不写入目标
// 输出目标不得覆盖源文件;目标自身写入失败时调用方应丢弃部分输出
// 入参: writer 输出目标, editor 编辑器, options 签署选项
// 返回: int64 写出字节数, error 错误信息
func SignEditorTo(writer io.Writer, editor *Editor, options SignatureWriteOptions) (int64, error) {
if writer == nil || editor == nil {
return 0, fmt.Errorf("nil signature editor or writer")
}
if options.Encryption == nil && editor.encryption != nil && editor.encryption.options == nil {
return 0, ErrEncryptionPolicyRequired
}
var plain bytes.Buffer
defer func() { clear(plain.Bytes()) }()
if _, err := editor.writePlaintext(&plain); err != nil {
return 0, err
}
r, err := NewReader(bytes.NewReader(plain.Bytes()), int64(plain.Len()))
if err != nil {
return 0, err
}
r.encryption = editor.encryption
options.progress = editorProgress(editor.OnWriteProgress)
return r.SignTo(writer, options)
}
// SignTo 从阅读器包内容创建独立快照,完成签署和回验后写出
// 不采用可变的Doc/OFD模型;调用方应先将Editor写出为最终包
// 准备失败不写入writer;writer自身失败可能已写入部分输出,不保证目标原子替换
// 入参: writer 独立的输出目标,不得覆盖源文件, options 签署选项
// 返回: int64 写出字节数, error 错误信息
func (r *Reader) SignTo(writer io.Writer, options SignatureWriteOptions) (int64, error) {
if writer == nil {
return 0, fmt.Errorf("nil signature writer")
}
data, err := signatureWriteOutput(r, options)
if err != nil {
return 0, err
}
n, err := writer.Write(data)
if err == nil && n != len(data) {
err = io.ErrShortWrite
}
return int64(n), err
}
// signatureWriteOutput 签署后恢复加密策略,禁止加密来源静默输出明文
// 入参: r 阅读器, options 签署选项
// 返回: []byte 输出包, error 错误信息
func signatureWriteOutput(r *Reader, options SignatureWriteOptions) ([]byte, error) {
if r == nil {
return nil, fmt.Errorf("nil signature reader")
}
encryption := options.Encryption
if encryption == nil && r.encryption != nil {
encryption = r.encryption.options
if encryption == nil {
return nil, ErrEncryptionPolicyRequired
}
}
if encryption != nil {
if err := validateEncryptionOptions(*encryption); err != nil {
return nil, err
}
}
parts, err := signatureWriteParts(r)
if err != nil {
return nil, err
}
if encryption != nil {
defer func() {
for _, data := range parts {
clear(data)
}
}()
}
if err := options.progress.report("sign", 0, 1); err != nil {
return nil, err
}
data, err := signatureWritePackage(parts, options)
if err != nil {
return nil, err
}
if err := options.progress.report("sign", 1, 1); err != nil {
clear(data)
return nil, err
}
if encryption != nil {
defer clear(data)
if options.progress != nil {
signed, err := NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, err
}
files, err := signatureWriteParts(signed)
if err != nil {
return nil, err
}
defer func() {
for _, data := range files {
clear(data)
}
}()
return encryptPackageParts(files, *encryption, options.progress)
}
return EncryptPackage(data, *encryption)
}
return data, nil
}
// signatureWriteParts 使用阅读器统一路径规则读取独立包快照
// 入参: r 阅读器
// 返回: map[string][]byte 包文件, error 错误信息
func signatureWriteParts(r *Reader) (map[string][]byte, error) {
if r == nil {
return nil, fmt.Errorf("nil signature reader")
}
if err := r.indexPackage(); err != nil {
return nil, err
}
return r.packageData()
}
// signatureWritePackage 基于未重新编码的文件字节创建签名
// 签名目录采用Sign_N兼容OFDRW容器,标识使用独立的sN签章域
// 入参: parts 独立包文件, options 签署选项
// 返回: []byte 完整包, error 错误信息
func signatureWritePackage(parts map[string][]byte, options SignatureWriteOptions) ([]byte, error) {
if options.Mode != SignatureAppend && options.Mode != SignatureReplace {
return nil, fmt.Errorf("unsupported signature write mode")
}
r := &Reader{files: parts}
if err := r.initRoot(); err != nil {
return nil, err
}
if len(r.OFD.DocBody) != 1 {
return nil, fmt.Errorf("signature writing requires exactly one document")
}
doc, err := r.Doc()
if err != nil {
return nil, err
}
if options.Time.IsZero() {
options.Time = time.Now()
}
options.Time = options.Time.UTC().Truncate(time.Second)
identity, err := signatureWriteIdentity(options)
if err != nil {
return nil, err
}
stamps, err := signatureWriteStamps(r, options.Stamps, len(options.Seal) != 0)
if err != nil {
return nil, err
}
var list Signatures
listPath := r.ResPath(doc.Signatures)
if doc.Signatures != "" {
data, ok := parts[listPath]
if !ok {
return nil, fmt.Errorf("signature list not found: %s", listPath)
}
if err := xml.Unmarshal(data, &list); err != nil {
return nil, err
}
}
oldCount := len(list.List)
verifyOptions := append([]SignatureVerifyOption(nil), options.ExistingVerifyOptions...)
verifyOptions = append(verifyOptions, WithSignatureCert(identity.cert.Raw))
if options.Mode == SignatureAppend && oldCount > 0 {
reports, err := r.VerifySignatures(verifyOptions...)
if err != nil {
return nil, err
}
ids := make(map[string]bool)
for _, report := range reports {
if !report.Valid || report.StampPositionError != "" || ids[report.ID] || report.ID == "" {
return nil, fmt.Errorf("existing signature %s is invalid: %s", report.ID, report.Error)
}
ids[report.ID] = true
for _, ref := range report.References {
if ref.Path == listPath {
return nil, fmt.Errorf("signature %s protects the signature list", report.ID)
}
}
}
}
if doc.Signatures == "" {
listPath = signatureWriteAvailable(parts, path.Join(r.RootDir, "Signs", "Signatures.xml"))
root, err := parseEditorXML(parts["OFD.xml"])
if err != nil {
return nil, err
}
body := root.child("DocBody")
if body == nil {
return nil, fmt.Errorf("missing document body")
}
parts["OFD.xml"] = editorXMLSetText(parts["OFD.xml"], body, [][2]string{{"Signatures", "/" + listPath}})
}
if options.Mode == SignatureReplace {
list.List = nil
oldCount = 0
}
id, stamps, err := signatureWriteIDs(&list, listPath, parts, stamps)
if err != nil {
return nil, err
}
directory := ""
for n := 1; ; n++ {
directory = path.Join(path.Dir(listPath), "Sign_"+strconv.Itoa(n))
occupied := false
for name := range parts {
if strings.EqualFold(name, directory) || strings.HasPrefix(strings.ToLower(name), strings.ToLower(directory)+"/") {
occupied = true
break
}
}
if !occupied {
break
}
}
sigPath, valuePath, sealPath := path.Join(directory, "Signature.xml"), path.Join(directory, "SignedValue.dat"), path.Join(directory, "Seal.esl")
kind := SignTypeSign
if len(options.Seal) > 0 {
kind = SignTypeSeal
parts[sealPath] = bytes.Clone(options.Seal)
}
list.List = append(list.List, Signature{ID: id, Type: kind, BaseLoc: "/" + sigPath})
parts[listPath], err = signatureWriteXML("Signatures", list)
if err != nil {
return nil, err
}
refs := SignatureReferences{CheckMethod: identity.digest}
names := make([]string, 0, len(parts))
for name := range parts {
names = append(names, name)
}
slices.Sort(names)
for index, name := range names {
if err := options.progress.report("signature-references", index, len(names)); err != nil {
return nil, err
}
if name == listPath && !options.Lock {
continue
}
digest, err := signatureDigest(identity.digest, parts[name])
if err != nil {
return nil, err
}
refs.Reference = append(refs.Reference, SignatureReference{FileRef: "/" + name, CheckValue: base64.StdEncoding.EncodeToString(digest)})
}
if err := options.progress.report("signature-references", len(names), len(names)); err != nil {
return nil, err
}
provider := options.Provider
if provider.ProviderName == "" {
provider.ProviderName = "ofdgo"
}
info := signatureWriteInfo{Provider: provider, SignatureMethod: identity.method, SignatureDateTime: options.Time.Format(time.RFC3339), References: refs, Stamps: stamps}
if kind == SignTypeSeal {
info.Seal = &SignatureSeal{BaseLoc: "/" + sealPath}
}
file := struct {
Info signatureWriteInfo `xml:"SignedInfo"`
Value string `xml:"SignedValue"`
}{info, "/" + valuePath}
parts[sigPath], err = signatureWriteXML("Signature", file)
if err != nil {
return nil, err
}
parts[valuePath], err = identity.sign(parts[sigPath], "/"+sigPath, options)
if err != nil {
return nil, err
}
output, err := signatureWriteZIP(parts)
if err != nil {
return nil, err
}
check, err := NewReader(bytes.NewReader(output), int64(len(output)))
if err != nil {
return nil, err
}
reports, err := check.VerifySignatures(verifyOptions...)
if err != nil {
return nil, err
}
if len(reports) != oldCount+1 {
return nil, fmt.Errorf("signature verification count mismatch")
}
for _, report := range reports {
if !report.Valid || report.StampPositionError != "" {
return nil, fmt.Errorf("written signature %s failed verification: %s", report.ID, report.Error)
}
}
return output, nil
}
// signatureWriteIDs 为签名和全部外观分配同一签章域的标识,不修改文档图元编号
// 入参: list 签名列表, listPath 列表路径, parts 包文件, stamps 外观
// 返回: string 签名标识, []SignatureStamp 外观, error 错误信息
func signatureWriteIDs(list *Signatures, listPath string, parts map[string][]byte, stamps []SignatureStamp) (string, []SignatureStamp, error) {
used := make(map[string]bool)
var maximum uint64
track := func(id string) {
n, err := strconv.ParseUint(strings.TrimPrefix(id, "s"), 10, 64)
if err == nil && n > maximum {
maximum = n
}
}
track(list.MaxSignID)
add := func(id string) error {
if !signatureWriteIDValid(id) || used[id] {
return fmt.Errorf("invalid or duplicate signature ID: %s", id)
}
used[id] = true
track(id)
return nil
}
for _, ref := range list.List {
if err := add(ref.ID); err != nil {
return "", nil, err
}
file, err := parseSignatureFile(parts[signatureRefPath(listPath, ref.BaseLoc)])
if err != nil {
return "", nil, err
}
for _, stamp := range file.SignedInfo.StampAnnot {
if err := add(stamp.ID); err != nil {
return "", nil, err
}
}
}
for _, stamp := range stamps {
if stamp.ID != "" {
if err := add(stamp.ID); err != nil {
return "", nil, err
}
}
}
next := func() (string, error) {
if maximum == ^uint64(0) {
return "", fmt.Errorf("signature ID exhausted")
}
maximum++
return "s" + strconv.FormatUint(maximum, 10), nil
}
id, err := next()
if err != nil {
return "", nil, err
}
for i := range stamps {
if stamps[i].ID == "" {
stamps[i].ID, err = next()
if err != nil {
return "", nil, err
}
}
}
list.MaxSignID = "s" + strconv.FormatUint(maximum, 10)
return id, stamps, nil
}
// signatureWriteInfo 写出签名信息,省略数字签名中不存在的印章
type signatureWriteInfo struct {
Provider SignatureProvider `xml:"Provider"`
SignatureMethod string `xml:"SignatureMethod"`
SignatureDateTime string `xml:"SignatureDateTime"`
References SignatureReferences `xml:"References"`
Stamps []SignatureStamp `xml:"StampAnnot,omitempty"`
Seal *SignatureSeal `xml:"Seal,omitempty"`
}
// signatureWriteXML 编码带OFD命名空间的签名文件
// 入参: name 根节点名称, value 内容
// 返回: []byte XML, error 错误信息
func signatureWriteXML(name string, value any) ([]byte, error) {
var out bytes.Buffer
out.WriteString(xml.Header)
err := xml.NewEncoder(&out).EncodeElement(value, xml.StartElement{Name: xml.Name{Space: ofdNamespace, Local: name}})
return out.Bytes(), err
}
// signatureWriteAvailable 分配不覆盖原文件的路径
// 入参: parts 原包文件, name 候选路径
// 返回: string 可用路径
func signatureWriteAvailable(parts map[string][]byte, name string) string {
original := name
for i := 1; ; i++ {
found := false
for existing := range parts {
if strings.EqualFold(existing, name) {
found = true
break
}
}
if !found {
return name
}
name = path.Join(path.Dir(original), strconv.Itoa(i)+"_"+path.Base(original))
}
}
// signatureWriteZIP 写出完整包,不重新编码受保护文件
// 入参: parts 文件集合
// 返回: []byte 包字节, error 错误信息
func signatureWriteZIP(parts map[string][]byte) ([]byte, error) {
var out bytes.Buffer
w := zip.NewWriter(&out)
names := make([]string, 0, len(parts))
for name := range parts {
names = append(names, name)
}
slices.Sort(names)
for _, name := range names {
f, err := w.Create(name)
if err != nil {
return nil, err
}
if _, err := f.Write(parts[name]); err != nil {
return nil, err
}
}
if err := w.Close(); err != nil {
return nil, err
}
return out.Bytes(), nil
}
+334
View File
@@ -0,0 +1,334 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/pem"
"fmt"
"math/big"
"reflect"
"time"
"github.com/emmansun/gmsm/pkcs8"
"github.com/emmansun/gmsm/sm2"
"github.com/emmansun/gmsm/sm3"
"github.com/emmansun/gmsm/smx509"
)
// ParseSignatureIdentity 解析签署私钥和证书,并检查公私钥对应关系
// 支持普通PKCS8、SEC1、PKCS1以及加密PKCS8,不支持传统加密PEM
// 不加载系统信任或暗中信任该证书,返回的SM2/RSA私钥同时实现crypto.Decrypter
// 入参: certificate 证书DER或PEM, privateKey 私钥DER或PEM, password 可选的单一私钥口令
// 返回: crypto.Signer 签署器, []byte 证书DER, error 错误信息
func ParseSignatureIdentity(certificate, privateKey []byte, password ...[]byte) (crypto.Signer, []byte, error) {
if len(password) > 1 {
return nil, nil, fmt.Errorf("expected at most one private key password")
}
cert, err := parseSignatureCertificate(certificate)
if err != nil {
return nil, nil, err
}
der := privateKey
encrypted := false
if block, rest := pem.Decode(privateKey); block != nil {
if len(bytes.TrimSpace(rest)) != 0 || len(block.Headers) != 0 {
return nil, nil, fmt.Errorf("legacy encrypted PEM or multiple private keys are unsupported")
}
encrypted = block.Type == "ENCRYPTED PRIVATE KEY"
if !encrypted && block.Type != "PRIVATE KEY" && block.Type != "RSA PRIVATE KEY" && block.Type != "EC PRIVATE KEY" {
return nil, nil, fmt.Errorf("unsupported private key PEM type")
}
der = block.Bytes
}
var raw asn1.RawValue
if rest, err := asn1.Unmarshal(der, &raw); err != nil || len(rest) != 0 || raw.Tag != asn1.TagSequence || raw.Class != asn1.ClassUniversal {
return nil, nil, fmt.Errorf("invalid private key encoding")
}
var pass []byte
if len(password) == 1 {
pass = bytes.Clone(password[0])
defer clear(pass)
}
if encrypted && len(pass) == 0 {
return nil, nil, fmt.Errorf("private key password required")
}
key, err := pkcs8.ParsePKCS8PrivateKey(der, pass)
if err != nil && !encrypted {
key, err = smx509.ParsePKCS8PrivateKey(der)
if err != nil {
key, err = smx509.ParseTypedECPrivateKey(der)
}
if err != nil {
key, err = x509.ParsePKCS1PrivateKey(der)
}
}
if err != nil {
return nil, nil, fmt.Errorf("invalid private key or incorrect password")
}
signer, ok := key.(crypto.Signer)
if !ok {
return nil, nil, fmt.Errorf("unsupported signature private key")
}
if _, _, err := signatureWriteAlgorithm(signer, cert); err != nil {
return nil, nil, err
}
return signer, bytes.Clone(cert.Raw), nil
}
// signatureWriteKey 已验证签署身份
type signatureWriteKey struct {
cert *smx509.Certificate
method string
digest string
}
// signatureWriteIdentity 检查签署身份、证书信任与印章授权
// 制章证书按制章时间验证,不以签署时间代替制章时间
// 入参: options 签署选项
// 返回: *signatureWriteKey 已验证身份, error 错误信息
func signatureWriteIdentity(options SignatureWriteOptions) (*signatureWriteKey, error) {
cert, err := parseSignatureCertificate(options.Certificate)
if err != nil {
return nil, err
}
method, digest, err := signatureWriteAlgorithm(options.Signer, cert)
if err != nil {
return nil, err
}
if err := signatureWriteTrust(cert, options.Time, options); err != nil {
return nil, fmt.Errorf("signer certificate: %w", err)
}
if len(options.Seal) > 0 {
if method != signMethodSM2SM3 {
return nil, fmt.Errorf("SES v4 writing requires SM2/SM3")
}
var raw asn1.RawValue
rest, err := asn1.Unmarshal(options.Seal, &raw)
if err != nil || len(rest) != 0 || raw.Tag != asn1.TagSequence || raw.Class != asn1.ClassUniversal {
return nil, fmt.Errorf("invalid SES seal DER; image files are not electronic seals")
}
if err := signatureWriteSealStructure(raw); err != nil {
return nil, err
}
seal, err := parseSESSeal(raw)
if err != nil {
return nil, err
}
if seal.Info.Version != 4 || seal.SignAlg != signMethodSM2SM3 {
return nil, fmt.Errorf("only SES v4 SM2/SM3 seals are supported")
}
if !sesCertInList(cert.Raw, seal.CertList) {
return nil, fmt.Errorf("signer certificate is not authorized by the seal")
}
if seal.Info.CreateTime.IsZero() || seal.Info.ValidStart.IsZero() || seal.Info.ValidEnd.IsZero() || options.Time.Before(seal.Info.CreateTime) || options.Time.Before(seal.Info.ValidStart) || options.Time.After(seal.Info.ValidEnd) || seal.Info.ValidEnd.Before(seal.Info.ValidStart) {
return nil, fmt.Errorf("seal is not valid at signature time")
}
maker, err := parseSignatureCertificate(seal.Cert)
if err != nil {
return nil, err
}
if err := signatureWriteTrust(maker, seal.Info.CreateTime, options); err != nil {
return nil, fmt.Errorf("seal maker certificate: %w", err)
}
pub, ok := maker.PublicKey.(*ecdsa.PublicKey)
if !ok || !sm2.IsSM2PublicKey(pub) || !sm2.VerifyASN1WithSM2(pub, nil, seal.SignData, seal.Signature) {
return nil, fmt.Errorf("invalid seal maker signature")
}
mediaType, media := probeSealMedia(seal.PicData)
if len(media) == 0 || mediaType != normalizeSealType(seal.PicType) {
return nil, fmt.Errorf("invalid seal picture")
}
}
return &signatureWriteKey{cert: cert, method: method, digest: digest}, nil
}
// signatureWriteSealStructure 限定SES v4印章结构,拒绝旧版容器伪装和无效图像尺寸
// 入参: raw 印章DER节点
// 返回: error 错误信息
func signatureWriteSealStructure(raw asn1.RawValue) error {
items, ok := asn1Children(raw.Bytes)
if !ok || len(items) != 4 || items[0].Tag != asn1.TagSequence || items[1].Tag != asn1.TagOctetString || items[2].Tag != asn1.TagOID || items[3].Tag != asn1.TagBitString {
return fmt.Errorf("unsupported SES v4 seal structure")
}
for _, item := range items {
if item.Class != asn1.ClassUniversal {
return fmt.Errorf("invalid SES v4 seal tag")
}
}
info, ok := asn1Children(items[0].Bytes)
if !ok || len(info) < 4 || len(info) > 5 {
return fmt.Errorf("invalid SES v4 seal info")
}
header, ok := asn1Children(info[0].Bytes)
if !ok || len(header) != 3 || header[0].Tag != asn1.TagIA5String || header[2].Tag != asn1.TagIA5String || info[1].Tag != asn1.TagIA5String {
return fmt.Errorf("invalid SES v4 seal header")
}
picture, ok := asn1Children(info[3].Bytes)
if !ok || len(picture) != 4 || picture[0].Tag != asn1.TagIA5String || picture[1].Tag != asn1.TagOctetString {
return fmt.Errorf("invalid SES v4 picture structure")
}
w, err := asn1Integer(picture[2])
if err != nil || w <= 0 {
return fmt.Errorf("invalid SES v4 picture width")
}
h, err := asn1Integer(picture[3])
if err != nil || h <= 0 {
return fmt.Errorf("invalid SES v4 picture height")
}
return nil
}
// signatureWriteAlgorithm 校验证书、公钥匹配和支持的安全算法
// 入参: signer 签署器, cert 证书
// 返回: string 签名算法OID, string 摘要OID, error 错误信息
func signatureWriteAlgorithm(signer crypto.Signer, cert *smx509.Certificate) (string, string, error) {
if signer == nil || reflect.ValueOf(signer).Kind() == reflect.Ptr && reflect.ValueOf(signer).IsNil() {
return "", "", fmt.Errorf("nil signature signer")
}
pub := signer.Public()
match, ok := cert.PublicKey.(interface{ Equal(crypto.PublicKey) bool })
if !ok || !match.Equal(pub) {
return "", "", fmt.Errorf("signature certificate and private key do not match")
}
switch key := pub.(type) {
case *rsa.PublicKey:
if key.N == nil || key.N.BitLen() < 2048 || key.E < 65537 {
return "", "", fmt.Errorf("RSA signature key must be at least 2048 bits with exponent at least 65537")
}
return "1.2.840.113549.1.1.11", "2.16.840.1.101.3.4.2.1", nil
case *ecdsa.PublicKey:
if sm2.IsSM2PublicKey(key) {
return signMethodSM2SM3, signDigestSM3, nil
}
if key.Curve != elliptic.P256() && key.Curve != elliptic.P384() && key.Curve != elliptic.P521() {
return "", "", fmt.Errorf("unsupported ECDSA signature curve")
}
return "1.2.840.10045.4.3.2", "2.16.840.1.101.3.4.2.1", nil
default:
return "", "", fmt.Errorf("unsupported signature public key")
}
}
// signatureWriteTrust 校验证书用途、有效期和调用方显式指定的信任链
// 入参: cert 证书, at 验证时间, options 签署选项
// 返回: error 错误信息
func signatureWriteTrust(cert *smx509.Certificate, at time.Time, options SignatureWriteOptions) error {
if cert.KeyUsage != 0 && cert.KeyUsage&(smx509.KeyUsageDigitalSignature|smx509.KeyUsageContentCommitment) == 0 {
return fmt.Errorf("certificate does not authorize digital signatures")
}
if len(options.TrustRoots) == 0 {
return fmt.Errorf("explicit signature trust roots are required")
}
return verifySignatureCertificateChain(cert, options.Intermediates, options.TrustRoots, at, smx509.ExtKeyUsageAny)
}
// sign 编码数字签名或SES v4签章
// 入参: data 最终Signature.xml原文, property 包内绝对路径, options 签署选项
// 返回: []byte 签名值, error 错误信息
func (key *signatureWriteKey) sign(data []byte, property string, options SignatureWriteOptions) ([]byte, error) {
if len(options.Seal) == 0 {
if key.method != signMethodSM2SM3 {
return key.signMessage(data, options.Signer)
}
digest := sm3.Sum(data)
signature, err := key.signMessage(digest[:], options.Signer)
if err != nil {
return nil, err
}
return signatureWriteSignedData(key.cert, digest[:], signature)
}
digest := sm3.Sum(data)
tbs, err := asn1.Marshal(struct {
Version int
Seal asn1.RawValue
Time time.Time `asn1:"generalized"`
Hash asn1.BitString
Property string `asn1:"ia5"`
}{4, asn1.RawValue{FullBytes: options.Seal}, options.Time, asn1.BitString{Bytes: digest[:], BitLength: 256}, property})
if err != nil {
return nil, err
}
signature, err := key.signMessage(tbs, options.Signer)
if err != nil {
return nil, err
}
return asn1.Marshal(struct {
TBS asn1.RawValue
Certificate []byte
Algorithm asn1.ObjectIdentifier
Signature asn1.BitString
}{asn1.RawValue{FullBytes: tbs}, key.cert.Raw, asn1.ObjectIdentifier{1, 2, 156, 10197, 1, 501}, asn1.BitString{Bytes: signature, BitLength: len(signature) * 8}})
}
// signMessage 通过crypto.Signer调用成熟实现或硬件签署器
// SM2签署器必须接受gmsm的SM2SignerOption,接收原文并计算默认用户标识的ZA
// 入参: data 原文, signer 签署器
// 返回: []byte 签名值, error 错误信息
func (key *signatureWriteKey) signMessage(data []byte, signer crypto.Signer) ([]byte, error) {
if key.method == signMethodSM2SM3 {
return signer.Sign(rand.Reader, data, sm2.NewSM2SignerOption(true, nil))
}
digest := sha256.Sum256(data)
return signer.Sign(rand.Reader, digest[:], crypto.SHA256)
}
// signatureWriteSignedData 编码带签署证书的SM2数字签名消息
// 入参: cert 证书, digest 文档描述摘要, signature SM2签名值
// 返回: []byte SignedData编码, error 错误信息
func signatureWriteSignedData(cert *smx509.Certificate, digest, signature []byte) ([]byte, error) {
type contentInfo struct {
Type asn1.ObjectIdentifier
Content asn1.RawValue
}
type signerInfo struct {
Version int
IssuerAndSerial struct {
Issuer asn1.RawValue
Serial *big.Int
}
Digest pkix.AlgorithmIdentifier
Algorithm pkix.AlgorithmIdentifier
Signature []byte
}
digestAlg := pkix.AlgorithmIdentifier{Algorithm: asn1.ObjectIdentifier{1, 2, 156, 10197, 1, 401}, Parameters: asn1.NullRawValue}
signer := signerInfo{Version: 1, Digest: digestAlg, Algorithm: pkix.AlgorithmIdentifier{Algorithm: asn1.ObjectIdentifier{1, 2, 156, 10197, 1, 301, 1}}, Signature: signature}
signer.IssuerAndSerial.Issuer = asn1.RawValue{FullBytes: cert.RawIssuer}
signer.IssuerAndSerial.Serial = cert.SerialNumber
content, err := asn1.Marshal(digest)
if err != nil {
return nil, err
}
sd, err := asn1.Marshal(struct {
Version int
Digests []pkix.AlgorithmIdentifier `asn1:"set"`
Content contentInfo
Certificates asn1.RawValue
Signers []signerInfo `asn1:"set"`
}{1, []pkix.AlgorithmIdentifier{digestAlg}, contentInfo{asn1.ObjectIdentifier{1, 2, 156, 10197, 6, 1, 4, 2, 1}, asn1.RawValue{Class: 2, Tag: 0, IsCompound: true, Bytes: content}}, asn1.RawValue{Class: 2, Tag: 0, IsCompound: true, Bytes: cert.Raw}, []signerInfo{signer}})
if err != nil {
return nil, err
}
return asn1.Marshal(contentInfo{asn1.ObjectIdentifier{1, 2, 156, 10197, 6, 1, 4, 2, 2}, asn1.RawValue{Class: 2, Tag: 0, IsCompound: true, Bytes: sd}})
}
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"fmt"
"math"
"strconv"
"strings"
)
// SignatureSeamStamps 生成页面右侧的等宽骑缝印章位置
// 页码从1开始,按传入顺序分割同一印章;box.X为右侧内缩,Y为纵坐标,W/H为完整印章尺寸
// 入参: pages 至少两页且不重复, box 印章参数,单位毫米
// 返回: []SignatureStamp 可传入签署选项的位置, error 错误信息
func (r *Reader) SignatureSeamStamps(pages []int, box Box) ([]SignatureStamp, error) {
if r == nil || len(pages) < 2 || !signatureWriteBoxValid(box) || box.X < 0 {
return nil, fmt.Errorf("invalid seam stamp parameters")
}
doc, err := r.Doc()
if err != nil {
return nil, err
}
seen := make(map[int]bool)
stamps := make([]SignatureStamp, 0, len(pages))
width := box.W / float64(len(pages))
for i, page := range pages {
if page < 1 || page > len(doc.Pages.Page) || seen[page] {
return nil, fmt.Errorf("invalid seam stamp page: %d", page)
}
seen[page] = true
area, err := r.PageArea(doc.Pages.Page[page-1])
if err != nil {
return nil, err
}
physical, err := ParseBox(area.PhysicalBox)
if err != nil {
return nil, err
}
clip := Box{X: float64(i) * width, W: width, H: box.H}
boundary := Box{X: physical.X + physical.W - box.X - width - clip.X, Y: box.Y, W: box.W, H: box.H}
stamps = append(stamps, SignatureStamp{PageRef: doc.Pages.Page[page-1].ID, Boundary: signatureWriteBox(boundary), Clip: signatureWriteBox(clip)})
}
return signatureWriteStamps(r, stamps, true)
}
// signatureWriteStamps 复用Reader定位并检查外观几何参数
// 入参: r 阅读器, stamps 位置, seal 是否电子印章
// 返回: []SignatureStamp 独立位置列表, error 错误信息
func signatureWriteStamps(r *Reader, stamps []SignatureStamp, seal bool) ([]SignatureStamp, error) {
if len(stamps) != 0 && !seal {
return nil, fmt.Errorf("digital signatures cannot contain seal appearances")
}
positions, err := r.SignatureStampPositions(stamps)
if err != nil {
return nil, err
}
doc, err := r.Doc()
if err != nil {
return nil, err
}
result := append([]SignatureStamp(nil), stamps...)
if len(result) != 0 {
pages := make(map[string]bool)
for _, page := range doc.Pages.Page {
if page.ID == "" || pages[page.ID] {
return nil, fmt.Errorf("ambiguous stamp page ID: %s", page.ID)
}
pages[page.ID] = true
}
}
ids := make(map[string]bool)
for _, stamp := range stamps {
if stamp.ID == "" {
continue
}
if !signatureWriteIDValid(stamp.ID) || ids[stamp.ID] {
return nil, fmt.Errorf("invalid or duplicate stamp ID: %s", stamp.ID)
}
ids[stamp.ID] = true
}
for _, position := range positions {
box := position.Box
if !signatureWriteBoxValid(box) {
return nil, fmt.Errorf("invalid stamp boundary")
}
visible := box
if clip := position.ClipBox; clip != nil {
if !signatureWriteBoxValid(*clip) || clip.X < 0 || clip.Y < 0 || clip.X+clip.W > box.W+1e-9 || clip.Y+clip.H > box.H+1e-9 {
return nil, fmt.Errorf("invalid stamp clip")
}
visible = Box{X: box.X + clip.X, Y: box.Y + clip.Y, W: clip.W, H: clip.H}
}
area, err := r.PageArea(doc.Pages.Page[position.Page-1])
if err != nil {
return nil, err
}
page, err := ParseBox(area.PhysicalBox)
if err != nil || !signatureWriteBoxValid(page) {
return nil, fmt.Errorf("invalid stamp page area")
}
if visible.X < page.X-1e-9 || visible.Y < page.Y-1e-9 || visible.X+visible.W > page.X+page.W+1e-9 || visible.Y+visible.H > page.Y+page.H+1e-9 {
return nil, fmt.Errorf("stamp is outside page %d", position.Page)
}
}
return result, nil
}
// signatureWriteIDValid 检查可互通的签章标识字符
// 入参: id 标识
// 返回: bool 是否有效
func signatureWriteIDValid(id string) bool {
return id != "" && len(id) <= 128 && strings.IndexFunc(id, func(c rune) bool {
return !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '-' || c == '.')
}) == -1
}
// signatureWriteBoxValid 判断印章区域是否有限且尺寸为正
// 入参: box 区域
// 返回: bool 是否有效
func signatureWriteBoxValid(box Box) bool {
for _, value := range []float64{box.X, box.Y, box.W, box.H} {
if math.IsInf(value, 0) || math.IsNaN(value) {
return false
}
}
return box.W > 0 && box.H > 0
}
// signatureWriteBox 编码印章区域
// 入参: box 区域
// 返回: string 区域文本
func signatureWriteBox(box Box) string {
return strconv.FormatFloat(box.X, 'f', -1, 64) + " " + strconv.FormatFloat(box.Y, 'f', -1, 64) + " " + strconv.FormatFloat(box.W, 'f', -1, 64) + " " + strconv.FormatFloat(box.H, 'f', -1, 64)
}
+12 -1
View File
@@ -50,6 +50,16 @@ func (progress editorProgress) report(stage string, completed, total int) error
// 入参: writer 输出流
// 返回: int64 已写入字节数, error 错误信息
func (e *Editor) WriteTo(writer io.Writer) (int64, error) {
if e.encryption != nil {
return e.writeEncrypted(writer)
}
return e.writePlaintext(writer)
}
// writePlaintext 写出未加密包,供显式明文输出与加密封装共用
// 入参: writer 输出流
// 返回: int64 写入字节数, error 错误信息
func (e *Editor) writePlaintext(writer io.Writer) (int64, error) {
if err := e.validate(); err != nil {
return 0, err
}
@@ -129,6 +139,7 @@ func (e *Editor) WritePagesTo(writer io.Writer, indexes []int) (int64, error) {
}
defer reader.Close()
selected := NewEditor()
selected.encryption = e.encryption
id := selected.Info.DocID
selected.Info = cloneEditorData(e.Info)
selected.Info.DocID = id
@@ -156,7 +167,7 @@ func (e *Editor) reader(progress editorProgress) (*Reader, error) {
if e.source != nil {
return e.sourceReader(progress)
}
r := &Reader{files: make(map[string][]byte)}
r := &Reader{files: make(map[string][]byte), encryption: e.encryption}
if err := e.writeParts(func(name string, data []byte, _ bool) error {
r.files[name] = data
return nil