commit d9b24e317fd6b036e6a826e8f11b6a9c3e33648d Author: Paul van Brouwershaven Date: Fri Jun 23 08:57:08 2017 +0000 Initial commit, without tests, docs, benchmarks etc diff --git a/signatures.go b/signatures.go new file mode 100644 index 0000000..e3cad70 --- /dev/null +++ b/signatures.go @@ -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) + } + } +}