feat(签章验签): 完善OFD加密、签章与验签能力,统一国密实现,优化界面文案与交互
build.yaml / build (push) Successful in 40s

This commit is contained in:
xiaoqidun committed 2026-09-23 23:02:48 +08:00
1 parent d6f70de7be
commit 3927929822
30 files changed
+4707 -1161

No files matched your search

+435
View File
@@ -0,0 +1,435 @@
// Copyright 2025-2026 肖其顿 (XIAO QI DUN)
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ofdgo
import (
"bytes"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"time"
"github.com/emmansun/gmsm/smx509"
"golang.org/x/crypto/ocsp"
)
// SignatureRevocationEvidence 离线吊销证据
type SignatureRevocationEvidence struct {
CRLs [][]byte
OCSPResponses [][]byte
}
// SignatureRevocationRequest 外部吊销证据请求
type SignatureRevocationRequest struct {
Certificate []byte
Issuer []byte
VerifyTime time.Time
}
// SignatureRevocationOptions 离线吊销检查选项
// 检查签署者及制章者证书, 不代表整条证书链或TSA的吊销状态
// Fetch仅提供DER证据, 本库不主动联网, 缺少NextUpdate时必须显式设置MaxAge
// VerifyTime缺省取验签指定时间或当前时间, 不采用签名自报时间
type SignatureRevocationOptions struct {
SignatureRevocationEvidence
Certificates [][]byte
Fetch func(SignatureRevocationRequest) (SignatureRevocationEvidence, error)
Required bool
VerifyTime *time.Time
MaxAge time.Duration
}
// SignatureRevocationReport 单张证书吊销检查结果
// Checked仅表示已取得经验证的状态结论, OK仅在状态为good时为true
type SignatureRevocationReport struct {
Certificate SignatureCertInfo
Source string
Status string
Checked bool
OK bool
ThisUpdate time.Time
NextUpdate time.Time
RevokedAt time.Time
Error string
}
// WithSignatureRevocation 设置离线吊销证据检查
// 入参: options 吊销选项
// 返回: SignatureVerifyOption 验签选项
func WithSignatureRevocation(options SignatureRevocationOptions) SignatureVerifyOption {
options.CRLs = cloneSignatureEvidence(options.CRLs)
options.OCSPResponses = cloneSignatureEvidence(options.OCSPResponses)
options.Certificates = appendSignatureCerts(nil, options.Certificates...)
if options.VerifyTime != nil {
t := *options.VerifyTime
options.VerifyTime = &t
}
return func(o *signatureVerifyOptions) { o.Revocation = &options }
}
// VerifySignatureRevocation 验证单张证书的离线吊销证据
// 入参: cert 证书DER, issuer 直接颁发者DER, options 吊销选项
// 返回: SignatureRevocationReport 验证结果
func VerifySignatureRevocation(cert, issuer []byte, options SignatureRevocationOptions) SignatureRevocationReport {
result := SignatureRevocationReport{Certificate: signatureCertInfo(cert), Status: "unknown"}
c, err := parseSignatureCertificate(cert)
if err != nil {
result.Error = err.Error()
return result
}
i, err := parseSignatureCertificate(issuer)
if err != nil || !i.IsCA || i.KeyUsage != 0 && i.KeyUsage&smx509.KeyUsageCertSign == 0 || !bytes.Equal(c.RawIssuer, i.RawSubject) {
result.Error = "invalid revocation issuer"
return result
}
valid, err := verifyCertificateSignature(c, issuer)
if err != nil || !valid {
result.Error = "certificate issuer signature mismatch"
return result
}
at := time.Now()
if options.VerifyTime != nil {
at = *options.VerifyTime
}
if !timeInRange(at, i.NotBefore, i.NotAfter) || len(i.UnhandledCriticalExtensions) != 0 {
result.Error = "invalid revocation issuer validity"
return result
}
evidence := options.SignatureRevocationEvidence
if options.Fetch != nil {
fetched, err := options.Fetch(SignatureRevocationRequest{Certificate: bytes.Clone(cert), Issuer: bytes.Clone(issuer), VerifyTime: at})
if err != nil {
result.Error = err.Error()
return result
}
evidence.CRLs = append(cloneSignatureEvidence(evidence.CRLs), fetched.CRLs...)
evidence.OCSPResponses = append(cloneSignatureEvidence(evidence.OCSPResponses), fetched.OCSPResponses...)
}
var good *SignatureRevocationReport
for _, raw := range evidence.CRLs {
item := verifySignatureCRL(raw, cert, issuer, at, options.MaxAge)
if item.Checked && item.Status == "revoked" {
return item
}
if item.OK {
copy := item
good = &copy
} else if item.Error != "" {
result.Error = item.Error
}
}
for _, raw := range evidence.OCSPResponses {
item := verifySignatureOCSP(raw, cert, issuer, at, options.MaxAge)
if item.Checked && item.Status == "revoked" {
return item
}
if item.OK {
copy := item
good = &copy
} else if item.Error != "" {
result.Error = item.Error
}
if item.Checked && item.Status == "unknown" && !result.Checked {
result = item
}
}
if good != nil {
return *good
}
if result.Error == "" {
result.Error = "usable revocation evidence not found"
}
return result
}
// signatureEvidenceFresh 检查证据时间窗口及最大年龄
// 入参: thisUpdate 生效时间, nextUpdate 下次更新时间, at 验证时间, maxAge 最大年龄
// 返回: bool 证据是否新鲜
func signatureEvidenceFresh(thisUpdate, nextUpdate, at time.Time, maxAge time.Duration) bool {
if thisUpdate.IsZero() || thisUpdate.After(at) || maxAge < 0 {
return false
}
if maxAge > 0 && at.Sub(thisUpdate) > maxAge {
return false
}
if nextUpdate.IsZero() {
return maxAge > 0
}
return nextUpdate.After(thisUpdate) && at.Before(nextUpdate)
}
// verifySignatureCRL 验证直接完整CRL及目标序列号
// 入参: raw CRL字节, cert 证书, issuer 颁发者, at 验证时间, maxAge 最大年龄
// 返回: SignatureRevocationReport 吊销结果
func verifySignatureCRL(raw, cert, issuer []byte, at time.Time, maxAge time.Duration) (result SignatureRevocationReport) {
result = SignatureRevocationReport{Certificate: signatureCertInfo(cert), Source: "CRL", Status: "unknown"}
err := func() error {
crl, err := smx509.ParseRevocationList(raw)
if err != nil {
return err
}
parent, err := parseSignatureCertificate(issuer)
if err != nil {
return err
}
if !bytes.Equal(crl.RawIssuer, parent.RawSubject) {
return fmt.Errorf("CRL issuer mismatch")
}
if len(crl.AuthorityKeyId) != 0 && !bytes.Equal(crl.AuthorityKeyId, parent.SubjectKeyId) {
return fmt.Errorf("CRL authority key mismatch")
}
if err := crl.CheckSignatureFrom(parent); err != nil {
return err
}
for _, ext := range crl.Extensions {
if ext.Id.String() == "2.5.29.27" || ext.Id.String() == "2.5.29.28" {
return fmt.Errorf("delta or scoped CRL not supported")
}
if ext.Critical {
return fmt.Errorf("unsupported critical CRL extension")
}
}
result.ThisUpdate, result.NextUpdate = crl.ThisUpdate, crl.NextUpdate
if !signatureEvidenceFresh(crl.ThisUpdate, crl.NextUpdate, at, maxAge) {
return fmt.Errorf("stale or future CRL")
}
c, err := parseSignatureCertificate(cert)
if err != nil {
return err
}
for _, entry := range crl.RevokedCertificateEntries {
for _, ext := range entry.Extensions {
if ext.Critical || ext.Id.String() == "2.5.29.29" {
return fmt.Errorf("unsupported CRL entry extension")
}
}
if entry.SerialNumber.Cmp(c.SerialNumber) == 0 {
if entry.ReasonCode == 8 {
return fmt.Errorf("removeFromCRL requires delta processing")
}
if entry.RevocationTime.After(crl.ThisUpdate) {
return fmt.Errorf("CRL revocation time is in the future")
}
result.Checked, result.Status, result.RevokedAt = true, "revoked", entry.RevocationTime
return nil
}
}
result.Checked, result.OK, result.Status = true, true, "good"
return nil
}()
if err != nil {
result.Error = err.Error()
}
return result
}
// verifySignatureOCSP 验证OCSP签名、授权、颁发者绑定及状态
// 入参: raw 响应DER, cert 证书, issuer 颁发者, at 验证时间, maxAge 最大年龄
// 返回: SignatureRevocationReport 吊销结果
func verifySignatureOCSP(raw, cert, issuer []byte, at time.Time, maxAge time.Duration) (result SignatureRevocationReport) {
result = SignatureRevocationReport{Certificate: signatureCertInfo(cert), Source: "OCSP", Status: "unknown"}
err := func() error {
c, err := x509.ParseCertificate(cert)
if err != nil {
return fmt.Errorf("unsupported OCSP certificate: %w", err)
}
parent, err := x509.ParseCertificate(issuer)
if err != nil {
return err
}
response, err := ocsp.ParseResponseForCert(raw, c, parent)
if err != nil {
return err
}
if err := verifyOCSPCertID(response.TBSResponseData, c, parent, response.IssuerHash); err != nil {
return err
}
responder := parent
if response.Certificate != nil && !bytes.Equal(response.Certificate.Raw, parent.Raw) {
responder = response.Certificate
if !bytes.Equal(responder.RawIssuer, parent.RawSubject) || responder.CheckSignatureFrom(parent) != nil {
return fmt.Errorf("OCSP responder is not issued by certificate issuer")
}
allowed := false
for _, usage := range responder.ExtKeyUsage {
allowed = allowed || usage == x509.ExtKeyUsageOCSPSigning
}
if !allowed || responder.KeyUsage != 0 && responder.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
return fmt.Errorf("unauthorized OCSP responder")
}
}
if len(responder.UnhandledCriticalExtensions) != 0 || !timeInRange(at, responder.NotBefore, responder.NotAfter) || !timeInRange(response.ProducedAt, responder.NotBefore, responder.NotAfter) {
return fmt.Errorf("OCSP responder certificate not valid")
}
if len(response.RawResponderName) != 0 && !bytes.Equal(response.RawResponderName, responder.RawSubject) {
return fmt.Errorf("OCSP responder name mismatch")
}
if len(response.ResponderKeyHash) != 0 {
var spki struct {
Algorithm pkix.AlgorithmIdentifier
Key asn1.BitString
}
if _, err := asn1.Unmarshal(responder.RawSubjectPublicKeyInfo, &spki); err != nil {
return err
}
if !bytes.Equal(response.ResponderKeyHash, signatureHashBytes(crypto.SHA1, spki.Key.Bytes)) {
return fmt.Errorf("OCSP responder key mismatch")
}
}
result.ThisUpdate, result.NextUpdate, result.RevokedAt = response.ThisUpdate, response.NextUpdate, response.RevokedAt
if !signatureEvidenceFresh(response.ThisUpdate, response.NextUpdate, at, maxAge) || response.ProducedAt.After(at) || response.ProducedAt.Before(response.ThisUpdate) {
return fmt.Errorf("stale or future OCSP response")
}
switch response.Status {
case ocsp.Good:
result.Checked, result.OK, result.Status = true, true, "good"
case ocsp.Revoked:
if response.RevokedAt.IsZero() || response.RevokedAt.After(response.ThisUpdate) {
return fmt.Errorf("invalid OCSP revocation time")
}
result.Checked, result.Status = true, "revoked"
case ocsp.Unknown:
result.Checked = true
default:
return fmt.Errorf("unsupported OCSP status")
}
return nil
}()
if err != nil {
result.Error = err.Error()
}
return result
}
// verifyOCSPCertID 补充校验x/crypto未验证的颁发者摘要和重复CertID
// 入参: tbs 响应待签名数据, cert 证书, issuer 颁发者, hash CertID摘要算法
// 返回: error 绑定错误
func verifyOCSPCertID(tbs []byte, cert, issuer *x509.Certificate, hash crypto.Hash) error {
var data struct {
Version int `asn1:"optional,explicit,tag:0"`
Responder asn1.RawValue
ProducedAt time.Time `asn1:"generalized"`
Responses []asn1.RawValue
Extensions []pkix.Extension `asn1:"optional,explicit,tag:1"`
}
rest, err := asn1.Unmarshal(tbs, &data)
if err != nil || len(rest) != 0 || data.Version != 0 {
return fmt.Errorf("invalid OCSP response data")
}
for _, ext := range data.Extensions {
if ext.Critical {
return fmt.Errorf("unsupported critical OCSP extension")
}
}
var spki struct {
Algorithm pkix.AlgorithmIdentifier
Key asn1.BitString
}
if _, err := asn1.Unmarshal(issuer.RawSubjectPublicKeyInfo, &spki); err != nil {
return err
}
matched := 0
for _, raw := range data.Responses {
fields, ok := asn1Children(raw.Bytes)
if !ok || len(fields) < 3 {
return fmt.Errorf("invalid OCSP single response")
}
id, ok := asn1Children(fields[0].Bytes)
if !ok || len(id) != 4 {
return fmt.Errorf("invalid OCSP CertID")
}
serial, err := asn1IntegerBig(id[3])
if err != nil {
return err
}
if serial.Cmp(cert.SerialNumber) != 0 {
continue
}
matched++
nameHash, err := asn1OctetString(id[1])
if err != nil {
return err
}
keyHash, err := asn1OctetString(id[2])
if err != nil {
return err
}
if !bytes.Equal(nameHash, signatureHashBytes(hash, issuer.RawSubject)) || !bytes.Equal(keyHash, signatureHashBytes(hash, spki.Key.Bytes)) {
return fmt.Errorf("OCSP issuer hash mismatch")
}
}
if matched != 1 {
return fmt.Errorf("ambiguous OCSP certificate status")
}
return nil
}
// applySignatureRevocation 检查签署者及制章者的离线吊销证据
// 入参: o 验签选项, certs 目标证书, extra 辅助证书
func (report *SignatureVerifyReport) applySignatureRevocation(o *signatureVerifyOptions, certs, extra [][]byte) {
var options SignatureRevocationOptions
if o.Revocation != nil {
options = *o.Revocation
}
if o.Policy != nil {
options.Required = options.Required || o.Policy.RequireRevocation
}
if o.Revocation == nil && !options.Required {
return
}
at := time.Now()
if o.VerifyTime != nil {
at = *o.VerifyTime
}
if options.VerifyTime != nil {
at = *options.VerifyTime
}
options.VerifyTime = &at
pool := appendSignatureCerts(nil, options.Certificates...)
pool = append(pool, extra...)
pool = append(pool, o.SignCerts...)
pool = append(pool, o.TrustCerts...)
pool = compactSignatureCerts(pool)
for _, raw := range compactSignatureCerts(certs) {
result := SignatureRevocationReport{Certificate: signatureCertInfo(raw), Status: "unknown", Error: "revocation issuer not found"}
cert, err := parseSignatureCertificate(raw)
if err == nil {
for _, issuerRaw := range pool {
issuer, err := parseSignatureCertificate(issuerRaw)
if err != nil || !bytes.Equal(cert.RawIssuer, issuer.RawSubject) {
continue
}
ok, err := verifyCertificateSignature(cert, issuerRaw)
if err != nil || !ok {
continue
}
result = VerifySignatureRevocation(raw, issuerRaw, options)
break
}
}
report.Revocations = append(report.Revocations, result)
}
if len(report.Revocations) != 0 {
report.RevocationChecked, report.RevocationOK = true, true
for _, result := range report.Revocations {
report.RevocationOK = report.RevocationOK && result.Checked && result.OK
}
}
if options.Required && !report.RevocationChecked {
report.PolicyChecked, report.PolicyOK, report.PolicyError = true, false, "required revocation evidence missing"
}
}