...

Source file src/github.com/sassoftware/relic/lib/authenticode/powershell.go

Documentation: github.com/sassoftware/relic/lib/authenticode

     1  //
     2  // Copyright (c) SAS Institute Inc.
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //     http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  //
    16  
    17  package authenticode
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"context"
    23  	"crypto"
    24  	"crypto/hmac"
    25  	"encoding/base64"
    26  	"encoding/binary"
    27  	"errors"
    28  	"fmt"
    29  	"io"
    30  	"io/ioutil"
    31  	"path/filepath"
    32  	"strings"
    33  	"unicode/utf16"
    34  
    35  	"github.com/sassoftware/relic/lib/binpatch"
    36  	"github.com/sassoftware/relic/lib/certloader"
    37  	"github.com/sassoftware/relic/lib/pkcs7"
    38  	"github.com/sassoftware/relic/lib/pkcs9"
    39  	"github.com/sassoftware/relic/lib/x509tools"
    40  	"github.com/sassoftware/relic/signers/sigerrors"
    41  )
    42  
    43  // Type of signature formatting used for different PowerShell file formats
    44  type PsSigStyle int
    45  
    46  const (
    47  	// "Hash" style used by e.g. .ps1 files
    48  	SigStyleHash PsSigStyle = iota + 1
    49  	// XML style used by e.g. .ps1xml files
    50  	SigStyleXML
    51  	// C# style used by .mof files
    52  	SigStyleC
    53  )
    54  
    55  var psExtMap = map[string]PsSigStyle{
    56  	".ps1":    SigStyleHash,
    57  	".ps1xml": SigStyleXML,
    58  	".psc1":   SigStyleXML,
    59  	".psd1":   SigStyleHash,
    60  	".psm1":   SigStyleHash,
    61  	".cdxml":  SigStyleXML,
    62  	".mof":    SigStyleC,
    63  }
    64  
    65  const psBegin = "SIG # Begin signature block"
    66  const psEnd = "SIG # End signature block"
    67  
    68  type sigStyle struct{ start, end string }
    69  
    70  var psStyles = map[PsSigStyle]sigStyle{
    71  	SigStyleHash: sigStyle{"# ", ""},
    72  	SigStyleXML:  sigStyle{"<!-- ", " -->"},
    73  	SigStyleC:    sigStyle{"/* ", " */"},
    74  }
    75  
    76  // Get the PowerShell signature style for a filename or extension
    77  func GetSigStyle(filename string) (PsSigStyle, bool) {
    78  	style, ok := psExtMap[filepath.Ext(filename)]
    79  	return style, ok
    80  }
    81  
    82  // Return all supported PowerShell signature styles
    83  func AllSigStyles() []string {
    84  	var ret []string
    85  	for k := range psExtMap {
    86  		ret = append(ret, k)
    87  	}
    88  	return ret
    89  }
    90  
    91  type PsDigest struct {
    92  	Imprint           []byte
    93  	HashFunc          crypto.Hash
    94  	TextSize, SigSize int64
    95  	SigStyle          PsSigStyle
    96  	IsUtf16           bool
    97  }
    98  
    99  // Digest a PowerShell script from a stream, returning the sum and the length of the digested bytes.
   100  //
   101  // PowerShell scripts are digested in UTF-16-LE format so, unless already in
   102  // that format, the text is converted first. Existing signatures are discarded.
   103  func DigestPowershell(r io.Reader, style PsSigStyle, hash crypto.Hash) (*PsDigest, error) {
   104  	si, ok := psStyles[style]
   105  	if !ok {
   106  		return nil, errors.New("invalid powershell signature style")
   107  	}
   108  	br := bufio.NewReader(r)
   109  	isUtf16, first, _ := detectUtf16(br, si.start, si.end)
   110  	d := hash.New()
   111  	var textSize, sigSize int64
   112  	var saved string
   113  	for {
   114  		line, err := readLine(br, isUtf16)
   115  		if err != nil && err != io.EOF {
   116  			return nil, err
   117  		}
   118  		if line == first {
   119  			// remove EOL from previous line
   120  			if isUtf16 {
   121  				saved = saved[:len(saved)-4]
   122  				sigSize = 4
   123  			} else {
   124  				saved = saved[:len(saved)-2]
   125  				sigSize = 2
   126  			}
   127  			// count the size of the signature
   128  			sigSize += int64(len(line))
   129  			n, err := io.Copy(ioutil.Discard, br)
   130  			if err != nil {
   131  				return nil, err
   132  			}
   133  			sigSize += n
   134  			break
   135  		} else {
   136  			writeUtf16(d, saved, isUtf16)
   137  			textSize += int64(len(saved))
   138  			saved = line
   139  		}
   140  		if err == io.EOF {
   141  			break
   142  		}
   143  	}
   144  	writeUtf16(d, saved, isUtf16)
   145  	textSize += int64(len(saved))
   146  	return &PsDigest{d.Sum(nil), hash, textSize, sigSize, style, isUtf16}, nil
   147  }
   148  
   149  func detectUtf16(br *bufio.Reader, start, end string) (bool, string, string) {
   150  	first := start + psBegin + end + "\r\n"
   151  	last := start + psEnd + end + "\r\n"
   152  	if bom, err := br.Peek(2); err == nil && bom[0] == 0xff && bom[1] == 0xfe {
   153  		// UTF-16-LE
   154  		return true, toUtf16(first), toUtf16(last)
   155  	}
   156  	return false, first, last
   157  }
   158  
   159  // Verify a PowerShell script. The signature "style" must already have been
   160  // determined by calling GetSigStyle
   161  func VerifyPowershell(r io.ReadSeeker, style PsSigStyle, skipDigests bool) (*pkcs9.TimestampedSignature, error) {
   162  	si, ok := psStyles[style]
   163  	if !ok {
   164  		return nil, errors.New("invalid powershell signature style")
   165  	}
   166  	br := bufio.NewReader(r)
   167  	isUtf16, first, last := detectUtf16(br, si.start, si.end)
   168  	found := false
   169  	var textSize int64
   170  	var pkcsb bytes.Buffer
   171  	for {
   172  		line, err := readLine(br, isUtf16)
   173  		if err == io.EOF && !found {
   174  			return nil, sigerrors.NotSignedError{Type: "powershell document"}
   175  		} else if err != nil {
   176  			return nil, err
   177  		}
   178  		if found && line == last {
   179  			break
   180  		} else if found {
   181  			lstr := string(line)
   182  			if isUtf16 {
   183  				lstr = fromUtf16(line)
   184  			}
   185  			if !strings.HasPrefix(lstr, si.start) || !strings.HasSuffix(lstr, si.end+"\r\n") {
   186  				return nil, errors.New("malformed powershell signature")
   187  			}
   188  			i := len(si.start)
   189  			j := len(lstr) - len(si.end) - 2
   190  			lder, err := base64.StdEncoding.DecodeString(lstr[i:j])
   191  			if err != nil {
   192  				return nil, err
   193  			}
   194  			pkcsb.Write(lder)
   195  		} else if line == first {
   196  			// remove preceding \r\n from size
   197  			if isUtf16 {
   198  				textSize -= 4
   199  			} else {
   200  				textSize -= 2
   201  			}
   202  			found = true
   203  		} else {
   204  			textSize += int64(len(line))
   205  		}
   206  	}
   207  	psd, err := pkcs7.Unmarshal(pkcsb.Bytes())
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  	if !psd.Content.ContentInfo.ContentType.Equal(OidSpcIndirectDataContent) {
   212  		return nil, errors.New("not an authenticode signature")
   213  	}
   214  	sig, err := psd.Content.Verify(nil, false)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	ts, err := pkcs9.VerifyOptionalTimestamp(sig)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  	indirect := new(SpcIndirectDataContentMsi)
   223  	if err := psd.Content.ContentInfo.Unmarshal(indirect); err != nil {
   224  		return nil, err
   225  	}
   226  	hash, err := x509tools.PkixDigestToHashE(indirect.MessageDigest.DigestAlgorithm)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  	if !skipDigests {
   231  		if _, err := r.Seek(0, 0); err != nil {
   232  			return nil, err
   233  		}
   234  		digest, err := DigestPowershell(r, style, hash)
   235  		if err != nil {
   236  			return nil, err
   237  		}
   238  		if !hmac.Equal(digest.Imprint, indirect.MessageDigest.Digest) {
   239  			return nil, fmt.Errorf("digest mismatch: %x != %x", digest.Imprint, indirect.MessageDigest.Digest)
   240  		}
   241  	}
   242  	return &ts, nil
   243  }
   244  
   245  // Sign a previously digested PowerShell script and return the Authenticode structure
   246  func (pd *PsDigest) Sign(ctx context.Context, cert *certloader.Certificate) (*binpatch.PatchSet, *pkcs9.TimestampedSignature, error) {
   247  	ts, err := SignSip(ctx, pd.Imprint, pd.HashFunc, psSipInfo, cert)
   248  	if err != nil {
   249  		return nil, nil, err
   250  	}
   251  	patch, err := pd.MakePatch(ts.Raw)
   252  	if err != nil {
   253  		return nil, nil, err
   254  	}
   255  	return patch, ts, nil
   256  }
   257  
   258  // Create a patchset that will add or replace the signature on the digested script
   259  func (pd *PsDigest) MakePatch(sig []byte) (*binpatch.PatchSet, error) {
   260  	si, ok := psStyles[pd.SigStyle]
   261  	if !ok {
   262  		return nil, errors.New("invalid powershell signature style")
   263  	}
   264  	var buf bytes.Buffer
   265  	buf.WriteString("\r\n" + si.start + psBegin + si.end + "\r\n")
   266  	b64 := base64.StdEncoding.EncodeToString(sig)
   267  	for i := 0; i < len(b64); i += 64 {
   268  		j := i + 64
   269  		if j > len(b64) {
   270  			j = len(b64)
   271  		}
   272  		buf.WriteString(si.start + b64[i:j] + si.end + "\r\n")
   273  	}
   274  	buf.WriteString(si.start + psEnd + si.end + "\r\n")
   275  	patch := binpatch.New()
   276  	var encoded []byte
   277  	if pd.IsUtf16 {
   278  		encoded = []byte(toUtf16(buf.String()))
   279  	} else {
   280  		encoded = buf.Bytes()
   281  	}
   282  	patch.Add(pd.TextSize, int64(pd.SigSize), encoded)
   283  	return patch, nil
   284  }
   285  
   286  func readLine(br *bufio.Reader, isUtf16 bool) (string, error) {
   287  	line, err := br.ReadString('\n')
   288  	if isUtf16 && err == nil {
   289  		// \n\0
   290  		var zero byte
   291  		zero, err = br.ReadByte()
   292  		if zero != 0 {
   293  			return "", errors.New("malformed utf16")
   294  		}
   295  		line += "\x00"
   296  	}
   297  	return line, err
   298  }
   299  
   300  // Convert UTF8 to UTF-16-LE
   301  func toUtf16(x string) string {
   302  	runes := utf16.Encode([]rune(x))
   303  	buf := bytes.NewBuffer(make([]byte, 0, 2*len(runes)))
   304  	binary.Write(buf, binary.LittleEndian, runes)
   305  	return buf.String()
   306  }
   307  
   308  // Convert UTF8 to UTF-16-LE and write it to "d"
   309  func writeUtf16(d io.Writer, x string, isUtf16 bool) error {
   310  	if isUtf16 {
   311  		_, err := d.Write([]byte(x))
   312  		return err
   313  	}
   314  	runes := utf16.Encode([]rune(x))
   315  	return binary.Write(d, binary.LittleEndian, runes)
   316  }
   317  
   318  // Convert UTF-16-LE to UTF8
   319  func fromUtf16(x string) string {
   320  	runes := make([]uint16, len(x)/2)
   321  	binary.Read(bytes.NewReader([]byte(x)), binary.LittleEndian, runes)
   322  	return string(utf16.Decode(runes))
   323  }
   324  

View as plain text