Switched module path and imports to gitea.tryanks.com. Removed GitHub-specific files like workflows, dependabot, and devcontainer as part of migration. This streamlines the codebase for the new hosting environment.
410 lines
12 KiB
Go
410 lines
12 KiB
Go
package verify
|
||
|
||
import (
|
||
"bytes"
|
||
"crypto/x509"
|
||
"encoding/asn1"
|
||
"fmt"
|
||
"io"
|
||
"os"
|
||
"reflect"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"gitea.tryanks.com/Tryanks/pdfsign/revocation"
|
||
"github.com/digitorus/pdf"
|
||
|
||
"github.com/digitorus/pkcs7"
|
||
"github.com/digitorus/timestamp"
|
||
"golang.org/x/crypto/ocsp"
|
||
)
|
||
|
||
type Response struct {
|
||
Error string
|
||
|
||
DocumentInfo DocumentInfo
|
||
Signers []Signer
|
||
}
|
||
|
||
type Signer struct {
|
||
Name string `json:"name"`
|
||
Reason string `json:"reason"`
|
||
Location string `json:"location"`
|
||
ContactInfo string `json:"contact_info"`
|
||
ValidSignature bool `json:"valid_signature"`
|
||
TrustedIssuer bool `json:"trusted_issuer"`
|
||
RevokedCertificate bool `json:"revoked_certificate"`
|
||
Certificates []Certificate `json:"certificates"`
|
||
TimeStamp *timestamp.Timestamp `json:"time_stamp"`
|
||
}
|
||
|
||
type Certificate struct {
|
||
Certificate *x509.Certificate `json:"certificate"`
|
||
VerifyError string `json:"verify_error"`
|
||
OCSPResponse *ocsp.Response `json:"ocsp_response"`
|
||
OCSPEmbedded bool `json:"ocsp_embedded"`
|
||
CRLRevoked time.Time `json:"crl_revoked"`
|
||
CRLEmbedded bool `json:"crl_embedded"`
|
||
}
|
||
|
||
// DocumentInfo contains document information.
|
||
type DocumentInfo struct {
|
||
Author string `json:"author"`
|
||
Creator string `json:"creator"`
|
||
Hash string `json:"hash"`
|
||
Name string `json:"name"`
|
||
Permission string `json:"permission"`
|
||
Producer string `json:"producer"`
|
||
Subject string `json:"subject"`
|
||
Title string `json:"title"`
|
||
|
||
Pages int `json:"pages"`
|
||
Keywords []string `json:"keywords"`
|
||
ModDate time.Time `json:"mod_date"`
|
||
CreationDate time.Time `json:"creation_date"`
|
||
}
|
||
|
||
func File(file *os.File) (apiResp *Response, err error) {
|
||
finfo, _ := file.Stat()
|
||
if _, err := file.Seek(0, 0); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return Reader(file, finfo.Size())
|
||
}
|
||
|
||
func Reader(file io.ReaderAt, size int64) (apiResp *Response, err error) {
|
||
var documentInfo DocumentInfo
|
||
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
apiResp = nil
|
||
err = fmt.Errorf("Failed to verify file (%v)", r)
|
||
}
|
||
}()
|
||
apiResp = &Response{}
|
||
|
||
rdr, err := pdf.NewReader(file, size)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("Failed to open file: %v", err)
|
||
}
|
||
|
||
// AcroForm will contain a SigFlags value if the form contains a digital signature
|
||
t := rdr.Trailer().Key("Root").Key("AcroForm").Key("SigFlags")
|
||
if t.IsNull() {
|
||
return nil, fmt.Errorf("No digital signature in document")
|
||
}
|
||
|
||
// Walk over the cross references in the document
|
||
for _, x := range rdr.Xref() {
|
||
// Get the xref object Value
|
||
v := rdr.Resolve(x.Ptr(), x.Ptr())
|
||
|
||
// get document info
|
||
parseDocumentInfo(v, &documentInfo)
|
||
|
||
// We must have a Filter Adobe.PPKLite
|
||
if v.Key("Filter").Name() != "Adobe.PPKLite" {
|
||
continue
|
||
}
|
||
|
||
signer := Signer{
|
||
Name: v.Key("Name").Text(),
|
||
Reason: v.Key("Reason").Text(),
|
||
Location: v.Key("Location").Text(),
|
||
ContactInfo: v.Key("ContactInfo").Text(),
|
||
}
|
||
|
||
// (Required) The signature value. When ByteRange is present, the
|
||
// value shall be a hexadecimal string (see 7.3.4.3, “Hexadecimal
|
||
// Stringsâ€) representing the value of the byte range digest.
|
||
// For public-key signatures, Contents should be either a DER-encoded
|
||
// PKCS#1 binary data object or a DER-encoded PKCS#7 binary data object.
|
||
// Space for the Contents value must be allocated before the message
|
||
// digest is computed. (See 7.3.4, “String Objects“)
|
||
p7, err := pkcs7.Parse([]byte(v.Key("Contents").RawString()))
|
||
if err != nil {
|
||
// fmt.Println(err)
|
||
continue
|
||
}
|
||
|
||
// An array of pairs of integers (starting byte offset, length in
|
||
// bytes) that shall describe the exact byte range for the digest
|
||
// calculation. Multiple discontiguous byte ranges shall be used to
|
||
// describe a digest that does not include the signature value (the
|
||
// Contents entry) itself.
|
||
for i := 0; i < v.Key("ByteRange").Len(); i++ {
|
||
// As the byte range comes in pairs, we increment one extra
|
||
i++
|
||
|
||
// Read the byte range from the raw file and add it to the contents.
|
||
// This content will be hashed with the corresponding algorithm to
|
||
// verify the signature.
|
||
|
||
content, err := io.ReadAll(io.NewSectionReader(file, v.Key("ByteRange").Index(i-1).Int64(), v.Key("ByteRange").Index(i).Int64()))
|
||
if err != nil {
|
||
apiResp.Error = fmt.Sprintln("Failed to get ByteRange:", i, err)
|
||
}
|
||
|
||
p7.Content = append(p7.Content, content...)
|
||
}
|
||
|
||
// Signer certificate
|
||
// http://www.alvestrand.no/objectid/1.2.840.113549.1.9.html
|
||
// http://www.alvestrand.no/objectid/1.2.840.113583.1.1.8.html
|
||
// var isn []byte
|
||
for _, s := range p7.Signers {
|
||
// isn = s.IssuerAndSerialNumber.IssuerName.FullBytes
|
||
// for _, a := range s.AuthenticatedAttributes {
|
||
// fmt.Printf("A: %v, %#v\n", s.IssuerAndSerialNumber.SerialNumber, a.Type)
|
||
//}
|
||
|
||
// Timestamp
|
||
// http://www.alvestrand.no/objectid/1.2.840.113549.1.9.16.2.14.html
|
||
// Timestamp
|
||
// 1.2.840.113549.1.9.16.2.14 - RFC 3161 id-aa-timeStampToken
|
||
for _, attr := range s.UnauthenticatedAttributes {
|
||
// fmt.Printf("U: %v, %#v\n", s.IssuerAndSerialNumber.SerialNumber, attr.Type)
|
||
|
||
if attr.Type.Equal(asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}) {
|
||
// fmt.Println("Found timestamp")
|
||
|
||
signer.TimeStamp, err = timestamp.Parse(attr.Value.Bytes)
|
||
if err != nil {
|
||
apiResp.Error = fmt.Sprintln("Failed to parse timestamp", err)
|
||
} else {
|
||
r := bytes.NewReader(s.EncryptedDigest)
|
||
|
||
h := signer.TimeStamp.HashAlgorithm.New()
|
||
b := make([]byte, h.Size())
|
||
for {
|
||
n, err := r.Read(b)
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
|
||
h.Write(b[:n])
|
||
}
|
||
|
||
if !bytes.Equal(h.Sum(nil), signer.TimeStamp.HashedMessage) {
|
||
apiResp.Error = fmt.Sprintln("Timestamp hash does not match")
|
||
}
|
||
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Directory of certificates, including OCSP
|
||
certPool := x509.NewCertPool()
|
||
for _, cert := range p7.Certificates {
|
||
certPool.AddCert(cert)
|
||
}
|
||
|
||
// Verify the digital signature of the pdf file.
|
||
err = p7.VerifyWithChain(certPool)
|
||
if err != nil {
|
||
err = p7.Verify()
|
||
if err == nil {
|
||
signer.ValidSignature = true
|
||
signer.TrustedIssuer = false
|
||
} else {
|
||
apiResp.Error = fmt.Sprintln("Failed to verify signature:", err)
|
||
}
|
||
} else {
|
||
signer.ValidSignature = true
|
||
signer.TrustedIssuer = true
|
||
}
|
||
|
||
// PDF signature certificate revocation information attribute (1.2.840.113583.1.1.8)
|
||
var revInfo revocation.InfoArchival
|
||
_ = p7.UnmarshalSignedAttribute(asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, &revInfo)
|
||
|
||
// Parse OCSP response
|
||
ocspStatus := make(map[string]*ocsp.Response)
|
||
for _, o := range revInfo.OCSP {
|
||
resp, err := ocsp.ParseResponse(o.FullBytes, nil)
|
||
if err != nil {
|
||
apiResp.Error = fmt.Sprintln("Failed to parse or verify OCSP response", err)
|
||
ocspStatus[fmt.Sprintf("%x", resp.SerialNumber)] = nil
|
||
} else {
|
||
ocspStatus[fmt.Sprintf("%x", resp.SerialNumber)] = resp
|
||
}
|
||
}
|
||
|
||
// Build certificate chains and verify revocation status
|
||
for _, cert := range p7.Certificates {
|
||
var c Certificate
|
||
c.Certificate = cert
|
||
|
||
chain, err := cert.Verify(x509.VerifyOptions{
|
||
Intermediates: certPool,
|
||
CurrentTime: cert.NotBefore,
|
||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||
})
|
||
if err != nil {
|
||
c.VerifyError = err.Error()
|
||
}
|
||
|
||
if resp, ok := ocspStatus[fmt.Sprintf("%x", cert.SerialNumber)]; ok {
|
||
c.OCSPResponse = resp
|
||
c.OCSPEmbedded = true
|
||
|
||
if resp.Status != ocsp.Good {
|
||
signer.RevokedCertificate = true
|
||
}
|
||
|
||
if len(chain) > 0 && len(chain[0]) > 1 {
|
||
issuer := chain[0][1]
|
||
if resp.Certificate != nil {
|
||
err = resp.Certificate.CheckSignatureFrom(issuer)
|
||
if err != nil {
|
||
apiResp.Error = fmt.Sprintln("OCSP signing cerificate not from certificate issuer:", err)
|
||
}
|
||
} else {
|
||
// CA Signed response
|
||
err = resp.CheckSignatureFrom(issuer)
|
||
if err != nil {
|
||
apiResp.Error = fmt.Sprintln("Failed to verify OCSP response signature:", err)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// else {
|
||
// // Check OCSP status for certificate out of band
|
||
// }
|
||
|
||
// Add certificate to result
|
||
signer.Certificates = append(signer.Certificates, c)
|
||
}
|
||
|
||
// Certificate revocation lists when included in this document
|
||
// for _, crl := range p7.CRLs {
|
||
// //var crlissuer *pkix.Name
|
||
// //crlissuerdr.FillFromRDNSequence(&crl.TBSCertList.Issuer)
|
||
// if len(crl.TBSCertList.RevokedCertificates) > 0 {
|
||
|
||
// }
|
||
// //apiResp.Error = fmt.Sprintf("CRL %v , with %d entries\n", crl.TBSCertList.Issuer, len(crl.TBSCertList.RevokedCertificates))
|
||
// // TODO(vanbroup): Check revocation via CRL
|
||
// // signer.RevokedCertificate = true
|
||
// }
|
||
|
||
// Parse CRL file
|
||
// for _, c := range revInfo.CRL {
|
||
// crl, err := x509.ParseCRL(c.FullBytes)
|
||
// if err != nil {
|
||
// apiResp.Error = fmt.Sprintln("Failed to parse or verify embedded CRL")
|
||
// }
|
||
|
||
// if len(crl.TBSCertList.RevokedCertificates) > 0 {
|
||
|
||
// }
|
||
|
||
// //var crlissuer *pkix.Name
|
||
// //crlissuerdr.FillFromRDNSequence(&crl.TBSCertList.Issuer)
|
||
// //apiResp.Error = fmt.Sprintf("CRL %v , with %d entries\n", crl.TBSCertList.Issuer, len(crl.TBSCertList.RevokedCertificates))
|
||
// // TODO(vanbroup): Check revocation via CRL
|
||
// // signer.RevokedCertificate = true
|
||
// }
|
||
|
||
// If SubFilter is adbe.pkcs7.detached or adbe.pkcs7.sha1, this entry
|
||
// shall not be used, and the certificate chain shall be put in the PKCS#7
|
||
// envelope in Contents.
|
||
// v.Key("Cert").Text()
|
||
|
||
apiResp.Signers = append(apiResp.Signers, signer)
|
||
}
|
||
|
||
if apiResp == nil {
|
||
err = fmt.Errorf("Document looks to have a signature but got no results")
|
||
}
|
||
|
||
apiResp.DocumentInfo = documentInfo
|
||
|
||
return
|
||
}
|
||
|
||
// parseDocumentInfo parses document information.
|
||
func parseDocumentInfo(v pdf.Value, documentInfo *DocumentInfo) {
|
||
keys := []string{
|
||
"Author", "CreationDate", "Creator", "Hash", "Keywords", "ModDate",
|
||
"Name", "Pages", "Permission", "Producer", "Subject", "Title",
|
||
}
|
||
|
||
for _, key := range keys {
|
||
value := v.Key(key)
|
||
if !value.IsNull() {
|
||
// get string value
|
||
valueStr := value.Text()
|
||
|
||
// get struct field
|
||
elem := reflect.ValueOf(documentInfo).Elem()
|
||
field := elem.FieldByName(key)
|
||
|
||
switch key {
|
||
// parse dates
|
||
case "CreationDate", "ModDate":
|
||
t, _ := parseDate(valueStr)
|
||
field.Set(reflect.ValueOf(t))
|
||
// parse pages
|
||
case "Pages":
|
||
i, _ := strconv.Atoi(valueStr)
|
||
documentInfo.Pages = i
|
||
case "Keywords":
|
||
documentInfo.Keywords = parseKeywords(valueStr)
|
||
default:
|
||
field.Set(reflect.ValueOf(valueStr))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// parseDate parses pdf formatted dates.
|
||
func parseDate(v string) (time.Time, error) {
|
||
// PDF Date Format
|
||
// (D:YYYYMMDDHHmmSSOHH'mm')
|
||
//
|
||
// where
|
||
//
|
||
// YYYY is the year
|
||
// MM is the month
|
||
// DD is the day (01-31)
|
||
// HH is the hour (00-23)
|
||
// mm is the minute (00-59)
|
||
// SS is the second (00-59)
|
||
// O is the relationship of local time to Universal Time (UT), denoted by one of the characters +, -, or Z (see below)
|
||
// HH followed by ' is the absolute value of the offset from UT in hours (00-23)
|
||
// mm followed by ' is the absolute value of the offset from UT in minutes (00-59)
|
||
|
||
// 2006-01-02T15:04:05Z07:00
|
||
// (D:YYYYMMDDHHmmSSOHH'mm')
|
||
return time.Parse("D:20060102150405Z07'00'", v)
|
||
}
|
||
|
||
// parseKeywords parses keywords pdf meta data.
|
||
func parseKeywords(value string) []string {
|
||
// keywords must be separated by commas or semicolons or could be just separated with spaces, after the semicolon could be a space
|
||
// https://stackoverflow.com/questions/44608608/the-separator-between-keywords-in-pdf-meta-data
|
||
separators := []string{", ", ": ", ",", ":", " "}
|
||
for _, s := range separators {
|
||
if strings.Contains(value, s) {
|
||
return strings.Split(value, s)
|
||
}
|
||
}
|
||
|
||
return []string{value}
|
||
}
|
||
|
||
// func walk(t pdf.Value, pad int) {
|
||
// for _, k := range t.Keys() {
|
||
// v := t.Key(k)
|
||
// if v.Kind() == pdf.Array || v.Kind() == pdf.Dict {
|
||
// pad++
|
||
// walk(v, pad)
|
||
// }
|
||
// }
|
||
// }
|