Initial commit, without tests, docs, benchmarks etc
This commit is contained in:
306
signatures.go
Normal file
306
signatures.go
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
// http://wwwimages.adobe.com/content/dam/Adobe/en/devnet/pdf/pdfs/PDF32000_2008.pdf
|
||||||
|
|
||||||
|
package signatures
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/asn1"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"io/ioutil"
|
||||||
|
// "log"
|
||||||
|
// "strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bitbucket.org/digitorus/pdf"
|
||||||
|
"github.com/digitorus/timestamp"
|
||||||
|
"go.mozilla.org/pkcs7"
|
||||||
|
"golang.org/x/crypto/ocsp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RevocationInfoArchival struct {
|
||||||
|
CRL RevCRL `asn1:"tag:0,optional,explicit"`
|
||||||
|
OCSP RevOCSP `asn1:"tag:1,optional,explicit"`
|
||||||
|
OtherRevInfo OtherRevInfo `asn1:"tag:2,optional,explicit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RevCRL []asn1.RawValue
|
||||||
|
type RevOCSP []asn1.RawValue
|
||||||
|
|
||||||
|
type OtherRevInfo struct {
|
||||||
|
Type asn1.ObjectIdentifier
|
||||||
|
Value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Error string
|
||||||
|
|
||||||
|
DocumentInfo string
|
||||||
|
Signers []Signer
|
||||||
|
}
|
||||||
|
|
||||||
|
type Signer struct {
|
||||||
|
Name string
|
||||||
|
Reason string
|
||||||
|
Location string
|
||||||
|
ContactInfo string
|
||||||
|
ValidSignature bool
|
||||||
|
TrustedIssuer bool
|
||||||
|
RevokedCertificate bool
|
||||||
|
Certificates []Certificate
|
||||||
|
TimeStamp *timestamp.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
type Certificate struct {
|
||||||
|
Certificate *x509.Certificate
|
||||||
|
VerifyError string
|
||||||
|
OCSPResponse *ocsp.Response
|
||||||
|
OCSPEmbedded bool
|
||||||
|
CRLRevoked time.Time
|
||||||
|
CRLEmbedded bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func Verify(file *os.File) (apiResp *Response, err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
apiResp = nil
|
||||||
|
err = fmt.Errorf("Failed to verify file (%v)", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
apiResp = &Response{}
|
||||||
|
|
||||||
|
finfo, _ := file.Stat()
|
||||||
|
size := finfo.Size()
|
||||||
|
|
||||||
|
rdr, err := pdf.NewReader(file, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to open file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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())
|
||||||
|
|
||||||
|
// 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 := ioutil.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory of certificates, including OCSP
|
||||||
|
//var ica *x509.Certificate
|
||||||
|
certPool := x509.NewCertPool()
|
||||||
|
for _, cert := range p7.Certificates {
|
||||||
|
certPool.AddCert(cert)
|
||||||
|
if bytes.Equal(isn, cert.RawSubject) {
|
||||||
|
//ica = 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
|
||||||
|
}
|
||||||
|
//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 RevocationInfoArchival
|
||||||
|
p7.UnmarshalSignedAttribute(asn1.ObjectIdentifier{1, 2, 840, 113583, 1, 1, 8}, &revInfo)
|
||||||
|
|
||||||
|
// Parse OCSP response
|
||||||
|
var 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) > 1 && 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user