mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 20:22:39 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61da0f3f5d | ||
|
|
98b37d865a |
@@ -80,11 +80,22 @@
|
|||||||
<dt>类型</dt>
|
<dt>类型</dt>
|
||||||
<dd id="metaType">-</dd>
|
<dd id="metaType">-</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>签名</dt>
|
||||||
|
<dd id="metaSignatures">0</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>字体</dt>
|
<dt>字体</dt>
|
||||||
<dd id="metaFonts">0</dd>
|
<dd id="metaFonts">0</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
<section id="signaturePanel" class="signature-panel" aria-labelledby="signaturePanelTitle" hidden>
|
||||||
|
<div class="panel-row">
|
||||||
|
<div id="signaturePanelTitle" class="panel-title compact">签名</div>
|
||||||
|
<span id="signatureSummary" class="count-pill">0</span>
|
||||||
|
</div>
|
||||||
|
<div id="signatureList" class="signature-list"></div>
|
||||||
|
</section>
|
||||||
<section class="doc-font-panel" aria-labelledby="docFontPanelTitle">
|
<section class="doc-font-panel" aria-labelledby="docFontPanelTitle">
|
||||||
<div class="panel-row">
|
<div class="panel-row">
|
||||||
<div id="docFontPanelTitle" class="panel-title compact">OFD 字体</div>
|
<div id="docFontPanelTitle" class="panel-title compact">OFD 字体</div>
|
||||||
|
|||||||
+118
-7
@@ -519,6 +519,23 @@ body[data-hide-meta] .meta-panel {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stamp-highlight {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 4;
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
background: rgba(15, 123, 104, 0.08);
|
||||||
|
box-shadow: 0 0 0 3px rgba(15, 123, 104, 0.16);
|
||||||
|
pointer-events: none;
|
||||||
|
animation: stampPulse 0.9s ease-in-out 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stampPulse {
|
||||||
|
50% {
|
||||||
|
border-color: var(--accent-strong);
|
||||||
|
box-shadow: 0 0 0 5px rgba(15, 123, 104, 0.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ofd-svg {
|
.ofd-svg {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: none;
|
max-width: none;
|
||||||
@@ -548,7 +565,8 @@ body[data-hide-meta] .meta-panel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.font-panel,
|
.font-panel,
|
||||||
.doc-font-panel {
|
.doc-font-panel,
|
||||||
|
.signature-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);
|
||||||
@@ -569,12 +587,14 @@ body[data-hide-meta] .meta-panel {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-font-list {
|
.doc-font-list,
|
||||||
|
.signature-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-font-row {
|
.doc-font-row,
|
||||||
|
.signature-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 9px;
|
padding: 9px;
|
||||||
@@ -585,6 +605,7 @@ body[data-hide-meta] .meta-panel {
|
|||||||
|
|
||||||
.page-list-item:not(:disabled):hover,
|
.page-list-item:not(:disabled):hover,
|
||||||
.doc-font-row:hover,
|
.doc-font-row:hover,
|
||||||
|
.signature-row:hover,
|
||||||
.font-row:hover {
|
.font-row:hover {
|
||||||
border-color: var(--line-strong);
|
border-color: var(--line-strong);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -600,14 +621,26 @@ body[data-hide-meta] .meta-panel {
|
|||||||
background: #fffdf7;
|
background: #fffdf7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-font-head {
|
.signature-row.valid {
|
||||||
|
border-color: #bdd8cc;
|
||||||
|
background: #fbfefd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-row.invalid {
|
||||||
|
border-color: #e2c7c4;
|
||||||
|
background: #fffdfc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-font-head,
|
||||||
|
.signature-head {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-font-name {
|
.doc-font-name,
|
||||||
|
.signature-name {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: #1f2428;
|
color: #1f2428;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -616,7 +649,51 @@ body[data-hide-meta] .meta-panel {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-font-badges {
|
.signature-name-button {
|
||||||
|
display: inline;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-name-button:not(:disabled):hover {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-name-with-stamps {
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-stamp-group {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-stamp-link {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-stamp-link:not(:disabled):hover {
|
||||||
|
background: transparent;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-font-badges,
|
||||||
|
.signature-badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -647,11 +724,45 @@ body[data-hide-meta] .meta-panel {
|
|||||||
color: #705719;
|
color: #705719;
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-badge.missing {
|
.font-badge.missing,
|
||||||
|
.font-badge.invalid {
|
||||||
background: #f9e8e5;
|
background: #f9e8e5;
|
||||||
color: #803f38;
|
color: #803f38;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-badge.valid {
|
||||||
|
background: #e4f3ef;
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-line {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 30px minmax(0, 1fr);
|
||||||
|
align-items: start;
|
||||||
|
gap: 7px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-label {
|
||||||
|
color: #7b8791;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-value {
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-line.ok .signature-value {
|
||||||
|
color: var(--accent-strong);
|
||||||
|
}
|
||||||
|
|
||||||
|
.signature-line.fail .signature-value {
|
||||||
|
color: #803f38;
|
||||||
|
}
|
||||||
|
|
||||||
.doc-font-detail {
|
.doc-font-detail {
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ const el = {
|
|||||||
metaVersion: document.querySelector("#metaVersion"),
|
metaVersion: document.querySelector("#metaVersion"),
|
||||||
metaType: document.querySelector("#metaType"),
|
metaType: document.querySelector("#metaType"),
|
||||||
metaFonts: document.querySelector("#metaFonts"),
|
metaFonts: document.querySelector("#metaFonts"),
|
||||||
|
metaSignatures: document.querySelector("#metaSignatures"),
|
||||||
|
signaturePanel: document.querySelector("#signaturePanel"),
|
||||||
|
signatureSummary: document.querySelector("#signatureSummary"),
|
||||||
|
signatureList: document.querySelector("#signatureList"),
|
||||||
docFontList: document.querySelector("#docFontList"),
|
docFontList: document.querySelector("#docFontList"),
|
||||||
docFontSummary: document.querySelector("#docFontSummary"),
|
docFontSummary: document.querySelector("#docFontSummary"),
|
||||||
availableFontSummary: document.querySelector("#availableFontSummary"),
|
availableFontSummary: document.querySelector("#availableFontSummary"),
|
||||||
@@ -1500,12 +1504,201 @@ function renderMeta() {
|
|||||||
el.metaVersion.textContent = doc.version || "-";
|
el.metaVersion.textContent = doc.version || "-";
|
||||||
el.metaType.textContent = doc.docType || "-";
|
el.metaType.textContent = doc.docType || "-";
|
||||||
el.metaFonts.textContent = String(doc.fontCount || 0);
|
el.metaFonts.textContent = String(doc.fontCount || 0);
|
||||||
|
el.metaSignatures.textContent = String(doc.signatureCount || 0);
|
||||||
el.pageTotal.textContent = String(doc.pageCount || 0);
|
el.pageTotal.textContent = String(doc.pageCount || 0);
|
||||||
|
renderSignatures();
|
||||||
renderDocumentFonts();
|
renderDocumentFonts();
|
||||||
renderFontList();
|
renderFontList();
|
||||||
updateLocalFontButton();
|
updateLocalFontButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSignatures() {
|
||||||
|
const signatures = state.doc?.signatures || [];
|
||||||
|
const showPanel = Boolean(state.doc?.signatureError || signatures.length);
|
||||||
|
el.signaturePanel.hidden = !showPanel;
|
||||||
|
el.signatureList.replaceChildren();
|
||||||
|
el.signatureSummary.textContent = signatureSummary(signatures);
|
||||||
|
if (!showPanel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.doc?.signatureError) {
|
||||||
|
const empty = document.createElement("div");
|
||||||
|
empty.className = "font-empty";
|
||||||
|
empty.textContent = state.doc.signatureError;
|
||||||
|
el.signatureList.append(empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
for (const signature of signatures) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
row.className = `signature-row ${signature.valid ? "valid" : "invalid"}`;
|
||||||
|
|
||||||
|
const head = document.createElement("div");
|
||||||
|
head.className = "signature-head";
|
||||||
|
|
||||||
|
const name = signatureNameNode(signature);
|
||||||
|
|
||||||
|
const badges = document.createElement("div");
|
||||||
|
badges.className = "signature-badges";
|
||||||
|
badges.append(fontBadge(signature.valid ? "有效" : "无效", signature.valid ? "valid" : "invalid"));
|
||||||
|
|
||||||
|
head.append(name, badges);
|
||||||
|
row.append(head);
|
||||||
|
appendSignatureLine(row, "签者", signature.signer);
|
||||||
|
appendSignatureLine(row, "时间", formatSignatureTime(signature.signatureDateTime));
|
||||||
|
appendSignatureLine(row, "机构", signatureAgency(signature));
|
||||||
|
appendSignatureLine(row, "版本", signature.version);
|
||||||
|
appendSignatureLine(row, "保护", `${signature.referencePassed || 0}/${signature.referenceCount || 0}`);
|
||||||
|
appendSignatureCheck(row, "原文", signature.dataHashOK);
|
||||||
|
appendSignatureCheck(row, "摘要", signature.digestOK);
|
||||||
|
appendSignatureCheck(row, "签名", signature.signedValueOK);
|
||||||
|
appendSignatureCheck(row, "证书", signature.certOK);
|
||||||
|
if (signature.type !== "Sign") {
|
||||||
|
appendSignatureCheck(row, "印章", signature.sealOK && signature.sealMatchOK);
|
||||||
|
appendSignatureCheck(row, "匹配", signature.sealMatchOK);
|
||||||
|
}
|
||||||
|
appendSignatureLine(row, "算法", signature.signatureMethod);
|
||||||
|
appendSignatureLine(row, "散列", signature.digestMethod);
|
||||||
|
appendSignatureLine(row, "主体", signature.signSubject && signature.signSubject !== signature.signer ? signature.signSubject : "");
|
||||||
|
appendSignatureLine(row, "颁发", signature.signIssuer);
|
||||||
|
appendSignatureLine(row, "序列", signature.signSerial);
|
||||||
|
appendSignatureLine(row, "章图", signature.sealType);
|
||||||
|
appendSignatureLine(row, "章证", signature.sealSubject);
|
||||||
|
appendSignatureLine(row, "编号", signature.id);
|
||||||
|
appendSignatureLine(row, "错误", signature.error, "fail");
|
||||||
|
fragment.append(row);
|
||||||
|
}
|
||||||
|
el.signatureList.append(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function signatureSummary(signatures) {
|
||||||
|
if (!signatures.length) {
|
||||||
|
return "0";
|
||||||
|
}
|
||||||
|
const invalid = signatures.filter((signature) => !signature.valid).length;
|
||||||
|
if (invalid) {
|
||||||
|
return `${signatures.length} · 异常 ${invalid}`;
|
||||||
|
}
|
||||||
|
return `通过 ${signatures.length}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function signatureName() {
|
||||||
|
return "签名";
|
||||||
|
}
|
||||||
|
|
||||||
|
function signatureNameNode(signature) {
|
||||||
|
const stamps = signature.stamps || [];
|
||||||
|
if (!stamps.length) {
|
||||||
|
const name = document.createElement("div");
|
||||||
|
name.className = "signature-name";
|
||||||
|
name.textContent = signatureName();
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
const name = document.createElement("div");
|
||||||
|
name.className = "signature-name signature-name-with-stamps";
|
||||||
|
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "signature-name-button";
|
||||||
|
button.textContent = signatureName();
|
||||||
|
button.addEventListener("click", () => focusSignatureStamp(stamps[0]));
|
||||||
|
name.append(button, signatureStampGroup(stamps));
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function signatureStampGroup(stamps) {
|
||||||
|
const group = document.createElement("span");
|
||||||
|
group.className = "signature-stamp-group";
|
||||||
|
group.append("(");
|
||||||
|
for (const [index, stamp] of stamps.entries()) {
|
||||||
|
if (index > 0) {
|
||||||
|
group.append("、");
|
||||||
|
}
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.type = "button";
|
||||||
|
button.className = "signature-stamp-link";
|
||||||
|
button.textContent = `第${index + 1}处`;
|
||||||
|
button.addEventListener("click", () => focusSignatureStamp(stamp));
|
||||||
|
group.append(button);
|
||||||
|
}
|
||||||
|
group.append(")");
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
|
||||||
|
function signatureAgency(signature) {
|
||||||
|
if (signature.company && signature.provider) {
|
||||||
|
return `${signature.company} · ${signature.provider}`;
|
||||||
|
}
|
||||||
|
return signature.company || signature.provider || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSignatureTime(value) {
|
||||||
|
const text = String(value || "").trim();
|
||||||
|
const digits = text.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/);
|
||||||
|
if (digits) {
|
||||||
|
return `${digits[1]}-${digits[2]}-${digits[3]} ${digits[4]}:${digits[5]}:${digits[6]}`;
|
||||||
|
}
|
||||||
|
return text.replace("T", " ").replace(/(?:Z|[+-]\d{2}:?\d{2})$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSignatureLine(row, label, value, status = "") {
|
||||||
|
if (!value && value !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const line = document.createElement("div");
|
||||||
|
line.className = status ? `signature-line ${status}` : "signature-line";
|
||||||
|
|
||||||
|
const key = document.createElement("span");
|
||||||
|
key.className = "signature-label";
|
||||||
|
key.textContent = label;
|
||||||
|
|
||||||
|
const text = document.createElement("span");
|
||||||
|
text.className = "signature-value";
|
||||||
|
text.textContent = String(value);
|
||||||
|
|
||||||
|
line.append(key, text);
|
||||||
|
row.append(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSignatureCheck(row, label, ok) {
|
||||||
|
appendSignatureLine(row, label, ok ? "通过" : "失败", ok ? "ok" : "fail");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function focusSignatureStamp(stamp) {
|
||||||
|
const pageIndex = (stamp.page || 0) - 1;
|
||||||
|
if (!state.doc || pageIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await renderPage(pageIndex);
|
||||||
|
await nextFrame();
|
||||||
|
highlightSignatureStamp(stamp);
|
||||||
|
setStatus(stamp.page ? `已定位签名外观 第 ${stamp.page} 页` : "已定位签名外观");
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlightSignatureStamp(stamp) {
|
||||||
|
clearStampHighlights();
|
||||||
|
const pageIndex = (stamp.page || 0) - 1;
|
||||||
|
const shell = pageShell(pageIndex);
|
||||||
|
if (!shell || !stamp.width || !stamp.height) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mark = document.createElement("div");
|
||||||
|
mark.className = "stamp-highlight";
|
||||||
|
mark.style.left = `${stamp.x * MM_TO_PX * state.scale}px`;
|
||||||
|
mark.style.top = `${stamp.y * MM_TO_PX * state.scale}px`;
|
||||||
|
mark.style.width = `${stamp.width * MM_TO_PX * state.scale}px`;
|
||||||
|
mark.style.height = `${stamp.height * MM_TO_PX * state.scale}px`;
|
||||||
|
shell.append(mark);
|
||||||
|
mark.scrollIntoView({ block: "center", inline: "center" });
|
||||||
|
window.setTimeout(() => mark.remove(), 1800);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStampHighlights() {
|
||||||
|
for (const mark of el.svgHost.querySelectorAll(".stamp-highlight")) {
|
||||||
|
mark.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderDocumentFonts() {
|
function renderDocumentFonts() {
|
||||||
const fonts = state.doc?.fonts || [];
|
const fonts = state.doc?.fonts || [];
|
||||||
el.docFontList.replaceChildren();
|
el.docFontList.replaceChildren();
|
||||||
@@ -1654,6 +1847,7 @@ function applyFit(updateStatus = true) {
|
|||||||
function setScale(nextScale, updateStatus = true, fitMode = "free") {
|
function setScale(nextScale, updateStatus = true, fitMode = "free") {
|
||||||
state.fitMode = fitMode;
|
state.fitMode = fitMode;
|
||||||
state.scale = Math.min(4, Math.max(0.2, nextScale));
|
state.scale = Math.min(4, Math.max(0.2, nextScale));
|
||||||
|
clearStampHighlights();
|
||||||
layoutPages();
|
layoutPages();
|
||||||
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
|
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
|
||||||
if (updateStatus && state.doc) {
|
if (updateStatus && state.doc) {
|
||||||
|
|||||||
+165
-11
@@ -39,6 +39,49 @@ type FontFile = ofdgo.FontFile
|
|||||||
// FontInfo OFD字体信息
|
// FontInfo OFD字体信息
|
||||||
type FontInfo = ofdgo.FontInfo
|
type FontInfo = ofdgo.FontInfo
|
||||||
|
|
||||||
|
// SignatureInfo 签名验证信息
|
||||||
|
type SignatureInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
Company string `json:"company,omitempty"`
|
||||||
|
Version string `json:"version,omitempty"`
|
||||||
|
Signer string `json:"signer,omitempty"`
|
||||||
|
SignSubject string `json:"signSubject,omitempty"`
|
||||||
|
SignIssuer string `json:"signIssuer,omitempty"`
|
||||||
|
SignSerial string `json:"signSerial,omitempty"`
|
||||||
|
SealSubject string `json:"sealSubject,omitempty"`
|
||||||
|
SealIssuer string `json:"sealIssuer,omitempty"`
|
||||||
|
SealSerial string `json:"sealSerial,omitempty"`
|
||||||
|
SealType string `json:"sealType,omitempty"`
|
||||||
|
SignatureMethod string `json:"signatureMethod,omitempty"`
|
||||||
|
SignatureDateTime string `json:"signatureDateTime,omitempty"`
|
||||||
|
DigestMethod string `json:"digestMethod,omitempty"`
|
||||||
|
ReferenceCount int `json:"referenceCount"`
|
||||||
|
ReferencePassed int `json:"referencePassed"`
|
||||||
|
DigestOK bool `json:"digestOK"`
|
||||||
|
DataHashOK bool `json:"dataHashOK"`
|
||||||
|
SignedValueOK bool `json:"signedValueOK"`
|
||||||
|
SealOK bool `json:"sealOK"`
|
||||||
|
SealMatchOK bool `json:"sealMatchOK"`
|
||||||
|
CertOK bool `json:"certOK"`
|
||||||
|
Stamps []SignatureStampInfo `json:"stamps,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureStampInfo 签名外观信息
|
||||||
|
type SignatureStampInfo struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Page int `json:"page,omitempty"`
|
||||||
|
PageID string `json:"pageId,omitempty"`
|
||||||
|
Boundary string `json:"boundary,omitempty"`
|
||||||
|
X float64 `json:"x,omitempty"`
|
||||||
|
Y float64 `json:"y,omitempty"`
|
||||||
|
Width float64 `json:"width,omitempty"`
|
||||||
|
Height float64 `json:"height,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Session WebUI文档会话
|
// Session WebUI文档会话
|
||||||
type Session struct {
|
type Session struct {
|
||||||
Reader *ofdgo.Reader
|
Reader *ofdgo.Reader
|
||||||
@@ -50,17 +93,20 @@ type Session struct {
|
|||||||
|
|
||||||
// DocumentInfo 文档信息
|
// DocumentInfo 文档信息
|
||||||
type DocumentInfo struct {
|
type DocumentInfo struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
DocType string `json:"docType"`
|
DocType string `json:"docType"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
Subject string `json:"subject"`
|
Subject string `json:"subject"`
|
||||||
CreationDate string `json:"creationDate"`
|
CreationDate string `json:"creationDate"`
|
||||||
ModDate string `json:"modDate"`
|
ModDate string `json:"modDate"`
|
||||||
PageCount int `json:"pageCount"`
|
PageCount int `json:"pageCount"`
|
||||||
FontCount int `json:"fontCount"`
|
FontCount int `json:"fontCount"`
|
||||||
Fonts []FontInfo `json:"fonts"`
|
SignatureCount int `json:"signatureCount"`
|
||||||
Pages []PageInfo `json:"pages"`
|
SignatureError string `json:"signatureError,omitempty"`
|
||||||
|
Fonts []FontInfo `json:"fonts"`
|
||||||
|
Signatures []SignatureInfo `json:"signatures"`
|
||||||
|
Pages []PageInfo `json:"pages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageInfo 页面信息
|
// PageInfo 页面信息
|
||||||
@@ -179,6 +225,12 @@ func (s *Session) Info() DocumentInfo {
|
|||||||
info.Fonts = fonts
|
info.Fonts = fonts
|
||||||
}
|
}
|
||||||
info.FontCount = len(info.Fonts)
|
info.FontCount = len(info.Fonts)
|
||||||
|
if signatures, err := s.signatureInfos(); err == nil {
|
||||||
|
info.Signatures = signatures
|
||||||
|
} else {
|
||||||
|
info.SignatureError = err.Error()
|
||||||
|
}
|
||||||
|
info.SignatureCount = len(info.Signatures)
|
||||||
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
|
if docInfo, err := s.Reader.DocInfo(); err == nil && docInfo != nil {
|
||||||
info.Title = docInfo.Title
|
info.Title = docInfo.Title
|
||||||
info.Author = docInfo.Author
|
info.Author = docInfo.Author
|
||||||
@@ -257,6 +309,108 @@ func (s *Session) ExportPage(index int, value string) ([]byte, ExportFormat, err
|
|||||||
return buf.Bytes(), format, nil
|
return buf.Bytes(), format, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// signatureInfos 获取签名验证信息
|
||||||
|
// 返回: []SignatureInfo 签名验证信息, error 错误信息
|
||||||
|
func (s *Session) signatureInfos() ([]SignatureInfo, error) {
|
||||||
|
reports, err := s.Reader.VerifySignatures()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
infos := make([]SignatureInfo, 0, len(reports))
|
||||||
|
for _, report := range reports {
|
||||||
|
positions, err := s.Reader.SignatureStampPositions(report.Stamps)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
infos = append(infos, signatureInfo(report, positions))
|
||||||
|
}
|
||||||
|
return infos, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// signatureInfo 转换签名验证信息
|
||||||
|
// 入参: report 签名验证报告, positions 签名外观位置
|
||||||
|
// 返回: SignatureInfo 签名验证信息
|
||||||
|
func signatureInfo(report ofdgo.SignatureVerifyReport, positions []ofdgo.SignatureStampPosition) SignatureInfo {
|
||||||
|
return SignatureInfo{
|
||||||
|
ID: report.ID,
|
||||||
|
Type: string(report.Type),
|
||||||
|
Valid: report.Valid,
|
||||||
|
Error: report.Error,
|
||||||
|
Provider: report.Provider.ProviderName,
|
||||||
|
Company: report.Provider.Company,
|
||||||
|
Version: report.Provider.Version,
|
||||||
|
Signer: signatureSigner(report),
|
||||||
|
SignSubject: report.SignCert.Subject,
|
||||||
|
SignIssuer: report.SignCert.Issuer,
|
||||||
|
SignSerial: report.SignCert.SerialNumber,
|
||||||
|
SealSubject: report.SealCert.Subject,
|
||||||
|
SealIssuer: report.SealCert.Issuer,
|
||||||
|
SealSerial: report.SealCert.SerialNumber,
|
||||||
|
SealType: report.SealType,
|
||||||
|
SignatureMethod: report.SignatureMethod,
|
||||||
|
SignatureDateTime: report.SignatureDateTime,
|
||||||
|
DigestMethod: report.DigestMethod,
|
||||||
|
ReferenceCount: len(report.References),
|
||||||
|
ReferencePassed: signatureReferencePassed(report.References),
|
||||||
|
DigestOK: report.DigestOK,
|
||||||
|
DataHashOK: report.DataHashOK,
|
||||||
|
SignedValueOK: report.SignedValueOK,
|
||||||
|
SealOK: report.SealOK,
|
||||||
|
SealMatchOK: report.SealMatchOK,
|
||||||
|
CertOK: report.CertOK,
|
||||||
|
Stamps: signatureStampInfos(positions),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// signatureSigner 获取签名人名称
|
||||||
|
// 入参: report 签名验证报告
|
||||||
|
// 返回: string 签名人名称
|
||||||
|
func signatureSigner(report ofdgo.SignatureVerifyReport) string {
|
||||||
|
if report.Signer != "" {
|
||||||
|
return report.Signer
|
||||||
|
}
|
||||||
|
if report.SignCert.CommonName != "" {
|
||||||
|
return report.SignCert.CommonName
|
||||||
|
}
|
||||||
|
if report.SignCert.Organization != "" {
|
||||||
|
return report.SignCert.Organization
|
||||||
|
}
|
||||||
|
return report.SignCert.Subject
|
||||||
|
}
|
||||||
|
|
||||||
|
// signatureReferencePassed 获取已通过保护文件数量
|
||||||
|
// 入参: refs 保护文件验证结果
|
||||||
|
// 返回: int 已通过保护文件数量
|
||||||
|
func signatureReferencePassed(refs []ofdgo.SignatureReferenceVerify) int {
|
||||||
|
count := 0
|
||||||
|
for _, ref := range refs {
|
||||||
|
if ref.OK {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// signatureStampInfos 转换签名外观位置
|
||||||
|
// 入参: positions 签名外观位置
|
||||||
|
// 返回: []SignatureStampInfo 签名外观信息
|
||||||
|
func signatureStampInfos(positions []ofdgo.SignatureStampPosition) []SignatureStampInfo {
|
||||||
|
infos := make([]SignatureStampInfo, 0, len(positions))
|
||||||
|
for _, position := range positions {
|
||||||
|
infos = append(infos, SignatureStampInfo{
|
||||||
|
ID: position.ID,
|
||||||
|
Page: position.Page,
|
||||||
|
PageID: position.PageID,
|
||||||
|
Boundary: position.Boundary,
|
||||||
|
X: position.Box.X,
|
||||||
|
Y: position.Box.Y,
|
||||||
|
Width: position.Box.W,
|
||||||
|
Height: position.Box.H,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return infos
|
||||||
|
}
|
||||||
|
|
||||||
// ExportPDF 导出文档为PDF
|
// ExportPDF 导出文档为PDF
|
||||||
// 返回: []byte PDF文件数据, error 错误信息
|
// 返回: []byte PDF文件数据, error 错误信息
|
||||||
func (s *Session) ExportPDF() ([]byte, error) {
|
func (s *Session) ExportPDF() ([]byte, error) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func Open(path string) (*Reader, error) {
|
|||||||
return reader, nil
|
return reader, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReader 从流创建一个 OFD 阅读器
|
// NewReader 从IO读取器创建OFD阅读器
|
||||||
// 入参: r IO读取器, size 数据大小
|
// 入参: r IO读取器, size 数据大小
|
||||||
// 返回: *Reader 阅读器实例, error 错误信息
|
// 返回: *Reader 阅读器实例, error 错误信息
|
||||||
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
|
func NewReader(r io.ReaderAt, size int64) (*Reader, error) {
|
||||||
|
|||||||
+96
-13
@@ -22,11 +22,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SignType 签名类型
|
||||||
type SignType string
|
type SignType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SignTypeSeal = "Seal"
|
SignTypeSeal SignType = "Seal"
|
||||||
SignTypeSign = "Sign"
|
SignTypeSign SignType = "Sign"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Signatures 签名列表
|
// Signatures 签名列表
|
||||||
@@ -47,16 +48,81 @@ type Signature struct {
|
|||||||
type SignatureFile struct {
|
type SignatureFile struct {
|
||||||
XMLName xml.Name `xml:"Signature"`
|
XMLName xml.Name `xml:"Signature"`
|
||||||
SignedValue string `xml:"SignedValue"`
|
SignedValue string `xml:"SignedValue"`
|
||||||
SignedInfo struct {
|
SignedInfo SignedInfo
|
||||||
Seal struct {
|
}
|
||||||
BaseLoc string `xml:"BaseLoc"`
|
|
||||||
} `xml:"Seal"`
|
// SignedInfo 签名信息
|
||||||
StampAnnot []struct {
|
type SignedInfo struct {
|
||||||
ID string `xml:"ID,attr"`
|
Provider SignatureProvider `xml:"Provider"`
|
||||||
PageRef string `xml:"PageRef,attr"`
|
SignatureMethod string `xml:"SignatureMethod"`
|
||||||
Boundary string `xml:"Boundary,attr"`
|
SignatureDateTime string `xml:"SignatureDateTime"`
|
||||||
} `xml:"StampAnnot"`
|
Seal SignatureSeal `xml:"Seal"`
|
||||||
} `xml:"SignedInfo"`
|
StampAnnot []SignatureStamp `xml:"StampAnnot"`
|
||||||
|
References SignatureReferences `xml:"References"`
|
||||||
|
Raw []byte `xml:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureProvider 签名提供者信息
|
||||||
|
type SignatureProvider struct {
|
||||||
|
ProviderName string `xml:"ProviderName,attr"`
|
||||||
|
Company string `xml:"Company,attr"`
|
||||||
|
Version string `xml:"Version,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureSeal 签名印章引用
|
||||||
|
type SignatureSeal struct {
|
||||||
|
BaseLoc string `xml:"BaseLoc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureStamp 签名印章注释
|
||||||
|
type SignatureStamp struct {
|
||||||
|
ID string `xml:"ID,attr"`
|
||||||
|
PageRef string `xml:"PageRef,attr"`
|
||||||
|
Boundary string `xml:"Boundary,attr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureStampPosition 签名外观位置信息
|
||||||
|
type SignatureStampPosition struct {
|
||||||
|
ID string
|
||||||
|
Page int
|
||||||
|
PageID string
|
||||||
|
Boundary string
|
||||||
|
Box Box
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureReferences 签名保护文件列表
|
||||||
|
type SignatureReferences struct {
|
||||||
|
CheckMethod string `xml:"CheckMethod,attr"`
|
||||||
|
Reference []SignatureReference `xml:"Reference"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureReference 签名保护文件引用
|
||||||
|
type SignatureReference struct {
|
||||||
|
FileRef string `xml:"FileRef,attr"`
|
||||||
|
CheckValue string `xml:"CheckValue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureStampPositions 获取签名外观位置信息
|
||||||
|
// 入参: stamps 签名外观列表
|
||||||
|
// 返回: []SignatureStampPosition 签名外观位置信息, error 错误信息
|
||||||
|
func (r *Reader) SignatureStampPositions(stamps []SignatureStamp) ([]SignatureStampPosition, error) {
|
||||||
|
doc, err := r.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pages := signaturePageNumbers(doc)
|
||||||
|
positions := make([]SignatureStampPosition, 0, len(stamps))
|
||||||
|
for _, stamp := range stamps {
|
||||||
|
box, _ := ParseBox(stamp.Boundary)
|
||||||
|
positions = append(positions, SignatureStampPosition{
|
||||||
|
ID: stamp.ID,
|
||||||
|
Page: pages[stamp.PageRef],
|
||||||
|
PageID: stamp.PageRef,
|
||||||
|
Boundary: stamp.Boundary,
|
||||||
|
Box: box,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return positions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseSignatures 解析签名文件
|
// parseSignatures 解析签名文件
|
||||||
@@ -99,7 +165,11 @@ func (r *Reader) parseSignatures(doc *Document) error {
|
|||||||
if len(sealData) == 0 && sigFile.SignedValue != "" {
|
if len(sealData) == 0 && sigFile.SignedValue != "" {
|
||||||
signedValuePath := resolveResourcePath(sigPath, "", sigFile.SignedValue)
|
signedValuePath := resolveResourcePath(sigPath, "", sigFile.SignedValue)
|
||||||
if data, err := r.ResData(signedValuePath); err == nil {
|
if data, err := r.ResData(signedValuePath); err == nil {
|
||||||
sealType, sealData = extractSeal(data)
|
if sig, err := parseSESSignature(data); err == nil {
|
||||||
|
sealType, sealData = sig.Seal.PicType, sig.Seal.PicData
|
||||||
|
} else {
|
||||||
|
sealType, sealData = extractSeal(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(sealData) == 0 {
|
if len(sealData) == 0 {
|
||||||
@@ -115,6 +185,19 @@ func (r *Reader) parseSignatures(doc *Document) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// signaturePageNumbers 获取签名页面编号索引
|
||||||
|
// 入参: doc 文档结构
|
||||||
|
// 返回: map[string]int 页面编号索引
|
||||||
|
func signaturePageNumbers(doc *Document) map[string]int {
|
||||||
|
pages := make(map[string]int, len(doc.Pages.Page))
|
||||||
|
for index, page := range doc.Pages.Page {
|
||||||
|
if page.ID != "" {
|
||||||
|
pages[page.ID] = index + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pages
|
||||||
|
}
|
||||||
|
|
||||||
// extractSeal 尝试提取印章数据
|
// extractSeal 尝试提取印章数据
|
||||||
// 入参: data 签名值数据
|
// 入参: data 签名值数据
|
||||||
// 返回: string 印章类型, []byte 印章数据
|
// 返回: string 印章类型, []byte 印章数据
|
||||||
|
|||||||
@@ -0,0 +1,477 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
signContentData = "1.2.156.10197.6.1.4.2.1"
|
||||||
|
signContentSignedData = "1.2.156.10197.6.1.4.2.2"
|
||||||
|
signAttrMessageDigest = "1.2.840.113549.1.9.4"
|
||||||
|
)
|
||||||
|
|
||||||
|
// digitalVerifyResult 数字签名验证结果
|
||||||
|
type digitalVerifyResult struct {
|
||||||
|
DataHashOK bool
|
||||||
|
SignedOK bool
|
||||||
|
CertOK bool
|
||||||
|
CertInfo SignatureCertInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// gbtSignedData GB/T 35275 SignedData结构
|
||||||
|
type gbtSignedData struct {
|
||||||
|
ContentDigest []byte
|
||||||
|
Certs []gbtCertificate
|
||||||
|
Signers []gbtSignerInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// gbtCertificate SignedData证书索引信息
|
||||||
|
type gbtCertificate struct {
|
||||||
|
Raw []byte
|
||||||
|
Issuer []byte
|
||||||
|
Serial *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// gbtSignerInfo SignedData签名者信息
|
||||||
|
type gbtSignerInfo struct {
|
||||||
|
Issuer []byte
|
||||||
|
Serial *big.Int
|
||||||
|
DigestAlg string
|
||||||
|
SignatureAlg string
|
||||||
|
Signature []byte
|
||||||
|
AuthAttrs []byte
|
||||||
|
AttrDigest []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyDigitalSignature 验证OFD数字签名
|
||||||
|
// 入参: method 签名算法, signedValue 签名值, signedData 被签名原文, options 验证选项
|
||||||
|
// 返回: *digitalVerifyResult 验证结果, error 错误信息
|
||||||
|
func verifyDigitalSignature(method string, signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
|
||||||
|
if !isSM2SignatureMethod(method) {
|
||||||
|
return nil, fmt.Errorf("unsupported signature method")
|
||||||
|
}
|
||||||
|
if isGBT35275SignedValue(signedValue) {
|
||||||
|
return verifyGBT35275SignedData(signedValue, signedData, options)
|
||||||
|
}
|
||||||
|
return verifyRawDigitalSignature(signedValue, signedData, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyRawDigitalSignature 验证裸SM2数字签名
|
||||||
|
// 入参: signedValue 签名值, signedData 被签名原文, options 验证选项
|
||||||
|
// 返回: *digitalVerifyResult 验证结果, error 错误信息
|
||||||
|
func verifyRawDigitalSignature(signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
|
||||||
|
if len(options.SignCerts) == 0 {
|
||||||
|
return nil, fmt.Errorf("signature certificate not found")
|
||||||
|
}
|
||||||
|
result := &digitalVerifyResult{DataHashOK: true}
|
||||||
|
for _, cert := range options.SignCerts {
|
||||||
|
pub, err := parseSM2PublicKeyFromCert(cert)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.CertOK = true
|
||||||
|
result.CertInfo = signatureCertInfo(cert)
|
||||||
|
if sm2VerifySignature(pub, nil, signedData, signedValue) {
|
||||||
|
result.SignedOK = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyGBT35275SignedData 验证GB/T 35275 SignedData签名值
|
||||||
|
// 入参: signedValue 签名值, signedData 被签名原文, options 验证选项
|
||||||
|
// 返回: *digitalVerifyResult 验证结果, error 错误信息
|
||||||
|
func verifyGBT35275SignedData(signedValue, signedData []byte, options *signatureVerifyOptions) (*digitalVerifyResult, error) {
|
||||||
|
sd, err := parseGBT35275SignedData(signedValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, cert := range options.SignCerts {
|
||||||
|
c, err := parseGBTCertificate(cert)
|
||||||
|
if err == nil {
|
||||||
|
sd.Certs = append(sd.Certs, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(sd.Signers) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid signed data signer info")
|
||||||
|
}
|
||||||
|
result := &digitalVerifyResult{}
|
||||||
|
digest := signSM3(signedData)
|
||||||
|
if len(sd.ContentDigest) != 0 && !bytes.Equal(sd.ContentDigest, digest) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
result.DataHashOK = true
|
||||||
|
for _, signer := range sd.Signers {
|
||||||
|
if !isSM3DigestMethod(signer.DigestAlg) {
|
||||||
|
return nil, fmt.Errorf("unsupported digest method")
|
||||||
|
}
|
||||||
|
plain := sd.ContentDigest
|
||||||
|
if len(signer.AuthAttrs) != 0 {
|
||||||
|
if !bytes.Equal(signer.AttrDigest, digest) {
|
||||||
|
result.DataHashOK = false
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
plain = signer.AuthAttrs
|
||||||
|
}
|
||||||
|
if len(plain) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid signed data content")
|
||||||
|
}
|
||||||
|
cert := sd.findCert(signer.Issuer, signer.Serial)
|
||||||
|
if cert == nil {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
result.CertInfo = signatureCertInfo(cert.Raw)
|
||||||
|
if !isSM2SignatureMethod(signer.SignatureAlg) {
|
||||||
|
return nil, fmt.Errorf("unsupported signature method")
|
||||||
|
}
|
||||||
|
pub, err := parseSM2PublicKeyFromCert(cert.Raw)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
result.CertOK = true
|
||||||
|
if !sm2VerifySignature(pub, nil, plain, signer.Signature) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.SignedOK = true
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isGBT35275SignedValue 判断签名值是否为GB/T 35275 SignedData
|
||||||
|
// 入参: data 签名值数据
|
||||||
|
// 返回: bool 是否为SignedData
|
||||||
|
func isGBT35275SignedValue(data []byte) bool {
|
||||||
|
var root asn1.RawValue
|
||||||
|
rest, err := asn1.Unmarshal(data, &root)
|
||||||
|
if err != nil || len(rest) != 0 || root.Tag != signASN1Sequence {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
items, ok := asn1Children(root.Bytes)
|
||||||
|
if !ok || len(items) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
oid, err := asn1OIDString(items[0])
|
||||||
|
return err == nil && oid == signContentSignedData
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBT35275SignedData 解析GB/T 35275 SignedData
|
||||||
|
// 入参: data 签名值数据
|
||||||
|
// 返回: *gbtSignedData SignedData结构, error 错误信息
|
||||||
|
func parseGBT35275SignedData(data []byte) (*gbtSignedData, error) {
|
||||||
|
contentType, content, ok, err := parseGBTContentInfoBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !ok || contentType != signContentSignedData {
|
||||||
|
return nil, fmt.Errorf("invalid signed data content type")
|
||||||
|
}
|
||||||
|
items, ok := asn1Children(content.Bytes)
|
||||||
|
if !ok || len(items) < 4 {
|
||||||
|
return nil, fmt.Errorf("invalid signed data")
|
||||||
|
}
|
||||||
|
sd := &gbtSignedData{}
|
||||||
|
contentOID, content, hasContent, err := parseGBTContentInfo(items[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if hasContent {
|
||||||
|
if contentOID != signContentData {
|
||||||
|
return nil, fmt.Errorf("invalid signed data inner content type")
|
||||||
|
}
|
||||||
|
if content.Tag == asn1.TagOctetString {
|
||||||
|
sd.ContentDigest, err = asn1OctetString(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 3; i < len(items); i++ {
|
||||||
|
item := items[i]
|
||||||
|
if item.Class == asn1.ClassContextSpecific && item.Tag == 0 {
|
||||||
|
certs, err := parseGBTCertificates(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sd.Certs = certs
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.Class == asn1.ClassContextSpecific && item.Tag == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
signers, err := parseGBTSignerInfos(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sd.Signers = signers
|
||||||
|
}
|
||||||
|
return sd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBTContentInfoBytes 解析ContentInfo字节
|
||||||
|
// 入参: data DER编码数据
|
||||||
|
// 返回: string 内容类型, asn1.RawValue 内容, bool 是否存在内容, error 错误信息
|
||||||
|
func parseGBTContentInfoBytes(data []byte) (string, asn1.RawValue, bool, error) {
|
||||||
|
var raw asn1.RawValue
|
||||||
|
rest, err := asn1.Unmarshal(data, &raw)
|
||||||
|
if err != nil || len(rest) != 0 {
|
||||||
|
return "", asn1.RawValue{}, false, fmt.Errorf("invalid content info")
|
||||||
|
}
|
||||||
|
return parseGBTContentInfo(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBTContentInfo 解析ContentInfo结构
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: string 内容类型, asn1.RawValue 内容, bool 是否存在内容, error 错误信息
|
||||||
|
func parseGBTContentInfo(raw asn1.RawValue) (string, asn1.RawValue, bool, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(items) == 0 {
|
||||||
|
return "", asn1.RawValue{}, false, fmt.Errorf("invalid content info")
|
||||||
|
}
|
||||||
|
oid, err := asn1OIDString(items[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", asn1.RawValue{}, false, err
|
||||||
|
}
|
||||||
|
if len(items) == 1 {
|
||||||
|
return oid, asn1.RawValue{}, false, nil
|
||||||
|
}
|
||||||
|
content, err := asn1Explicit(items[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", asn1.RawValue{}, false, err
|
||||||
|
}
|
||||||
|
return oid, content, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBTCertificates 解析SignedData证书集合
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: []gbtCertificate 证书列表, error 错误信息
|
||||||
|
func parseGBTCertificates(raw asn1.RawValue) ([]gbtCertificate, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid signed data certificates")
|
||||||
|
}
|
||||||
|
certs := make([]gbtCertificate, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
cert, err := parseGBTCertificate(item.FullBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
certs = append(certs, cert)
|
||||||
|
}
|
||||||
|
return certs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBTCertificate 解析X.509证书索引字段
|
||||||
|
// 入参: data DER编码证书
|
||||||
|
// 返回: gbtCertificate 证书索引信息, error 错误信息
|
||||||
|
func parseGBTCertificate(data []byte) (gbtCertificate, 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 gbtCertificate{}, fmt.Errorf("invalid certificate")
|
||||||
|
}
|
||||||
|
items, ok := asn1Children(cert.TBSCertificate.Bytes)
|
||||||
|
if !ok {
|
||||||
|
return gbtCertificate{}, 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+2 {
|
||||||
|
return gbtCertificate{}, fmt.Errorf("invalid certificate issuer")
|
||||||
|
}
|
||||||
|
serial, err := asn1IntegerBig(items[idx])
|
||||||
|
if err != nil {
|
||||||
|
return gbtCertificate{}, err
|
||||||
|
}
|
||||||
|
return gbtCertificate{
|
||||||
|
Raw: append([]byte(nil), data...),
|
||||||
|
Issuer: append([]byte(nil), items[idx+2].FullBytes...),
|
||||||
|
Serial: serial,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBTSignerInfos 解析签名者信息集合
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: []gbtSignerInfo 签名者列表, error 错误信息
|
||||||
|
func parseGBTSignerInfos(raw asn1.RawValue) ([]gbtSignerInfo, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid signer infos")
|
||||||
|
}
|
||||||
|
signers := make([]gbtSignerInfo, 0, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
signer, err := parseGBTSignerInfo(item)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signers = append(signers, signer)
|
||||||
|
}
|
||||||
|
return signers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBTSignerInfo 解析签名者信息
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: gbtSignerInfo 签名者信息, error 错误信息
|
||||||
|
func parseGBTSignerInfo(raw asn1.RawValue) (gbtSignerInfo, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(items) < 5 {
|
||||||
|
return gbtSignerInfo{}, fmt.Errorf("invalid signer info")
|
||||||
|
}
|
||||||
|
issuer, serial, err := parseGBTIssuerAndSerial(items[1])
|
||||||
|
if err != nil {
|
||||||
|
return gbtSignerInfo{}, err
|
||||||
|
}
|
||||||
|
digestAlg, err := parseGBTAlgorithm(items[2])
|
||||||
|
if err != nil {
|
||||||
|
return gbtSignerInfo{}, err
|
||||||
|
}
|
||||||
|
idx := 3
|
||||||
|
var authAttrs []byte
|
||||||
|
var attrDigest []byte
|
||||||
|
if items[idx].Class == asn1.ClassContextSpecific && items[idx].Tag == 0 {
|
||||||
|
authAttrs = asn1SetBytes(items[idx].Bytes)
|
||||||
|
attrDigest, err = parseGBTMessageDigestAttr(items[idx])
|
||||||
|
if err != nil {
|
||||||
|
return gbtSignerInfo{}, err
|
||||||
|
}
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
if len(items) <= idx+1 {
|
||||||
|
return gbtSignerInfo{}, fmt.Errorf("invalid signer info")
|
||||||
|
}
|
||||||
|
signatureAlg, err := parseGBTAlgorithm(items[idx])
|
||||||
|
if err != nil {
|
||||||
|
return gbtSignerInfo{}, err
|
||||||
|
}
|
||||||
|
signature, err := asn1OctetString(items[idx+1])
|
||||||
|
if err != nil {
|
||||||
|
return gbtSignerInfo{}, err
|
||||||
|
}
|
||||||
|
return gbtSignerInfo{
|
||||||
|
Issuer: issuer,
|
||||||
|
Serial: serial,
|
||||||
|
DigestAlg: digestAlg,
|
||||||
|
SignatureAlg: signatureAlg,
|
||||||
|
Signature: signature,
|
||||||
|
AuthAttrs: authAttrs,
|
||||||
|
AttrDigest: attrDigest,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBTIssuerAndSerial 解析证书颁发者和序列号
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: []byte 颁发者DN, *big.Int 序列号, error 错误信息
|
||||||
|
func parseGBTIssuerAndSerial(raw asn1.RawValue) ([]byte, *big.Int, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(items) < 2 {
|
||||||
|
return nil, nil, fmt.Errorf("invalid issuer and serial")
|
||||||
|
}
|
||||||
|
serial, err := asn1IntegerBig(items[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return append([]byte(nil), items[0].FullBytes...), serial, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBTAlgorithm 解析算法标识
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: string 算法OID, error 错误信息
|
||||||
|
func parseGBTAlgorithm(raw asn1.RawValue) (string, error) {
|
||||||
|
if raw.Tag == asn1.TagOID {
|
||||||
|
return asn1OIDString(raw)
|
||||||
|
}
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(items) == 0 {
|
||||||
|
return "", fmt.Errorf("invalid algorithm identifier")
|
||||||
|
}
|
||||||
|
return asn1OIDString(items[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseGBTMessageDigestAttr 解析认证属性中的message-digest
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: []byte 摘要值, error 错误信息
|
||||||
|
func parseGBTMessageDigestAttr(raw asn1.RawValue) ([]byte, error) {
|
||||||
|
attrs, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid authenticated attributes")
|
||||||
|
}
|
||||||
|
for _, attr := range attrs {
|
||||||
|
items, ok := asn1Children(attr.Bytes)
|
||||||
|
if !ok || len(items) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
oid, err := asn1OIDString(items[0])
|
||||||
|
if err != nil || oid != signAttrMessageDigest {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values, ok := asn1Children(items[1].Bytes)
|
||||||
|
if !ok || len(values) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid message digest attribute")
|
||||||
|
}
|
||||||
|
return asn1OctetString(values[0])
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("message digest attribute not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// findCert 查找签名者证书
|
||||||
|
// 入参: issuer 颁发者DN, serial 证书序列号
|
||||||
|
// 返回: *gbtCertificate 证书信息
|
||||||
|
func (sd *gbtSignedData) findCert(issuer []byte, serial *big.Int) *gbtCertificate {
|
||||||
|
for i := range sd.Certs {
|
||||||
|
cert := &sd.Certs[i]
|
||||||
|
if bytes.Equal(cert.Issuer, issuer) && cert.Serial.Cmp(serial) == 0 {
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1Explicit 解析显式标签内容
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: asn1.RawValue 标签内容, error 错误信息
|
||||||
|
func asn1Explicit(raw asn1.RawValue) (asn1.RawValue, error) {
|
||||||
|
if raw.Class != asn1.ClassContextSpecific || raw.Tag != 0 || !raw.IsCompound {
|
||||||
|
return asn1.RawValue{}, fmt.Errorf("invalid explicit content")
|
||||||
|
}
|
||||||
|
var out asn1.RawValue
|
||||||
|
rest, err := asn1.Unmarshal(raw.Bytes, &out)
|
||||||
|
if err != nil || len(rest) != 0 {
|
||||||
|
return asn1.RawValue{}, fmt.Errorf("invalid explicit content")
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1IntegerBig 解析ASN.1整数
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: *big.Int 整数值, error 错误信息
|
||||||
|
func asn1IntegerBig(raw asn1.RawValue) (*big.Int, error) {
|
||||||
|
var out *big.Int
|
||||||
|
rest, err := asn1.Unmarshal(raw.FullBytes, &out)
|
||||||
|
if err != nil || len(rest) != 0 || out == nil {
|
||||||
|
return nil, fmt.Errorf("invalid integer")
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,643 @@
|
|||||||
|
// 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"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
signDigestSM3 = "1.2.156.10197.1.401"
|
||||||
|
signDigestSM3NoKey = "1.2.156.10197.1.401.1"
|
||||||
|
signDigestSM3Key = "1.2.156.10197.1.401.2"
|
||||||
|
signMethodSM2SM3 = "1.2.156.10197.1.501"
|
||||||
|
signMethodSM2SM3B = "1.2.156.10197.501"
|
||||||
|
signMethodSM2Sign = "1.2.156.10197.1.301.1"
|
||||||
|
signCurveSM2P256 = "1.2.156.10197.1.301"
|
||||||
|
signECPublicKey = "1.2.840.10045.2.1"
|
||||||
|
signASN1Sequence = 16
|
||||||
|
signPublicKeySize = 65
|
||||||
|
)
|
||||||
|
|
||||||
|
// sesSignature SES签章值
|
||||||
|
type sesSignature struct {
|
||||||
|
ToSign []byte
|
||||||
|
Cert []byte
|
||||||
|
SignAlg string
|
||||||
|
Signature []byte
|
||||||
|
DataHash []byte
|
||||||
|
Seal *sesSeal
|
||||||
|
}
|
||||||
|
|
||||||
|
// sesSeal SES电子印章
|
||||||
|
type sesSeal struct {
|
||||||
|
Raw []byte
|
||||||
|
SignData []byte
|
||||||
|
Cert []byte
|
||||||
|
SignAlg string
|
||||||
|
Signature []byte
|
||||||
|
PicType string
|
||||||
|
PicData []byte
|
||||||
|
CertList sesCertList
|
||||||
|
}
|
||||||
|
|
||||||
|
// sesCertList SES印章证书列表
|
||||||
|
type sesCertList struct {
|
||||||
|
Certs [][]byte
|
||||||
|
Digests [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// sesVerifyResult SES签章验证结果
|
||||||
|
type sesVerifyResult struct {
|
||||||
|
DataHashOK bool
|
||||||
|
SignedOK bool
|
||||||
|
SealOK bool
|
||||||
|
CertOK bool
|
||||||
|
SignCert SignatureCertInfo
|
||||||
|
SealCert SignatureCertInfo
|
||||||
|
SealType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESSignature 解析SES签章值
|
||||||
|
// 入参: data 签章值数据
|
||||||
|
// 返回: *sesSignature SES签章值, error 错误信息
|
||||||
|
func parseSESSignature(data []byte) (*sesSignature, error) {
|
||||||
|
var root asn1.RawValue
|
||||||
|
rest, err := asn1.Unmarshal(data, &root)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rest) != 0 || root.Tag != signASN1Sequence || !root.IsCompound {
|
||||||
|
return nil, fmt.Errorf("invalid ses signature")
|
||||||
|
}
|
||||||
|
items, ok := asn1Children(root.Bytes)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid ses signature items")
|
||||||
|
}
|
||||||
|
if len(items) == 2 {
|
||||||
|
return parseSESSignatureV1(items)
|
||||||
|
}
|
||||||
|
if len(items) < 4 || len(items) > 5 {
|
||||||
|
return nil, fmt.Errorf("invalid ses signature items")
|
||||||
|
}
|
||||||
|
cert, err := asn1OctetString(items[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
alg, err := asn1OIDString(items[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signature, err := asn1BitStringBytes(items[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tbsItems, ok := asn1Children(items[0].Bytes)
|
||||||
|
if !ok || len(tbsItems) < 5 {
|
||||||
|
return nil, fmt.Errorf("invalid ses toSign")
|
||||||
|
}
|
||||||
|
seal, err := parseSESSeal(tbsItems[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sesSignature{
|
||||||
|
ToSign: append([]byte(nil), items[0].FullBytes...),
|
||||||
|
Cert: cert,
|
||||||
|
SignAlg: alg,
|
||||||
|
Signature: signature,
|
||||||
|
DataHash: dataHash,
|
||||||
|
Seal: seal,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESSignatureV1 解析SES V1签章值
|
||||||
|
// 入参: items ASN.1子元素
|
||||||
|
// 返回: *sesSignature SES签章值, error 错误信息
|
||||||
|
func parseSESSignatureV1(items []asn1.RawValue) (*sesSignature, error) {
|
||||||
|
signature, err := asn1BitStringBytes(items[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tbsItems, ok := asn1Children(items[0].Bytes)
|
||||||
|
if !ok || len(tbsItems) != 7 {
|
||||||
|
return nil, fmt.Errorf("invalid ses v1 toSign")
|
||||||
|
}
|
||||||
|
seal, err := parseSESSeal(tbsItems[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dataHash, err := asn1BitOrOctetBytes(tbsItems[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cert, err := asn1OctetString(tbsItems[5])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
alg, err := asn1OIDString(tbsItems[6])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sesSignature{
|
||||||
|
ToSign: append([]byte(nil), items[0].FullBytes...),
|
||||||
|
Cert: cert,
|
||||||
|
SignAlg: alg,
|
||||||
|
Signature: signature,
|
||||||
|
DataHash: dataHash,
|
||||||
|
Seal: seal,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifySESSignature 验证SES签章值
|
||||||
|
// 入参: data 签章值数据, signedData 被签名数据原文
|
||||||
|
// 返回: *sesVerifyResult 验证结果, error 错误信息
|
||||||
|
func verifySESSignature(data, signedData []byte) (*sesVerifyResult, error) {
|
||||||
|
sig, err := parseSESSignature(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !isSM2SignatureMethod(sig.SignAlg) || !isSM2SignatureMethod(sig.Seal.SignAlg) {
|
||||||
|
return nil, fmt.Errorf("unsupported signature method")
|
||||||
|
}
|
||||||
|
result := &sesVerifyResult{}
|
||||||
|
result.SignCert = signatureCertInfo(sig.Cert)
|
||||||
|
result.SealCert = signatureCertInfo(sig.Seal.Cert)
|
||||||
|
result.SealType = sig.Seal.PicType
|
||||||
|
result.DataHashOK = bytes.Equal(sig.DataHash, signSM3(signedData))
|
||||||
|
signPub, err := parseSM2PublicKeyFromCert(sig.Cert)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
sealPub, err := parseSM2PublicKeyFromCert(sig.Seal.Cert)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
result.SignedOK = sm2VerifySignature(signPub, nil, sig.ToSign, sig.Signature)
|
||||||
|
result.SealOK = sm2VerifySignature(sealPub, nil, sig.Seal.SignData, sig.Seal.Signature)
|
||||||
|
result.CertOK = sesCertInList(sig.Cert, sig.Seal.CertList)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESSeal 解析SES电子印章
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: *sesSeal SES电子印章, error 错误信息
|
||||||
|
func parseSESSeal(raw asn1.RawValue) (*sesSeal, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("invalid ses seal")
|
||||||
|
}
|
||||||
|
if len(items) == 2 {
|
||||||
|
return parseSESSealV1(raw, items)
|
||||||
|
}
|
||||||
|
if len(items) < 4 || len(items) > 5 {
|
||||||
|
return nil, fmt.Errorf("invalid ses seal")
|
||||||
|
}
|
||||||
|
cert, err := asn1OctetString(items[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
alg, err := asn1OIDString(items[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signature, err := asn1BitStringBytes(items[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
infoItems, ok := asn1Children(items[0].Bytes)
|
||||||
|
if !ok || len(infoItems) < 4 || len(infoItems) > 5 {
|
||||||
|
return nil, fmt.Errorf("invalid ses seal info")
|
||||||
|
}
|
||||||
|
version, err := parseSESHeaderVersion(infoItems[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
certList, err := parseSESCertList(infoItems[2], version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
picType, picData, err := parseSESPicture(infoItems[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &sesSeal{
|
||||||
|
Raw: append([]byte(nil), raw.FullBytes...),
|
||||||
|
SignData: append([]byte(nil), items[0].FullBytes...),
|
||||||
|
Cert: cert,
|
||||||
|
SignAlg: alg,
|
||||||
|
Signature: signature,
|
||||||
|
PicType: picType,
|
||||||
|
PicData: picData,
|
||||||
|
CertList: certList,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESSealV1 解析SES V1电子印章
|
||||||
|
// 入参: raw ASN.1原始值, items ASN.1子元素
|
||||||
|
// 返回: *sesSeal SES电子印章, error 错误信息
|
||||||
|
func parseSESSealV1(raw asn1.RawValue, items []asn1.RawValue) (*sesSeal, error) {
|
||||||
|
infoItems, ok := asn1Children(items[0].Bytes)
|
||||||
|
if !ok || len(infoItems) < 4 || len(infoItems) > 5 {
|
||||||
|
return nil, fmt.Errorf("invalid ses v1 seal info")
|
||||||
|
}
|
||||||
|
signItems, ok := asn1Children(items[1].Bytes)
|
||||||
|
if !ok || len(signItems) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid ses v1 sign info")
|
||||||
|
}
|
||||||
|
cert, err := asn1OctetString(signItems[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
alg, err := asn1OIDString(signItems[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signature, err := asn1BitStringBytes(signItems[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
certList, err := parseSESCertListV1(infoItems[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
picType, picData, err := parseSESPicture(infoItems[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signData := asn1SequenceBytes(items[0].FullBytes, signItems[0].FullBytes, signItems[1].FullBytes)
|
||||||
|
return &sesSeal{
|
||||||
|
Raw: append([]byte(nil), raw.FullBytes...),
|
||||||
|
SignData: signData,
|
||||||
|
Cert: cert,
|
||||||
|
SignAlg: alg,
|
||||||
|
Signature: signature,
|
||||||
|
PicType: picType,
|
||||||
|
PicData: picData,
|
||||||
|
CertList: certList,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESHeaderVersion 解析印章头版本
|
||||||
|
// 入参: raw 印章头信息
|
||||||
|
// 返回: int 版本号, error 错误信息
|
||||||
|
func parseSESHeaderVersion(raw asn1.RawValue) (int, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(items) < 2 {
|
||||||
|
return 0, fmt.Errorf("invalid ses header")
|
||||||
|
}
|
||||||
|
return asn1Integer(items[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESCertList 解析印章证书列表
|
||||||
|
// 入参: raw 印章属性信息
|
||||||
|
// 返回: sesCertList 证书列表, error 错误信息
|
||||||
|
func parseSESCertList(raw asn1.RawValue, version int) (sesCertList, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(items) < 3 {
|
||||||
|
return sesCertList{}, fmt.Errorf("invalid ses property")
|
||||||
|
}
|
||||||
|
if version < 4 {
|
||||||
|
return parseSESCertInfoList(items[2])
|
||||||
|
}
|
||||||
|
if len(items) < 4 {
|
||||||
|
return sesCertList{}, fmt.Errorf("invalid ses cert list")
|
||||||
|
}
|
||||||
|
listType, err := asn1Integer(items[2])
|
||||||
|
if err != nil {
|
||||||
|
return sesCertList{}, err
|
||||||
|
}
|
||||||
|
switch listType {
|
||||||
|
case 1:
|
||||||
|
return parseSESCertInfoList(items[3])
|
||||||
|
case 2:
|
||||||
|
return parseSESCertDigestList(items[3])
|
||||||
|
default:
|
||||||
|
return sesCertList{}, fmt.Errorf("unsupported ses cert list type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESCertListV1 解析SES V1证书列表
|
||||||
|
// 入参: raw 证书列表ASN.1值
|
||||||
|
// 返回: sesCertList 证书列表, error 错误信息
|
||||||
|
func parseSESCertListV1(raw asn1.RawValue) (sesCertList, error) {
|
||||||
|
certs, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(certs) == 0 {
|
||||||
|
return sesCertList{}, fmt.Errorf("invalid ses cert list")
|
||||||
|
}
|
||||||
|
list := sesCertList{Certs: make([][]byte, 0, len(certs))}
|
||||||
|
for _, item := range certs {
|
||||||
|
list.Certs = append(list.Certs, append([]byte(nil), item.FullBytes...))
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESCertInfoList 解析SES证书信息列表
|
||||||
|
// 入参: raw 证书信息列表ASN.1值
|
||||||
|
// 返回: sesCertList 证书列表, error 错误信息
|
||||||
|
func parseSESCertInfoList(raw asn1.RawValue) (sesCertList, error) {
|
||||||
|
certs, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(certs) == 0 {
|
||||||
|
return sesCertList{}, fmt.Errorf("invalid ses cert list")
|
||||||
|
}
|
||||||
|
list := sesCertList{Certs: make([][]byte, 0, len(certs))}
|
||||||
|
for _, item := range certs {
|
||||||
|
cert, err := asn1OctetString(item)
|
||||||
|
if err != nil {
|
||||||
|
return sesCertList{}, err
|
||||||
|
}
|
||||||
|
list.Certs = append(list.Certs, cert)
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESCertDigestList 解析SES证书摘要列表
|
||||||
|
// 入参: raw 证书摘要列表ASN.1值
|
||||||
|
// 返回: sesCertList 证书列表, error 错误信息
|
||||||
|
func parseSESCertDigestList(raw asn1.RawValue) (sesCertList, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(items) == 0 {
|
||||||
|
return sesCertList{}, fmt.Errorf("invalid ses cert digest list")
|
||||||
|
}
|
||||||
|
list := sesCertList{Digests: make([][]byte, 0, len(items))}
|
||||||
|
for _, item := range items {
|
||||||
|
fields, ok := asn1Children(item.Bytes)
|
||||||
|
if !ok || len(fields) < 2 {
|
||||||
|
return sesCertList{}, fmt.Errorf("invalid ses cert digest")
|
||||||
|
}
|
||||||
|
digest, err := asn1OctetString(fields[1])
|
||||||
|
if err != nil {
|
||||||
|
return sesCertList{}, err
|
||||||
|
}
|
||||||
|
list.Digests = append(list.Digests, digest)
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSESPicture 解析印章图片
|
||||||
|
// 入参: raw 印章图片信息
|
||||||
|
// 返回: string 图片类型, []byte 图片数据, error 错误信息
|
||||||
|
func parseSESPicture(raw asn1.RawValue) (string, []byte, error) {
|
||||||
|
items, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok || len(items) < 4 {
|
||||||
|
return "", nil, fmt.Errorf("invalid ses picture")
|
||||||
|
}
|
||||||
|
picType := normalizeSealType(strings.ToLower(strings.TrimSpace(asn1String(items[0]))))
|
||||||
|
picData, err := asn1OctetString(items[1])
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if picType == "ofd" {
|
||||||
|
if data := trimOFDPackage(picData); len(data) > 0 {
|
||||||
|
picData = data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return picType, picData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSM2PublicKeyFromCert 从证书解析SM2公钥
|
||||||
|
// 入参: data DER编码证书
|
||||||
|
// 返回: sm2PublicKey SM2公钥, error 错误信息
|
||||||
|
func parseSM2PublicKeyFromCert(data []byte) (sm2PublicKey, error) {
|
||||||
|
var cert struct {
|
||||||
|
TBSCertificate asn1.RawValue
|
||||||
|
SignatureAlgorithm asn1.RawValue
|
||||||
|
SignatureValue asn1.BitString
|
||||||
|
}
|
||||||
|
rest, err := asn1.Unmarshal(data, &cert)
|
||||||
|
if err != nil {
|
||||||
|
return sm2PublicKey{}, err
|
||||||
|
}
|
||||||
|
if len(rest) != 0 {
|
||||||
|
return sm2PublicKey{}, fmt.Errorf("invalid certificate")
|
||||||
|
}
|
||||||
|
items, ok := asn1Children(cert.TBSCertificate.Bytes)
|
||||||
|
if !ok {
|
||||||
|
return sm2PublicKey{}, 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 sm2PublicKey{}, fmt.Errorf("invalid public key info")
|
||||||
|
}
|
||||||
|
return parseSM2PublicKeyInfo(items[idx+5])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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摘要
|
||||||
|
// 入参: data 原文数据
|
||||||
|
// 返回: []byte 摘要值
|
||||||
|
func signSM3(data []byte) []byte {
|
||||||
|
h := newSM3()
|
||||||
|
h.Write(data)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSM2SignatureMethod 判断是否为SM2签名算法
|
||||||
|
// 入参: method 算法标识
|
||||||
|
// 返回: bool 是否为SM2签名算法
|
||||||
|
func isSM2SignatureMethod(method string) bool {
|
||||||
|
method = strings.TrimSpace(method)
|
||||||
|
return method == signMethodSM2SM3 || method == signMethodSM2SM3B || method == signMethodSM2Sign
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSM3DigestMethod 判断是否为SM3摘要算法
|
||||||
|
// 入参: method 算法标识
|
||||||
|
// 返回: bool 是否为SM3摘要算法
|
||||||
|
func isSM3DigestMethod(method string) bool {
|
||||||
|
method = strings.TrimSpace(method)
|
||||||
|
return method == signDigestSM3 || method == signDigestSM3NoKey || method == signDigestSM3Key || method == "SM3"
|
||||||
|
}
|
||||||
|
|
||||||
|
// sesCertInList 判断证书是否在印章证书列表中
|
||||||
|
// 入参: cert DER编码证书, list 证书列表
|
||||||
|
// 返回: bool 是否存在
|
||||||
|
func sesCertInList(cert []byte, list sesCertList) bool {
|
||||||
|
for _, item := range list.Certs {
|
||||||
|
if bytes.Equal(cert, item) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
digest := signSM3(cert)
|
||||||
|
for _, item := range list.Digests {
|
||||||
|
if bytes.Equal(digest, item) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1OctetString 解析ASN.1八位字符串
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: []byte 字节数据, error 错误信息
|
||||||
|
func asn1OctetString(raw asn1.RawValue) ([]byte, error) {
|
||||||
|
var out []byte
|
||||||
|
rest, err := asn1.Unmarshal(raw.FullBytes, &out)
|
||||||
|
if err != nil || len(rest) != 0 {
|
||||||
|
return nil, fmt.Errorf("invalid octet string")
|
||||||
|
}
|
||||||
|
return append([]byte(nil), out...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1BitStringBytes 解析ASN.1位字符串
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: []byte 字节数据, error 错误信息
|
||||||
|
func asn1BitStringBytes(raw asn1.RawValue) ([]byte, error) {
|
||||||
|
var bits asn1.BitString
|
||||||
|
rest, err := asn1.Unmarshal(raw.FullBytes, &bits)
|
||||||
|
if err != nil || len(rest) != 0 || bits.BitLength%8 != 0 {
|
||||||
|
return nil, fmt.Errorf("invalid bit string")
|
||||||
|
}
|
||||||
|
return append([]byte(nil), bits.Bytes...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1BitOrOctetBytes 解析ASN.1位字符串或八位字符串
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: []byte 字节数据, error 错误信息
|
||||||
|
func asn1BitOrOctetBytes(raw asn1.RawValue) ([]byte, error) {
|
||||||
|
if raw.Tag == asn1.TagOctetString {
|
||||||
|
return asn1OctetString(raw)
|
||||||
|
}
|
||||||
|
return asn1BitStringBytes(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1OIDString 解析ASN.1对象标识符
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: string OID字符串, error 错误信息
|
||||||
|
func asn1OIDString(raw asn1.RawValue) (string, error) {
|
||||||
|
var oid asn1.ObjectIdentifier
|
||||||
|
rest, err := asn1.Unmarshal(raw.FullBytes, &oid)
|
||||||
|
if err != nil || len(rest) != 0 {
|
||||||
|
return "", fmt.Errorf("invalid oid")
|
||||||
|
}
|
||||||
|
return oid.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1Integer 解析ASN.1整数
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: int 整数值, error 错误信息
|
||||||
|
func asn1Integer(raw asn1.RawValue) (int, error) {
|
||||||
|
var out int
|
||||||
|
rest, err := asn1.Unmarshal(raw.FullBytes, &out)
|
||||||
|
if err != nil || len(rest) != 0 {
|
||||||
|
return 0, fmt.Errorf("invalid integer")
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1String 解析ASN.1字符串
|
||||||
|
// 入参: raw ASN.1原始值
|
||||||
|
// 返回: string 字符串
|
||||||
|
func asn1String(raw asn1.RawValue) string {
|
||||||
|
var s string
|
||||||
|
if _, err := asn1.Unmarshal(raw.FullBytes, &s); err != nil {
|
||||||
|
return strings.TrimSpace(string(raw.Bytes))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1SequenceBytes 编码ASN.1序列
|
||||||
|
// 入参: parts 序列元素DER数据
|
||||||
|
// 返回: []byte ASN.1序列DER数据
|
||||||
|
func asn1SequenceBytes(parts ...[]byte) []byte {
|
||||||
|
var content []byte
|
||||||
|
for _, part := range parts {
|
||||||
|
content = append(content, part...)
|
||||||
|
}
|
||||||
|
return asn1Wrap(0x30, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1SetBytes 编码ASN.1集合
|
||||||
|
// 入参: content 集合内容DER数据
|
||||||
|
// 返回: []byte ASN.1集合DER数据
|
||||||
|
func asn1SetBytes(content []byte) []byte {
|
||||||
|
return asn1Wrap(0x31, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1Wrap 包装ASN.1标签
|
||||||
|
// 入参: tag ASN.1标签, content 内容DER数据
|
||||||
|
// 返回: []byte ASN.1 DER数据
|
||||||
|
func asn1Wrap(tag byte, content []byte) []byte {
|
||||||
|
out := []byte{tag}
|
||||||
|
out = append(out, asn1LengthBytes(len(content))...)
|
||||||
|
out = append(out, content...)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// asn1LengthBytes 编码ASN.1长度
|
||||||
|
// 入参: n 内容长度
|
||||||
|
// 返回: []byte ASN.1长度编码
|
||||||
|
func asn1LengthBytes(n int) []byte {
|
||||||
|
if n < 128 {
|
||||||
|
return []byte{byte(n)}
|
||||||
|
}
|
||||||
|
var buf [8]byte
|
||||||
|
i := len(buf)
|
||||||
|
for n > 0 {
|
||||||
|
i--
|
||||||
|
buf[i] = byte(n)
|
||||||
|
n >>= 8
|
||||||
|
}
|
||||||
|
out := []byte{0x80 | byte(len(buf)-i)}
|
||||||
|
return append(out, buf[i:]...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
// 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 (
|
||||||
|
"crypto/elliptic"
|
||||||
|
"encoding/asn1"
|
||||||
|
"encoding/binary"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sm2DefaultUserID = "1234567812345678"
|
||||||
|
|
||||||
|
var sm2P256 = newSM2P256()
|
||||||
|
|
||||||
|
// sm2PublicKey SM2公钥
|
||||||
|
type sm2PublicKey struct {
|
||||||
|
X *big.Int
|
||||||
|
Y *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSM2P256 创建SM2椭圆曲线
|
||||||
|
// 返回: elliptic.Curve SM2椭圆曲线
|
||||||
|
func newSM2P256() elliptic.Curve {
|
||||||
|
c := &elliptic.CurveParams{Name: "SM2-P-256"}
|
||||||
|
c.P = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF")
|
||||||
|
c.N = sm2Big("FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123")
|
||||||
|
c.B = sm2Big("28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93")
|
||||||
|
c.Gx = sm2Big("32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7")
|
||||||
|
c.Gy = sm2Big("BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0")
|
||||||
|
c.BitSize = 256
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Params().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
|
||||||
|
}
|
||||||
|
x1, y1 := sm2P256.ScalarBaseMult(s.Bytes())
|
||||||
|
x2, y2 := sm2P256.ScalarMult(pub.X, pub.Y, t.Bytes())
|
||||||
|
x, _ := sm2P256.Add(x1, y1, x2, y2)
|
||||||
|
if x == nil {
|
||||||
|
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.Params().B))
|
||||||
|
h.Write(sm2Fixed(sm2P256.Params().Gx))
|
||||||
|
h.Write(sm2Fixed(sm2P256.Params().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.Params().P, big.NewInt(3))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
// 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,528 @@
|
|||||||
|
// 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/subtle"
|
||||||
|
"encoding/asn1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignatureVerifyReport 签名验证报告
|
||||||
|
type SignatureVerifyReport struct {
|
||||||
|
ID string
|
||||||
|
BaseLoc string
|
||||||
|
Type SignType
|
||||||
|
Provider SignatureProvider
|
||||||
|
Signer string
|
||||||
|
SignCert SignatureCertInfo
|
||||||
|
SealCert SignatureCertInfo
|
||||||
|
SealType string
|
||||||
|
SignatureMethod string
|
||||||
|
SignatureDateTime string
|
||||||
|
DigestMethod string
|
||||||
|
References []SignatureReferenceVerify
|
||||||
|
Stamps []SignatureStamp
|
||||||
|
DigestOK bool
|
||||||
|
DataHashOK bool
|
||||||
|
SignedValueOK bool
|
||||||
|
SealOK bool
|
||||||
|
SealMatchOK bool
|
||||||
|
CertOK bool
|
||||||
|
Valid bool
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureCertInfo 签名证书信息
|
||||||
|
type SignatureCertInfo struct {
|
||||||
|
Subject string
|
||||||
|
CommonName string
|
||||||
|
Organization string
|
||||||
|
Issuer string
|
||||||
|
SerialNumber string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureReferenceVerify 签名保护文件验证结果
|
||||||
|
type SignatureReferenceVerify struct {
|
||||||
|
FileRef string
|
||||||
|
Path string
|
||||||
|
CheckValue []byte
|
||||||
|
Actual []byte
|
||||||
|
OK bool
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
// signatureVerifyOptions 签名验证选项
|
||||||
|
type signatureVerifyOptions struct {
|
||||||
|
SignCerts [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureVerifyOption 签名验证选项函数
|
||||||
|
type SignatureVerifyOption func(*signatureVerifyOptions)
|
||||||
|
|
||||||
|
// WithSignatureCert 添加数字签名验证证书
|
||||||
|
// 入参: cert DER或PEM编码证书
|
||||||
|
// 返回: SignatureVerifyOption 签名验证选项
|
||||||
|
func WithSignatureCert(cert []byte) SignatureVerifyOption {
|
||||||
|
return func(o *signatureVerifyOptions) {
|
||||||
|
o.SignCerts = append(o.SignCerts, parseSignatureCerts(cert)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSignatureCerts 添加多张数字签名验证证书
|
||||||
|
// 入参: certs DER或PEM编码证书列表
|
||||||
|
// 返回: SignatureVerifyOption 签名验证选项
|
||||||
|
func WithSignatureCerts(certs ...[]byte) SignatureVerifyOption {
|
||||||
|
return func(o *signatureVerifyOptions) {
|
||||||
|
for _, cert := range certs {
|
||||||
|
o.SignCerts = append(o.SignCerts, parseSignatureCerts(cert)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySignaturesBytes 验证OFD字节数据签名
|
||||||
|
// 入参: data OFD字节数据, opts 签名验证选项
|
||||||
|
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
|
||||||
|
func VerifySignaturesBytes(data []byte, opts ...SignatureVerifyOption) ([]SignatureVerifyReport, error) {
|
||||||
|
return VerifySignaturesReader(bytes.NewReader(data), int64(len(data)), opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySignaturesStream 验证OFD顺序流签名
|
||||||
|
// 入参: r IO顺序读取器, opts 签名验证选项
|
||||||
|
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
|
||||||
|
func VerifySignaturesStream(r io.Reader, opts ...SignatureVerifyOption) ([]SignatureVerifyReport, error) {
|
||||||
|
data, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return VerifySignaturesBytes(data, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySignaturesReader 验证OFD读取器签名
|
||||||
|
// 入参: r IO读取器, size 数据大小, opts 签名验证选项
|
||||||
|
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
|
||||||
|
func VerifySignaturesReader(r io.ReaderAt, size int64, opts ...SignatureVerifyOption) ([]SignatureVerifyReport, error) {
|
||||||
|
reader, err := NewReader(r, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return reader.VerifySignatures(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySignatures 验证文档签名
|
||||||
|
// 入参: opts 签名验证选项
|
||||||
|
// 返回: []SignatureVerifyReport 签名验证报告, error 错误信息
|
||||||
|
func (r *Reader) VerifySignatures(opts ...SignatureVerifyOption) ([]SignatureVerifyReport, error) {
|
||||||
|
options := signatureVerifyOptions{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
doc, err := r.Doc()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if doc.Signatures == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
sigListPath := r.ResPath(doc.Signatures)
|
||||||
|
data, err := r.readFileExact(sigListPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var signatures Signatures
|
||||||
|
if err := xml.Unmarshal(data, &signatures); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
reports := make([]SignatureVerifyReport, 0, len(signatures.List))
|
||||||
|
for _, sigRef := range signatures.List {
|
||||||
|
reports = append(reports, r.verifySignature(sigListPath, sigRef, &options))
|
||||||
|
}
|
||||||
|
return reports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifySignature 验证单个签名
|
||||||
|
// 入参: sigListPath 签名列表路径, sigRef 签名引用, options 验证选项
|
||||||
|
// 返回: SignatureVerifyReport 签名验证报告
|
||||||
|
func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *signatureVerifyOptions) SignatureVerifyReport {
|
||||||
|
sigPath := signatureRefPath(sigListPath, sigRef.BaseLoc)
|
||||||
|
report := SignatureVerifyReport{
|
||||||
|
ID: sigRef.ID,
|
||||||
|
BaseLoc: sigRef.BaseLoc,
|
||||||
|
Type: sigRef.Type,
|
||||||
|
SealMatchOK: true,
|
||||||
|
}
|
||||||
|
sigData, err := r.readFileExact(sigPath)
|
||||||
|
if err != nil {
|
||||||
|
report.Error = err.Error()
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
sigFile, err := parseSignatureFile(sigData)
|
||||||
|
if err != nil {
|
||||||
|
report.Error = err.Error()
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
report.Provider = sigFile.SignedInfo.Provider
|
||||||
|
report.SignatureMethod = sigFile.SignedInfo.SignatureMethod
|
||||||
|
report.SignatureDateTime = sigFile.SignedInfo.SignatureDateTime
|
||||||
|
report.DigestMethod = sigFile.SignedInfo.References.CheckMethod
|
||||||
|
report.References = r.verifySignatureReferences(sigPath, sigFile.SignedInfo.References)
|
||||||
|
report.Stamps = append(report.Stamps, sigFile.SignedInfo.StampAnnot...)
|
||||||
|
report.DigestOK = referencesOK(report.References)
|
||||||
|
signedValuePath := signatureRefPath(sigPath, sigFile.SignedValue)
|
||||||
|
signedValue, err := r.readFileExact(signedValuePath)
|
||||||
|
if err != nil {
|
||||||
|
report.Error = err.Error()
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
switch sigRef.Type {
|
||||||
|
case SignTypeSign:
|
||||||
|
result, err := verifyDigitalSignature(report.SignatureMethod, signedValue, sigData, options)
|
||||||
|
if err != nil {
|
||||||
|
report.Error = err.Error()
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
report.DataHashOK = result.DataHashOK
|
||||||
|
report.SignedValueOK = result.SignedOK
|
||||||
|
report.SealOK = true
|
||||||
|
report.CertOK = result.CertOK
|
||||||
|
report.SignCert = result.CertInfo
|
||||||
|
report.Signer = result.CertInfo.CommonName
|
||||||
|
report.Valid = report.DigestOK && report.DataHashOK && report.SignedValueOK && report.CertOK
|
||||||
|
return report
|
||||||
|
case "", SignTypeSeal:
|
||||||
|
default:
|
||||||
|
report.Error = fmt.Sprintf("unsupported signature type: %s", sigRef.Type)
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
sesResult, err := verifySESSignature(signedValue, sigData)
|
||||||
|
if sesResult != nil {
|
||||||
|
report.SignCert = sesResult.SignCert
|
||||||
|
report.SealCert = sesResult.SealCert
|
||||||
|
report.SealType = sesResult.SealType
|
||||||
|
report.Signer = sesResult.SignCert.CommonName
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
report.Error = err.Error()
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
report.DataHashOK = sesResult.DataHashOK
|
||||||
|
report.SignedValueOK = sesResult.SignedOK
|
||||||
|
report.SealOK = sesResult.SealOK
|
||||||
|
report.CertOK = sesResult.CertOK
|
||||||
|
if sigFile.SignedInfo.Seal.BaseLoc != "" {
|
||||||
|
sealPath := signatureRefPath(sigPath, sigFile.SignedInfo.Seal.BaseLoc)
|
||||||
|
sealData, err := r.readFileExact(sealPath)
|
||||||
|
if err != nil {
|
||||||
|
report.Error = err.Error()
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
sig, err := parseSESSignature(signedValue)
|
||||||
|
if err != nil {
|
||||||
|
report.Error = err.Error()
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
report.SealMatchOK = bytes.Equal(sealData, sig.Seal.Raw)
|
||||||
|
}
|
||||||
|
report.Valid = report.DigestOK && report.DataHashOK && report.SignedValueOK && report.SealOK && report.SealMatchOK && report.CertOK
|
||||||
|
return report
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifySignatureReferences 验证签名保护文件列表
|
||||||
|
// 入参: sigPath 签名文件路径, refs 签名保护文件列表
|
||||||
|
// 返回: []SignatureReferenceVerify 保护文件验证结果
|
||||||
|
func (r *Reader) verifySignatureReferences(sigPath string, refs SignatureReferences) []SignatureReferenceVerify {
|
||||||
|
results := make([]SignatureReferenceVerify, 0, len(refs.Reference))
|
||||||
|
for _, ref := range refs.Reference {
|
||||||
|
results = append(results, r.verifySignatureReference(sigPath, refs.CheckMethod, ref))
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifySignatureReference 验证签名保护文件
|
||||||
|
// 入参: sigPath 签名文件路径, method 摘要算法, ref 保护文件引用
|
||||||
|
// 返回: SignatureReferenceVerify 保护文件验证结果
|
||||||
|
func (r *Reader) verifySignatureReference(sigPath, method string, ref SignatureReference) SignatureReferenceVerify {
|
||||||
|
refPath := signatureRefPath(sigPath, ref.FileRef)
|
||||||
|
result := SignatureReferenceVerify{
|
||||||
|
FileRef: ref.FileRef,
|
||||||
|
Path: refPath,
|
||||||
|
}
|
||||||
|
checkValue, err := base64.StdEncoding.DecodeString(strings.TrimSpace(ref.CheckValue))
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
data, err := r.readFileExact(refPath)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
actual, err := signatureDigest(method, data)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.CheckValue = checkValue
|
||||||
|
result.Actual = actual
|
||||||
|
result.OK = subtle.ConstantTimeCompare(checkValue, actual) == 1
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSignatureFile 解析签名文件
|
||||||
|
// 入参: data 签名文件XML数据
|
||||||
|
// 返回: *SignatureFile 签名文件结构, error 错误信息
|
||||||
|
func parseSignatureFile(data []byte) (*SignatureFile, error) {
|
||||||
|
var sigFile SignatureFile
|
||||||
|
if err := xml.Unmarshal(data, &sigFile); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
raw, err := xmlElementRaw(data, "SignedInfo")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sigFile.SignedInfo.Raw = raw
|
||||||
|
return &sigFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readFileExact 读取OFD包内文件
|
||||||
|
// 入参: name 包内文件路径
|
||||||
|
// 返回: []byte 文件数据, error 错误信息
|
||||||
|
func (r *Reader) readFileExact(name string) ([]byte, error) {
|
||||||
|
name = cleanPackagePath(name)
|
||||||
|
if f, ok := r.fileIndex[name]; ok {
|
||||||
|
return readZipFile(f)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("file not found: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// signatureDigest 计算签名摘要
|
||||||
|
// 入参: method 摘要算法, data 原文数据
|
||||||
|
// 返回: []byte 摘要值, error 错误信息
|
||||||
|
func signatureDigest(method string, data []byte) ([]byte, error) {
|
||||||
|
if isSM3DigestMethod(method) {
|
||||||
|
return signSM3(data), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unsupported digest method: %s", method)
|
||||||
|
}
|
||||||
|
|
||||||
|
// signatureRefPath 解析签名文件引用路径
|
||||||
|
// 入参: basePath 基准路径, refPath 引用路径
|
||||||
|
// 返回: string 包内文件路径
|
||||||
|
func signatureRefPath(basePath, refPath string) string {
|
||||||
|
p := strings.TrimSpace(refPath)
|
||||||
|
p = strings.ReplaceAll(p, "\\", "/")
|
||||||
|
if strings.HasPrefix(p, "/") {
|
||||||
|
return cleanPackagePath(p)
|
||||||
|
}
|
||||||
|
return path.Clean(resolveResourcePath(basePath, "", p))
|
||||||
|
}
|
||||||
|
|
||||||
|
// referencesOK 判断保护文件摘要是否全部通过
|
||||||
|
// 入参: refs 保护文件验证结果
|
||||||
|
// 返回: bool 是否全部通过
|
||||||
|
func referencesOK(refs []SignatureReferenceVerify) bool {
|
||||||
|
if len(refs) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, ref := range refs {
|
||||||
|
if !ref.OK {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// xmlElementRaw 提取XML元素原始字节
|
||||||
|
// 入参: data XML数据, localName 元素名称
|
||||||
|
// 返回: []byte 元素原始字节, error 错误信息
|
||||||
|
func xmlElementRaw(data []byte, localName string) ([]byte, error) {
|
||||||
|
dec := xml.NewDecoder(bytes.NewReader(data))
|
||||||
|
for {
|
||||||
|
tok, err := dec.Token()
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
start, ok := tok.(xml.StartElement)
|
||||||
|
if !ok || start.Name.Local != localName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
end := int(dec.InputOffset())
|
||||||
|
begin := bytes.LastIndex(data[:end], []byte("<"))
|
||||||
|
if begin < 0 {
|
||||||
|
return nil, fmt.Errorf("xml element not found: %s", localName)
|
||||||
|
}
|
||||||
|
depth := 1
|
||||||
|
for depth > 0 {
|
||||||
|
tok, err = dec.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch tok.(type) {
|
||||||
|
case xml.StartElement:
|
||||||
|
depth++
|
||||||
|
case xml.EndElement:
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return append([]byte(nil), data[begin:int(dec.InputOffset())]...), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("xml element not found: %s", localName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// signatureCertInfo 解析签名证书信息
|
||||||
|
// 入参: data DER编码证书
|
||||||
|
// 返回: SignatureCertInfo 签名证书信息
|
||||||
|
func signatureCertInfo(data []byte) SignatureCertInfo {
|
||||||
|
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 SignatureCertInfo{}
|
||||||
|
}
|
||||||
|
items, ok := asn1Children(cert.TBSCertificate.Bytes)
|
||||||
|
if !ok {
|
||||||
|
return SignatureCertInfo{}
|
||||||
|
}
|
||||||
|
idx := 0
|
||||||
|
if len(items) > 0 && items[0].Class == asn1.ClassContextSpecific && items[0].Tag == 0 {
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
if len(items) <= idx+4 {
|
||||||
|
return SignatureCertInfo{}
|
||||||
|
}
|
||||||
|
serial, _ := asn1IntegerBig(items[idx])
|
||||||
|
subject := certificateNameValues(items[idx+4])
|
||||||
|
issuer := certificateNameValues(items[idx+2])
|
||||||
|
info := SignatureCertInfo{
|
||||||
|
Subject: certificateNameString(subject),
|
||||||
|
CommonName: certificateNameFirst(subject, "2.5.4.3"),
|
||||||
|
Organization: certificateNameFirst(subject, "2.5.4.10"),
|
||||||
|
Issuer: certificateNameString(issuer),
|
||||||
|
}
|
||||||
|
if serial != nil {
|
||||||
|
info.SerialNumber = serial.String()
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// certificateNameValues 解析证书名称字段
|
||||||
|
// 入参: raw 名称原始值
|
||||||
|
// 返回: map[string][]string OID字段列表
|
||||||
|
func certificateNameValues(raw asn1.RawValue) map[string][]string {
|
||||||
|
out := make(map[string][]string)
|
||||||
|
sets, ok := asn1Children(raw.Bytes)
|
||||||
|
if !ok {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
for _, set := range sets {
|
||||||
|
attrs, ok := asn1Children(set.Bytes)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, attr := range attrs {
|
||||||
|
items, ok := asn1Children(attr.Bytes)
|
||||||
|
if !ok || len(items) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
oid, err := asn1OIDString(items[0])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if value := strings.TrimSpace(asn1String(items[1])); value != "" {
|
||||||
|
out[oid] = append(out[oid], value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// certificateNameFirst 获取证书名称字段首值
|
||||||
|
// 入参: values OID字段列表, oid 字段OID
|
||||||
|
// 返回: string 字段值
|
||||||
|
func certificateNameFirst(values map[string][]string, oid string) string {
|
||||||
|
if items := values[oid]; len(items) > 0 {
|
||||||
|
return items[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// certificateNameString 格式化证书名称
|
||||||
|
// 入参: values OID字段列表
|
||||||
|
// 返回: string 证书名称
|
||||||
|
func certificateNameString(values map[string][]string) string {
|
||||||
|
var parts []string
|
||||||
|
for _, item := range []struct {
|
||||||
|
OID string
|
||||||
|
Label string
|
||||||
|
}{
|
||||||
|
{"2.5.4.3", "CN"},
|
||||||
|
{"2.5.4.10", "O"},
|
||||||
|
{"2.5.4.11", "OU"},
|
||||||
|
{"2.5.4.6", "C"},
|
||||||
|
{"2.5.4.8", "ST"},
|
||||||
|
{"2.5.4.7", "L"},
|
||||||
|
{"2.5.4.5", "SN"},
|
||||||
|
{"1.2.840.113549.1.9.1", "E"},
|
||||||
|
} {
|
||||||
|
for _, value := range values[item.OID] {
|
||||||
|
parts = append(parts, item.Label+"="+value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseSignatureCerts 解析签名验证证书
|
||||||
|
// 入参: data DER或PEM编码证书
|
||||||
|
// 返回: [][]byte DER编码证书列表
|
||||||
|
func parseSignatureCerts(data []byte) [][]byte {
|
||||||
|
data = bytes.TrimSpace(data)
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var certs [][]byte
|
||||||
|
rest := data
|
||||||
|
hasPEM := false
|
||||||
|
for {
|
||||||
|
block, next := pem.Decode(rest)
|
||||||
|
if block == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
hasPEM = true
|
||||||
|
if block.Type == "CERTIFICATE" {
|
||||||
|
certs = append(certs, append([]byte(nil), block.Bytes...))
|
||||||
|
}
|
||||||
|
rest = next
|
||||||
|
}
|
||||||
|
if len(certs) != 0 {
|
||||||
|
return certs
|
||||||
|
}
|
||||||
|
if hasPEM {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return [][]byte{append([]byte(nil), data...)}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user