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 }