Skip to content

Commit f9f7d40

Browse files
authored
feature: parse all certificates from multi-signed files (#95)
* feat: parse multiple certificates in section Multiple certificates are not, like the current code assumes, appended; instead, a specific (unsigned) attribute is nested inside the PKCS7 struct that contains the next PKCS7 struct. Also improve the returned certificates to include structs for all parsed, PKCS7 structs. * fix: Extract signature algorithm from authenticode The signature algorithm listed in the signing certificate is not necessarily the signature algorithm used for the file signature. Extract the algorithm for the file signature from the authemticode instead.
1 parent 8846571 commit f9f7d40

File tree

6 files changed

+231
-161
lines changed

6 files changed

+231
-161
lines changed

cmd/dump.go

+12-8
Original file line numberDiff line numberDiff line change
@@ -575,14 +575,18 @@ func parsePE(filename string, cfg config) {
575575
w.Flush()
576576
fmt.Print("\n ---Raw Certificate dump---\n")
577577
hexDump(cert.Raw)
578-
fmt.Print("\n---Certificate ---\n\n")
579-
fmt.Fprintf(w, "Issuer Name:\t %s\n", cert.Info.Issuer)
580-
fmt.Fprintf(w, "Subject Name:\t %s\n", cert.Info.Subject)
581-
fmt.Fprintf(w, "Serial Number:\t %x\n", cert.Info.SerialNumber)
582-
fmt.Fprintf(w, "Validity From:\t %s to %s\n", cert.Info.NotBefore.String(), cert.Info.NotAfter.String())
583-
fmt.Fprintf(w, "Signature Algorithm:\t %s\n", cert.Info.SignatureAlgorithm.String())
584-
fmt.Fprintf(w, "PublicKey Algorithm:\t %s\n", cert.Info.PublicKeyAlgorithm.String())
585-
w.Flush()
578+
for _, cert := range cert.Certificates {
579+
fmt.Print("\n---Certificate ---\n\n")
580+
fmt.Fprintf(w, "Issuer Name:\t %s\n", cert.Info.Issuer)
581+
fmt.Fprintf(w, "Subject Name:\t %s\n", cert.Info.Subject)
582+
fmt.Fprintf(w, "Serial Number:\t %x\n", cert.Info.SerialNumber)
583+
fmt.Fprintf(w, "Validity From:\t %s to %s\n", cert.Info.NotBefore.String(), cert.Info.NotAfter.String())
584+
fmt.Fprintf(w, "Signature Algorithm:\t %s\n", cert.Info.SignatureAlgorithm.String())
585+
fmt.Fprintf(w, "PublicKey Algorithm:\t %s\n", cert.Info.PublicKeyAlgorithm.String())
586+
fmt.Fprintf(w, "Certificate valid:\t %v\n", cert.Verified)
587+
fmt.Fprintf(w, "Signature valid:\t %v\n", cert.SignatureValid)
588+
w.Flush()
589+
}
586590

587591
// Calculate the PE authentihash.
588592
pe.Authentihash()

file.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type File struct {
2727
TLS TLSDirectory `json:"tls,omitempty"`
2828
LoadConfig LoadConfig `json:"load_config,omitempty"`
2929
Exceptions []Exception `json:"exceptions,omitempty"`
30-
Certificates Certificate `json:"certificates,omitempty"`
30+
Certificates CertificateSection `json:"certificates,omitempty"`
3131
DelayImports []DelayImport `json:"delay_imports,omitempty"`
3232
BoundImports []BoundImportDescriptorData `json:"bound_imports,omitempty"`
3333
GlobalPtr uint32 `json:"global_ptr,omitempty"`

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ go 1.15
44

55
require (
66
github.com/edsrzf/mmap-go v1.1.0
7-
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
7+
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d
88
golang.org/x/text v0.7.0
99
)

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
22
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
3+
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d h1:RQqyEogx5J6wPdoxqL132b100j8KjcVHO1c0KLRoIhc=
4+
github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d/go.mod h1:PegD7EVqlN88z7TpCqH92hHP+GBpfomGCCnw1PFtNOA=
35
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
4-
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak=
5-
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
66
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
77
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
88
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=

security.go

+112-101
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@ import (
1919
"os"
2020
"os/exec"
2121
"path/filepath"
22-
"reflect"
2322
"runtime"
2423
"sort"
2524
"strings"
2625
"time"
2726

28-
"go.mozilla.org/pkcs7"
27+
"github.com/secDre4mer/pkcs7"
2928
)
3029

3130
// The options for the WIN_CERTIFICATE Revision member include
@@ -66,13 +65,18 @@ var (
6665
`invalid certificate header in security directory`)
6766
)
6867

68+
type CertificateSection struct {
69+
Header WinCertificate `json:"header"`
70+
Raw []byte `json:"-"`
71+
72+
Certificates []Certificate
73+
}
74+
6975
// Certificate directory.
7076
type Certificate struct {
71-
Header WinCertificate `json:"header"`
7277
Content pkcs7.PKCS7 `json:"-"`
7378
SignatureContent AuthenticodeContent `json:"-"`
7479
SignatureValid bool `json:"signature_valid"`
75-
Raw []byte `json:"-"`
7680
Info CertInfo `json:"info"`
7781
Verified bool `json:"verified"`
7882
}
@@ -309,101 +313,68 @@ func (pe *File) AuthentihashExt(hashers ...hash.Hash) [][]byte {
309313
// bind an Authenticode-signed file to the identity of a software publisher.
310314
// This data are not loaded into memory as part of the image file.
311315
func (pe *File) parseSecurityDirectory(rva, size uint32) error {
312-
313-
var pkcs *pkcs7.PKCS7
314-
var certValid bool
315-
certInfo := CertInfo{}
316-
certHeader := WinCertificate{}
316+
var certHeader WinCertificate
317317
certSize := uint32(binary.Size(certHeader))
318318
signatureContent := AuthenticodeContent{}
319-
var signatureValid bool
320-
var certContent []byte
321319

322320
// The virtual address value from the Certificate Table entry in the
323321
// Optional Header Data Directory is a file offset to the first attribute
324322
// certificate entry.
325323
fileOffset := rva
326324

327-
// PE file can be dual signed by applying multiple signatures, which is
328-
// strongly recommended when using deprecated hashing algorithms such as MD5.
329-
for {
330-
err := pe.structUnpack(&certHeader, fileOffset, certSize)
331-
if err != nil {
332-
return ErrOutsideBoundary
333-
}
325+
err := pe.structUnpack(&certHeader, fileOffset, certSize)
326+
if err != nil {
327+
return ErrOutsideBoundary
328+
}
334329

335-
if fileOffset+certHeader.Length > pe.size {
336-
return ErrOutsideBoundary
337-
}
330+
if certHeader.Length > size {
331+
return ErrOutsideBoundary
332+
}
338333

339-
if certHeader.Length == 0 {
340-
return ErrSecurityDataDirInvalid
341-
}
334+
if fileOffset+certHeader.Length > pe.size {
335+
return ErrOutsideBoundary
336+
}
342337

343-
certContent = pe.data[fileOffset+certSize : fileOffset+certHeader.Length]
344-
pkcs, err = pkcs7.Parse(certContent)
338+
if certHeader.Length == 0 {
339+
return ErrSecurityDataDirInvalid
340+
}
341+
342+
pe.HasCertificate = true
343+
pe.Certificates.Header = certHeader
344+
pe.Certificates.Raw = pe.data[fileOffset+certSize : fileOffset+certHeader.Length]
345+
346+
certContent := pe.Certificates.Raw
347+
for {
348+
pkcs, err := pkcs7.Parse(certContent)
345349
if err != nil {
346-
pe.Certificates = Certificate{Header: certHeader, Raw: certContent}
347-
pe.HasCertificate = true
348350
return err
349351
}
350-
351352
// The pkcs7.PKCS7 structure contains many fields that we are not
352353
// interested to, so create another structure, similar to _CERT_INFO
353354
// structure which contains only the important information.
354-
serialNumber := pkcs.Signers[0].IssuerAndSerialNumber.SerialNumber
355-
for _, cert := range pkcs.Certificates {
356-
if !reflect.DeepEqual(cert.SerialNumber, serialNumber) {
357-
continue
358-
}
359-
360-
certInfo.SerialNumber = hex.EncodeToString(cert.SerialNumber.Bytes())
361-
certInfo.PublicKeyAlgorithm = cert.PublicKeyAlgorithm
362-
certInfo.SignatureAlgorithm = cert.SignatureAlgorithm
363-
364-
certInfo.NotAfter = cert.NotAfter
365-
certInfo.NotBefore = cert.NotBefore
366-
367-
// Issuer infos
368-
if len(cert.Issuer.Country) > 0 {
369-
certInfo.Issuer = cert.Issuer.Country[0]
370-
}
371-
372-
if len(cert.Issuer.Province) > 0 {
373-
certInfo.Issuer += ", " + cert.Issuer.Province[0]
374-
}
375-
376-
if len(cert.Issuer.Locality) > 0 {
377-
certInfo.Issuer += ", " + cert.Issuer.Locality[0]
378-
}
379-
380-
certInfo.Issuer += ", " + cert.Issuer.CommonName
381-
382-
// Subject infos
383-
if len(cert.Subject.Country) > 0 {
384-
certInfo.Subject = cert.Subject.Country[0]
385-
}
355+
var signerCertificate = pkcs.GetOnlySigner()
356+
if signerCertificate == nil {
357+
return errors.New("could not find signer certificate")
358+
}
386359

387-
if len(cert.Subject.Province) > 0 {
388-
certInfo.Subject += ", " + cert.Subject.Province[0]
389-
}
360+
var certInfo CertInfo
390361

391-
if len(cert.Subject.Locality) > 0 {
392-
certInfo.Subject += ", " + cert.Subject.Locality[0]
393-
}
362+
certInfo.SerialNumber = hex.EncodeToString(signerCertificate.SerialNumber.Bytes())
363+
certInfo.PublicKeyAlgorithm = signerCertificate.PublicKeyAlgorithm
394364

395-
if len(cert.Subject.Organization) > 0 {
396-
certInfo.Subject += ", " + cert.Subject.Organization[0]
397-
}
365+
certInfo.NotAfter = signerCertificate.NotAfter
366+
certInfo.NotBefore = signerCertificate.NotBefore
398367

399-
certInfo.Subject += ", " + cert.Subject.CommonName
368+
// Issuer infos
369+
certInfo.Issuer = formatPkixName(signerCertificate.Issuer)
400370

401-
break
402-
}
371+
// Subject infos
372+
certInfo.Subject = formatPkixName(signerCertificate.Subject)
403373

404374
// Let's mark the file as signed, then we verify if the signature is valid.
405375
pe.IsSigned = true
406376

377+
var certValid bool
407378
// Let's load the system root certs.
408379
if !pe.opts.DisableCertValidation {
409380
var certPool *x509.CertPool
@@ -425,6 +396,7 @@ func (pe *File) parseSecurityDirectory(rva, size uint32) error {
425396
}
426397
}
427398

399+
var signatureValid bool
428400
signatureContent, err = parseAuthenticodeContent(pkcs.Content)
429401
if err != nil {
430402
pe.logger.Errorf("could not parse authenticode content: %v", err)
@@ -434,24 +406,30 @@ func (pe *File) parseSecurityDirectory(rva, size uint32) error {
434406
signatureValid = bytes.Equal(authentihash, signatureContent.HashResult)
435407
}
436408

437-
// Subsequent entries are accessed by advancing that entry's dwLength
438-
// bytes, rounded up to an 8-byte multiple, from the start of the
439-
// current attribute certificate entry.
440-
nextOffset := certHeader.Length + fileOffset
441-
nextOffset = ((nextOffset + 8 - 1) / 8) * 8
409+
certInfo.SignatureAlgorithm = signatureContent.Algorithm
442410

443-
// Check if we walked the entire table.
444-
if nextOffset == fileOffset+size {
445-
break
446-
}
411+
pe.Certificates.Certificates = append(pe.Certificates.Certificates, Certificate{
412+
Content: *pkcs,
413+
SignatureContent: signatureContent,
414+
SignatureValid: signatureValid,
415+
Info: certInfo,
416+
Verified: certValid,
417+
})
447418

448-
fileOffset = nextOffset
419+
// Subsequent certificates are an (unsigned) attribute of the PKCS#7
420+
var newCert asn1.RawValue
421+
nestedSignatureOid := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 2, 4, 1}
422+
err = pkcs.UnmarshalUnsignedAttribute(nestedSignatureOid, &newCert)
423+
if err != nil {
424+
var attributeNotFound pkcs7.AttributeNotFoundError
425+
if errors.As(err, &attributeNotFound) {
426+
break // No further nested certificates
427+
}
428+
return err
429+
}
430+
certContent = newCert.FullBytes
449431
}
450432

451-
pe.Certificates = Certificate{Header: certHeader, Content: *pkcs,
452-
Raw: certContent, Info: certInfo, Verified: certValid,
453-
SignatureContent: signatureContent, SignatureValid: signatureValid}
454-
pe.HasCertificate = true
455433
return nil
456434
}
457435

@@ -542,26 +520,35 @@ type DigestInfo struct {
542520
}
543521

544522
// Translation of algorithm identifier to hash algorithm, copied from pkcs7.getHashForOID
545-
func parseHashAlgorithm(identifier pkix.AlgorithmIdentifier) (crypto.Hash, error) {
523+
func parseHashAlgorithm(identifier pkix.AlgorithmIdentifier) (crypto.Hash, x509.SignatureAlgorithm, error) {
546524
oid := identifier.Algorithm
547525
switch {
548-
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA1), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA1),
549-
oid.Equal(pkcs7.OIDDigestAlgorithmDSA), oid.Equal(pkcs7.OIDDigestAlgorithmDSASHA1),
550-
oid.Equal(pkcs7.OIDEncryptionAlgorithmRSA):
551-
return crypto.SHA1, nil
552-
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA256), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA256):
553-
return crypto.SHA256, nil
554-
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA384), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA384):
555-
return crypto.SHA384, nil
556-
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA512), oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA512):
557-
return crypto.SHA512, nil
558-
}
559-
return crypto.Hash(0), pkcs7.ErrUnsupportedAlgorithm
526+
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA1), oid.Equal(pkcs7.OIDEncryptionAlgorithmRSA):
527+
return crypto.SHA1, x509.SHA1WithRSA, nil
528+
case oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA1):
529+
return crypto.SHA1, x509.ECDSAWithSHA1, nil
530+
case oid.Equal(pkcs7.OIDDigestAlgorithmDSA), oid.Equal(pkcs7.OIDDigestAlgorithmDSASHA1):
531+
return crypto.SHA1, x509.DSAWithSHA1, nil
532+
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA256):
533+
return crypto.SHA256, x509.SHA256WithRSA, nil
534+
case oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA256):
535+
return crypto.SHA256, x509.ECDSAWithSHA256, nil
536+
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA384):
537+
return crypto.SHA384, x509.SHA256WithRSA, nil
538+
case oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA384):
539+
return crypto.SHA384, x509.ECDSAWithSHA384, nil
540+
case oid.Equal(pkcs7.OIDDigestAlgorithmSHA512):
541+
return crypto.SHA512, x509.ECDSAWithSHA512, nil
542+
case oid.Equal(pkcs7.OIDDigestAlgorithmECDSASHA512):
543+
return crypto.SHA512, x509.ECDSAWithSHA512, nil
544+
}
545+
return 0, 0, pkcs7.ErrUnsupportedAlgorithm
560546
}
561547

