mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 15:06:46 +08:00
This commit is contained in:
1 parent
d6f70de7be
commit
3927929822
30 files changed
+4677
-1131
No files matched your search
@@ -353,8 +353,59 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</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="导出文档">
|
<dialog id="exportPanel" class="form-dialog" aria-label="导出文档">
|
||||||
<form id="exportForm">
|
<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">
|
<div class="form-row">
|
||||||
<span id="exportScopeLabel">范围</span>
|
<span id="exportScopeLabel">范围</span>
|
||||||
<div class="form-options" role="radiogroup" aria-labelledby="exportScopeLabel">
|
<div class="form-options" role="radiogroup" aria-labelledby="exportScopeLabel">
|
||||||
@@ -500,6 +551,10 @@
|
|||||||
<dd id="metaFonts">0</dd>
|
<dd id="metaFonts">0</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</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>
|
<section id="attachmentPanel" class="attachment-panel" aria-labelledby="attachmentPanelTitle" hidden>
|
||||||
<div id="attachmentPanelTitle" class="panel-title">附件</div>
|
<div id="attachmentPanelTitle" class="panel-title">附件</div>
|
||||||
<div id="attachmentList" class="attachment-list"></div>
|
<div id="attachmentList" class="attachment-list"></div>
|
||||||
|
|||||||
+66
-22
@@ -645,6 +645,23 @@ button:not(:disabled):hover {
|
|||||||
padding: 12px;
|
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 {
|
.form-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2em minmax(0, 1fr);
|
grid-template-columns: 2em minmax(0, 1fr);
|
||||||
@@ -711,6 +728,7 @@ button:not(:disabled):hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-row input[type="text"],
|
.form-row input[type="text"],
|
||||||
|
.form-row input[type="password"],
|
||||||
.form-row input[type="number"],
|
.form-row input[type="number"],
|
||||||
.form-row input[type="file"],
|
.form-row input[type="file"],
|
||||||
.form-row select,
|
.form-row select,
|
||||||
@@ -895,6 +913,8 @@ body[data-hide-meta] .meta-panel {
|
|||||||
border-right: 1px solid var(--line);
|
border-right: 1px solid var(--line);
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-list-panel {
|
.page-list-panel {
|
||||||
@@ -1168,7 +1188,7 @@ body[data-hide-meta] .meta-panel {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1205,7 +1225,7 @@ body[data-hide-meta] .meta-panel {
|
|||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-result mark {
|
.search-result mark {
|
||||||
@@ -1284,7 +1304,13 @@ button.outline-link:hover {
|
|||||||
transition: border-color 0.16s ease, background-color 0.16s ease;
|
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);
|
border-color: var(--accent);
|
||||||
color: var(--accent-strong);
|
color: var(--accent-strong);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -1297,7 +1323,8 @@ button.outline-link:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.page-list-item[aria-current]:not([aria-pressed="false"]),
|
.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);
|
border-color: var(--accent);
|
||||||
background: #e4f3ef;
|
background: #e4f3ef;
|
||||||
color: var(--accent-strong);
|
color: var(--accent-strong);
|
||||||
@@ -1351,7 +1378,7 @@ button.outline-link:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.thumb-size {
|
.thumb-size {
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
@@ -2037,7 +2064,8 @@ button.outline-link:hover {
|
|||||||
.font-panel,
|
.font-panel,
|
||||||
.doc-font-panel,
|
.doc-font-panel,
|
||||||
.attachment-panel,
|
.attachment-panel,
|
||||||
.signature-panel {
|
.signature-panel,
|
||||||
|
.security-panel {
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
padding-top: 12px;
|
padding-top: 12px;
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
@@ -2070,7 +2098,7 @@ button.outline-link:hover {
|
|||||||
|
|
||||||
.attachment-detail {
|
.attachment-detail {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.count-pill {
|
.count-pill {
|
||||||
@@ -2083,7 +2111,7 @@ button.outline-link:hover {
|
|||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -2104,7 +2132,6 @@ button.outline-link:hover {
|
|||||||
transition: border-color 0.16s ease, background-color 0.16s ease;
|
transition: border-color 0.16s ease, background-color 0.16s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-list-item:not(:disabled):hover,
|
|
||||||
.doc-font-row:hover,
|
.doc-font-row:hover,
|
||||||
.signature-row:hover,
|
.signature-row:hover,
|
||||||
.font-row:hover {
|
.font-row:hover {
|
||||||
@@ -2194,10 +2221,6 @@ button.outline-link:hover {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
|
||||||
|
|
||||||
.doc-font-badges,
|
|
||||||
.signature-badges {
|
|
||||||
float: right;
|
float: right;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
@@ -2210,7 +2233,7 @@ button.outline-link:hover {
|
|||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
background: #eef1f4;
|
background: #eef1f4;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -2235,19 +2258,40 @@ button.outline-link:hover {
|
|||||||
|
|
||||||
.info-line {
|
.info-line {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 30px minmax(0, 1fr);
|
grid-template-columns: 2em minmax(0, 1fr);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
line-height: 1.35;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-label {
|
.info-label {
|
||||||
color: #7b8791;
|
color: var(--text-muted);
|
||||||
white-space: nowrap;
|
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 {
|
.info-value {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
@@ -2263,8 +2307,8 @@ button.outline-link:hover {
|
|||||||
|
|
||||||
.doc-font-detail {
|
.doc-font-detail {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
line-height: 1.35;
|
line-height: 1.5;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2337,7 +2381,7 @@ button.outline-link:hover {
|
|||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
background: #f8fafb;
|
background: #f8fafb;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -2348,7 +2392,7 @@ button.outline-link:hover {
|
|||||||
gap: 3px;
|
gap: 3px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+498
-42
@@ -24,6 +24,7 @@ let wasmWorker = null;
|
|||||||
let wasmRequestID = 0;
|
let wasmRequestID = 0;
|
||||||
let wasmRecoveryTimer = 0;
|
let wasmRecoveryTimer = 0;
|
||||||
let textMeasure = null;
|
let textMeasure = null;
|
||||||
|
let credentialRequest = null;
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
composite: null,
|
composite: null,
|
||||||
@@ -35,6 +36,8 @@ const state = {
|
|||||||
wasmRecovering: false,
|
wasmRecovering: false,
|
||||||
wasmRecoveries: 0,
|
wasmRecoveries: 0,
|
||||||
exporting: false,
|
exporting: false,
|
||||||
|
signing: false,
|
||||||
|
signCanceled: false,
|
||||||
exportRequestID: 0,
|
exportRequestID: 0,
|
||||||
editing: false,
|
editing: false,
|
||||||
selectObjects: true,
|
selectObjects: true,
|
||||||
@@ -44,6 +47,7 @@ const state = {
|
|||||||
savedRevision: null,
|
savedRevision: null,
|
||||||
insertObject: null,
|
insertObject: null,
|
||||||
importPageCount: 0,
|
importPageCount: 0,
|
||||||
|
importEncrypted: false,
|
||||||
importing: false,
|
importing: false,
|
||||||
pageSelection: new Set(),
|
pageSelection: new Set(),
|
||||||
pageSelectionAnchor: null,
|
pageSelectionAnchor: null,
|
||||||
@@ -102,11 +106,74 @@ const state = {
|
|||||||
exportFormats: [],
|
exportFormats: [],
|
||||||
exportPages: null,
|
exportPages: null,
|
||||||
exportBackdrop: false,
|
exportBackdrop: false,
|
||||||
|
exportEncrypted: false,
|
||||||
showPages: !COMPACT_LAYOUT.matches,
|
showPages: !COMPACT_LAYOUT.matches,
|
||||||
showMeta: !COMPACT_LAYOUT.matches,
|
showMeta: !COMPACT_LAYOUT.matches,
|
||||||
};
|
};
|
||||||
|
|
||||||
const el = {
|
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"),
|
ofdInput: document.querySelector("#ofdInput"),
|
||||||
ofdButton: document.querySelector("#ofdButton"),
|
ofdButton: document.querySelector("#ofdButton"),
|
||||||
newButton: document.querySelector("#newButton"),
|
newButton: document.querySelector("#newButton"),
|
||||||
@@ -755,6 +822,7 @@ el.importPanel.addEventListener("cancel", event => {
|
|||||||
});
|
});
|
||||||
el.importPanel.addEventListener("close", () => {
|
el.importPanel.addEventListener("close", () => {
|
||||||
state.importPageCount = 0;
|
state.importPageCount = 0;
|
||||||
|
state.importEncrypted = false;
|
||||||
el.importForm.reset();
|
el.importForm.reset();
|
||||||
if (state.ready) callWASM("ofdgoLoadImport", null).catch(err => showError(err, false));
|
if (state.ready) callWASM("ofdgoLoadImport", null).catch(err => showError(err, false));
|
||||||
});
|
});
|
||||||
@@ -770,7 +838,12 @@ el.importForm.addEventListener("submit", async event => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (document.body.hasAttribute("aria-busy") || !state.importPageCount || !el.importForm.reportValidity()) return;
|
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 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();
|
if (saved) el.importPanel.close();
|
||||||
});
|
});
|
||||||
editorClick(el.copyPageButton, () => changeSelectedPages("copy"));
|
editorClick(el.copyPageButton, () => changeSelectedPages("copy"));
|
||||||
@@ -926,6 +999,60 @@ el.insertPanel.addEventListener("close", () => { state.insertObject = null; });
|
|||||||
el.insertForm.addEventListener("submit", insertObject);
|
el.insertForm.addEventListener("submit", insertObject);
|
||||||
el.textFontAdd.addEventListener("click", () => openFontFile(el.fontInput));
|
el.textFontAdd.addEventListener("click", () => openFontFile(el.fontInput));
|
||||||
el.saveButton.addEventListener("click", () => exportFile(true, null, "ofd"));
|
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.togglePagesButton.addEventListener("click", () => toggleSidebar("pages"));
|
||||||
el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta"));
|
el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta"));
|
||||||
el.pagesTab.addEventListener("click", () => showNavigation(el.pagesTab));
|
el.pagesTab.addEventListener("click", () => showNavigation(el.pagesTab));
|
||||||
@@ -983,8 +1110,10 @@ el.viewerPanel.addEventListener("paste", pasteEditorContent);
|
|||||||
document.addEventListener("selectionchange", syncSelection);
|
document.addEventListener("selectionchange", syncSelection);
|
||||||
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
||||||
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
||||||
el.exportButton.addEventListener("click", openExportPanel);
|
el.exportButton.addEventListener("click", () => openExportPanel());
|
||||||
el.exportCancel.addEventListener("click", () => el.exportPanel.close());
|
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.cancelExportButton.addEventListener("click", cancelExport);
|
||||||
el.exportPanel.addEventListener("pointerdown", (event) => {
|
el.exportPanel.addEventListener("pointerdown", (event) => {
|
||||||
state.exportBackdrop = event.target === el.exportPanel;
|
state.exportBackdrop = event.target === el.exportPanel;
|
||||||
@@ -1000,9 +1129,32 @@ el.exportSpecified.addEventListener("change", () => {
|
|||||||
el.exportRange.focus();
|
el.exportRange.focus();
|
||||||
});
|
});
|
||||||
el.exportRange.addEventListener("input", updateExportRange);
|
el.exportRange.addEventListener("input", updateExportRange);
|
||||||
el.exportForm.addEventListener("submit", (event) => {
|
el.exportForm.addEventListener("submit", async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!el.exportSubmit.disabled) {
|
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);
|
return exportFile(true, state.exportPages);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1317,18 +1469,20 @@ async function loadImportFile() {
|
|||||||
const file = el.importFile.files[0];
|
const file = el.importFile.files[0];
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
state.importPageCount = 0;
|
state.importPageCount = 0;
|
||||||
|
state.importEncrypted = false;
|
||||||
el.importSubmit.disabled = true;
|
el.importSubmit.disabled = true;
|
||||||
el.importStatus.textContent = "";
|
el.importStatus.textContent = "";
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const data = file ? new Uint8Array(await file.arrayBuffer()) : null;
|
const data = file ? new Uint8Array(await file.arrayBuffer()) : null;
|
||||||
if (openSeq !== state.openSeq) return;
|
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) {
|
if (openSeq !== state.openSeq || !el.importPanel.open) {
|
||||||
await callWASM("ofdgoLoadImport", null);
|
await callWASM("ofdgoLoadImport", null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.importPageCount = info?.pageCount || 0;
|
state.importPageCount = info?.pageCount || 0;
|
||||||
|
state.importEncrypted = Boolean(info?.encrypted);
|
||||||
el.importPages.title = state.importPageCount ? `页码 1-${state.importPageCount}` : "页码";
|
el.importPages.title = state.importPageCount ? `页码 1-${state.importPageCount}` : "页码";
|
||||||
el.importStatus.textContent = info?.signed ? "保留签章外观,原签名不再有效" : "";
|
el.importStatus.textContent = info?.signed ? "保留签章外观,原签名不再有效" : "";
|
||||||
el.importSubmit.disabled = !state.importPageCount;
|
el.importSubmit.disabled = !state.importPageCount;
|
||||||
@@ -1494,6 +1648,7 @@ async function createDocument(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const title = el.createName.value.trim() || "未命名";
|
const title = el.createName.value.trim() || "未命名";
|
||||||
|
clearSecurityForms();
|
||||||
const openSeq = ++state.openSeq;
|
const openSeq = ++state.openSeq;
|
||||||
setBusy(true, "正在新建文档", null, "正在新建文档");
|
setBusy(true, "正在新建文档", null, "正在新建文档");
|
||||||
try {
|
try {
|
||||||
@@ -2714,10 +2869,11 @@ async function loadWASM() {
|
|||||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
||||||
if (data.stage === "save") {
|
if (data.stage === "save") {
|
||||||
el.cancelExportButton.disabled = true;
|
el.cancelExportButton.disabled = true;
|
||||||
|
if (state.signing) el.signCancel.disabled = true;
|
||||||
setProgress("正在保存", null);
|
setProgress("正在保存", null);
|
||||||
} else if (!el.cancelExportButton.disabled) {
|
} else if (!el.cancelExportButton.disabled) {
|
||||||
if (data.stage === "prepare") {
|
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);
|
setProgress(label, data.total ? data.completed / data.total * 100 : null);
|
||||||
} else if (data.completed === data.total) {
|
} else if (data.completed === data.total) {
|
||||||
setProgress("正在收尾", null);
|
setProgress("正在收尾", null);
|
||||||
@@ -2753,6 +2909,7 @@ async function loadWASM() {
|
|||||||
request.resolve(data.data);
|
request.resolve(data.data);
|
||||||
} else {
|
} else {
|
||||||
const err = new Error(missingGlyphMessage(data.missingGlyphs) || (data.reasonCode === "layoutRequired" ? "跨段或换行修改需先设置段落排版" : data.error));
|
const err = new Error(missingGlyphMessage(data.missingGlyphs) || (data.reasonCode === "layoutRequired" ? "跨段或换行修改需先设置段落排版" : data.error));
|
||||||
|
err.code = data.code;
|
||||||
if (data.canceled) {
|
if (data.canceled) {
|
||||||
err.name = "AbortError";
|
err.name = "AbortError";
|
||||||
}
|
}
|
||||||
@@ -2768,6 +2925,7 @@ function markWASMExited(wasmSeq = state.wasmSeq, err) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.ready = false;
|
state.ready = false;
|
||||||
|
clearSecurityForms();
|
||||||
state.wasmExited = true;
|
state.wasmExited = true;
|
||||||
wasmWorker?.terminate();
|
wasmWorker?.terminate();
|
||||||
wasmWorker = null;
|
wasmWorker = null;
|
||||||
@@ -2830,6 +2988,7 @@ async function openOFD(file) {
|
|||||||
if (!discardChanges()) {
|
if (!discardChanges()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
clearSecurityForms();
|
||||||
state.wasmRecoveries = 0;
|
state.wasmRecoveries = 0;
|
||||||
const openSeq = ++state.openSeq;
|
const openSeq = ++state.openSeq;
|
||||||
setBusy(true, "正在读取文档", 10, STATUS.opening);
|
setBusy(true, "正在读取文档", 10, STATUS.opening);
|
||||||
@@ -3306,6 +3465,86 @@ function updateFontPermissionHint() {
|
|||||||
el.fontPermissionHint.hidden = !fontManager.canReadLocal() || fontManager.permission === "granted";
|
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 = {}) {
|
async function openDocument(options = {}) {
|
||||||
if (!state.ofdBytes && !state.editorInfo) {
|
if (!state.ofdBytes && !state.editorInfo) {
|
||||||
return;
|
return;
|
||||||
@@ -3341,7 +3580,7 @@ async function openDocument(options = {}) {
|
|||||||
}
|
}
|
||||||
const doc = options.doc || (options.reuseSession
|
const doc = options.doc || (options.reuseSession
|
||||||
? await callWASM("ofdgoConfigure", fonts, state.renderAnnotations)
|
? await callWASM("ofdgoConfigure", fonts, state.renderAnnotations)
|
||||||
: await callWASM("ofdgoOpen", state.ofdBytes, fonts, state.renderAnnotations));
|
: await openWithCredentials(fonts, openSeq));
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3439,6 +3678,12 @@ async function openDocument(options = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (options.throwError) throw err;
|
if (options.throwError) throw err;
|
||||||
|
if (!options.reuseSession && !options.doc) {
|
||||||
|
state.ofdBytes = null;
|
||||||
|
state.doc = null;
|
||||||
|
renderSecurity();
|
||||||
|
updateControls();
|
||||||
|
}
|
||||||
showError(err, true);
|
showError(err, true);
|
||||||
} finally {
|
} finally {
|
||||||
if (openSeq === state.openSeq) {
|
if (openSeq === state.openSeq) {
|
||||||
@@ -3522,6 +3767,7 @@ async function downloadAttachment(attachment) {
|
|||||||
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (state.doc.encryption?.encrypted && !window.confirm("附件将按明文下载,是否继续?")) return;
|
||||||
const openSeq = state.openSeq;
|
const openSeq = state.openSeq;
|
||||||
state.exporting = true;
|
state.exporting = true;
|
||||||
updateControls();
|
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")) {
|
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.exportForm.reset();
|
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) {
|
if (state.editing && state.pageSelection.size) {
|
||||||
el.exportSpecified.checked = true;
|
el.exportSpecified.checked = true;
|
||||||
el.exportRange.value = selectedPageRange();
|
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")) {
|
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const saving = value === "ofd";
|
const saving = value === "ofd";
|
||||||
if (saving && !state.editorInfo) {
|
if (saving && !state.editorInfo && !encryption) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!saving && state.doc.encryption?.encrypted && !window.confirm("此格式将输出明文,是否继续?")) return;
|
||||||
const format = saving ? { value: "ofd", label: "OFD", extension: "ofd", mime: "application/ofd" } : exportFormatInfo(value);
|
const format = saving ? { value: "ofd", label: "OFD", extension: "ofd", mime: "application/ofd" } : exportFormatInfo(value);
|
||||||
if (!format) {
|
if (!format) {
|
||||||
return;
|
return;
|
||||||
@@ -3640,7 +4041,12 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
|||||||
openSeq = state.openSeq;
|
openSeq = state.openSeq;
|
||||||
setBusy(true, `正在生成 ${label}`, null, status);
|
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("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, file)
|
||||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, file);
|
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, file);
|
||||||
if (openSeq !== state.openSeq) {
|
if (openSeq !== state.openSeq) {
|
||||||
@@ -3649,7 +4055,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
|||||||
if (result.blob) {
|
if (result.blob) {
|
||||||
downloadBytes(result.blob, result.mime, fileName);
|
downloadBytes(result.blob, result.mime, fileName);
|
||||||
}
|
}
|
||||||
if (saving && indices === null) {
|
if (saving && indices === null && state.editorInfo) {
|
||||||
state.savedRevision = state.editorInfo.revision;
|
state.savedRevision = state.editorInfo.revision;
|
||||||
setDirty(false);
|
setDirty(false);
|
||||||
}
|
}
|
||||||
@@ -5344,6 +5750,7 @@ function markThumbnailError(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderMeta(keepDetails = false) {
|
function renderMeta(keepDetails = false) {
|
||||||
|
renderSecurity();
|
||||||
const doc = state.doc || {};
|
const doc = state.doc || {};
|
||||||
el.metaPanel.setAttribute("aria-busy", String(!!doc.detailsPending));
|
el.metaPanel.setAttribute("aria-busy", String(!!doc.detailsPending));
|
||||||
document.title = `OFDGo WebUI - ${state.fileName}`;
|
document.title = `OFDGo WebUI - ${state.fileName}`;
|
||||||
@@ -5440,41 +5847,88 @@ function renderSignatures() {
|
|||||||
|
|
||||||
const badges = document.createElement("div");
|
const badges = document.createElement("div");
|
||||||
badges.className = "signature-badges";
|
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);
|
head.append(badges, name);
|
||||||
row.append(head);
|
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.sealName);
|
||||||
appendInfoLine(row, "厂商", signature.sealVendor);
|
appendInfoLine(row, "签署人", signature.signer);
|
||||||
appendInfoLine(row, "签者", signature.signer);
|
|
||||||
appendInfoLine(row, "时间", formatDocumentTime(signature.signatureDateTime));
|
appendInfoLine(row, "时间", formatDocumentTime(signature.signatureDateTime));
|
||||||
appendInfoLine(row, "机构", signatureAgency(signature));
|
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);
|
appendSignatureCheck(row, "信任", signature.certTrustChecked, signature.certTrustOK);
|
||||||
appendInfoLine(row, "保护", signatureReferenceText(signature), signatureReferenceStatus(signature));
|
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, "错误", signature.error, "fail");
|
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);
|
fragment.append(row);
|
||||||
}
|
}
|
||||||
el.signatureList.append(fragment);
|
el.signatureList.append(fragment);
|
||||||
@@ -5493,11 +5947,11 @@ function signatureSummary(signatures) {
|
|||||||
if (unchecked) {
|
if (unchecked) {
|
||||||
parts.push(`未验 ${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) {
|
function signatureNameNode(signature) {
|
||||||
const stamps = signature.stamps || [];
|
const stamps = signature.docIndex ? [] : signature.stamps || [];
|
||||||
const name = document.createElement("div");
|
const name = document.createElement("div");
|
||||||
name.className = "signature-name";
|
name.className = "signature-name";
|
||||||
if (!stamps.length) {
|
if (!stamps.length) {
|
||||||
@@ -6060,6 +6514,7 @@ function updateFitSpace() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateControls() {
|
function updateControls() {
|
||||||
|
renderSecurity();
|
||||||
const hasDoc = Boolean(state.doc);
|
const hasDoc = Boolean(state.doc);
|
||||||
updateRenderBackend();
|
updateRenderBackend();
|
||||||
updateEditorTools();
|
updateEditorTools();
|
||||||
@@ -6221,11 +6676,11 @@ async function callWASM(name, ...args) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = ++wasmRequestID;
|
const id = ++wasmRequestID;
|
||||||
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
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;
|
state.exportRequestID = id;
|
||||||
el.cancelExportButton.hidden = false;
|
el.cancelExportButton.hidden = false;
|
||||||
el.cancelExportButton.disabled = 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);
|
el.cancelExportButton.setAttribute("aria-label", el.cancelExportButton.title);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -6253,6 +6708,7 @@ function showError(err, empty = !state.doc) {
|
|||||||
|
|
||||||
function setBusy(busy, text = "", percent = 0, status = "") {
|
function setBusy(busy, text = "", percent = 0, status = "") {
|
||||||
document.body.toggleAttribute("aria-busy", busy);
|
document.body.toggleAttribute("aria-busy", busy);
|
||||||
|
renderSecurity();
|
||||||
updateRenderBackend();
|
updateRenderBackend();
|
||||||
el.editButton.disabled = busy || !state.doc || !state.ready || state.exporting;
|
el.editButton.disabled = busy || !state.doc || !state.ready || state.exporting;
|
||||||
el.createForm.inert = busy;
|
el.createForm.inert = busy;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ self.onmessage = ({ data }) => {
|
|||||||
operations.get(data.id)?.abort();
|
operations.get(data.id)?.abort();
|
||||||
return;
|
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());
|
operations.set(data.id, new AbortController());
|
||||||
}
|
}
|
||||||
pending = pending.then(() => handleMessage(data));
|
pending = pending.then(() => handleMessage(data));
|
||||||
@@ -17,6 +17,10 @@ async function handleMessage({ id, name, args }) {
|
|||||||
let channel;
|
let channel;
|
||||||
const signal = operations.get(id)?.signal;
|
const signal = operations.get(id)?.signal;
|
||||||
const importing = name === "ofdgoImportPages";
|
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 {
|
try {
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
const chunks = [];
|
const chunks = [];
|
||||||
@@ -37,7 +41,7 @@ async function handleMessage({ id, name, args }) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const file = args.pop();
|
const file = args.pop();
|
||||||
const indices = name === "ofdgoSaveDocument" ? args.shift() : null;
|
const indices = saving ? args.shift() : null;
|
||||||
if (file) output = await file.createWritable();
|
if (file) output = await file.createWritable();
|
||||||
signal.throwIfAborted();
|
signal.throwIfAborted();
|
||||||
args.push((bytes, done) => {
|
args.push((bytes, done) => {
|
||||||
@@ -58,7 +62,7 @@ async function handleMessage({ id, name, args }) {
|
|||||||
self.postMessage({ id, type: "export", stage: "pages", completed, total });
|
self.postMessage({ id, type: "export", stage: "pages", completed, total });
|
||||||
finish(done);
|
finish(done);
|
||||||
});
|
});
|
||||||
} else if (name === "ofdgoSaveDocument") {
|
} else if (saving) {
|
||||||
args.push((phase, completed, total, done) => {
|
args.push((phase, completed, total, done) => {
|
||||||
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
||||||
finish(done);
|
finish(done);
|
||||||
@@ -94,6 +98,9 @@ async function handleMessage({ id, name, args }) {
|
|||||||
}
|
}
|
||||||
self.postMessage(result);
|
self.postMessage(result);
|
||||||
} finally {
|
} finally {
|
||||||
|
password?.fill(0);
|
||||||
|
key?.fill(0);
|
||||||
|
keyPassword?.fill(0);
|
||||||
channel?.port1.close();
|
channel?.port1.close();
|
||||||
channel?.port2.close();
|
channel?.port2.close();
|
||||||
operations.delete(id);
|
operations.delete(id);
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ module github.com/xiaoqidun/ofdgo
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
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/go-text/typesetting v0.3.5
|
||||||
github.com/gogpu/gg v0.52.5
|
github.com/gogpu/gg v0.52.5
|
||||||
github.com/tdewolff/canvas v0.0.0-20260913163248-dd4999d1c76a
|
github.com/tdewolff/canvas v0.0.0-20260913163248-dd4999d1c76a
|
||||||
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
github.com/tdewolff/font v0.0.0-20260913163313-54f98bb59ee6
|
||||||
github.com/xiaoqidun/jbig2 v1.1.1
|
github.com/xiaoqidun/jbig2 v1.1.1
|
||||||
|
golang.org/x/crypto v0.57.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.42.0
|
||||||
)
|
)
|
||||||
@@ -22,7 +25,6 @@ require (
|
|||||||
github.com/andybalholm/brotli v1.2.2 // indirect
|
github.com/andybalholm/brotli v1.2.2 // indirect
|
||||||
github.com/benoitkugler/textlayout v0.3.2 // indirect
|
github.com/benoitkugler/textlayout v0.3.2 // indirect
|
||||||
github.com/benoitkugler/textprocessing v0.0.6 // 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/gpucontext v0.28.0 // indirect
|
||||||
github.com/gogpu/gputypes v0.5.2 // indirect
|
github.com/gogpu/gputypes v0.5.2 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
|||||||
@@ -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/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 h1:obkMyj62GEPg3xUVYqROlCN22z1OleuZm6ULqX9Om1g=
|
||||||
github.com/benoitkugler/textprocessing v0.0.6/go.mod h1:Io0gN08/PXEzrSOWFa88xHx2Xv3VjvLMY7H76YoI23A=
|
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 h1:g2xNgI8yzdNzIVm+qvbMryB6yGPe0pSMss8QT3QwlJ0=
|
||||||
github.com/go-fonts/latin-modern v0.3.3/go.mod h1:tHaiWDGze4EPB0Go4cLT5M3QzRY3peya09Z/8KSCrpY=
|
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=
|
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/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 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||||
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
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.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 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||||
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew=
|
||||||
|
|||||||
+309
-12
@@ -20,6 +20,7 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -91,6 +92,7 @@ var copiedStyle *ofdgo.GraphicObject
|
|||||||
// apiResult 浏览器接口返回结果
|
// apiResult 浏览器接口返回结果
|
||||||
type apiResult struct {
|
type apiResult struct {
|
||||||
OK bool `json:"ok"`
|
OK bool `json:"ok"`
|
||||||
|
Code string `json:"code,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
Data any `json:"data,omitempty"`
|
Data any `json:"data,omitempty"`
|
||||||
MissingGlyphs *ofdgo.MissingGlyphError `json:"missingGlyphs,omitempty"`
|
MissingGlyphs *ofdgo.MissingGlyphError `json:"missingGlyphs,omitempty"`
|
||||||
@@ -102,6 +104,7 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoOpen", openDocument)
|
registerCallback("ofdgoOpen", openDocument)
|
||||||
registerCallback("ofdgoConfigure", configureDocument)
|
registerCallback("ofdgoConfigure", configureDocument)
|
||||||
registerCallback("ofdgoDocumentInfo", documentInfo)
|
registerCallback("ofdgoDocumentInfo", documentInfo)
|
||||||
|
registerCallback("ofdgoVerifySignatures", verifySignatures)
|
||||||
registerCallback("ofdgoRenderPage", renderPage)
|
registerCallback("ofdgoRenderPage", renderPage)
|
||||||
registerCallback("ofdgoRenderBackends", renderBackends)
|
registerCallback("ofdgoRenderBackends", renderBackends)
|
||||||
registerCallback("ofdgoSVGFontData", svgFontData)
|
registerCallback("ofdgoSVGFontData", svgFontData)
|
||||||
@@ -171,6 +174,8 @@ func RunWASM() {
|
|||||||
registerCallback("ofdgoUndo", func([]js.Value) (any, error) { return restoreEditor(false) })
|
registerCallback("ofdgoUndo", func([]js.Value) (any, error) { return restoreEditor(false) })
|
||||||
registerCallback("ofdgoRedo", func([]js.Value) (any, error) { return restoreEditor(true) })
|
registerCallback("ofdgoRedo", func([]js.Value) (any, error) { return restoreEditor(true) })
|
||||||
registerAsyncCallback("ofdgoSaveDocument", saveDocument)
|
registerAsyncCallback("ofdgoSaveDocument", saveDocument)
|
||||||
|
registerAsyncCallback("ofdgoSaveEncrypted", saveEncrypted)
|
||||||
|
registerAsyncCallback("ofdgoSaveSigned", saveSigned)
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +219,16 @@ func callbackResult(fn func([]js.Value) (any, error), args []js.Value) any {
|
|||||||
data, err := safeCall(fn, args)
|
data, err := safeCall(fn, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result := apiResult{Error: err.Error()}
|
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
|
var failure *ofdgo.EditError
|
||||||
if errors.As(err, &failure) {
|
if errors.As(err, &failure) {
|
||||||
result.ReasonCode = failure.Code
|
result.ReasonCode = failure.Code
|
||||||
@@ -257,6 +272,19 @@ func openDocument(args []js.Value) (any, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
renderAnnotations := args[2].Bool()
|
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()
|
clearImport()
|
||||||
currentEditor = nil
|
currentEditor = nil
|
||||||
copiedObjects = nil
|
copiedObjects = nil
|
||||||
@@ -265,14 +293,49 @@ func openDocument(args []js.Value) (any, error) {
|
|||||||
_ = currentSession.Close()
|
_ = currentSession.Close()
|
||||||
currentSession = nil
|
currentSession = nil
|
||||||
}
|
}
|
||||||
session, err := Open(data, OpenOptions{Fonts: fonts, RenderAnnotations: renderAnnotations})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
currentSession = session
|
currentSession = session
|
||||||
return currentSession.Summary(), nil
|
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 更新当前会话的字体和注解设置
|
// configureDocument 更新当前会话的字体和注解设置
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any 文档信息, error 错误信息
|
// 返回: any 文档信息, error 错误信息
|
||||||
@@ -315,6 +378,70 @@ func documentInfo(args []js.Value) (any, error) {
|
|||||||
return currentSession.Info(), nil
|
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 分块导出附件
|
// exportAttachment 分块导出附件
|
||||||
// 入参: args 浏览器参数
|
// 入参: args 浏览器参数
|
||||||
// 返回: any 附件数据, error 错误信息
|
// 返回: any 附件数据, error 错误信息
|
||||||
@@ -2686,7 +2813,16 @@ func loadImport(args []js.Value) (any, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -2700,7 +2836,7 @@ func loadImport(args []js.Value) (any, error) {
|
|||||||
return nil, fmt.Errorf("document has no pages")
|
return nil, fmt.Errorf("document has no pages")
|
||||||
}
|
}
|
||||||
pendingImport = reader
|
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 插入指定来源页面并保持目标文档元数据
|
// importPages 插入指定来源页面并保持目标文档元数据
|
||||||
@@ -2727,8 +2863,13 @@ func importPages(args []js.Value) (any, error) {
|
|||||||
}
|
}
|
||||||
at := args[1].Int()
|
at := args[1].Int()
|
||||||
options := ofdgo.PageImportOptions{Outlines: len(args) > 2 && args[2].Bool()}
|
options := ofdgo.PageImportOptions{Outlines: len(args) > 2 && args[2].Bool()}
|
||||||
if len(args) > 3 {
|
progressIndex := 3
|
||||||
options.OnProgress = editorOperationProgress(args[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 {
|
if _, err := currentEditor.ImportPagesWithOptions(pendingImport, indexes, at, options); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -3454,16 +3595,172 @@ func saveDocument(args []js.Value) (any, error) {
|
|||||||
if currentEditor == nil {
|
if currentEditor == nil {
|
||||||
return nil, fmt.Errorf("no document is being edited")
|
return nil, fmt.Errorf("no document is being edited")
|
||||||
}
|
}
|
||||||
if len(args) > 1 {
|
return saveEditor(currentEditor, args)
|
||||||
currentEditor.OnWriteProgress = editorOperationProgress(args[1])
|
}
|
||||||
|
|
||||||
|
// 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 }()
|
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)
|
writer := bufio.NewWriterSize(exportWriter{write: args[0]}, 1<<20)
|
||||||
var err error
|
var err error
|
||||||
if len(args) > 2 {
|
if len(args) > 2 {
|
||||||
_, err = currentEditor.WritePagesTo(writer, indexesFromJS(args[2]))
|
_, err = editor.WritePagesTo(writer, indexesFromJS(args[2]))
|
||||||
} else {
|
} else {
|
||||||
_, err = currentEditor.WriteTo(writer)
|
_, err = editor.WriteTo(writer)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+110
-2
@@ -30,6 +30,7 @@ import (
|
|||||||
type OpenOptions struct {
|
type OpenOptions struct {
|
||||||
Fonts []FontFile
|
Fonts []FontFile
|
||||||
RenderAnnotations bool
|
RenderAnnotations bool
|
||||||
|
ReaderOptions []ofdgo.ReaderOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// FontFile 字体文件
|
// FontFile 字体文件
|
||||||
@@ -40,6 +41,23 @@ type FontInfo = ofdgo.FontInfo
|
|||||||
|
|
||||||
// SignatureInfo 签名验证信息
|
// SignatureInfo 签名验证信息
|
||||||
type SignatureInfo struct {
|
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"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
@@ -72,6 +90,7 @@ type SignatureInfo struct {
|
|||||||
CertTimeOK bool `json:"certTimeOK,omitempty"`
|
CertTimeOK bool `json:"certTimeOK,omitempty"`
|
||||||
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
|
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
|
||||||
CertTrustOK bool `json:"certTrustOK,omitempty"`
|
CertTrustOK bool `json:"certTrustOK,omitempty"`
|
||||||
|
CertTrustError string `json:"certTrustError,omitempty"`
|
||||||
ReferenceCount int `json:"referenceCount"`
|
ReferenceCount int `json:"referenceCount"`
|
||||||
ReferenceChecked int `json:"referenceChecked"`
|
ReferenceChecked int `json:"referenceChecked"`
|
||||||
ReferencePassed int `json:"referencePassed"`
|
ReferencePassed int `json:"referencePassed"`
|
||||||
@@ -85,6 +104,34 @@ type SignatureInfo struct {
|
|||||||
Error string `json:"error,omitempty"`
|
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 签名外观信息
|
// SignatureStampInfo 签名外观信息
|
||||||
type SignatureStampInfo struct {
|
type SignatureStampInfo struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
@@ -119,6 +166,7 @@ type Session struct {
|
|||||||
|
|
||||||
// DocumentInfo 文档信息
|
// DocumentInfo 文档信息
|
||||||
type DocumentInfo struct {
|
type DocumentInfo struct {
|
||||||
|
Encryption EncryptionInfo `json:"encryption"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
DocType string `json:"docType"`
|
DocType string `json:"docType"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
@@ -139,6 +187,21 @@ type DocumentInfo struct {
|
|||||||
DetailsPending bool `json:"detailsPending,omitempty"`
|
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 目录节点信息
|
// OutlineInfo 目录节点信息
|
||||||
type OutlineInfo = ofdgo.OutlineInfo
|
type OutlineInfo = ofdgo.OutlineInfo
|
||||||
|
|
||||||
@@ -222,7 +285,7 @@ func Open(data []byte, opts OpenOptions) (*Session, error) {
|
|||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil, fmt.Errorf("empty ofd data")
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -381,6 +444,7 @@ func (s *Session) SearchPage(index int, query string) ([]ofdgo.TextMatch, error)
|
|||||||
// 返回: DocumentInfo 文档信息
|
// 返回: DocumentInfo 文档信息
|
||||||
func (s *Session) Summary() DocumentInfo {
|
func (s *Session) Summary() DocumentInfo {
|
||||||
info := DocumentInfo{
|
info := DocumentInfo{
|
||||||
|
Encryption: s.encryptionInfo(),
|
||||||
Version: s.Reader.Version(),
|
Version: s.Reader.Version(),
|
||||||
DocType: s.Reader.DocType(),
|
DocType: s.Reader.DocType(),
|
||||||
PageCount: len(s.doc.Pages.Page),
|
PageCount: len(s.doc.Pages.Page),
|
||||||
@@ -425,6 +489,7 @@ func (s *Session) Summary() DocumentInfo {
|
|||||||
// 返回: DocumentInfo 文档信息
|
// 返回: DocumentInfo 文档信息
|
||||||
func (s *Session) Info() DocumentInfo {
|
func (s *Session) Info() DocumentInfo {
|
||||||
info := DocumentInfo{
|
info := DocumentInfo{
|
||||||
|
Encryption: s.encryptionInfo(),
|
||||||
Version: s.Reader.Version(),
|
Version: s.Reader.Version(),
|
||||||
DocType: s.Reader.DocType(),
|
DocType: s.Reader.DocType(),
|
||||||
PageCount: len(s.doc.Pages.Page),
|
PageCount: len(s.doc.Pages.Page),
|
||||||
@@ -613,7 +678,22 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
|
|||||||
dateTime = report.SignatureTime.Format(time.RFC3339Nano)
|
dateTime = report.SignatureTime.Format(time.RFC3339Nano)
|
||||||
}
|
}
|
||||||
checked, passed := signatureReferenceCounts(report.References)
|
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,
|
ID: report.ID,
|
||||||
Type: string(report.Type),
|
Type: string(report.Type),
|
||||||
Status: status,
|
Status: status,
|
||||||
@@ -646,6 +726,7 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
|
|||||||
CertTimeOK: report.CertTimeOK,
|
CertTimeOK: report.CertTimeOK,
|
||||||
CertTrustChecked: report.CertTrustChecked,
|
CertTrustChecked: report.CertTrustChecked,
|
||||||
CertTrustOK: report.CertTrustOK,
|
CertTrustOK: report.CertTrustOK,
|
||||||
|
CertTrustError: report.CertTrustError,
|
||||||
ReferenceCount: len(report.References),
|
ReferenceCount: len(report.References),
|
||||||
ReferenceChecked: checked,
|
ReferenceChecked: checked,
|
||||||
ReferencePassed: passed,
|
ReferencePassed: passed,
|
||||||
@@ -658,6 +739,33 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
|
|||||||
Stamps: signatureStampInfos(report.StampPositions),
|
Stamps: signatureStampInfos(report.StampPositions),
|
||||||
Error: signatureReportError(report),
|
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 获取签名人名称
|
// signatureSigner 获取签名人名称
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import (
|
|||||||
|
|
||||||
// Open 打开OFD文件
|
// Open 打开OFD文件
|
||||||
// 返回的阅读器持有文件,使用完毕后需调用Close
|
// 返回的阅读器持有文件,使用完毕后需调用Close
|
||||||
// 入参: path 文件路径
|
// 入参: path 文件路径, options 阅读选项
|
||||||
// 返回: *Reader 阅读器实例, error 错误信息
|
// 返回: *Reader 阅读器实例, error 错误信息
|
||||||
func Open(path string) (*Reader, error) {
|
func Open(path string, options ...ReaderOption) (*Reader, error) {
|
||||||
r, err := zip.OpenReader(path)
|
r, err := zip.OpenReader(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -36,7 +36,7 @@ func Open(path string) (*Reader, error) {
|
|||||||
Zip: &r.Reader,
|
Zip: &r.Reader,
|
||||||
Closer: r,
|
Closer: r,
|
||||||
}
|
}
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.open(options); err != nil {
|
||||||
reader.Close()
|
reader.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -45,9 +45,9 @@ func Open(path string) (*Reader, error) {
|
|||||||
|
|
||||||
// NewReader 从IO读取器创建OFD阅读器
|
// NewReader 从IO读取器创建OFD阅读器
|
||||||
// 读取器的生命周期由调用方管理,Reader.Close不会关闭它
|
// 读取器的生命周期由调用方管理,Reader.Close不会关闭它
|
||||||
// 入参: r IO读取器, size 数据大小
|
// 入参: r IO读取器, size 数据大小, options 阅读选项
|
||||||
// 返回: *Reader 阅读器实例, error 错误信息
|
// 返回: *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)
|
zr, err := zip.NewReader(r, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -55,7 +55,7 @@ func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
|
|||||||
reader := &Reader{
|
reader := &Reader{
|
||||||
Zip: zr,
|
Zip: zr,
|
||||||
}
|
}
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.open(options); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return reader, nil
|
return reader, nil
|
||||||
|
|||||||
+137
@@ -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 ©
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ type Editor struct {
|
|||||||
source *editorSource
|
source *editorSource
|
||||||
origins map[string]*editorObjectOrigin
|
origins map[string]*editorObjectOrigin
|
||||||
outlines []byte
|
outlines []byte
|
||||||
|
encryption *encryptionState
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFontFS 设置编辑与几何度量使用的外部字体来源,不嵌入或替换文档字体
|
// SetFontFS 设置编辑与几何度量使用的外部字体来源,不嵌入或替换文档字体
|
||||||
|
|||||||
@@ -62,8 +62,10 @@ type editorPageImport struct {
|
|||||||
// PageImportOptions 页面导入选项,零值仅导入页面及其关联资源
|
// PageImportOptions 页面导入选项,零值仅导入页面及其关联资源
|
||||||
// OnProgress按ids、pages、resources、commit阶段回报进度,total为0表示总量未知
|
// OnProgress按ids、pages、resources、commit阶段回报进度,total为0表示总量未知
|
||||||
// 回调返回错误则取消导入,commit检查点之后统一提交;回调不可重入修改编辑器
|
// 回调返回错误则取消导入,commit检查点之后统一提交;回调不可重入修改编辑器
|
||||||
|
// AllowDecrypted显式允许将加密来源导入未加密目标,不影响原文件
|
||||||
type PageImportOptions struct {
|
type PageImportOptions struct {
|
||||||
Outlines bool
|
Outlines bool
|
||||||
|
AllowDecrypted bool
|
||||||
OnProgress func(stage string, completed, total int) error
|
OnProgress func(stage string, completed, total int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +85,9 @@ func (e *Editor) ImportPages(source *Reader, indexes []int, at int) ([]string, e
|
|||||||
// 入参: source 来源阅读器, indexes 来源页面索引, at 目标插入位置, options 导入选项
|
// 入参: source 来源阅读器, indexes 来源页面索引, at 目标插入位置, options 导入选项
|
||||||
// 返回: []string 新页面标识, error 错误信息
|
// 返回: []string 新页面标识, error 错误信息
|
||||||
func (e *Editor) ImportPagesWithOptions(source *Reader, indexes []int, at int, options PageImportOptions) ([]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)
|
return e.importPages(source, indexes, at, false, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-11
@@ -116,17 +116,7 @@ type PageCapabilities struct {
|
|||||||
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader
|
// 页面按需解析,未修改条目直接保留;编辑器及其Reader快照使用期间不得关闭输入Reader
|
||||||
// 返回: *Editor 编辑器, error 错误信息
|
// 返回: *Editor 编辑器, error 错误信息
|
||||||
func (r *Reader) Editor() (*Editor, error) {
|
func (r *Reader) Editor() (*Editor, error) {
|
||||||
if r.Zip != nil {
|
reader := &Reader{Zip: r.Zip, files: r.files, encryption: r.encryption}
|
||||||
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}
|
|
||||||
if err := reader.initRoot(); err != nil {
|
if err := reader.initRoot(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -143,6 +133,7 @@ func (r *Reader) Editor() (*Editor, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
e := NewEditor()
|
e := NewEditor()
|
||||||
|
e.encryption = r.encryption
|
||||||
e.Info, e.maxID = cloneEditorData(*info), maximum
|
e.Info, e.maxID = cloneEditorData(*info), maximum
|
||||||
directory := cleanPackagePath(reader.RootDir)
|
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)}
|
e.source = &editorSource{reader: reader, document: doc, info: cloneEditorData(*info), directory: directory, pages: make(map[string]*editorSourcePage), origins: make(map[string]*editorObjectOrigin)}
|
||||||
|
|||||||
@@ -538,7 +538,7 @@ func (e *Editor) sourceReader(progress editorProgress) (*Reader, error) {
|
|||||||
files = make(map[string][]byte)
|
files = make(map[string][]byte)
|
||||||
}
|
}
|
||||||
maps.Copy(files, parts)
|
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 {
|
if err := reader.initRoot(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+54
-11
@@ -49,6 +49,8 @@ type Reader struct {
|
|||||||
fileIndex map[string]*zip.File
|
fileIndex map[string]*zip.File
|
||||||
fileIndexFold map[string]*zip.File
|
fileIndexFold map[string]*zip.File
|
||||||
files map[string][]byte
|
files map[string][]byte
|
||||||
|
fileNamesFold map[string]string
|
||||||
|
encryption *encryptionState
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close 关闭阅读器
|
// Close 关闭阅读器
|
||||||
@@ -63,17 +65,8 @@ func (r *Reader) Close() error {
|
|||||||
// initRoot 读取根节点信息
|
// initRoot 读取根节点信息
|
||||||
// 返回: error 错误信息
|
// 返回: error 错误信息
|
||||||
func (r *Reader) initRoot() error {
|
func (r *Reader) initRoot() error {
|
||||||
r.fileIndex = make(map[string]*zip.File)
|
if err := r.indexPackage(); err != nil {
|
||||||
r.fileIndexFold = make(map[string]*zip.File)
|
return err
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
data, err := r.readFile("OFD.xml")
|
data, err := r.readFile("OFD.xml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -96,11 +89,58 @@ func (r *Reader) initRoot() error {
|
|||||||
return nil
|
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 读取文档内的文件
|
// readFile 读取文档内的文件
|
||||||
// 入参: name 文件名
|
// 入参: name 文件名
|
||||||
// 返回: []byte 文件内容, error 错误信息
|
// 返回: []byte 文件内容, error 错误信息
|
||||||
func (r *Reader) readFile(name string) ([]byte, error) {
|
func (r *Reader) readFile(name string) ([]byte, error) {
|
||||||
name = cleanPackagePath(name)
|
name = cleanPackagePath(name)
|
||||||
|
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
|
||||||
|
name = actual
|
||||||
|
}
|
||||||
if data, ok := r.files[name]; ok {
|
if data, ok := r.files[name]; ok {
|
||||||
return bytes.Clone(data), nil
|
return bytes.Clone(data), nil
|
||||||
}
|
}
|
||||||
@@ -118,6 +158,9 @@ func (r *Reader) readFile(name string) ([]byte, error) {
|
|||||||
// 返回: io.ReadCloser 文件流, error 错误信息
|
// 返回: io.ReadCloser 文件流, error 错误信息
|
||||||
func (r *Reader) openFile(name string) (io.ReadCloser, error) {
|
func (r *Reader) openFile(name string) (io.ReadCloser, error) {
|
||||||
name = cleanPackagePath(name)
|
name = cleanPackagePath(name)
|
||||||
|
if actual, ok := r.fileNamesFold[strings.ToLower(name)]; ok {
|
||||||
|
name = actual
|
||||||
|
}
|
||||||
if data, ok := r.files[name]; ok {
|
if data, ok := r.files[name]; ok {
|
||||||
return io.NopCloser(bytes.NewReader(data)), nil
|
return io.NopCloser(bytes.NewReader(data)), nil
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-10
@@ -29,6 +29,7 @@ const (
|
|||||||
|
|
||||||
// digitalVerifyResult 数字签名验证结果
|
// digitalVerifyResult 数字签名验证结果
|
||||||
type digitalVerifyResult struct {
|
type digitalVerifyResult struct {
|
||||||
|
Timestamps []signatureTimestampEvidence
|
||||||
DataHashChecked bool
|
DataHashChecked bool
|
||||||
DataHashOK bool
|
DataHashOK bool
|
||||||
SignedChecked bool
|
SignedChecked bool
|
||||||
@@ -42,6 +43,7 @@ type digitalVerifyResult struct {
|
|||||||
|
|
||||||
// gbtSignedData GB/T 35275 SignedData结构
|
// gbtSignedData GB/T 35275 SignedData结构
|
||||||
type gbtSignedData struct {
|
type gbtSignedData struct {
|
||||||
|
Version int
|
||||||
ContentDigest []byte
|
ContentDigest []byte
|
||||||
Certs []gbtCertificate
|
Certs []gbtCertificate
|
||||||
Signers []gbtSignerInfo
|
Signers []gbtSignerInfo
|
||||||
@@ -56,6 +58,8 @@ type gbtCertificate struct {
|
|||||||
|
|
||||||
// gbtSignerInfo SignedData签名者信息
|
// gbtSignerInfo SignedData签名者信息
|
||||||
type gbtSignerInfo struct {
|
type gbtSignerInfo struct {
|
||||||
|
Version int
|
||||||
|
UnauthAttrs asn1.RawValue
|
||||||
Issuer []byte
|
Issuer []byte
|
||||||
Serial *big.Int
|
Serial *big.Int
|
||||||
DigestAlg string
|
DigestAlg string
|
||||||
@@ -70,6 +74,15 @@ type gbtSignerInfo struct {
|
|||||||
// 返回: *digitalVerifyResult 验证结果, error 错误信息
|
// 返回: *digitalVerifyResult 验证结果, error 错误信息
|
||||||
func verifyDigitalSignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
|
func verifyDigitalSignature(method, digestMethod string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
|
||||||
if value, ok := normalizeGBT35275SignedValue(signedValue); ok {
|
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)
|
return verifyGBT35275SignedData(value, signedData, options)
|
||||||
}
|
}
|
||||||
if isSM2SignatureMethod(method) {
|
if isSM2SignatureMethod(method) {
|
||||||
@@ -99,7 +112,7 @@ func verifyRawDigitalSignature(signedValue, signedData []byte, options *signatur
|
|||||||
result.SignerCerts = [][]byte{cert}
|
result.SignerCerts = [][]byte{cert}
|
||||||
result.CertInfo = signatureCertInfo(cert)
|
result.CertInfo = signatureCertInfo(cert)
|
||||||
result.SignedChecked = true
|
result.SignedChecked = true
|
||||||
if sm2VerifySignature(pub, nil, signedData, signedValue) {
|
if verifySM2Signature(pub, nil, signedData, signedValue) {
|
||||||
result.SignedOK = true
|
result.SignedOK = true
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -144,6 +157,15 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
for _, cert := range options.SignCerts {
|
||||||
c, err := parseGBTCertificate(cert)
|
c, err := parseGBTCertificate(cert)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -155,6 +177,12 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
|
|||||||
}
|
}
|
||||||
result := &digitalVerifyResult{Certs: sd.rawCerts()}
|
result := &digitalVerifyResult{Certs: sd.rawCerts()}
|
||||||
for index, signer := range sd.Signers {
|
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)
|
digest, err := signatureDigest(signer.DigestAlg, signedData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -184,6 +212,13 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
result.SignerCerts = append(result.SignerCerts, cert.Raw)
|
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)
|
result.CertInfo = signatureCertInfo(cert.Raw)
|
||||||
if isSM2SignatureMethod(signer.SignatureAlg) {
|
if isSM2SignatureMethod(signer.SignatureAlg) {
|
||||||
pub, err := parseSM2PublicKeyFromCert(cert.Raw)
|
pub, err := parseSM2PublicKeyFromCert(cert.Raw)
|
||||||
@@ -191,7 +226,7 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
|
|||||||
result.CertOK = false
|
result.CertOK = false
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
if !sm2VerifySignature(pub, nil, plain, signer.Signature) {
|
if !verifySM2Signature(pub, nil, plain, signer.Signature) {
|
||||||
result.SignedChecked = true
|
result.SignedChecked = true
|
||||||
result.CertChecked = index == len(sd.Signers)-1
|
result.CertChecked = index == len(sd.Signers)-1
|
||||||
result.CertOK = true
|
result.CertOK = true
|
||||||
@@ -376,7 +411,11 @@ func parseGBT35275SignedData(data []byte) (*gbtSignedData, error) {
|
|||||||
if !ok || len(items) < 4 {
|
if !ok || len(items) < 4 {
|
||||||
return nil, fmt.Errorf("invalid signed data")
|
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])
|
contentOID, content, hasContent, err := parseGBTContentInfo(items[2])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -431,7 +470,7 @@ func parseGBTContentInfoBytes(data []byte) (string, asn1.RawValue, bool, error)
|
|||||||
// 返回: string 内容类型, asn1.RawValue 内容, bool 是否存在内容, error 错误信息
|
// 返回: string 内容类型, asn1.RawValue 内容, bool 是否存在内容, error 错误信息
|
||||||
func parseGBTContentInfo(raw asn1.RawValue) (string, asn1.RawValue, bool, error) {
|
func parseGBTContentInfo(raw asn1.RawValue) (string, asn1.RawValue, bool, error) {
|
||||||
items, ok := asn1Children(raw.Bytes)
|
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")
|
return "", asn1.RawValue{}, false, fmt.Errorf("invalid content info")
|
||||||
}
|
}
|
||||||
oid, err := asn1OIDString(items[0])
|
oid, err := asn1OIDString(items[0])
|
||||||
@@ -477,8 +516,8 @@ func parseGBTCertificate(data []byte) (gbtCertificate, error) {
|
|||||||
}
|
}
|
||||||
return gbtCertificate{
|
return gbtCertificate{
|
||||||
Raw: cert.Raw,
|
Raw: cert.Raw,
|
||||||
Issuer: cert.Issuer,
|
Issuer: cert.RawIssuer,
|
||||||
Serial: cert.Serial,
|
Serial: cert.SerialNumber,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -509,6 +548,10 @@ func parseGBTSignerInfo(raw asn1.RawValue) (gbtSignerInfo, error) {
|
|||||||
if !ok || len(items) < 5 {
|
if !ok || len(items) < 5 {
|
||||||
return gbtSignerInfo{}, fmt.Errorf("invalid signer info")
|
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])
|
issuer, serial, err := parseGBTIssuerAndSerial(items[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gbtSignerInfo{}, err
|
return gbtSignerInfo{}, err
|
||||||
@@ -539,7 +582,16 @@ func parseGBTSignerInfo(raw asn1.RawValue) (gbtSignerInfo, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return gbtSignerInfo{}, err
|
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{
|
return gbtSignerInfo{
|
||||||
|
Version: version,
|
||||||
|
UnauthAttrs: unauth,
|
||||||
Issuer: issuer,
|
Issuer: issuer,
|
||||||
Serial: serial,
|
Serial: serial,
|
||||||
DigestAlg: digestAlg,
|
DigestAlg: digestAlg,
|
||||||
@@ -587,20 +639,29 @@ func parseGBTMessageDigestAttr(raw asn1.RawValue) ([]byte, error) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("invalid authenticated attributes")
|
return nil, fmt.Errorf("invalid authenticated attributes")
|
||||||
}
|
}
|
||||||
|
var digest []byte
|
||||||
|
found := false
|
||||||
for _, attr := range attrs {
|
for _, attr := range attrs {
|
||||||
items, ok := asn1Children(attr.Bytes)
|
items, ok := asn1Children(attr.Bytes)
|
||||||
if !ok || len(items) < 2 {
|
if !ok || len(items) != 2 {
|
||||||
continue
|
return nil, fmt.Errorf("invalid authenticated attribute")
|
||||||
}
|
}
|
||||||
oid, err := asn1OIDString(items[0])
|
oid, err := asn1OIDString(items[0])
|
||||||
if err != nil || oid != signAttrMessageDigest {
|
if err != nil || oid != signAttrMessageDigest {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
values, ok := asn1Children(items[1].Bytes)
|
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 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")
|
return nil, fmt.Errorf("message digest attribute not found")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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 = ©
|
||||||
|
} 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 = ©
|
||||||
|
} 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+53
-53
@@ -16,11 +16,14 @@ package ofdgo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/emmansun/gmsm/sm2"
|
||||||
|
"github.com/emmansun/gmsm/sm3"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -30,14 +33,12 @@ const (
|
|||||||
signMethodSM2SM3 = "1.2.156.10197.1.501"
|
signMethodSM2SM3 = "1.2.156.10197.1.501"
|
||||||
signMethodSM2SM3B = "1.2.156.10197.501"
|
signMethodSM2SM3B = "1.2.156.10197.501"
|
||||||
signMethodSM2Sign = "1.2.156.10197.1.301.1"
|
signMethodSM2Sign = "1.2.156.10197.1.301.1"
|
||||||
signCurveSM2P256 = "1.2.156.10197.1.301"
|
|
||||||
signECPublicKey = "1.2.840.10045.2.1"
|
|
||||||
signASN1Sequence = 16
|
signASN1Sequence = 16
|
||||||
signPublicKeySize = 65
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// sesSignature SES签章值
|
// sesSignature SES签章值
|
||||||
type sesSignature struct {
|
type sesSignature struct {
|
||||||
|
Timestamp []byte
|
||||||
ToSign []byte
|
ToSign []byte
|
||||||
Cert []byte
|
Cert []byte
|
||||||
SignAlg string
|
SignAlg string
|
||||||
@@ -49,6 +50,7 @@ type sesSignature struct {
|
|||||||
|
|
||||||
// sesSeal SES电子印章
|
// sesSeal SES电子印章
|
||||||
type sesSeal struct {
|
type sesSeal struct {
|
||||||
|
TimestampRaw []byte
|
||||||
Raw []byte
|
Raw []byte
|
||||||
SignData []byte
|
SignData []byte
|
||||||
Cert []byte
|
Cert []byte
|
||||||
@@ -74,6 +76,8 @@ type sesCertDigest struct {
|
|||||||
|
|
||||||
// sesVerifyResult SES签章验证结果
|
// sesVerifyResult SES签章验证结果
|
||||||
type sesVerifyResult struct {
|
type sesVerifyResult struct {
|
||||||
|
SignatureMethod string
|
||||||
|
Timestamps []signatureTimestampEvidence
|
||||||
DataHashChecked bool
|
DataHashChecked bool
|
||||||
DataHashOK bool
|
DataHashOK bool
|
||||||
SignedChecked bool
|
SignedChecked bool
|
||||||
@@ -147,7 +151,22 @@ func parseSESSignature(data []byte) (*sesSignature, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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{
|
return &sesSignature{
|
||||||
|
Timestamp: timestamp,
|
||||||
ToSign: append([]byte(nil), items[0].FullBytes...),
|
ToSign: append([]byte(nil), items[0].FullBytes...),
|
||||||
Cert: cert,
|
Cert: cert,
|
||||||
SignAlg: alg,
|
SignAlg: alg,
|
||||||
@@ -175,7 +194,7 @@ func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
version, err := asn1Integer(tbsItems[0])
|
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")
|
return nil, fmt.Errorf("invalid ses version")
|
||||||
}
|
}
|
||||||
signatureTime := parseSESSignatureTimeV1(tbsItems[2])
|
signatureTime := parseSESSignatureTimeV1(tbsItems[2])
|
||||||
@@ -233,7 +252,16 @@ func verifySESSignature(data, signedData []byte, options *signatureVerifyOptions
|
|||||||
if !isSM2SignatureMethod(sig.SignAlg) || !isSM2SignatureMethod(sig.Seal.SignAlg) {
|
if !isSM2SignatureMethod(sig.SignAlg) || !isSM2SignatureMethod(sig.Seal.SignAlg) {
|
||||||
return nil, fmt.Errorf("unsupported signature method")
|
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 := &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.SignCert = signatureCertInfo(sig.Cert)
|
||||||
result.SealCert = signatureCertInfo(sig.Seal.Cert)
|
result.SealCert = signatureCertInfo(sig.Seal.Cert)
|
||||||
result.SignCertRaw = sig.Cert
|
result.SignCertRaw = sig.Cert
|
||||||
@@ -255,9 +283,9 @@ func verifySESSignature(data, signedData []byte, options *signatureVerifyOptions
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
result.SignedChecked = true
|
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.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.CertChecked = true
|
||||||
result.CertOK = sesCertInList(sig.Cert, sig.Seal.CertList)
|
result.CertOK = sesCertInList(sig.Cert, sig.Seal.CertList)
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -297,11 +325,20 @@ func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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])
|
picType, picData, err := parseSESPicture(infoItems[3])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &sesSeal{
|
return &sesSeal{
|
||||||
|
TimestampRaw: timestampRaw,
|
||||||
Raw: append([]byte(nil), raw.FullBytes...),
|
Raw: append([]byte(nil), raw.FullBytes...),
|
||||||
SignData: append([]byte(nil), items[0].FullBytes...),
|
SignData: append([]byte(nil), items[0].FullBytes...),
|
||||||
Cert: cert,
|
Cert: cert,
|
||||||
@@ -531,62 +568,25 @@ func parseSESPicture(raw asn1.RawValue) (string, []byte, error) {
|
|||||||
|
|
||||||
// parseSM2PublicKeyFromCert 从证书解析SM2公钥
|
// parseSM2PublicKeyFromCert 从证书解析SM2公钥
|
||||||
// 入参: data DER编码证书
|
// 入参: data DER编码证书
|
||||||
// 返回: sm2PublicKey SM2公钥, error 错误信息
|
// 返回: *ecdsa.PublicKey SM2公钥, error 错误信息
|
||||||
func parseSM2PublicKeyFromCert(data []byte) (sm2PublicKey, error) {
|
func parseSM2PublicKeyFromCert(data []byte) (*ecdsa.PublicKey, error) {
|
||||||
cert, err := parseSignatureCertificate(data)
|
cert, err := parseSignatureCertificate(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sm2PublicKey{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return parseSM2PublicKeyInfo(cert.PublicKey)
|
pub, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
||||||
|
if !ok || !sm2.IsSM2PublicKey(pub) {
|
||||||
|
return nil, fmt.Errorf("SM2 public key required")
|
||||||
}
|
}
|
||||||
|
return pub, nil
|
||||||
// parseSM2PublicKeyInfo 解析SM2公钥信息
|
|
||||||
// 入参: raw SubjectPublicKeyInfo原始值
|
|
||||||
// 返回: sm2PublicKey SM2公钥, error 错误信息
|
|
||||||
func parseSM2PublicKeyInfo(raw asn1.RawValue) (sm2PublicKey, error) {
|
|
||||||
var spki struct {
|
|
||||||
Algorithm asn1.RawValue
|
|
||||||
SubjectPublicKey asn1.BitString
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// signSM3 计算SM3摘要
|
// signSM3 计算SM3摘要
|
||||||
// 入参: data 原文数据
|
// 入参: data 原文数据
|
||||||
// 返回: []byte 摘要值
|
// 返回: []byte 摘要值
|
||||||
func signSM3(data []byte) []byte {
|
func signSM3(data []byte) []byte {
|
||||||
h := newSM3()
|
digest := sm3.Sum(data)
|
||||||
h.Write(data)
|
return digest[:]
|
||||||
return h.Sum(nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isSM2SignatureMethod 判断是否为SM2签名算法
|
// isSM2SignatureMethod 判断是否为SM2签名算法
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
+169
-419
@@ -24,7 +24,6 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"crypto/x509"
|
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
@@ -35,13 +34,32 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/emmansun/gmsm/sm2"
|
||||||
|
"github.com/emmansun/gmsm/smx509"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SignatureVerifyReport 签名验证报告
|
// SignatureVerifyReport 签名验证报告
|
||||||
// Valid表示签名完整性、签名时间语义及调用方指定的证书策略均通过
|
// Valid表示签名完整性、签名时间语义及调用方指定的全部策略均通过
|
||||||
// Checked表示对应检查已得出结论, 未检查时不能将OK的零值视为失败
|
// Checked表示对应检查已得出结论, 未检查时不能将OK的零值视为失败
|
||||||
// SealCertTimeOK仅提供制章证书在签名时间的状态信息, 不参与Valid判断
|
// SealCertTimeOK仅提供制章证书在签名时间的状态信息, 不参与Valid判断
|
||||||
type SignatureVerifyReport struct {
|
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
|
ID string
|
||||||
BaseLoc string
|
BaseLoc string
|
||||||
Type SignType
|
Type SignType
|
||||||
@@ -80,6 +98,7 @@ type SignatureVerifyReport struct {
|
|||||||
CertTimeOK bool
|
CertTimeOK bool
|
||||||
CertTrustChecked bool
|
CertTrustChecked bool
|
||||||
CertTrustOK bool
|
CertTrustOK bool
|
||||||
|
CertTrustError string
|
||||||
Valid bool
|
Valid bool
|
||||||
Error string
|
Error string
|
||||||
}
|
}
|
||||||
@@ -87,7 +106,10 @@ type SignatureVerifyReport struct {
|
|||||||
// IntegrityValid 判断签名完整性是否有效
|
// IntegrityValid 判断签名完整性是否有效
|
||||||
// 返回: bool 是否有效
|
// 返回: bool 是否有效
|
||||||
func (report SignatureVerifyReport) 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 判断签名是否可信有效
|
// TrustedValid 判断签名是否可信有效
|
||||||
@@ -151,6 +173,11 @@ type signatureVerifyOptions struct {
|
|||||||
SignCerts [][]byte
|
SignCerts [][]byte
|
||||||
TrustCerts [][]byte
|
TrustCerts [][]byte
|
||||||
VerifyTime *time.Time
|
VerifyTime *time.Time
|
||||||
|
Policy *SignaturePolicy
|
||||||
|
Timestamp *SignatureTimestampOptions
|
||||||
|
Revocation *SignatureRevocationOptions
|
||||||
|
DocIndex int
|
||||||
|
DocRoot string
|
||||||
}
|
}
|
||||||
|
|
||||||
var signatureMethodReplacer = strings.NewReplacer("-", "", "_", "", " ", "")
|
var signatureMethodReplacer = strings.NewReplacer("-", "", "_", "", " ", "")
|
||||||
@@ -177,6 +204,7 @@ func WithSignatureCerts(certs ...[]byte) SignatureVerifyOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithSignatureTrustCert 添加签名信任证书
|
// WithSignatureTrustCert 添加签名信任证书
|
||||||
|
// 指定信任后同时验证证书链有效期,默认使用当前时间
|
||||||
// 入参: cert DER或PEM编码证书
|
// 入参: cert DER或PEM编码证书
|
||||||
// 返回: SignatureVerifyOption 签名验证选项
|
// 返回: SignatureVerifyOption 签名验证选项
|
||||||
func WithSignatureTrustCert(cert []byte) SignatureVerifyOption {
|
func WithSignatureTrustCert(cert []byte) SignatureVerifyOption {
|
||||||
@@ -186,6 +214,7 @@ func WithSignatureTrustCert(cert []byte) SignatureVerifyOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WithSignatureTrustCerts 添加多张签名信任证书
|
// WithSignatureTrustCerts 添加多张签名信任证书
|
||||||
|
// 指定信任后同时验证证书链有效期,默认使用当前时间
|
||||||
// 入参: certs DER或PEM编码证书列表
|
// 入参: certs DER或PEM编码证书列表
|
||||||
// 返回: SignatureVerifyOption 签名验证选项
|
// 返回: SignatureVerifyOption 签名验证选项
|
||||||
func WithSignatureTrustCerts(certs ...[]byte) SignatureVerifyOption {
|
func WithSignatureTrustCerts(certs ...[]byte) SignatureVerifyOption {
|
||||||
@@ -246,19 +275,56 @@ func VerifySignaturesReader(r io.ReaderAt, size int64, opts ...SignatureVerifyOp
|
|||||||
// 入参: opts 签名验证选项
|
// 入参: opts 签名验证选项
|
||||||
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
|
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
|
||||||
func (r *Reader) VerifySignatures(opts ...SignatureVerifyOption) ([]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{}
|
options := signatureVerifyOptions{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
if opt != nil {
|
||||||
opt(&options)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 == "" {
|
if doc.Signatures == "" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
sigListPath := r.ResPath(doc.Signatures)
|
view := *r
|
||||||
data, err := r.readFileExact(sigListPath)
|
view.doc, view.RootDir = &doc, path.Dir(body.DocRoot)
|
||||||
|
sigListPath := view.ResPath(doc.Signatures)
|
||||||
|
data, err = view.readFile(sigListPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -268,7 +334,7 @@ func (r *Reader) VerifySignatures(opts ...SignatureVerifyOption) ([]SignatureVer
|
|||||||
}
|
}
|
||||||
reports := make([]SignatureVerifyReport, 0, len(signatures.List))
|
reports := make([]SignatureVerifyReport, 0, len(signatures.List))
|
||||||
for _, sigRef := range 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
|
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 {
|
func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *signatureVerifyOptions) SignatureVerifyReport {
|
||||||
sigPath := signatureRefPath(sigListPath, sigRef.BaseLoc)
|
sigPath := signatureRefPath(sigListPath, sigRef.BaseLoc)
|
||||||
report := SignatureVerifyReport{
|
report := SignatureVerifyReport{
|
||||||
|
DocIndex: options.DocIndex,
|
||||||
|
DocRoot: options.DocRoot,
|
||||||
ID: sigRef.ID,
|
ID: sigRef.ID,
|
||||||
BaseLoc: sigRef.BaseLoc,
|
BaseLoc: sigRef.BaseLoc,
|
||||||
Type: sigRef.Type,
|
Type: sigRef.Type,
|
||||||
@@ -287,7 +355,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
|
|||||||
if report.Type == "" {
|
if report.Type == "" {
|
||||||
report.Type = SignTypeSeal
|
report.Type = SignTypeSeal
|
||||||
}
|
}
|
||||||
sigData, err := r.readFileExact(sigPath)
|
sigData, err := r.readFile(sigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
report.Error = err.Error()
|
report.Error = err.Error()
|
||||||
return report
|
return report
|
||||||
@@ -311,8 +379,10 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
|
|||||||
report.StampPositionError = err.Error()
|
report.StampPositionError = err.Error()
|
||||||
}
|
}
|
||||||
report.DigestOK = referencesOK(report.References)
|
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)
|
signedValuePath := signatureRefPath(sigPath, sigFile.SignedValue)
|
||||||
signedValue, err := r.readFileExact(signedValuePath)
|
signedValue, err := r.readFile(signedValuePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
report.Error = err.Error()
|
report.Error = err.Error()
|
||||||
return report
|
return report
|
||||||
@@ -331,6 +401,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
|
|||||||
report.Signer = result.CertInfo.CommonName
|
report.Signer = result.CertInfo.CommonName
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
report.applySignaturePolicyError(err)
|
||||||
report.Error = err.Error()
|
report.Error = err.Error()
|
||||||
return report
|
return report
|
||||||
}
|
}
|
||||||
@@ -338,6 +409,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
|
|||||||
report.SignatureTime = parseSignatureDateTime(report.SignatureDateTime)
|
report.SignatureTime = parseSignatureDateTime(report.SignatureDateTime)
|
||||||
report.applySignatureTimePolicy()
|
report.applySignatureTimePolicy()
|
||||||
report.applySignatureCertificatePolicy(options, result.SignerCerts, result.Certs)
|
report.applySignatureCertificatePolicy(options, result.SignerCerts, result.Certs)
|
||||||
|
report.applySignatureEvidence(options, signedValue, result.Timestamps, result.SignerCerts, result.Certs)
|
||||||
report.Valid = report.IntegrityValid() && report.certificatePolicyOK()
|
report.Valid = report.IntegrityValid() && report.certificatePolicyOK()
|
||||||
return report
|
return report
|
||||||
case SignTypeSeal:
|
case SignTypeSeal:
|
||||||
@@ -363,14 +435,21 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
|
|||||||
report.Signer = sesResult.SignCert.CommonName
|
report.Signer = sesResult.SignCert.CommonName
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
report.applySignaturePolicyError(err)
|
||||||
report.Error = err.Error()
|
report.Error = err.Error()
|
||||||
return report
|
return report
|
||||||
}
|
}
|
||||||
report.applySignatureTimePolicy()
|
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.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 != "" {
|
if sigFile.SignedInfo.Seal.BaseLoc != "" {
|
||||||
sealPath := signatureRefPath(sigPath, sigFile.SignedInfo.Seal.BaseLoc)
|
sealPath := signatureRefPath(sigPath, sigFile.SignedInfo.Seal.BaseLoc)
|
||||||
sealData, err := r.readFileExact(sealPath)
|
sealData, err := r.readFile(sealPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
report.Error = err.Error()
|
report.Error = err.Error()
|
||||||
return report
|
return report
|
||||||
@@ -407,7 +486,7 @@ func (r *Reader) verifySignatureReference(sigPath, method string, ref SignatureR
|
|||||||
result.Error = err.Error()
|
result.Error = err.Error()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
data, err := r.readFileExact(refPath)
|
data, err := r.readFile(refPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Error = err.Error()
|
result.Error = err.Error()
|
||||||
return result
|
return result
|
||||||
@@ -435,20 +514,6 @@ func parseSignatureFile(data []byte) (*SignatureFile, error) {
|
|||||||
return &sigFile, nil
|
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 计算签名摘要
|
// signatureDigest 计算签名摘要
|
||||||
// 入参: method 摘要算法, data 原文数据
|
// 入参: method 摘要算法, data 原文数据
|
||||||
// 返回: []byte 摘要值, error 错误信息
|
// 返回: []byte 摘要值, error 错误信息
|
||||||
@@ -612,7 +677,7 @@ func verifyPublicKeySignature(method, digestMethod string, cert, signedData, sig
|
|||||||
if len(digest) == 0 {
|
if len(digest) == 0 {
|
||||||
return false, fmt.Errorf("unsupported digest method")
|
return false, fmt.Errorf("unsupported digest method")
|
||||||
}
|
}
|
||||||
x509Cert, err := x509.ParseCertificate(cert)
|
x509Cert, err := parseSignatureCertificate(cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
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签名
|
// verifyECDSASignature 验证ECDSA签名
|
||||||
// 入参: pub 公钥, digest 摘要, sig 签名值
|
// 入参: pub 公钥, digest 摘要, sig 签名值
|
||||||
// 返回: bool 是否验证通过
|
// 返回: bool 是否验证通过
|
||||||
@@ -668,7 +750,7 @@ func referencesOK(refs []SignatureReferenceVerify) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, ref := range refs {
|
for _, ref := range refs {
|
||||||
if !ref.OK {
|
if !ref.Checked || !ref.OK || ref.Error != "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,9 +761,14 @@ func referencesOK(refs []SignatureReferenceVerify) bool {
|
|||||||
// 入参: options 验证选项, certs 待验证证书, extraCerts 证书池
|
// 入参: options 验证选项, certs 待验证证书, extraCerts 证书池
|
||||||
func (report *SignatureVerifyReport) applySignatureCertificatePolicy(options *signatureVerifyOptions, certs [][]byte, extraCerts [][]byte) {
|
func (report *SignatureVerifyReport) applySignatureCertificatePolicy(options *signatureVerifyOptions, certs [][]byte, extraCerts [][]byte) {
|
||||||
certs = compactSignatureCerts(certs)
|
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.CertTimeChecked = true
|
||||||
report.CertTimeOK = signatureCertsValidAt(certs, *options.VerifyTime)
|
report.CertTimeOK = signatureCertsValidAt(certs, *verifyTime)
|
||||||
}
|
}
|
||||||
if len(options.TrustCerts) != 0 {
|
if len(options.TrustCerts) != 0 {
|
||||||
report.CertTrustChecked = true
|
report.CertTrustChecked = true
|
||||||
@@ -691,13 +778,15 @@ func (report *SignatureVerifyReport) applySignatureCertificatePolicy(options *si
|
|||||||
pool = append(pool, extraCerts...)
|
pool = append(pool, extraCerts...)
|
||||||
pool = compactSignatureCerts(pool)
|
pool = compactSignatureCerts(pool)
|
||||||
for _, cert := range certs {
|
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.CertTrustOK = false
|
||||||
|
report.CertTrustError = err.Error()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(certs) == 0 {
|
if len(certs) == 0 {
|
||||||
report.CertTrustOK = false
|
report.CertTrustOK = false
|
||||||
|
report.CertTrustError = "signature certificate missing"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -728,6 +817,10 @@ func timeInRange(t, start, end time.Time) bool {
|
|||||||
// certificatePolicyOK 判断证书策略是否通过
|
// certificatePolicyOK 判断证书策略是否通过
|
||||||
// 返回: bool 是否通过
|
// 返回: bool 是否通过
|
||||||
func (report SignatureVerifyReport) 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 {
|
if report.SignatureTimeChecked && !report.SignatureTimeOK {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -759,112 +852,51 @@ func signatureCertsValidAt(certs [][]byte, t time.Time) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// signatureCertMaxChecks 最大证书签名验证次数
|
// verifySignatureCertificateTrust 按指定时间验证签署证书到显式信任根的证书链
|
||||||
const signatureCertMaxChecks = 100
|
// 入参: cert 证书, pool 证书池, trusts 信任证书, at 验证时间
|
||||||
|
// 返回: error 验证错误
|
||||||
// signatureCertPathState 证书路径状态
|
func verifySignatureCertificateTrust(cert []byte, pool, trusts [][]byte, at time.Time) error {
|
||||||
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)
|
|
||||||
c, err := parseSignatureCertificate(cert)
|
c, err := parseSignatureCertificate(cert)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return err
|
||||||
}
|
}
|
||||||
if c.UnhandledCritical {
|
if c.KeyUsage != 0 && c.KeyUsage&(smx509.KeyUsageDigitalSignature|smx509.KeyUsageContentCommitment) == 0 {
|
||||||
return false
|
return fmt.Errorf("certificate does not authorize digital signatures")
|
||||||
}
|
}
|
||||||
if !target && verifyTime != nil && (verifyTime.Before(c.NotBefore) || verifyTime.After(c.NotAfter)) {
|
return verifySignatureCertificateChain(c, pool, trusts, at, smx509.ExtKeyUsageAny)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
if target {
|
|
||||||
if c.KeyUsage != 0 && c.KeyUsage&(x509.KeyUsageDigitalSignature|x509.KeyUsageContentCommitment) == 0 {
|
// verifySignatureCertificateChain 使用统一X.509实现验证证书链
|
||||||
return false
|
// 入参: 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
|
||||||
}
|
}
|
||||||
} else {
|
group.pool.AddCert(ca)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if trusted {
|
_, err := cert.Verify(smx509.VerifyOptions{Roots: roots, Intermediates: intermediates, CurrentTime: at, KeyUsages: []smx509.ExtKeyUsage{usage}})
|
||||||
return true
|
return err
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// verifyCertificateSignature 验证证书签名
|
// verifyCertificateSignature 验证证书签名
|
||||||
// 入参: cert 证书信息, issuerCert 颁发者证书
|
// 入参: cert 证书信息, issuerCert 颁发者证书
|
||||||
// 返回: bool 是否验证通过, error 错误信息
|
// 返回: bool 是否验证通过, error 错误信息
|
||||||
func verifyCertificateSignature(cert signatureCertificate, issuerCert []byte) (bool, error) {
|
func verifyCertificateSignature(cert *smx509.Certificate, issuerCert []byte) (bool, error) {
|
||||||
if isSM2SignatureMethod(cert.SignatureAlg) {
|
issuer, err := parseSignatureCertificate(issuerCert)
|
||||||
pub, err := parseSM2PublicKeyFromCert(issuerCert)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return sm2VerifySignature(pub, nil, cert.TBS, cert.Signature), nil
|
err = issuer.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature)
|
||||||
}
|
return err == nil, err
|
||||||
return verifyPublicKeySignature(cert.SignatureAlg, "", issuerCert, cert.TBS, cert.Signature)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// compactSignatureCerts 清理证书列表
|
// compactSignatureCerts 清理证书列表
|
||||||
@@ -895,21 +927,16 @@ func signatureCertInfo(data []byte) SignatureCertInfo {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return SignatureCertInfo{}
|
return SignatureCertInfo{}
|
||||||
}
|
}
|
||||||
subject := certificateNameValues(cert.SubjectValue)
|
return SignatureCertInfo{
|
||||||
issuer := certificateNameValues(cert.IssuerValue)
|
Raw: bytes.Clone(cert.Raw),
|
||||||
info := SignatureCertInfo{
|
Subject: cert.Subject.String(),
|
||||||
Raw: append([]byte(nil), data...),
|
CommonName: cert.Subject.CommonName,
|
||||||
Subject: certificateNameString(subject),
|
Organization: strings.Join(cert.Subject.Organization, ", "),
|
||||||
CommonName: certificateNameFirst(subject, "2.5.4.3"),
|
Issuer: cert.Issuer.String(),
|
||||||
Organization: certificateNameFirst(subject, "2.5.4.10"),
|
|
||||||
Issuer: certificateNameString(issuer),
|
|
||||||
NotBefore: cert.NotBefore,
|
NotBefore: cert.NotBefore,
|
||||||
NotAfter: cert.NotAfter,
|
NotAfter: cert.NotAfter,
|
||||||
|
SerialNumber: cert.SerialNumber.String(),
|
||||||
}
|
}
|
||||||
if cert.Serial != nil {
|
|
||||||
info.SerialNumber = cert.Serial.String()
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSignatureDateTime 解析带时区的签名时间
|
// parseSignatureDateTime 解析带时区的签名时间
|
||||||
@@ -929,227 +956,17 @@ func parseSignatureDateTime(value string) time.Time {
|
|||||||
return time.Time{}
|
return time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// parseSignatureCertificate 解析唯一的PEM或DER证书
|
||||||
signatureExtensionKeyUsage = "2.5.29.15"
|
// 入参: data 证书数据
|
||||||
signatureExtensionBasicConstraints = "2.5.29.19"
|
// 返回: *smx509.Certificate 证书, error 错误信息
|
||||||
)
|
func parseSignatureCertificate(data []byte) (*smx509.Certificate, error) {
|
||||||
|
if block, rest := pem.Decode(data); block != nil {
|
||||||
// signatureCertificateExtensions 签名证书扩展
|
if block.Type != "CERTIFICATE" || len(block.Headers) != 0 || len(bytes.TrimSpace(rest)) != 0 {
|
||||||
type signatureCertificateExtensions struct {
|
return nil, fmt.Errorf("expected one certificate")
|
||||||
IsCA bool
|
|
||||||
MaxPathLen *big.Int
|
|
||||||
KeyUsage x509.KeyUsage
|
|
||||||
UnhandledCritical bool
|
|
||||||
}
|
}
|
||||||
|
data = block.Bytes
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
return smx509.ParseCertificate(data)
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// asn1Time 解析ASN.1时间
|
// asn1Time 解析ASN.1时间
|
||||||
@@ -1164,82 +981,15 @@ func asn1Time(raw asn1.RawValue) (time.Time, error) {
|
|||||||
return t, nil
|
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 解析签名验证证书
|
// parseSignatureCerts 解析签名验证证书
|
||||||
// 入参: data DER或PEM编码证书
|
// 入参: data DER或PEM编码证书
|
||||||
// 返回: [][]byte DER编码证书列表
|
// 返回: [][]byte DER编码证书列表
|
||||||
func parseSignatureCerts(data []byte) [][]byte {
|
func parseSignatureCerts(data []byte) [][]byte {
|
||||||
data = bytes.TrimSpace(data)
|
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var certs [][]byte
|
var certs [][]byte
|
||||||
rest := data
|
rest := bytes.TrimSpace(data)
|
||||||
hasPEM := false
|
hasPEM := false
|
||||||
for {
|
for {
|
||||||
block, next := pem.Decode(rest)
|
block, next := pem.Decode(rest)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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}})
|
||||||
|
}
|
||||||
@@ -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
@@ -50,6 +50,16 @@ func (progress editorProgress) report(stage string, completed, total int) error
|
|||||||
// 入参: writer 输出流
|
// 入参: writer 输出流
|
||||||
// 返回: int64 已写入字节数, error 错误信息
|
// 返回: int64 已写入字节数, error 错误信息
|
||||||
func (e *Editor) WriteTo(writer io.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 {
|
if err := e.validate(); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -129,6 +139,7 @@ func (e *Editor) WritePagesTo(writer io.Writer, indexes []int) (int64, error) {
|
|||||||
}
|
}
|
||||||
defer reader.Close()
|
defer reader.Close()
|
||||||
selected := NewEditor()
|
selected := NewEditor()
|
||||||
|
selected.encryption = e.encryption
|
||||||
id := selected.Info.DocID
|
id := selected.Info.DocID
|
||||||
selected.Info = cloneEditorData(e.Info)
|
selected.Info = cloneEditorData(e.Info)
|
||||||
selected.Info.DocID = id
|
selected.Info.DocID = id
|
||||||
@@ -156,7 +167,7 @@ func (e *Editor) reader(progress editorProgress) (*Reader, error) {
|
|||||||
if e.source != nil {
|
if e.source != nil {
|
||||||
return e.sourceReader(progress)
|
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 {
|
if err := e.writeParts(func(name string, data []byte, _ bool) error {
|
||||||
r.files[name] = data
|
r.files[name] = data
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in new issue
Block a user