From 61da0f3f5dc5c8f93c215c5ff92ca4dab20b338f Mon Sep 17 00:00:00 2001 From: xiaoqidun Date: Wed, 8 Jul 2026 11:02:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=AD=BE=E5=90=8D=E5=B1=95=E7=A4=BA):=20O?= =?UTF-8?q?FDGo=20WebUI=E6=94=AF=E6=8C=81=E7=AD=BE=E5=90=8D=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/webui/index.html | 11 +++ assets/webui/ofdgo.css | 125 ++++++++++++++++++++++++-- assets/webui/ofdgo.js | 194 ++++++++++++++++++++++++++++++++++++++++ internal/webui/webui.go | 176 +++++++++++++++++++++++++++++++++--- ofdgo_sign.go | 45 ++++++++++ ofdgo_sign_gbt.go | 3 + ofdgo_sign_ses.go | 6 ++ ofdgo_sign_verify.go | 129 ++++++++++++++++++++++++++ 8 files changed, 671 insertions(+), 18 deletions(-) diff --git a/assets/webui/index.html b/assets/webui/index.html index 3871800..7ed90d0 100644 --- a/assets/webui/index.html +++ b/assets/webui/index.html @@ -80,11 +80,22 @@
类型
-
+
+
签名
+
0
+
字体
0
+
OFD 字体
diff --git a/assets/webui/ofdgo.css b/assets/webui/ofdgo.css index f960cb7..2aa4e77 100644 --- a/assets/webui/ofdgo.css +++ b/assets/webui/ofdgo.css @@ -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; diff --git a/assets/webui/ofdgo.js b/assets/webui/ofdgo.js index e842c8b..5174897 100644 --- a/assets/webui/ofdgo.js +++ b/assets/webui/ofdgo.js @@ -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) { diff --git a/internal/webui/webui.go b/internal/webui/webui.go index 4575e07..3326dbe 100644 --- a/internal/webui/webui.go +++ b/internal/webui/webui.go @@ -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 @@ -50,17 +93,20 @@ type Session struct { // DocumentInfo 文档信息 type DocumentInfo struct { - Version string `json:"version"` - DocType string `json:"docType"` - Title string `json:"title"` - Author string `json:"author"` - Subject string `json:"subject"` - CreationDate string `json:"creationDate"` - ModDate string `json:"modDate"` - PageCount int `json:"pageCount"` - FontCount int `json:"fontCount"` - Fonts []FontInfo `json:"fonts"` - Pages []PageInfo `json:"pages"` + Version string `json:"version"` + DocType string `json:"docType"` + Title string `json:"title"` + Author string `json:"author"` + Subject string `json:"subject"` + CreationDate string `json:"creationDate"` + 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"` } // PageInfo 页面信息 @@ -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) { diff --git a/ofdgo_sign.go b/ofdgo_sign.go index 08b6cb7..0227716 100644 --- a/ofdgo_sign.go +++ b/ofdgo_sign.go @@ -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 印章数据 diff --git a/ofdgo_sign_gbt.go b/ofdgo_sign_gbt.go index 074a5d9..04bb221 100644 --- a/ofdgo_sign_gbt.go +++ b/ofdgo_sign_gbt.go @@ -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") } diff --git a/ofdgo_sign_ses.go b/ofdgo_sign_ses.go index 3a39244..08b48cd 100644 --- a/ofdgo_sign_ses.go +++ b/ofdgo_sign_ses.go @@ -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 { diff --git a/ofdgo_sign_verify.go b/ofdgo_sign_verify.go index 63ec79e..61a3dfb 100644 --- a/ofdgo_sign_verify.go +++ b/ofdgo_sign_verify.go @@ -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编码证书列表