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编码证书列表