mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:59:27 +08:00
372 lines
12 KiB
Go
372 lines
12 KiB
Go
// 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的吊销状态
|
|
// CRL支持直接完整列表及增量合并, 尚不支持间接或带作用域列表
|
|
// 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
|
|
if len(evidence.CRLs) != 0 {
|
|
item := verifySignatureCRLs(evidence.CRLs, c, i, at, options.MaxAge)
|
|
if item.Checked && item.Status == "revoked" {
|
|
return item
|
|
}
|
|
if item.OK {
|
|
copy := item
|
|
good = ©
|
|
} 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 = ©
|
|
} 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)
|
|
}
|
|
|
|
// 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"
|
|
}
|
|
}
|