| // Copyright 2020 The Monogon Project Authors. |
| // |
| // SPDX-License-Identifier: Apache-2.0 |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // This package implements the minimum of functionality needed to generate and |
| // map dm-verity images. It's provided in order to avoid a perceived higher |
| // long term cost of packaging, linking against and maintaining the original C |
| // veritysetup tool. |
| // |
| // dm-verity is a Linux device mapper target that allows integrity verification of |
| // a read-only block device. The block device whose integrity should be checked |
| // (the 'data device') must be first processed by a tool like veritysetup to |
| // generate a hash device and root hash. |
| // The original data device, hash device and root hash are then set up as a device |
| // mapper target, and any read performed from the data device through the verity |
| // target will be verified for integrity by Linux using the hash device and root |
| // hash. |
| // |
| // Internally, the hash device is a Merkle tree of all the bytes in the data |
| // device, layed out as layers of 'hash blocks'. Starting with data bytes, layers |
| // are built recursively, with each layer's output hash blocks becoming the next |
| // layer's data input, ending with the single root hash. |
| // |
| // For more information about the internals, see the Linux and cryptsetup |
| // upstream code: |
| // |
| // https://gitlab.com/cryptsetup/cryptsetup/wikis/DMVerity |
| package verity |
| |
| import ( |
| "bytes" |
| "crypto/rand" |
| "crypto/sha256" |
| "encoding/binary" |
| "encoding/hex" |
| "fmt" |
| "io" |
| "strconv" |
| "strings" |
| ) |
| |
| // superblock represents data layout inside of a dm-verity hash block |
| // device superblock. It follows a preexisting verity implementation: |
| // |
| // https://gitlab.com/cryptsetup/cryptsetup/wikis/DMVerity#verity-superblock-format |
| type superblock struct { |
| // signature is the magic signature of a verity hash device superblock, |
| // "verity\0\0". |
| signature [8]byte |
| // version specifies a superblock format. This structure describes version |
| // '1'. |
| version uint32 |
| // hashType defaults to '1' outside Chrome OS, according to scarce dm-verity |
| // documentation. |
| hashType uint32 |
| // uuid contains a UUID of the hash device. |
| uuid [16]byte |
| // algorithm stores an ASCII-encoded name of the hash function used. |
| algorithm [32]byte |
| |
| // dataBlockSize specifies a size of a single data device block, in bytes. |
| dataBlockSize uint32 |
| // hashBlockSize specifies a size of a single hash device block, in bytes. |
| hashBlockSize uint32 |
| // dataBlocks contains a count of blocks available on the data device. |
| dataBlocks uint64 |
| |
| // saltSize encodes the size of hash block salt, up to the maximum of 256 bytes. |
| saltSize uint16 |
| |
| // padding |
| _ [6]byte |
| // exactly saltSize bytes of salt are prepended to data blocks before hashing. |
| saltBuffer [256]byte |
| // padding |
| _ [168]byte |
| } |
| |
| // newSuperblock builds a dm-verity hash device superblock based on |
| // hardcoded defaults. dataBlocks is the only field left for later |
| // initialization. |
| // It returns either a partially initialized superblock, or an error. |
| func newSuperblock() (*superblock, error) { |
| // This implementation only handles SHA256-based verity hash images |
| // with a specific 4096-byte block size. |
| // Block sizes can be updated by adjusting the struct literal below. |
| // A change of a hashing algorithm would require a refactor of |
| // saltedDigest, and references to sha256.Size. |
| // |
| // Fill in the defaults (compare with superblock definition). |
| sb := superblock{ |
| signature: [8]byte{'v', 'e', 'r', 'i', 't', 'y', 0, 0}, |
| version: 1, |
| hashType: 1, |
| algorithm: [32]byte{'s', 'h', 'a', '2', '5', '6'}, |
| saltSize: 64, |
| dataBlockSize: 4096, |
| hashBlockSize: 4096, |
| } |
| |
| // Fill in the superblock UUID and cryptographic salt. |
| if _, err := rand.Read(sb.uuid[:]); err != nil { |
| return nil, fmt.Errorf("when generating UUID: %w", err) |
| } |
| if _, err := rand.Read(sb.saltBuffer[:]); err != nil { |
| return nil, fmt.Errorf("when generating salt: %w", err) |
| } |
| |
| return &sb, nil |
| } |
| |
| // salt returns a slice of sb.saltBuffer actually occupied by |
| // salt bytes, of sb.saltSize length. |
| func (sb *superblock) salt() []byte { |
| return sb.saltBuffer[:int(sb.saltSize)] |
| } |
| |
| // algorithmName returns a name of the algorithm used to hash data block |
| // digests. |
| func (sb *superblock) algorithmName() string { |
| size := bytes.IndexByte(sb.algorithm[:], 0x00) |
| return string(sb.algorithm[:size]) |
| } |
| |
| // saltedDigest computes and returns a SHA256 sum of a block prepended |
| // with a Superblock-defined salt. |
| func (sb *superblock) saltedDigest(data []byte) (digest [sha256.Size]byte) { |
| h := sha256.New() |
| h.Write(sb.salt()) |
| h.Write(data) |
| copy(digest[:], h.Sum(nil)) |
| return |
| } |
| |
| // dataBlocksPerHashBlock returns the count of hash operation outputs that |
| // fit in a hash device block. This is also the amount of data device |
| // blocks it takes to populate a hash device block. |
| func (sb *superblock) dataBlocksPerHashBlock() uint64 { |
| return uint64(sb.hashBlockSize) / sha256.Size |
| } |
| |
| // computeHashBlock reads at most sb.dataBlocksPerHashBlock blocks from |
| // the given reader object, returning a padded hash block of length |
| // defined by sb.hashBlockSize, the count of digests output, and an |
| // error, if encountered. |
| // In case a non-nil block is returned, it's guaranteed to contain at |
| // least one hash. An io.EOF signals that there is no more to be read. |
| func (sb *superblock) computeHashBlock(r io.Reader) ([]byte, uint64, error) { |
| // dcnt stores the total count of data blocks processed, which is the |
| // as the count of digests output. |
| var dcnt uint64 |
| // Preallocate a whole hash block. |
| hblk := bytes.NewBuffer(make([]byte, 0, sb.hashBlockSize)) |
| |
| // For every data block, compute a hash and place it in hblk. Continue |
| // till EOF. |
| for b := uint64(0); b < sb.dataBlocksPerHashBlock(); b++ { |
| dbuf := make([]byte, sb.dataBlockSize) |
| // Attempt to read enough data blocks to make a complete hash block. |
| n, err := io.ReadFull(r, dbuf) |
| // If any data was read, make a hash and add it to the hash buffer. |
| if n != 0 { |
| hash := sb.saltedDigest(dbuf) |
| hblk.Write(hash[:]) |
| dcnt++ |
| } |
| // Handle the read errors. |
| switch err { |
| case nil: |
| case io.ErrUnexpectedEOF, io.EOF: |
| // io.ReadFull returns io.ErrUnexpectedEOF after a partial read, |
| // and io.EOF if no bytes were read. In both cases it's possible |
| // to end up with a partially filled hash block. |
| if hblk.Len() != 0 { |
| // Return a zero-padded hash block if any hashes were written |
| // to it, and signal that no more blocks can be built. |
| res := hblk.Bytes() |
| return res[:cap(res)], dcnt, io.EOF |
| } |
| // Return nil if the block doesn't contain any hashes. |
| return nil, 0, io.EOF |
| default: |
| // Wrap unhandled read errors. |
| return nil, 0, fmt.Errorf("while computing a hash block: %w", err) |
| } |
| } |
| // Return a completely filled hash block. |
| res := hblk.Bytes() |
| return res[:cap(res)], dcnt, nil |
| } |
| |
| // WriteTo writes a verity superblock to a given writer object. |
| // It returns the count of bytes written, and a write error, if |
| // encountered. |
| func (sb *superblock) WriteTo(w io.Writer) (int64, error) { |
| // Write the superblock. |
| if err := binary.Write(w, binary.LittleEndian, sb); err != nil { |
| return -1, fmt.Errorf("while writing a header: %w", err) |
| } |
| |
| // Get the padding size by substracting current offset from a hash block |
| // size. |
| co := int(binary.Size(sb)) |
| pbc := int(sb.hashBlockSize) - int(co) |
| if pbc <= 0 { |
| return int64(co), fmt.Errorf("hash device block size smaller than dm-verity superblock") |
| } |
| |
| // Write the padding bytes at the end of the block. |
| n, err := w.Write(bytes.Repeat([]byte{0}, pbc)) |
| co += n |
| if err != nil { |
| return int64(co), fmt.Errorf("while writing padding: %w", err) |
| } |
| return int64(co), nil |
| } |
| |
| // computeLevel produces a verity hash tree level based on data read from |
| // a given reader object. |
| // It returns a byte slice containing one or more hash blocks, or an |
| // error. |
| // BUG(mz): Current implementation requires a 1/128th of the data image |
| // size to be allocatable on the heap. |
| func (sb *superblock) computeLevel(r io.Reader) ([]byte, error) { |
| // hbuf will store all the computed hash blocks. |
| var hbuf bytes.Buffer |
| // Compute one or more hash blocks, reading all data available in the |
| // 'r' reader object, and write them into hbuf. |
| for { |
| hblk, _, err := sb.computeHashBlock(r) |
| if err != nil && err != io.EOF { |
| return nil, fmt.Errorf("while building a hash tree level: %w", err) |
| } |
| if hblk != nil { |
| _, err := hbuf.Write(hblk) |
| if err != nil { |
| return nil, fmt.Errorf("while writing to hash block buffer: %w", err) |
| } |
| } |
| if err == io.EOF { |
| break |
| } |
| } |
| return hbuf.Bytes(), nil |
| } |
| |
| // hashTree stores hash tree levels, each level comprising one or more |
| // Verity hash blocks. Levels are ordered from bottom to top. |
| type hashTree [][]byte |
| |
| // push appends a level to the hash tree. |
| func (ht *hashTree) push(nl []byte) { |
| *ht = append(*ht, nl) |
| } |
| |
| // top returns the topmost level of the hash tree. |
| func (ht *hashTree) top() []byte { |
| if len(*ht) == 0 { |
| return nil |
| } |
| return (*ht)[len(*ht)-1] |
| } |
| |
| // WriteTo writes a verity-formatted hash tree to the given writer |
| // object. |
| // It returns a write error, if encountered. |
| func (ht *hashTree) WriteTo(w io.Writer) (int64, error) { |
| // t keeps the count of bytes written to w. |
| var t int64 |
| // Write the hash tree levels from top to bottom. |
| for l := len(*ht) - 1; l >= 0; l-- { |
| level := (*ht)[l] |
| // Call w.Write until a whole level is written. |
| for len(level) != 0 { |
| n, err := w.Write(level) |
| if err != nil { |
| return t, fmt.Errorf("while writing a level: %w", err) |
| } |
| level = level[n:] |
| t += int64(n) |
| } |
| } |
| return t, nil |
| } |
| |
| // MappingTable aggregates data needed to generate a complete Verity |
| // mapping table. |
| type MappingTable struct { |
| // superblock defines the following elements of the mapping table: |
| // - data device block size |
| // - hash device block size |
| // - total count of data blocks |
| // - hash algorithm used |
| // - cryptographic salt used |
| superblock *superblock |
| // DataDevicePath is the filesystem path of the data device used as part |
| // of the Verity Device Mapper target. |
| DataDevicePath string |
| // HashDevicePath is the filesystem path of the hash device used as part |
| // of the Verity Device Mapper target. |
| HashDevicePath string |
| // HashStart marks the starting block of the Verity hash tree. |
| HashStart int64 |
| // rootHash stores a cryptographic hash of the top hash tree block. |
| rootHash []byte |
| } |
| |
| // VerityParameterList returns a list of Verity target parameters, ordered |
| // as they would appear in a parameter string. |
| func (t *MappingTable) VerityParameterList() []string { |
| return []string{ |
| "1", |
| t.DataDevicePath, |
| t.HashDevicePath, |
| strconv.FormatUint(uint64(t.superblock.dataBlockSize), 10), |
| strconv.FormatUint(uint64(t.superblock.hashBlockSize), 10), |
| strconv.FormatUint(uint64(t.superblock.dataBlocks), 10), |
| strconv.FormatInt(int64(t.HashStart), 10), |
| t.superblock.algorithmName(), |
| hex.EncodeToString(t.rootHash), |
| hex.EncodeToString(t.superblock.salt()), |
| } |
| } |
| |
| // TargetParameters returns the mapping table as a list of Device Mapper |
| // target parameters, ordered as they would appear in a parameter string |
| // (see: String). |
| func (t *MappingTable) TargetParameters() []string { |
| return append( |
| []string{ |
| "0", |
| strconv.FormatUint(t.Length(), 10), |
| "verity", |
| }, |
| t.VerityParameterList()..., |
| ) |
| } |
| |
| // String returns a string-formatted mapping table for use with Device |
| // Mapper. |
| // BUG(mz): unescaped whitespace can appear in block device paths |
| func (t *MappingTable) String() string { |
| return strings.Join(t.TargetParameters(), " ") |
| } |
| |
| // Length returns the data device length, represented as a number of |
| // 512-byte sectors. |
| func (t *MappingTable) Length() uint64 { |
| return t.superblock.dataBlocks * uint64(t.superblock.dataBlockSize) / 512 |
| } |
| |
| // encoder transforms data blocks written into it into a verity hash |
| // tree. It writes out the hash tree only after Close is called on it. |
| type encoder struct { |
| // out is the writer object Encoder will write to. |
| out io.Writer |
| // writeSb, if true, will cause a Verity superblock to be written to the |
| // writer object. |
| writeSb bool |
| // sb contains the most of information needed to build a mapping table. |
| sb *superblock |
| // bottom stands for the bottom level of the hash tree. It contains |
| // complete hash blocks of data written to the encoder. |
| bottom bytes.Buffer |
| // dataBuffer stores incoming data for later processing. |
| dataBuffer bytes.Buffer |
| // rootHash stores the verity root hash set on Close. |
| rootHash []byte |
| } |
| |
| // computeHashTree builds a complete hash tree based on the encoder's |
| // state. Levels are appended to the returned hash tree starting from the |
| // bottom, with the top level written last. |
| // e.sb.dataBlocks is set according to the bottom level's length, which |
| // must be divisible by e.sb.hashBlockSize. |
| // e.rootHash is set on success. |
| // It returns an error, if encountered. |
| func (e *encoder) computeHashTree() (*hashTree, error) { |
| // Put b at the bottom of the tree. Don't perform a deep copy. |
| ht := hashTree{e.bottom.Bytes()} |
| |
| // Other levels are built by hashing the hash blocks comprising a level |
| // below. |
| for { |
| if len(ht.top()) == int(e.sb.hashBlockSize) { |
| // The last level to compute has a size of exactly one hash block. |
| // That's the root level. Its hash serves as a cryptographic root of |
| // trust and is saved into a encoder for later use. |
| // In case the bottom level consists of only one hash block, no more |
| // levels are computed. |
| sd := e.sb.saltedDigest(ht.top()) |
| e.rootHash = sd[:] |
| return &ht, nil |
| } |
| |
| // Create the next level by hashing the previous one. |
| nl, err := e.sb.computeLevel(bytes.NewReader(ht.top())) |
| if err != nil { |
| return nil, fmt.Errorf("while computing a level: %w", err) |
| } |
| // Append the resulting next level to a tree. |
| ht.push(nl) |
| } |
| } |
| |
| // processDataBuffer processes data blocks contained in e.dataBuffer |
| // until no more data is available to form a completely filled hash block. |
| // If 'incomplete' is true, all remaining data in e.dataBuffer will be |
| // processed, producing a terminating incomplete block. |
| // It returns the count of data blocks processed, or an error, if |
| // encountered. |
| func (e *encoder) processDataBuffer(incomplete bool) (uint64, error) { |
| // tdcnt stores the total count of data blocks processed. |
| var tdcnt uint64 |
| // Compute the count of bytes needed to produce a complete hash block. |
| bph := e.sb.dataBlocksPerHashBlock() * uint64(e.sb.dataBlockSize) |
| |
| // Iterate until no more data is available in e.dbuf. |
| for uint64(e.dataBuffer.Len()) >= bph || incomplete && e.dataBuffer.Len() != 0 { |
| hb, dcnt, err := e.sb.computeHashBlock(&e.dataBuffer) |
| if err != nil && err != io.EOF { |
| return 0, fmt.Errorf("while processing a data buffer: %w", err) |
| } |
| // Increment the total count of data blocks processed. |
| tdcnt += dcnt |
| // Write the resulting hash block into the level-zero buffer. |
| e.bottom.Write(hb[:]) |
| } |
| return tdcnt, nil |
| } |
| |
| // NewEncoder returns a fully initialized encoder, or an error. The |
| // encoder will write to the given io.Writer object. |
| // A verity superblock will be written, preceding the hash tree, if |
| // writeSb is true. |
| func NewEncoder(out io.Writer, dataBlockSize, hashBlockSize uint32, writeSb bool) (*encoder, error) { |
| sb, err := newSuperblock() |
| if err != nil { |
| return nil, fmt.Errorf("while creating a superblock: %w", err) |
| } |
| sb.dataBlockSize = dataBlockSize |
| sb.hashBlockSize = hashBlockSize |
| |
| e := encoder{ |
| out: out, |
| writeSb: writeSb, |
| sb: sb, |
| } |
| return &e, nil |
| } |
| |
| // Write hashes raw data to form the bottom hash tree level. |
| // It returns the number of bytes written, and an error, if encountered. |
| func (e *encoder) Write(data []byte) (int, error) { |
| // Copy the input into the data buffer. |
| n, _ := e.dataBuffer.Write(data) |
| // Process only enough data to form a complete hash block. This may |
| // leave excess data in e.dbuf to be processed later on. |
| dcnt, err := e.processDataBuffer(false) |
| if err != nil { |
| return n, fmt.Errorf("while processing the data buffer: %w", err) |
| } |
| // Update the superblock with the count of data blocks written. |
| e.sb.dataBlocks += dcnt |
| return n, nil |
| } |
| |
| // Close builds a complete hash tree based on cached bottom level blocks, |
| // then writes it to a preconfigured io.Writer object. A Verity superblock |
| // is written, if e.writeSb is true. No data, nor the superblock is written |
| // if the encoder is empty. |
| // It returns an error, if one was encountered. |
| func (e *encoder) Close() error { |
| // Process all buffered data, including data blocks that may not form |
| // a complete hash block. |
| dcnt, err := e.processDataBuffer(true) |
| if err != nil { |
| return fmt.Errorf("while processing the data buffer: %w", err) |
| } |
| // Update the superblock with the count of data blocks written. |
| e.sb.dataBlocks += dcnt |
| |
| // Don't write anything if nothing was written to the encoder. |
| if e.bottom.Len() == 0 { |
| return nil |
| } |
| |
| // Compute remaining hash tree levels based on the bottom level: e.bottom. |
| ht, err := e.computeHashTree() |
| if err != nil { |
| return fmt.Errorf("while encoding a hash tree: %w", err) |
| } |
| |
| // Write the Verity superblock if the encoder was configured to do so. |
| if e.writeSb { |
| if _, err = e.sb.WriteTo(e.out); err != nil { |
| return fmt.Errorf("while writing a superblock: %w", err) |
| } |
| } |
| // Write the hash tree. |
| _, err = ht.WriteTo(e.out) |
| if err != nil { |
| return fmt.Errorf("while writing a hash tree: %w", err) |
| } |
| |
| // Reset the encoder. |
| e, err = NewEncoder(e.out, e.sb.dataBlockSize, e.sb.hashBlockSize, e.writeSb) |
| if err != nil { |
| return fmt.Errorf("while resetting an encoder: %w", err) |
| } |
| return nil |
| } |
| |
| // MappingTable returns a complete, string-convertible Verity target mapping |
| // table for use with Device Mapper, or an error. Close must be called on the |
| // encoder before calling this function. dataDevicePath, hashDevicePath, and |
| // hashStart parameters are parts of the mapping table. See: |
| // https://www.kernel.org/doc/html/latest/admin-guide/device-mapper/verity.html |
| func (e *encoder) MappingTable(dataDevicePath, hashDevicePath string, hashStart int64) (*MappingTable, error) { |
| if e.rootHash == nil { |
| if e.bottom.Len() != 0 { |
| return nil, fmt.Errorf("encoder wasn't closed.") |
| } |
| return nil, fmt.Errorf("encoder is empty.") |
| } |
| |
| if e.writeSb { |
| // Account for the superblock. |
| hashStart += 1 |
| } |
| return &MappingTable{ |
| superblock: e.sb, |
| DataDevicePath: dataDevicePath, |
| HashDevicePath: hashDevicePath, |
| HashStart: hashStart, |
| rootHash: e.rootHash, |
| }, nil |
| } |