mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-08-30 12:12:40 +08:00
This commit is contained in:
@@ -80,11 +80,22 @@
|
||||
<dt>类型</dt>
|
||||
<dd id="metaType">-</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>签名</dt>
|
||||
<dd id="metaSignatures">0</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>字体</dt>
|
||||
<dd id="metaFonts">0</dd>
|
||||
</div>
|
||||
</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">
|
||||
<div class="panel-row">
|
||||
<div id="docFontPanelTitle" class="panel-title compact">OFD 字体</div>
|
||||
|
||||
+118
-7
@@ -519,6 +519,23 @@ body[data-hide-meta] .meta-panel {
|
||||
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 {
|
||||
display: block;
|
||||
max-width: none;
|
||||
@@ -548,7 +565,8 @@ body[data-hide-meta] .meta-panel {
|
||||
}
|
||||
|
||||
.font-panel,
|
||||
.doc-font-panel {
|
||||
.doc-font-panel,
|
||||
.signature-panel {
|
||||
margin-top: 18px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
@@ -569,12 +587,14 @@ body[data-hide-meta] .meta-panel {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.doc-font-list {
|
||||
.doc-font-list,
|
||||
.signature-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.doc-font-row {
|
||||
.doc-font-row,
|
||||
.signature-row {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 9px;
|
||||
@@ -585,6 +605,7 @@ body[data-hide-meta] .meta-panel {
|
||||
|
||||
.page-list-item:not(:disabled):hover,
|
||||
.doc-font-row:hover,
|
||||
.signature-row:hover,
|
||||
.font-row:hover {
|
||||
border-color: var(--line-strong);
|
||||
background: #ffffff;
|
||||
@@ -600,14 +621,26 @@ body[data-hide-meta] .meta-panel {
|
||||
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;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.doc-font-name {
|
||||
.doc-font-name,
|
||||
.signature-name {
|
||||
overflow: hidden;
|
||||
color: #1f2428;
|
||||
font-size: 13px;
|
||||
@@ -616,7 +649,51 @@ body[data-hide-meta] .meta-panel {
|
||||
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;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
@@ -647,11 +724,45 @@ body[data-hide-meta] .meta-panel {
|
||||
color: #705719;
|
||||
}
|
||||
|
||||
.font-badge.missing {
|
||||
.font-badge.missing,
|
||||
.font-badge.invalid {
|
||||
background: #f9e8e5;
|
||||
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 {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
|
||||
@@ -107,6 +107,10 @@ const el = {
|
||||
metaVersion: document.querySelector("#metaVersion"),
|
||||
metaType: document.querySelector("#metaType"),
|
||||
metaFonts: document.querySelector("#metaFonts"),
|
||||
metaSignatures: document.querySelector("#metaSignatures"),
|
||||
signaturePanel: document.querySelector("#signaturePanel"),
|
||||
signatureSummary: document.querySelector("#signatureSummary"),
|
||||
signatureList: document.querySelector("#signatureList"),
|
||||
docFontList: document.querySelector("#docFontList"),
|
||||
docFontSummary: document.querySelector("#docFontSummary"),
|
||||
availableFontSummary: document.querySelector("#availableFontSummary"),
|
||||
@@ -1500,12 +1504,201 @@ function renderMeta() {
|
||||
el.metaVersion.textContent = doc.version || "-";
|
||||
el.metaType.textContent = doc.docType || "-";
|
||||
el.metaFonts.textContent = String(doc.fontCount || 0);
|
||||
el.metaSignatures.textContent = String(doc.signatureCount || 0);
|
||||
el.pageTotal.textContent = String(doc.pageCount || 0);
|
||||
renderSignatures();
|
||||
renderDocumentFonts();
|
||||
renderFontList();
|
||||
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() {
|
||||
const fonts = state.doc?.fonts || [];
|
||||
el.docFontList.replaceChildren();
|
||||
@@ -1654,6 +1847,7 @@ function applyFit(updateStatus = true) {
|
||||
function setScale(nextScale, updateStatus = true, fitMode = "free") {
|
||||
state.fitMode = fitMode;
|
||||
state.scale = Math.min(4, Math.max(0.2, nextScale));
|
||||
clearStampHighlights();
|
||||
layoutPages();
|
||||
el.zoomLabel.textContent = `${Math.round(state.scale * 100)}%`;
|
||||
if (updateStatus && state.doc) {
|
||||
|
||||
@@ -39,6 +39,49 @@ type FontFile = ofdgo.FontFile
|
||||
// FontInfo OFD字体信息
|
||||
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文档会话
|
||||
type Session struct {
|
||||
Reader *ofdgo.Reader
|
||||
@@ -59,7 +102,10 @@ type DocumentInfo struct {
|
||||
ModDate string `json:"modDate"`
|
||||
PageCount int `json:"pageCount"`
|
||||
FontCount int `json:"fontCount"`
|
||||
SignatureCount int `json:"signatureCount"`
|
||||
SignatureError string `json:"signatureError,omitempty"`
|
||||
Fonts []FontInfo `json:"fonts"`
|
||||
Signatures []SignatureInfo `json:"signatures"`
|
||||
Pages []PageInfo `json:"pages"`
|
||||
}
|
||||
|
||||
@@ -179,6 +225,12 @@ func (s *Session) Info() DocumentInfo {
|
||||
info.Fonts = 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 {
|
||||
info.Title = docInfo.Title
|
||||
info.Author = docInfo.Author
|
||||
@@ -257,6 +309,108 @@ func (s *Session) ExportPage(index int, value string) ([]byte, ExportFormat, err
|
||||
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
|
||||
// 返回: []byte PDF文件数据, error 错误信息
|
||||
func (s *Session) ExportPDF() ([]byte, error) {
|
||||
|
||||
@@ -81,6 +81,15 @@ type SignatureStamp struct {
|
||||
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"`
|
||||
@@ -93,6 +102,29 @@ type SignatureReference struct {
|
||||
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 解析签名文件
|
||||
// 入参: doc 文档结构
|
||||
// 返回: error 错误信息
|
||||
@@ -153,6 +185,19 @@ func (r *Reader) parseSignatures(doc *Document) error {
|
||||
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 尝试提取印章数据
|
||||
// 入参: data 签名值数据
|
||||
// 返回: string 印章类型, []byte 印章数据
|
||||
|
||||
@@ -32,6 +32,7 @@ type digitalVerifyResult struct {
|
||||
DataHashOK bool
|
||||
SignedOK bool
|
||||
CertOK bool
|
||||
CertInfo SignatureCertInfo
|
||||
}
|
||||
|
||||
// gbtSignedData GB/T 35275 SignedData结构
|
||||
@@ -86,6 +87,7 @@ func verifyRawDigitalSignature(signedValue, signedData []byte, options *signatur
|
||||
continue
|
||||
}
|
||||
result.CertOK = true
|
||||
result.CertInfo = signatureCertInfo(cert)
|
||||
if sm2VerifySignature(pub, nil, signedData, signedValue) {
|
||||
result.SignedOK = true
|
||||
return result, nil
|
||||
@@ -136,6 +138,7 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
|
||||
if cert == nil {
|
||||
return result, nil
|
||||
}
|
||||
result.CertInfo = signatureCertInfo(cert.Raw)
|
||||
if !isSM2SignatureMethod(signer.SignatureAlg) {
|
||||
return nil, fmt.Errorf("unsupported signature method")
|
||||
}
|
||||
|
||||
@@ -69,6 +69,9 @@ type sesVerifyResult struct {
|
||||
SignedOK bool
|
||||
SealOK bool
|
||||
CertOK bool
|
||||
SignCert SignatureCertInfo
|
||||
SealCert SignatureCertInfo
|
||||
SealType string
|
||||
}
|
||||
|
||||
// parseSESSignature 解析SES签章值
|
||||
@@ -177,6 +180,9 @@ func verifySESSignature(data, signedData []byte) (*sesVerifyResult, error) {
|
||||
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 {
|
||||
|
||||
@@ -17,6 +17,7 @@ package ofdgo
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"encoding/asn1"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"encoding/xml"
|
||||
@@ -32,10 +33,15 @@ type SignatureVerifyReport struct {
|
||||
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
|
||||
@@ -46,6 +52,15 @@ type SignatureVerifyReport struct {
|
||||
Error string
|
||||
}
|
||||
|
||||
// SignatureCertInfo 签名证书信息
|
||||
type SignatureCertInfo struct {
|
||||
Subject string
|
||||
CommonName string
|
||||
Organization string
|
||||
Issuer string
|
||||
SerialNumber string
|
||||
}
|
||||
|
||||
// SignatureReferenceVerify 签名保护文件验证结果
|
||||
type SignatureReferenceVerify struct {
|
||||
FileRef string
|
||||
@@ -170,6 +185,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
|
||||
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)
|
||||
@@ -188,6 +204,8 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
|
||||
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:
|
||||
@@ -196,6 +214,12 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
|
||||
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
|
||||
@@ -367,6 +391,111 @@ func xmlElementRaw(data []byte, localName string) ([]byte, error) {
|
||||
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编码证书列表
|
||||
|
||||
Reference in New Issue
Block a user