github.com/opencontainers/umoci@v0.4.8-0.20240508124516-656e4836fb0d/pkg/hardening/verified_reader.go (about)

     1  /*
     2   * umoci: Umoci Modifies Open Containers' Images
     3   * Copyright (C) 2016-2020 SUSE LLC
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *    http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package hardening
    19  
    20  import (
    21  	"fmt"
    22  	"io"
    23  	"io/ioutil"
    24  	"os"
    25  
    26  	"github.com/apex/log"
    27  	"github.com/opencontainers/go-digest"
    28  	"github.com/opencontainers/umoci/pkg/system"
    29  	"github.com/pkg/errors"
    30  )
    31  
    32  // Exported errors for verification issues that occur during processing within
    33  // VerifiedReadCloser. Note that you will need to use
    34  // "github.com/pkg/errors".Cause to get these exported errors in most cases.
    35  var (
    36  	ErrDigestMismatch = errors.Errorf("verified reader digest mismatch")
    37  	ErrSizeMismatch   = errors.Errorf("verified reader size mismatch")
    38  )
    39  
    40  // VerifiedReadCloser is a basic io.ReadCloser which allows for simple
    41  // verification that a stream matches an expected hash. The entire stream is
    42  // hashed while being passed through this reader, and on EOF it will verify
    43  // that the hash matches the expected hash. If not, an error is returned. Note
    44  // that this means you need to read all input to EOF in order to find
    45  // verification errors.
    46  //
    47  // If Reader is a VerifiedReadCloser (with the same ExpectedDigest), all of the
    48  // methods are just piped to the underlying methods (with no verification in
    49  // the upper layer).
    50  type VerifiedReadCloser struct {
    51  	// Reader is the underlying reader.
    52  	Reader io.ReadCloser
    53  
    54  	// ExpectedDigest is the expected digest. When the underlying reader
    55  	// returns an EOF, the entire stream's sum will be compared to this hash
    56  	// and an error will be returned if they don't match.
    57  	ExpectedDigest digest.Digest
    58  
    59  	// ExpectedSize is the expected amount of data to be read overall. If the
    60  	// underlying reader hasn't returned an EOF by the time this value is
    61  	// exceeded, an error is returned and no further reads will occur.
    62  	ExpectedSize int64
    63  
    64  	// digester stores the current state of the stream's hash.
    65  	digester digest.Digester
    66  
    67  	// currentSize is the number of bytes that have been read so far.
    68  	currentSize int64
    69  }
    70  
    71  func (v *VerifiedReadCloser) init() {
    72  	// Define digester if not already set.
    73  	if v.digester == nil {
    74  		alg := v.ExpectedDigest.Algorithm()
    75  		if !alg.Available() {
    76  			log.Fatalf("verified reader: unsupported hash algorithm %s", alg)
    77  			panic("verified reader: unreachable section") // should never be hit
    78  		}
    79  		v.digester = alg.Digester()
    80  	}
    81  }
    82  
    83  func (v *VerifiedReadCloser) isNoop() bool {
    84  	innerV, ok := v.Reader.(*VerifiedReadCloser)
    85  	return ok &&
    86  		innerV.ExpectedDigest == v.ExpectedDigest &&
    87  		innerV.ExpectedSize == v.ExpectedSize
    88  }
    89  
    90  func (v *VerifiedReadCloser) verify(nilErr error) error {
    91  	// Digest mismatch (always takes precedence)?
    92  	if actualDigest := v.digester.Digest(); actualDigest != v.ExpectedDigest {
    93  		return errors.Wrapf(ErrDigestMismatch, "expected %s not %s", v.ExpectedDigest, actualDigest)
    94  	}
    95  	// Do we need to check the size for mismatches?
    96  	if v.ExpectedSize >= 0 {
    97  		switch {
    98  		// Not enough bytes in the stream.
    99  		case v.currentSize < v.ExpectedSize:
   100  			return errors.Wrapf(ErrSizeMismatch, "expected %d bytes (only %d bytes in stream)", v.ExpectedSize, v.currentSize)
   101  		// We don't read the entire blob, so the message needs to be slightly adjusted.
   102  		case v.currentSize > v.ExpectedSize:
   103  			return errors.Wrapf(ErrSizeMismatch, "expected %d bytes (extra bytes in stream)", v.ExpectedSize)
   104  		}
   105  	}
   106  	// Forward the provided error.
   107  	return nilErr
   108  }
   109  
   110  // Read is a wrapper around VerifiedReadCloser.Reader, with a digest check on
   111  // EOF.  Make sure that you always check for EOF and read-to-the-end for all
   112  // files.
   113  func (v *VerifiedReadCloser) Read(p []byte) (n int, err error) {
   114  	// Make sure we don't read after v.ExpectedSize has been passed.
   115  	err = io.EOF
   116  	left := v.ExpectedSize - v.currentSize
   117  	switch {
   118  	// ExpectedSize has been disabled.
   119  	case v.ExpectedSize < 0:
   120  		n, err = v.Reader.Read(p)
   121  
   122  	// We still have something left to read.
   123  	case left > 0:
   124  		if int64(len(p)) > left {
   125  			p = p[:left]
   126  		}
   127  		// Piped to the underling read.
   128  		n, err = v.Reader.Read(p)
   129  		v.currentSize += int64(n)
   130  
   131  	// We have either read everything, or just happened to land on a boundary
   132  	// (with potentially more things afterwards). So we must check if there is
   133  	// anything left by doing a 1-byte read (Go doesn't allow for zero-length
   134  	// Read()s to give EOFs).
   135  	case left == 0:
   136  		// We just want to know whether we read something (n>0). Whatever we
   137  		// read is irrelevant because if we read something that means the
   138  		// reader will fail to verify. #nosec G104
   139  		nTmp, _ := v.Reader.Read(make([]byte, 1))
   140  		v.currentSize += int64(nTmp)
   141  	}
   142  	// Are we going to be a noop?
   143  	if v.isNoop() {
   144  		return n, err
   145  	}
   146  	// Make sure we're ready.
   147  	v.init()
   148  	// Forward it to the digester.
   149  	if n > 0 {
   150  		// hash.Hash guarantees Write() never fails and is never short.
   151  		nWrite, err := v.digester.Hash().Write(p[:n])
   152  		if nWrite != n || err != nil {
   153  			log.Fatalf("verified reader: short write to %s Digester (err=%v)", v.ExpectedDigest.Algorithm(), err)
   154  			panic("verified reader: unreachable section") // should never be hit
   155  		}
   156  	}
   157  	// We have finished reading -- let's verify the state!
   158  	if errors.Cause(err) == io.EOF {
   159  		err = v.verify(err)
   160  	}
   161  	return n, err
   162  }
   163  
   164  // sourceName returns a debugging-friendly string to indicate to the user what
   165  // the source reader is for this verified reader.
   166  func (v *VerifiedReadCloser) sourceName() string {
   167  	switch inner := v.Reader.(type) {
   168  	case *VerifiedReadCloser:
   169  		return fmt.Sprintf("vrdr[%s]", inner.sourceName())
   170  	case *os.File:
   171  		return inner.Name()
   172  	case fmt.Stringer:
   173  		return inner.String()
   174  	// TODO: Maybe handle things like ioutil.NopCloser by using reflection?
   175  	default:
   176  		return fmt.Sprintf("%#v", inner)
   177  	}
   178  }
   179  
   180  // Close is a wrapper around VerifiedReadCloser.Reader, but with a digest check
   181  // which will return an error if the underlying Close() didn't.
   182  func (v *VerifiedReadCloser) Close() error {
   183  	// Consume any remaining bytes to make sure that we've actually read to the
   184  	// end of the stream. VerifiedReadCloser.Read will not read past
   185  	// ExpectedSize+1, so we don't need to add a limit here.
   186  	if n, err := system.Copy(ioutil.Discard, v); err != nil {
   187  		return errors.Wrap(err, "consume remaining unverified stream")
   188  	} else if n != 0 {
   189  		// If there's trailing bytes being discarded at this point, that
   190  		// indicates whatever you used to generate this blob is adding trailing
   191  		// gunk.
   192  		log.Infof("verified reader: %d bytes of trailing data discarded from %s", n, v.sourceName())
   193  	}
   194  	// Piped to underlying close.
   195  	err := v.Reader.Close()
   196  	if err != nil {
   197  		return err
   198  	}
   199  	// Are we going to be a noop?
   200  	if v.isNoop() {
   201  		return err
   202  	}
   203  	// Make sure we're ready.
   204  	v.init()
   205  	// Verify the state.
   206  	return v.verify(nil)
   207  }