feat(签名展示): OFDGo WebUI支持签名展示
build.yaml / build (push) Successful in 35s

This commit is contained in:
2026-07-08 11:02:20 +08:00
parent 98b37d865a
commit 61da0f3f5d
8 changed files with 671 additions and 18 deletions
+11
View File
@@ -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
View File
@@ -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;
+194
View File
@@ -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) {
+154
View File
@@ -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
@@ -59,7 +102,10 @@ type DocumentInfo struct {
ModDate string `json:"modDate"` ModDate string `json:"modDate"`
PageCount int `json:"pageCount"` PageCount int `json:"pageCount"`
FontCount int `json:"fontCount"` FontCount int `json:"fontCount"`
SignatureCount int `json:"signatureCount"`
SignatureError string `json:"signatureError,omitempty"`
Fonts []FontInfo `json:"fonts"` Fonts []FontInfo `json:"fonts"`
Signatures []SignatureInfo `json:"signatures"`
Pages []PageInfo `json:"pages"` Pages []PageInfo `json:"pages"`
} }
@@ -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) {
+45
View File
@@ -81,6 +81,15 @@ type SignatureStamp struct {
Boundary string `xml:"Boundary,attr"` Boundary string `xml:"Boundary,attr"`
} }
// SignatureStampPosition 签名外观位置信息
type SignatureStampPosition struct {
ID string
Page int
PageID string
Boundary string
Box Box
}
// SignatureReferences 签名保护文件列表 // SignatureReferences 签名保护文件列表
type SignatureReferences struct { type SignatureReferences struct {
CheckMethod string `xml:"CheckMethod,attr"` CheckMethod string `xml:"CheckMethod,attr"`
@@ -93,6 +102,29 @@ type SignatureReference struct {
CheckValue string `xml:"CheckValue"` 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 解析签名文件
// 入参: doc 文档结构 // 入参: doc 文档结构
// 返回: error 错误信息 // 返回: error 错误信息
@@ -153,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 印章数据
+3
View File
@@ -32,6 +32,7 @@ type digitalVerifyResult struct {
DataHashOK bool DataHashOK bool
SignedOK bool SignedOK bool
CertOK bool CertOK bool
CertInfo SignatureCertInfo
} }
// gbtSignedData GB/T 35275 SignedData结构 // gbtSignedData GB/T 35275 SignedData结构
@@ -86,6 +87,7 @@ func verifyRawDigitalSignature(signedValue, signedData []byte, options *signatur
continue continue
} }
result.CertOK = true result.CertOK = true
result.CertInfo = signatureCertInfo(cert)
if sm2VerifySignature(pub, nil, signedData, signedValue) { if sm2VerifySignature(pub, nil, signedData, signedValue) {
result.SignedOK = true result.SignedOK = true
return result, nil return result, nil
@@ -136,6 +138,7 @@ func verifyGBT35275SignedData(signedValue, signedData []byte, options *signature
if cert == nil { if cert == nil {
return result, nil return result, nil
} }
result.CertInfo = signatureCertInfo(cert.Raw)
if !isSM2SignatureMethod(signer.SignatureAlg) { if !isSM2SignatureMethod(signer.SignatureAlg) {
return nil, fmt.Errorf("unsupported signature method") return nil, fmt.Errorf("unsupported signature method")
} }
+6
View File
@@ -69,6 +69,9 @@ type sesVerifyResult struct {
SignedOK bool SignedOK bool
SealOK bool SealOK bool
CertOK bool CertOK bool
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SealType string
} }
// parseSESSignature 解析SES签章值 // parseSESSignature 解析SES签章值
@@ -177,6 +180,9 @@ func verifySESSignature(data, signedData []byte) (*sesVerifyResult, error) {
return nil, fmt.Errorf("unsupported signature method") return nil, fmt.Errorf("unsupported signature method")
} }
result := &sesVerifyResult{} 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)) result.DataHashOK = bytes.Equal(sig.DataHash, signSM3(signedData))
signPub, err := parseSM2PublicKeyFromCert(sig.Cert) signPub, err := parseSM2PublicKeyFromCert(sig.Cert)
if err != nil { if err != nil {
+129
View File
@@ -17,6 +17,7 @@ package ofdgo
import ( import (
"bytes" "bytes"
"crypto/subtle" "crypto/subtle"
"encoding/asn1"
"encoding/base64" "encoding/base64"
"encoding/pem" "encoding/pem"
"encoding/xml" "encoding/xml"
@@ -32,10 +33,15 @@ type SignatureVerifyReport struct {
BaseLoc string BaseLoc string
Type SignType Type SignType
Provider SignatureProvider Provider SignatureProvider
Signer string
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SealType string
SignatureMethod string SignatureMethod string
SignatureDateTime string SignatureDateTime string
DigestMethod string DigestMethod string
References []SignatureReferenceVerify References []SignatureReferenceVerify
Stamps []SignatureStamp
DigestOK bool DigestOK bool
DataHashOK bool DataHashOK bool
SignedValueOK bool SignedValueOK bool
@@ -46,6 +52,15 @@ type SignatureVerifyReport struct {
Error string Error string
} }
// SignatureCertInfo 签名证书信息
type SignatureCertInfo struct {
Subject string
CommonName string
Organization string
Issuer string
SerialNumber string
}
// SignatureReferenceVerify 签名保护文件验证结果 // SignatureReferenceVerify 签名保护文件验证结果
type SignatureReferenceVerify struct { type SignatureReferenceVerify struct {
FileRef string FileRef string
@@ -170,6 +185,7 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.SignatureDateTime = sigFile.SignedInfo.SignatureDateTime report.SignatureDateTime = sigFile.SignedInfo.SignatureDateTime
report.DigestMethod = sigFile.SignedInfo.References.CheckMethod report.DigestMethod = sigFile.SignedInfo.References.CheckMethod
report.References = r.verifySignatureReferences(sigPath, sigFile.SignedInfo.References) report.References = r.verifySignatureReferences(sigPath, sigFile.SignedInfo.References)
report.Stamps = append(report.Stamps, sigFile.SignedInfo.StampAnnot...)
report.DigestOK = referencesOK(report.References) report.DigestOK = referencesOK(report.References)
signedValuePath := signatureRefPath(sigPath, sigFile.SignedValue) signedValuePath := signatureRefPath(sigPath, sigFile.SignedValue)
signedValue, err := r.readFileExact(signedValuePath) signedValue, err := r.readFileExact(signedValuePath)
@@ -188,6 +204,8 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
report.SignedValueOK = result.SignedOK report.SignedValueOK = result.SignedOK
report.SealOK = true report.SealOK = true
report.CertOK = result.CertOK report.CertOK = result.CertOK
report.SignCert = result.CertInfo
report.Signer = result.CertInfo.CommonName
report.Valid = report.DigestOK && report.DataHashOK && report.SignedValueOK && report.CertOK report.Valid = report.DigestOK && report.DataHashOK && report.SignedValueOK && report.CertOK
return report return report
case "", SignTypeSeal: case "", SignTypeSeal:
@@ -196,6 +214,12 @@ func (r *Reader) verifySignature(sigListPath string, sigRef Signature, options *
return report return report
} }
sesResult, err := verifySESSignature(signedValue, sigData) 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 { if err != nil {
report.Error = err.Error() report.Error = err.Error()
return report return report
@@ -367,6 +391,111 @@ func xmlElementRaw(data []byte, localName string) ([]byte, error) {
return nil, fmt.Errorf("xml element not found: %s", localName) 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 解析签名验证证书 // parseSignatureCerts 解析签名验证证书
// 入参: data DER或PEM编码证书 // 入参: data DER或PEM编码证书
// 返回: [][]byte DER编码证书列表 // 返回: [][]byte DER编码证书列表