562548
// AuthenticodeContent provides a simplified view on SpcIndirectDataContent, which specifies the ASN.1 encoded values of
563549
// the authenticode signature content.
564550
type AuthenticodeContent struct {
551+
Algorithm x509.SignatureAlgorithm
565552
HashFunction crypto.Hash
566553
HashResult []byte
567554
}
@@ -576,12 +563,36 @@ func parseAuthenticodeContent(content []byte) (AuthenticodeContent, error) {
576563
if err != nil {
577564
return AuthenticodeContent{}, err
578565
}
579-
hashFunction, err := parseHashAlgorithm(authenticodeContent.MessageDigest.DigestAlgorithm)
566+
hashFunction, algorithmId, err := parseHashAlgorithm(authenticodeContent.MessageDigest.DigestAlgorithm)
580567
if err != nil {
581568
return AuthenticodeContent{}, err
582569
}
583570
return AuthenticodeContent{
571+
Algorithm: algorithmId,
584572
HashFunction: hashFunction,
585573
HashResult: authenticodeContent.MessageDigest.Digest,
586574
}, nil
587575
}
576+
577+
func formatPkixName(name pkix.Name) string {
578+
var formattedName string
579+
if len(name.Country) > 0 {
580+
formattedName = name.Country[0]
581+
}
582+
583+
if len(name.Province) > 0 {
584+
formattedName += ", " + name.Province[0]
585+
}
586+
587+
if len(name.Locality) > 0 {
588+
formattedName += ", " + name.Locality[0]
589+
}
590+
591+
if len(name.Organization) > 0 {
592+
formattedName += ", " + name.Organization[0]
593+
}
594+
595+
formattedName += ", " + name.CommonName
596+
597+
return formattedName
598+
}

0 commit comments

Comments
 (0)