pax_global_header00006660000000000000000000000064146460047550014525gustar00rootroot0000000000000052 comment=cfd532959af333da7546d3853bacbf536180b949 go-configfs-tsm-0.3.2/000077500000000000000000000000001464600475500145315ustar00rootroot00000000000000go-configfs-tsm-0.3.2/.gitignore000066400000000000000000000000021464600475500165110ustar00rootroot00000000000000*~go-configfs-tsm-0.3.2/CONTRIBUTING.md000066400000000000000000000021151464600475500167610ustar00rootroot00000000000000# How to Contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Contributor License Agreement Contributions to this project must be accompanied by a Contributor License Agreement. You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project. Head over to to see your current agreements on file or to sign a new one. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. ## Community Guidelines This project follows [Google's Open Source Community Guidelines](https://opensource.google.com/conduct/). go-configfs-tsm-0.3.2/LICENSE000066400000000000000000000261361464600475500155460ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. go-configfs-tsm-0.3.2/README.md000066400000000000000000000104121464600475500160060ustar00rootroot00000000000000# go-configfs-tsm This library wraps the configfs/tsm Linux subsystem for Trusted Security Module operations. ## `report` library This library wraps the configfs/tsm/report subsystem for safely generating attestation reports. The TSM `report` subsystem provides a vendor-agnostic interface for collecting a signed document for the Trusted Execution Environment's (TEE) state for remote verification. For simplicity, we call this document an "attestation report", though other sources may sometimes refer to it as a "quote". Signing keys are expected to be rooted back to the manufacturer. Certificates may be present in the `auxblob` attribute or as part of the report in `outblob`. The core functionality of attestation report interaction is nonce in, report out. For testability, we abstract the file operations that are needed for creating configfs report entries, reading and writing attributes, and final reclaiming of resources. ```golang func Get(client configfsi.Client, req *report.Request) (*report.Response, error) ``` Where ```golang type Request struct { InBlob []byte Privilege *Privilege GetAuxBlob bool } type Response struct { Provider string OutBlob []byte AuxBlob []byte } type Privilege struct { Level int } ``` The provider may not implement an `AuxBlob` delivery mechanism, so if `GetAuxBlob` is true, then `AuxBlob` still must be checked for length 0. ### Errors Since this is a file-based system, there's always a chance that an operation may fail with a permission error. By default, the TSM system requires root access. The host may also add rate limiting to requests, such that an outblob read fails with `EBUSY`. The kernel may or may not try again on behalf of the user. Finally, due to the fact that the TSM report system only requests an attestation report when reading `outblob` or `auxblob`, there is a chance the input attributes may have been changed to unexpected values from an interfering process. This interference is a bug in user space that the kernel does not block for simplicity. Interference is evident through the `generation` attribute. When `generation` does not match the expectations that the `report` package tracks, `report.Get` returns a `*report.GenerationErr` or an error that wraps `*report.GenerationErr`. Use `func GetGenerationErr(error) *GenerationErr` to extract a `*GenerationErr` from an error if it is or contains a `*GenerationErr`. If present, the caller should try to identify the source of interference and remove it. Meanwhile, the caller may try again. ## `configfsi.Client` interface Most users will only want to use the client from `linuxtsm.MakeClient`. A client on real hardware is just the filesystem, since the configfs interactions will interact with the hardware. In unit tests though, we can emulate the behavior that has been proposed in v7 of the patch series ```golang type Client interface { MkdirTemp(dir, pattern string) (string, error) ReadFile(name string) ([]byte, error) WriteFile(name string, contents []byte) error RemoveAll(path string) error } ``` The `RemoveAll` function is the only oddly named method, since the real interface would just `rmdir` the report directory ([`os.Remove`](https://pkg.go.dev/os#Remove) in Golang), even when there are apparent files underneath. Non-empty directory removal is generally not allowed, so the `RemoveAll` name is clearer with what it does. ## `linuxtsm` package The `linuxtsm` package defines an implementation of `configfsi.Client` with ```golang func MakeClient() (configfsi.Client, error) ``` For further convenience, `linuxtsm` provides an alias for `MakeClient` combined with `report.Get` as ```golang func GetReport(req *report.Request) (*report.Response, error) ``` The usage is the same as for `report.Get`. ## `faketsm` package The `faketsm.Client` implementation allows tests to provide custom behavior for subsystems by name: ```golang type Client struct { Subsystems map[string]configfsi.Client } ``` The `faketsm.ReportSubsystem` type implements a client that emulates the concurrent behavior and `generation` attribute semantics. To test negative behavior as well, the subsystem allows the user to override `Mkdir`, `ReadFile`, existing entries' values, and the error behavior of `WriteFile`. ## Disclaimer This is not an officially supported Google product. go-configfs-tsm-0.3.2/configfs/000077500000000000000000000000001464600475500163275ustar00rootroot00000000000000go-configfs-tsm-0.3.2/configfs/configfsi/000077500000000000000000000000001464600475500202765ustar00rootroot00000000000000go-configfs-tsm-0.3.2/configfs/configfsi/configfsi.go000066400000000000000000000030061464600475500225730ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. // Package configfsi defines an interface for interaction with the TSM configfs subsystem. package configfsi import ( "os" ) // Client abstracts the filesystem operations for interacting with configfs files. type Client interface { // MkdirTemp creates a new temporary directory in the directory dir and returns the pathname // of the new directory. Pattern semantics follow os.MkdirTemp. MkdirTemp(dir, pattern string) (string, error) // ReadFile reads the named file and returns the contents. ReadFile(name string) ([]byte, error) // ReadDir reads the directory named by dirname and returns a list of directory entries sorted by filename. ReadDir(dirname string) ([]os.DirEntry, error) // WriteFile writes data to the named file, creating it if necessary. The permissions // are implementation-defined. WriteFile(name string, contents []byte) error // RemoveAll removes path and any children it contains. RemoveAll(path string) error } go-configfs-tsm-0.3.2/configfs/configfsi/parse.go000066400000000000000000000016621464600475500217440ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. package configfsi import ( "strconv" "strings" ) // Kstrtouint returns the unsigned integer represented in data, following the same grammar // allowances as Linux, i.e., data may have a single trailing newline. func Kstrtouint(data []byte, base, bits int) (uint64, error) { return strconv.ParseUint(strings.TrimRight(string(data), "\n"), base, bits) } go-configfs-tsm-0.3.2/configfs/configfsi/path.go000066400000000000000000000062751464600475500215730ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. package configfsi import ( "fmt" "io" "path" "strings" ) const ( // TsmPrefix is the path to the configfs tsm system. TsmPrefix = "/sys/kernel/config/tsm" // How many random characters to use when replacing * in a temporary path pattern. randomPathSize = 10 ) // TsmPath represents a configfs file path decomposed into the components // that are expected for TSM. type TsmPath struct { // Subsystem is the TSM subsystem the path is targeting, e.g., "report" Subsystem string // Entry is the directory under the subsystem that represents a single // user's interface with the subsystem. Entry string // Attribute is a file under Entry that may be readable or writable depending // on its name. Attribute string } // String returns the configfs path that the TsmPath stands for. func (p *TsmPath) String() string { return path.Join(TsmPrefix, p.Subsystem, p.Entry, p.Attribute) } // ParseTsmPath decomposes a configfs path to TSM into its expected format, or returns // an error. func ParseTsmPath(filepath string) (*TsmPath, error) { p := path.Clean(filepath) if !strings.HasPrefix(p, TsmPrefix) { return nil, fmt.Errorf("%q does not begin with %q", p, TsmPrefix) } // If just the tsm folder is given, there won't be a "/", but if there is a subpath, // then it will have the leading "/". rest := strings.TrimPrefix(strings.TrimPrefix(p, TsmPrefix), "/") if rest == "" { return nil, fmt.Errorf("%q does not contain a subsystem", p) } dir := path.Dir(rest) file := path.Base(rest) if dir == "." { return &TsmPath{Subsystem: file}, nil } gdir := path.Dir(dir) // grand-dir mfile := path.Base(dir) if gdir == "." { return &TsmPath{Subsystem: mfile, Entry: file}, nil } ggdir := path.Dir(gdir) // grand-grand-dir subsystem := path.Base(gdir) if ggdir != "." { return nil, fmt.Errorf("%q suffix expected to be of form subsystem[/entry[/attribute]] (debug %q)", rest, ggdir) } return &TsmPath{Subsystem: subsystem, Entry: mfile, Attribute: file}, nil } func readableString(data []byte) string { var sb strings.Builder for _, b := range data { sb.WriteRune(rune('0' + (b % 10))) } return sb.String() } // TempName returns a random filename following the pattern semantics // of os.MkdirTemp. Does not have a root directory. func TempName(rand io.Reader, pattern string) string { data := make([]byte, randomPathSize) if n, err := rand.Read(data); err != nil || n != len(data) { return "rdfail" } randString := readableString(data) lastAsterisk := strings.LastIndex(pattern, "*") if lastAsterisk == -1 { return pattern + randString } return pattern[0:lastAsterisk] + randString + pattern[lastAsterisk+1:] } go-configfs-tsm-0.3.2/configfs/configfsi/path_test.go000066400000000000000000000072321464600475500226240ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. package configfsi import ( "crypto/rand" "strings" "testing" ) func TestTsmPathString(t *testing.T) { tcs := []struct { input *TsmPath want string }{ {input: &TsmPath{}, want: "/sys/kernel/config/tsm"}, {input: &TsmPath{Subsystem: "rebort"}, want: "/sys/kernel/config/tsm/rebort"}, { input: &TsmPath{Subsystem: "repart", Entry: "j"}, want: "/sys/kernel/config/tsm/repart/j", }, { input: &TsmPath{Subsystem: "report", Entry: "r", Attribute: "inblob"}, want: "/sys/kernel/config/tsm/report/r/inblob", }, } for _, tc := range tcs { got := tc.input.String() if got != tc.want { t.Errorf("%v.String() = %q, want %q", tc.input, got, tc.want) } } } func match(err error, want string) bool { if err == nil && want == "" { return true } return (err != nil && want != "" && strings.Contains(err.Error(), want)) } func TestParseTsmPath(t *testing.T) { tcs := []struct { input string want *TsmPath wantErr string }{ { input: "not/to/configfs", wantErr: `"not/to/configfs" does not begin with "/sys/kernel/config/tsm"`, }, { input: "///sys/kernel/config/tsm", wantErr: `"/sys/kernel/config/tsm" does not contain a subsystem`, }, { input: "/sys/kernel/config/tsm/report/is/way/too/long", wantErr: `"report/is/way/too/long" suffix expected to be of form`, }, { input: "/sys/kernel/config/tsm/a", want: &TsmPath{Subsystem: "a"}, }, { input: "/sys/kernel/config/tsm/a/b", want: &TsmPath{Subsystem: "a", Entry: "b"}, }, { input: "/sys/kernel/config/tsm/a/b/c", want: &TsmPath{Subsystem: "a", Entry: "b", Attribute: "c"}, }, } for _, tc := range tcs { got, err := ParseTsmPath(tc.input) if !match(err, tc.wantErr) { t.Errorf("ParseTsmPath(%q) = %v, %v errored unexpectedly. Want %s", tc.input, got, err, tc.wantErr) } if tc.wantErr == "" && *got != *tc.want { t.Errorf("ParseTsmPath(%q) = %v, nil. Want %v", tc.input, *got, *tc.want) } } } func TestTempName(t *testing.T) { tcs := []struct { name string pattern string wantPrefix string wantSuffix string }{ {name: "empty"}, { name: "no asterisk", pattern: "hi", wantPrefix: "hi", }, {name: "1 asterisk at end", pattern: "friend*", wantPrefix: "friend", }, {name: "many asterisks", pattern: "friend*ly*monster", wantPrefix: "friend*ly", wantSuffix: "monster", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { got := TempName(rand.Reader, tc.pattern) wantReplaceLen := len(tc.pattern) + randomPathSize if strings.LastIndex(tc.pattern, "*") != -1 { wantReplaceLen -= 1 // The * gets replaced, so subtract it. } if len(got) != wantReplaceLen { t.Errorf("TempName(_, %q) = %q, whose length is not %d", tc.pattern, got, wantReplaceLen) } if !strings.HasPrefix(got, tc.wantPrefix) { t.Errorf("TempName(_, %q) = %q, does not have prefix %q", tc.pattern, got, tc.wantPrefix) } if !strings.HasSuffix(got, tc.wantSuffix) { t.Errorf("TempName(_, %q) = %q, does not have suffix %q", tc.pattern, got, tc.wantSuffix) } }) } } go-configfs-tsm-0.3.2/configfs/fakertmr/000077500000000000000000000000001464600475500201425ustar00rootroot00000000000000go-configfs-tsm-0.3.2/configfs/fakertmr/fakertmr.go000066400000000000000000000150221464600475500223040ustar00rootroot00000000000000// Copyright 2024 Google LLC // // 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. // Package fakertmr defines a configfsi.Client for faking TSM behavior. // The current implementation only supports TDX. package fakertmr import ( "crypto" "crypto/rand" "crypto/sha512" "errors" "fmt" "io" "os" "path" "path/filepath" "strconv" "syscall" "github.com/google/go-configfs-tsm/configfs/configfsi" ) const ( tsmRtmrDigest = "digest" tsmPathIndex = "index" tsmPathTcgMap = "tcg_map" tsmRtmrSubsystem = "rtmr" ) // RtmrSubsystem represents a fake configfs-tsm rtmr subsystem. type RtmrSubsystem struct { // WriteAttr called on any WriteFile to an attribute. WriteAttr func(dirname string, attr string, contents []byte, indexMap map[int]bool) error // ReadAttr is called on any non-InAddr key. ReadAttr func(dirname string, attr string) ([]byte, error) // Random is the source of randomness to use for MkdirTemp Random io.Reader // We use a temp folder to store the rtmr entries. // The path to the fake rtmr subsystem. Path string // rtmrIndexMap contains set of rtmr indexes that have been initialized. // If true, the rtmr index is initialized. rtmrIndexMap map[int]bool } // RemoveAll implements configfsi.Client. func (r *RtmrSubsystem) RemoveAll(path string) error { return errors.New("rtmr subsystem does not support RemoveAll") } func readTdx(entry string, attr string) ([]byte, error) { return os.ReadFile(path.Join(entry, attr)) } func makeWriteTdx(root string) func(entry string, attr string, content []byte, indexMap map[int]bool) error { return func(entry string, attr string, content []byte, indexMap map[int]bool) error { switch attr { case tsmRtmrDigest: // Check if the content is a valid SHA384 hash. if len(content) != crypto.SHA384.Size() { return syscall.EINVAL } // Check if the entry is initialized. content, err := os.ReadFile(filepath.Join(entry, tsmPathIndex)) if err != nil { return err } rtmrIndex, err := strconv.Atoi(string(content)) if err != nil { return err } if rtmrIndex != 2 && rtmrIndex != 3 { return os.ErrPermission } oldDigest, err := os.ReadFile(filepath.Join(entry, tsmRtmrDigest)) if err != nil { return err } newDigest := sha512.Sum384(append(oldDigest[:], content...)) if err := os.WriteFile(filepath.Join(entry, tsmRtmrDigest), newDigest[:], 0666); err != nil { return err } case tsmPathIndex: rtmrIndex, e := strconv.Atoi(string(content)) if e != nil { return fmt.Errorf("WriteTdx: %v", e) } if rtmrIndex < 0 || rtmrIndex > 3 { return fmt.Errorf("WriteTdx: invalid rtmr index %d. Index can only be a non-negative number", rtmrIndex) } if indexMap[rtmrIndex] { return syscall.EBUSY } indexMap[rtmrIndex] = true if err := os.WriteFile(filepath.Join(entry, tsmPathIndex), content, 0666); err != nil { return err } var rtmrPcrMaps = map[int]string{ 0: "1,7\n", 1: "2-6\n", 2: "8-15\n", 3: "\n", } // Write the tcgmap into a temp file and rename it to keep the read-only permission. tempTsmPathTcgMap := filepath.Join(root, tsmPathTcgMap) if err := os.WriteFile(tempTsmPathTcgMap, []byte(rtmrPcrMaps[rtmrIndex]), 0400); err != nil { return err } if err := os.Rename(tempTsmPathTcgMap, filepath.Join(entry, tsmPathTcgMap)); err != nil { return err } case tsmPathTcgMap: return os.ErrPermission default: return fmt.Errorf("WriteTdx: unknown attribute %q", attr) } return nil } } // ReadDir reads the directory named by dirname // and returns a list of directory entries sorted by filename. func (r *RtmrSubsystem) ReadDir(dirname string) ([]os.DirEntry, error) { p, err := configfsi.ParseTsmPath(dirname) if err != nil { return nil, fmt.Errorf("ReadDir: %v", err) } if p.Entry != "" { return nil, fmt.Errorf("ReadDir: rtmr tsm %q cannot have subdirectories", dirname) } return os.ReadDir(r.Path) } // MkdirTemp creates a new temporary directory in the rtmr subsystem. func (r *RtmrSubsystem) MkdirTemp(dir, pattern string) (string, error) { p, err := configfsi.ParseTsmPath(dir) if err != nil { return "", fmt.Errorf("MkdirTemp: Error %v", err) } if p.Entry != "" { return "", fmt.Errorf("MkdirTemp: rtmr entry %q cannot have subdirectories", dir) } if err = os.MkdirAll(r.Path, 0755); err != nil { return "", fmt.Errorf("MkdirTemp: %v", err) } name := configfsi.TempName(r.Random, pattern) fakeRtmrPath := path.Join(r.Path, name) if err = os.Mkdir(fakeRtmrPath, 0755); err != nil { return "", fmt.Errorf("MkdirTemp: %v", err) } // Create empty index, digest and tcg_map files. perms := []int{os.O_RDWR, os.O_RDWR, os.O_RDONLY} modes := []os.FileMode{0600, 0600, 0400} for i, attr := range []string{tsmPathIndex, tsmRtmrDigest, tsmPathTcgMap} { p := filepath.Join(fakeRtmrPath, attr) f, err := os.OpenFile(p, perms[i]|os.O_CREATE, modes[i]) if err != nil { return "", fmt.Errorf("MkdirTemp: %v", err) } f.Close() } return path.Join(dir, name), nil } // ReadFile reads the contents of a file in the rtmr subsystem. func (r *RtmrSubsystem) ReadFile(name string) ([]byte, error) { p, err := configfsi.ParseTsmPath(name) if err != nil { return nil, fmt.Errorf("ReadFile: Error %v", err) } return r.ReadAttr(path.Join(r.Path, p.Entry), p.Attribute) } // WriteFile writes the contents to a file in the rtmr subsystem. func (r *RtmrSubsystem) WriteFile(name string, content []byte) error { p, err := configfsi.ParseTsmPath(name) if err != nil { return fmt.Errorf("WriteFile: %v", err) } if p.Attribute == "" { return fmt.Errorf("WriteFile: no attribute specified to %q", name) } return r.WriteAttr(path.Join(r.Path, p.Entry), p.Attribute, content, r.rtmrIndexMap) } // CreateRtmrSubsystem creates a new rtmr subsystem. // The current subsystem only supports TDX. func CreateRtmrSubsystem(tempDir string) *RtmrSubsystem { return &RtmrSubsystem{ Random: rand.Reader, WriteAttr: makeWriteTdx(tempDir), ReadAttr: readTdx, Path: path.Join(tempDir, tsmRtmrSubsystem), rtmrIndexMap: make(map[int]bool), } } go-configfs-tsm-0.3.2/configfs/faketsm/000077500000000000000000000000001464600475500177615ustar00rootroot00000000000000go-configfs-tsm-0.3.2/configfs/faketsm/fakereport.go000066400000000000000000000216461464600475500224630ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. package faketsm import ( "bytes" "crypto/rand" "encoding/hex" "errors" "fmt" "io" "os" "path" "sync" "syscall" "unicode/utf8" "github.com/google/go-configfs-tsm/configfs/configfsi" ) var ErrPrivLevelFormat = errors.New("privlevel must be 0-3") const ( // subsystemName is the expected subsystem path entry under tsm for reports. subsystemName = "report" tsmInBlobSize = 64 renderBase = 10 ) // ReportAttributeState rewrites a writable attribute's value state. May also be readable. type ReportAttributeState struct { Value []byte ReadWrite bool } // ReportEntry represents a report entry in the TSM report subsystem. type ReportEntry struct { mu sync.RWMutex destroyed bool ReadGeneration uint64 WriteGeneration uint64 // InAttrs represents the value of all WO attributes by name (relative to entry). // All possible attributes ought to be mapped on creation. InAttrs map[string]*ReportAttributeState // ROAttrs is populated on ReadFile under mu and acts as a cache when // generations align before calling ReadAttr. ROAttrs map[string][]byte } // ReportSubsystem represents the general behavior of the configfs-tsm report subsystem type ReportSubsystem struct { // CheckInAttr called on any WriteFile to an attribute. If non-nil, WriteFile returns // the error instead of writing. Called while holding client and entry locks. CheckInAttr func(e *ReportEntry, attr string, contents []byte) error // ReadAttr is called on any non-InAddr key while holding the client and entry locks. ReadAttr func(e *ReportEntry, attr string) ([]byte, error) // MakeEntry returns a fresh entry with all expected InAttrs. Called while holding // the client lock. MakeEntry func() *ReportEntry mu sync.RWMutex Entries map[string]*ReportEntry // Random is the source of randomness to use for MkdirTemp Random io.Reader } // Called while mu is held func (e *ReportEntry) tryAdvanceWriteGeneration() error { if e.destroyed { return os.ErrNotExist } if e.WriteGeneration == e.ReadGeneration-1 { return syscall.EBUSY } e.WriteGeneration += 1 return nil } // MkdirTemp creates a new temporary directory in the directory dir and returns the pathname // of the new directory. Pattern semantics follow os.MkdirTemp. func (r *ReportSubsystem) MkdirTemp(dir, pattern string) (string, error) { p, err := configfsi.ParseTsmPath(dir) if err != nil { return "", fmt.Errorf("MkdirTemp: %v", err) } if p.Entry != "" { return "", fmt.Errorf("report entry %q cannot have subdirectories", dir) } r.mu.Lock() defer r.mu.Unlock() if r.Entries == nil { r.Entries = make(map[string]*ReportEntry) } name := configfsi.TempName(r.Random, pattern) if _, ok := r.Entries[name]; ok { return "", os.ErrExist } r.Entries[name] = r.MakeEntry() return path.Join(dir, name), nil } func (e *ReportEntry) readCached(attr string) ([]byte, error) { e.mu.RLock() defer e.mu.RUnlock() if e.destroyed { return nil, os.ErrNotExist } // The only special attribute is "generation", since it peers into the // mechanics of mutation. if attr == "generation" { return []byte(fmt.Sprintf("%d\n", e.WriteGeneration)), nil } if e.ReadGeneration != e.WriteGeneration { return nil, syscall.EWOULDBLOCK } if a, ok := e.InAttrs[attr]; ok { if !a.ReadWrite { return nil, fmt.Errorf("%q is not readable", attr) } return bytes.Clone(a.Value), nil } if e.ROAttrs != nil { if a, ok := e.ROAttrs[attr]; ok { if len(a) != 0 { return bytes.Clone(a), nil } return nil, nil } } return nil, os.ErrNotExist } // ReadDir reads the directory named by dirname and returns a list of directory entries sorted by filename. func (r *ReportSubsystem) ReadDir(dirname string) ([]os.DirEntry, error) { return nil, errors.New("report subsystem does not support ReadDir") } // ReadFile reads the named file and returns the contents. func (r *ReportSubsystem) ReadFile(name string) ([]byte, error) { p, err := configfsi.ParseTsmPath(name) if err != nil { return nil, fmt.Errorf("ReadFile: %v", err) } if p.Attribute == "" { return nil, fmt.Errorf("not an attribute: %q", name) } r.mu.RLock() if r.Entries == nil { return nil, os.ErrNotExist } e, ok := r.Entries[p.Entry] if !ok { r.mu.RUnlock() return nil, os.ErrNotExist } r.mu.RUnlock() if b, err := e.readCached(p.Attribute); (err == nil && len(b) != 0) || err != syscall.EWOULDBLOCK { return b, err } e.mu.Lock() defer e.mu.Unlock() if e.ROAttrs == nil { e.ROAttrs = make(map[string][]byte) } // It's possible another thread has populated the report between RUnlock and Lock. if b, ok := e.ROAttrs[p.Attribute]; ok && e.ReadGeneration == e.WriteGeneration { return b, nil } e.ROAttrs[p.Attribute] = nil b, err := r.ReadAttr(e, p.Attribute) if err != nil { return nil, fmt.Errorf("ReadAttr(_, %q): %v", p.Attribute, err) } e.ROAttrs[p.Attribute] = b return b, nil } // WriteFile writes data to the named file, creating it if necessary. The permissions // are implementation-defined. func (r *ReportSubsystem) WriteFile(name string, contents []byte) error { p, err := configfsi.ParseTsmPath(name) if err != nil { return fmt.Errorf("WriteFile: %v", err) } if p.Attribute == "" { return fmt.Errorf("cannot write to non-attribute: %q", name) } r.mu.Lock() defer r.mu.Unlock() e, ok := r.Entries[p.Entry] if !ok { return os.ErrNotExist } e.mu.Lock() defer e.mu.Unlock() if e.destroyed { return os.ErrNotExist } if err := r.CheckInAttr(e, p.Attribute, contents); err != nil { return fmt.Errorf("could not write %q: %v", name, err) } if err := e.tryAdvanceWriteGeneration(); err != nil { return err } e.InAttrs[p.Attribute].Value = contents return nil } // RemoveAll removes path and any children it contains. func (r *ReportSubsystem) RemoveAll(name string) error { p, err := configfsi.ParseTsmPath(name) if err != nil { return fmt.Errorf("RemoveAll: %v", err) } if p.Attribute != "" || p.Entry == "" || p.Subsystem != subsystemName { return fmt.Errorf("RemoveAll(%q) expected report subsystem entry path", name) } r.mu.Lock() defer r.mu.Unlock() if r.Entries == nil { return os.ErrNotExist } e, ok := r.Entries[p.Entry] if !ok { return os.ErrNotExist } // Don't delete while another operation is using the entry. e.mu.Lock() e.destroyed = true delete(r.Entries, p.Entry) e.mu.Unlock() return nil } func renderOutBlob(privlevel, inblob []byte) []byte { // checkv7 already ensures this does not error priv, _ := configfsi.Kstrtouint(privlevel, renderBase, 2) return []byte(fmt.Sprintf("privlevel: %d\ninblob: %s", priv, hex.EncodeToString(inblob))) } func readV7(privlevelFloor uint) func(*ReportEntry, string) ([]byte, error) { return func(e *ReportEntry, attr string) ([]byte, error) { switch attr { case "provider": return []byte("fake\n"), nil case "auxblob": return []byte(`auxblob`), nil case "outblob": privlevel := []byte("") if a, ok := e.InAttrs["privlevel"]; ok && len(a.Value) > 0 { privlevel = a.Value } inblob, ok := e.InAttrs["inblob"] if !ok || len(inblob.Value) == 0 { return nil, syscall.EINVAL } return renderOutBlob(privlevel, inblob.Value), nil case "privlevel_floor": return []byte(fmt.Sprintf("%d\n", privlevelFloor)), nil } return nil, os.ErrNotExist } } func makeV7() *ReportEntry { return &ReportEntry{ InAttrs: map[string]*ReportAttributeState{ "privlevel": {Value: []byte("0\n")}, "inblob": {}, }, } } func checkV7(privlevelFloor uint) func(*ReportEntry, string, []byte) error { return func(e *ReportEntry, attr string, contents []byte) error { switch attr { case "inblob": if len(contents) > tsmInBlobSize { return syscall.EINVAL } case "privlevel": if !utf8.Valid(contents) { return ErrPrivLevelFormat } level, err := configfsi.Kstrtouint(contents, renderBase, 2) if err != nil { return ErrPrivLevelFormat } if uint(level) < privlevelFloor { return fmt.Errorf("privlevel %d cannot be less than %d", level, privlevelFloor) } default: return fmt.Errorf("unwritable attribute: %q", attr) } return nil } } // ReportV7 returns an empty report subsystem with attributes as specified in the configfs-tsm // Patch v7 series. func ReportV7(privlevelFloor uint) *ReportSubsystem { return &ReportSubsystem{ MakeEntry: makeV7, ReadAttr: readV7(privlevelFloor), CheckInAttr: checkV7(privlevelFloor), Random: rand.Reader, } } go-configfs-tsm-0.3.2/configfs/faketsm/fakereport_test.go000066400000000000000000000111571464600475500235160ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. package faketsm import ( "bytes" "fmt" "math/big" "path" "testing" "github.com/google/go-configfs-tsm/configfs/configfsi" "github.com/google/go-configfs-tsm/report" ) func checkOutblobExpectation(inblob []byte, privlevel uint, outblob []byte) error { want := renderOutBlob([]byte(fmt.Sprintf("%d\n", privlevel)), inblob) if !bytes.Equal(want, outblob) { return fmt.Errorf("got %q, want %q", string(outblob), string(want)) } return nil } func makeNonce(id uint) []byte { // The nonce is currently expected to always be size 64. result := make([]byte, 64) copy(result, []byte(big.NewInt(int64(id)).String())) return result } func checkErr(err error) (bool, error) { if err != nil { if err := report.GetGenerationErr(err); err != nil { return true, nil } return false, err } return false, nil } func runIteration(t testing.TB, c configfsi.Client, r *report.OpenReport, tc *runner) error { t.Helper() nonce := makeNonce(tc.id) t.Logf("Writing inblob %v", nonce) if err := r.WriteOption("inblob", nonce); err != nil { return fmt.Errorf("could not write inblob in %d: %v", tc.id, err) } t.Logf("Writing privlevel %d", (tc.id % 4)) if err := r.WriteOption("privlevel", []byte(fmt.Sprintf("%d", (tc.id%4)))); err != nil { return fmt.Errorf("could not set privlevel: %v", err) } t.Logf("Reading outblob") out, err := r.ReadOption("outblob") if done, err := checkErr(err); done || err != nil { if err != nil { return fmt.Errorf("outblob read on client %d failed: %v", tc.id, err) } return nil } t.Logf("Checking outblob %v", out) if err := checkOutblobExpectation(nonce, (tc.id % 4), out); err != nil { return fmt.Errorf("attestation invariant violated: %v", err) } return nil } type runner struct { iterations int id uint done chan int } func runInterference(t testing.TB, c configfsi.Client, entryPath string, tc *runner) { t.Helper() for i := 0; i < tc.iterations; i++ { r, err := report.UnsafeWrap(c, entryPath) if err != nil { t.Errorf("could not create report entry: %v", err) tc.done <- 1 return } if err := runIteration(t, c, r, tc); err != nil { t.Error(err) tc.done <- 1 return } } t.Logf("Posting done for %d", tc.id) tc.done <- 0 } func runNoninterference(t testing.TB, c configfsi.Client, tc *runner) { t.Helper() for i := 0; i < tc.iterations; i++ { nonce := makeNonce(tc.id) resp, err := report.Get(c, &report.Request{ InBlob: nonce, Privilege: &report.Privilege{Level: (tc.id % 4)}, }) if err == nil { err = checkOutblobExpectation(nonce, (tc.id % 4), resp.OutBlob) } if err != nil { t.Error(err) tc.done <- 1 return } } t.Logf("Posting done for %d", tc.id) tc.done <- 0 } // clients-many concurrent routines attempt to get an output on the same entry with // different inblobs. func nonceAnonceB(t testing.TB, clients, iterations int) { t.Helper() c := ReportV7(0) entryPath, err := c.MkdirTemp(path.Join(configfsi.TsmPrefix, "report"), "entry") if err != nil { t.Fatalf("could not create entry: %v", err) } t.Logf("made entry %s", entryPath) defer c.RemoveAll(entryPath) complete := make(chan int) for i := 0; i < clients; i++ { go runInterference(t, c, entryPath, &runner{ iterations: iterations, id: uint(i), done: complete}) } // Each client should write to the channel. for i := 0; i < clients; i++ { code := <-complete if code == 1 { t.Fatalf("early failure") } } t.Logf("doooone") } func noninterferenceByDesign(t testing.TB, clients, iterations int) { t.Helper() c := ReportV7(0) complete := make(chan int) for i := 0; i < clients; i++ { go runNoninterference(t, c, &runner{ iterations: iterations, id: uint(i), done: complete}) } // Each client should write to the channel. for i := 0; i < clients; i++ { code := <-complete if code == 1 { t.Fatalf("early failure") } } } func BenchmarkReportGenerationInterference(b *testing.B) { nonceAnonceB(b, 4, b.N) } func BenchmarkReportGenerationNoninterference(b *testing.B) { noninterferenceByDesign(b, 20, b.N) } go-configfs-tsm-0.3.2/configfs/faketsm/faketsm.go000066400000000000000000000057641464600475500217560ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. // Package faketsm defines a configfsi.Client for faking TSM behavior. // The provider attribute returns "fake" and the attestation report format // is just the state of the attributes. The certificate blob is part of the // Client definition. package faketsm import ( "fmt" "os" "github.com/google/go-configfs-tsm/configfs/configfsi" ) // Client provides a "fake" provider for configfs to emulate the /sys/kernel/config/tsm behavior. // Dispatches to specialized subsystem Client interfaces. type Client struct { Subsystems map[string]configfsi.Client } func (c *Client) getSubsystem(name string) (configfsi.Client, error) { p, err := configfsi.ParseTsmPath(name) if err != nil { return nil, fmt.Errorf("getSubsystem: %v", err) } if p.Subsystem == "" { return nil, fmt.Errorf("faketsm: expected tsm subsystem in %q", name) } sub, ok := c.Subsystems[p.Subsystem] if !ok { return nil, fmt.Errorf("faketsm: unsupported subsystem %q", p.Subsystem) } return sub, nil } // ReadDir reads the directory named by dir and returns a list of directory entries. func (c *Client) ReadDir(dir string) ([]os.DirEntry, error) { if dir == "" { return nil, fmt.Errorf("faketsm doesn't implement empty directory behavior") } sub, err := c.getSubsystem(dir) if err != nil { return nil, err } return sub.ReadDir(dir) } // MkdirTemp creates a new temporary directory in the directory dir and returns the pathname // of the new directory. Pattern semantics follow os.MkdirTemp. func (c *Client) MkdirTemp(dir, pattern string) (string, error) { if dir == "" { return "", fmt.Errorf("faketsm doesn't implement empty directory behavior") } sub, err := c.getSubsystem(dir) if err != nil { return "", err } return sub.MkdirTemp(dir, pattern) } // ReadFile reads the named file and returns the contents. func (c *Client) ReadFile(name string) ([]byte, error) { sub, err := c.getSubsystem(name) if err != nil { return nil, err } return sub.ReadFile(name) } // WriteFile writes data to the named file, creating it if necessary. The permissions // are implementation-defined. func (c *Client) WriteFile(name string, contents []byte) error { sub, err := c.getSubsystem(name) if err != nil { return err } return sub.WriteFile(name, contents) } // RemoveAll removes path and any children it contains. func (c *Client) RemoveAll(name string) error { sub, err := c.getSubsystem(name) if err != nil { return err } return sub.RemoveAll(name) } go-configfs-tsm-0.3.2/configfs/linuxtsm/000077500000000000000000000000001464600475500202125ustar00rootroot00000000000000go-configfs-tsm-0.3.2/configfs/linuxtsm/aliases.go000066400000000000000000000022221464600475500221600ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. package linuxtsm // The aliases.go file is for "convenience" functions when folks only want to use the // Linux client. import ( "github.com/google/go-configfs-tsm/report" "go.uber.org/multierr" ) // GetReport returns a one-shot configfs-tsm report given a report request. func GetReport(req *report.Request) (*report.Response, error) { var err error client, err := MakeClient() if err != nil { return nil, err } r, err := report.Create(client, req) if err != nil { return nil, err } response, err := r.Get() return response, multierr.Combine(r.Destroy(), err) } go-configfs-tsm-0.3.2/configfs/linuxtsm/linuxtsm.go000066400000000000000000000043441464600475500224310ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. // Package linuxtsm defines a configfsi.Client for Linux OS operations on configfs. package linuxtsm import ( "fmt" "os" "path" "github.com/google/go-configfs-tsm/configfs/configfsi" ) // client provides configfsi.Client for /sys/kernel/config/tsm file operations in Linux. type client struct{} // MkdirTemp creates a new temporary directory in the directory dir and returns the pathname // of the new directory. Pattern semantics follow os.MkdirTemp. func (*client) MkdirTemp(dir, pattern string) (string, error) { return os.MkdirTemp(dir, pattern) } // ReadFile reads the named file and returns the contents. func (*client) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) } // WriteFile writes data to the named file, creating it if necessary. The permissions // are implementation-defined. func (*client) WriteFile(name string, contents []byte) error { return os.WriteFile(name, contents, 0220) } // RemoveAll removes path and any children it contains. func (*client) RemoveAll(path string) error { return os.Remove(path) } // ReadDir reads the directory named by dirname and returns a list of directory // entries sorted by filename. func (*client) ReadDir(dirname string) ([]os.DirEntry, error) { return os.ReadDir(dirname) } // MakeClient returns a "real" client for using configfs for TSM use. func MakeClient() (configfsi.Client, error) { // Linux client expects just the "report" subsystem for now. checkPath := path.Join(configfsi.TsmPrefix, "report") info, err := os.Stat(checkPath) if err != nil { return nil, err } if !info.IsDir() { return nil, fmt.Errorf("expected %s to be a directory", checkPath) } return &client{}, nil } go-configfs-tsm-0.3.2/go.mod000066400000000000000000000001301464600475500156310ustar00rootroot00000000000000module github.com/google/go-configfs-tsm go 1.19 require go.uber.org/multierr v1.11.0 go-configfs-tsm-0.3.2/go.sum000066400000000000000000000007431464600475500156700ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= go-configfs-tsm-0.3.2/report/000077500000000000000000000000001464600475500160445ustar00rootroot00000000000000go-configfs-tsm-0.3.2/report/report.go000066400000000000000000000162261464600475500177150ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. // Package report provides an API to the configfs/tsm/report subsystem for collecting // attestation reports and associated certificates. package report import ( "errors" "fmt" "github.com/google/go-configfs-tsm/configfs/configfsi" "go.uber.org/multierr" ) const ( subsystem = "report" subsystemPath = configfsi.TsmPrefix + "/" + subsystem numberAttributeBase = 10 ) // Privilege represents the requested privilege information at which a report should // be created. type Privilege struct { Level uint } // Request represents an open request for an attestation report. type Request struct { InBlob []byte Privilege *Privilege GetAuxBlob bool } // OpenReport represents a created tsm report subtree with internal expectations for the generation. type OpenReport struct { InBlob []byte Privilege *Privilege GetAuxBlob bool entry *configfsi.TsmPath expectedGeneration uint64 client configfsi.Client } // Response represents a common case response for getting at attestation report to avoid // multiple attribute access calls. type Response struct { Provider string OutBlob []byte AuxBlob []byte } // GenerationErr is returned when an attribute's value is invalid due to mismatched expectations // on the number of writes to a report entry. type GenerationErr struct { Got uint64 Want uint64 Attribute string } // Error returns the human-readable explanation for the error. func (e *GenerationErr) Error() string { return fmt.Sprintf("report generation was %d when expecting %d while reading property %q", e.Got, e.Want, e.Attribute) } // GetGenerationErr returns the GenerationErr contained in an error with 0 or 1 wraps. func GetGenerationErr(err error) *GenerationErr { var result *GenerationErr if err != nil && (errors.As(err, &result) || errors.As(errors.Unwrap(err), &result)) { return result } return nil } func (r *OpenReport) attribute(subtree string) string { a := *r.entry a.Attribute = subtree return a.String() } func readUint64File(client configfsi.Client, p string) (uint64, error) { data, err := client.ReadFile(p) if err != nil { return 0, fmt.Errorf("could not read %q: %v", p, err) } return configfsi.Kstrtouint(data, numberAttributeBase, 64) } // CreateOpenReport returns a newly-created entry in the configfs-tsm report subtree with an initial // expected generation value. func CreateOpenReport(client configfsi.Client) (*OpenReport, error) { entry, err := client.MkdirTemp(subsystemPath, "entry") if err != nil { return nil, fmt.Errorf("could not create report entry in configfs: %v", err) } return UnsafeWrap(client, entry) } // UnsafeWrap returns a new OpenReport for a given report entry. func UnsafeWrap(client configfsi.Client, entryPath string) (r *OpenReport, err error) { p, _ := configfsi.ParseTsmPath(entryPath) r = &OpenReport{ client: client, entry: &configfsi.TsmPath{Subsystem: subsystem, Entry: p.Entry}, } r.expectedGeneration, err = readUint64File(client, r.attribute("generation")) if err != nil { // The report was created but couldn't be properly initialized. return nil, multierr.Combine(r.Destroy(), err) } return r, nil } // Create returns a newly-created entry in the configfs-tsm report subtree with common inputs // for the Get() method initialized from the request. func Create(client configfsi.Client, req *Request) (*OpenReport, error) { r, err := CreateOpenReport(client) if err != nil { return nil, err } r.InBlob = req.InBlob // InBlob is not a copy! r.Privilege = req.Privilege r.GetAuxBlob = req.GetAuxBlob return r, nil } // Destroy returns an error if the configfs report subtree cannot be removed. Will not error for // partially initialized or already-destroyed reports. func (r *OpenReport) Destroy() error { if r.entry != nil { if err := r.client.RemoveAll(r.entry.String()); err != nil { return err } r.entry = nil } return nil } // PrivilegeLevelFloor returns the privlevel_floor attribute interpreted as the uint type it is. func (r *OpenReport) PrivilegeLevelFloor() (uint, error) { data, err := r.ReadOption("privlevel_floor") if err != nil { return 0, err } i, err := configfsi.Kstrtouint(data, numberAttributeBase, 32) if err != nil { return 0, fmt.Errorf("could not parse privlevel_floor data %v as int: %v", data, err) } return uint(i), nil } // WriteOption sets a configfs report option to the provided data and internally tracks // the generation that should be expected on the next ReadOption. func (r *OpenReport) WriteOption(subtree string, data []byte) error { if err := r.client.WriteFile(r.attribute(subtree), data); err != nil { return fmt.Errorf("could not write report %s: %v", subtree, err) } r.expectedGeneration += 1 return nil } // ReadOption is a safe accessor to a readable attribute of a report. Returns an error if there is // any detected tampering to the ongoing request. func (r *OpenReport) ReadOption(subtree string) ([]byte, error) { data, err := r.client.ReadFile(r.attribute(subtree)) if err != nil { return nil, fmt.Errorf("could not read report property %q: %v", subtree, err) } gotGeneration, err := readUint64File(r.client, r.attribute("generation")) if err != nil { return nil, err } if gotGeneration != r.expectedGeneration { return nil, &GenerationErr{Got: gotGeneration, Want: r.expectedGeneration, Attribute: subtree} } return data, nil } // Get returns the requested report data after initializing the context to the expected // parameters. Returns an error if the kernel reports an error or there is a difference in expected // generation value. func (r *OpenReport) Get() (*Response, error) { var err error if err := r.WriteOption("inblob", r.InBlob); err != nil { return nil, err } if r.Privilege != nil { if err := r.WriteOption("privlevel", []byte(fmt.Sprintf("%d", r.Privilege.Level))); err != nil { return nil, err } } resp := &Response{} if r.GetAuxBlob { resp.AuxBlob, err = r.ReadOption("auxblob") if err != nil { return nil, fmt.Errorf("could not read report auxblob: %w", err) } } resp.OutBlob, err = r.ReadOption("outblob") if err != nil { return nil, fmt.Errorf("could not read report outblob: %w", err) } providerData, err := r.ReadOption("provider") if err != nil { return nil, err } resp.Provider = string(providerData) return resp, nil } // Get returns a one-shot configfs-tsm report given a report request. func Get(client configfsi.Client, req *Request) (*Response, error) { var err error r, err := Create(client, req) if err != nil { return nil, err } response, err := r.Get() return response, multierr.Combine(r.Destroy(), err) } go-configfs-tsm-0.3.2/report/report_test.go000066400000000000000000000050331464600475500207460ustar00rootroot00000000000000// Copyright 2023 Google LLC // // 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. package report import ( "bytes" "strings" "testing" "github.com/google/go-configfs-tsm/configfs/configfsi" "github.com/google/go-configfs-tsm/configfs/faketsm" ) func TestGet(t *testing.T) { c := &faketsm.Client{Subsystems: map[string]configfsi.Client{"report": faketsm.ReportV7(0)}} req := &Request{ InBlob: []byte("lessthan64bytesok"), GetAuxBlob: true, } resp, err := Get(c, req) if err != nil { t.Fatalf("Get(%+v) = %+v, %v, want nil", req, resp, err) } wantOut := "privlevel: 0\ninblob: 6c6573737468616e363462797465736f6b" if !bytes.Equal(resp.OutBlob, []byte(wantOut)) { t.Errorf("OutBlob %v is not %v", string(resp.OutBlob), wantOut) } wantProvider := "fake\n" if resp.Provider != wantProvider { t.Errorf("provider = %q, want %q", resp.Provider, wantProvider) } if !bytes.Equal(resp.AuxBlob, []byte(`auxblob`)) { t.Errorf("auxblob = %v, want %v", resp.AuxBlob, []byte(`auxblob`)) } } func TestGetErr(t *testing.T) { tcs := []struct { name string req *Request floor uint wantErr string }{ { name: "inblob too big", req: &Request{ InBlob: make([]byte, 4096), }, wantErr: "invalid argument", }, { name: "privlevel too high", req: &Request{ InBlob: make([]byte, 64), Privilege: &Privilege{Level: 300}, }, wantErr: "privlevel must be 0-3", }, { name: "missing inblob", req: &Request{}, wantErr: "invalid argument", }, { name: "privlevel too low", req: &Request{ InBlob: make([]byte, 64), Privilege: &Privilege{Level: 0}, }, floor: 1, wantErr: "privlevel 0 cannot be less than 1", }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { c := &faketsm.Client{Subsystems: map[string]configfsi.Client{"report": faketsm.ReportV7(tc.floor)}} resp, err := Get(c, tc.req) if err == nil || !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("Get(%+v) = %+v, %v, want %q", tc.req, resp, err, tc.wantErr) } }) } } go-configfs-tsm-0.3.2/rtmr/000077500000000000000000000000001464600475500155155ustar00rootroot00000000000000go-configfs-tsm-0.3.2/rtmr/rtmr.go000066400000000000000000000130411464600475500170270ustar00rootroot00000000000000// Copyright 2024 Google LLC // // 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. // Package rtmr provides an API to the configfs/tsm/rtmr subsystem for // extending runtime measurements to RTMR registers. package rtmr import ( "crypto" "fmt" "strconv" "github.com/google/go-configfs-tsm/configfs/configfsi" ) const ( rtmrSubsystem = "rtmrs" tsmRtmrPrefix = configfsi.TsmPrefix + "/" + rtmrSubsystem // The digest of the rtmr register. tsmRtmrDigest = "digest" // A Runtime Measurement Register (RTMR) hardware index. tsmPathIndex = "index" // A representation of the architecturally defined mapping between this RTMR and one or more TCG TPM PCRs tsmPathTcgMap = "tcg_map" ) // Extend is a struct that represents a rtmr entry in the configfs. type Extend struct { RtmrIndex int entry *configfsi.TsmPath client configfsi.Client } // Response is a struct that represents the response of reading a rtmr entry in the configfs. type Response struct { RtmrIndex int digest []byte tcgMap []byte } func (r *Extend) attribute(subtree string) string { a := *r.entry a.Attribute = subtree return a.String() } // extendDigest extends the measurement to the rtmr with the given hash. func (r *Extend) extendDigest(hash []byte) error { if err := r.client.WriteFile(r.attribute(tsmRtmrDigest), hash); err != nil { return fmt.Errorf("could not write digest to rmtr%d: %v", r.RtmrIndex, err) } return nil } // getDigest returns the digest of the rtmr. func (r *Extend) getDigest() ([]byte, error) { return r.client.ReadFile(r.attribute(tsmRtmrDigest)) } // getTcgMap returns the tcg map of the rtmr. func (r *Extend) getTcgMap() ([]byte, error) { return r.client.ReadFile(r.attribute(tsmPathTcgMap)) } // validateIndex checks if the rtmr index matches the expected value. func (r *Extend) validateIndex() bool { if r == nil { return false } indexBytes, err := r.client.ReadFile(r.attribute(tsmPathIndex)) if err != nil { return false } index, err := configfsi.Kstrtouint(indexBytes, 10, 64) if err != nil { return false } if int(index) != r.RtmrIndex { return false } return true } // setRtmrIndex sets a configfs rtmr entry to the given index. // It reports an error if the index cannot be written. func (r *Extend) setRtmrIndex() error { indexBytes := []byte(strconv.Itoa(r.RtmrIndex)) // Convert index to []byte indexPath := r.attribute(tsmPathIndex) if err := r.client.WriteFile(indexPath, indexBytes); err != nil { return fmt.Errorf("could not write index %s: %v", indexPath, err) } return nil } // searchRtmrInterface searches for an rtmr entry in the configfs. func searchRtmrInterface(client configfsi.Client, index int) *Extend { root := tsmRtmrPrefix entries, err := client.ReadDir(root) if err != nil { return nil } for _, d := range entries { if d.IsDir() { r := &Extend{ RtmrIndex: index, entry: &configfsi.TsmPath{Subsystem: rtmrSubsystem, Entry: d.Name()}, client: client, } if r.validateIndex() { return r } } } return nil } // createRtmrInterface creates a new rtmr entry in the configfs. func createRtmrInterface(client configfsi.Client, index int) (*Extend, error) { entryPath, err := client.MkdirTemp(tsmRtmrPrefix, fmt.Sprintf("rtmr%d-", index)) if err != nil { return nil, err } p, _ := configfsi.ParseTsmPath(entryPath) r := &Extend{ RtmrIndex: index, entry: &configfsi.TsmPath{Subsystem: rtmrSubsystem, Entry: p.Entry}, client: client, } if err := r.setRtmrIndex(); err != nil { return nil, fmt.Errorf("could not set rtmr index %d: %v", index, err) } return r, nil } // getRtmrInterface returns the rtmr entry in the configfs. func getRtmrInterface(client configfsi.Client, index int) (*Extend, error) { // The configfs-tsm interface only allows one rtmr entry for a given index. // If the rtmr entry already exists, we should extend the digest to it. var err error r := searchRtmrInterface(client, index) if r == nil { r, err = createRtmrInterface(client, index) } return r, err } // ExtendDigest extends the measurement to the rtmr with the given digest. func ExtendDigest(client configfsi.Client, rtmr int, digest []byte) error { if len(digest) != crypto.SHA384.Size() { return fmt.Errorf("the length of the digest must be %d bytes, the input is %d bytes", crypto.SHA384.Size(), len(digest)) } if rtmr < 0 { return fmt.Errorf("invalid rtmr index %d. Index can only be a non-negative number", rtmr) } r, err := getRtmrInterface(client, rtmr) if err != nil { return err } return r.extendDigest(digest) } // GetDigest returns the digest and the tcg map of a given rtmr index. func GetDigest(client configfsi.Client, rtmr int) (*Response, error) { if rtmr < 0 { return nil, fmt.Errorf("invalid rtmr index %d. Index can only be a non-negative number", rtmr) } r, err := getRtmrInterface(client, rtmr) if err != nil { return nil, err } digest, err := r.getDigest() if err != nil { return nil, err } tcgmap, err := r.getTcgMap() if err != nil { return nil, err } return &Response{ RtmrIndex: rtmr, digest: digest, tcgMap: tcgmap, }, nil } go-configfs-tsm-0.3.2/rtmr/rtmr_test.go000066400000000000000000000100121464600475500200610ustar00rootroot00000000000000// Copyright 2024 Google LLC // // 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. package rtmr import ( "bytes" "strings" "testing" "github.com/google/go-configfs-tsm/configfs/fakertmr" ) func TestExtendDigestErr(t *testing.T) { var sha384Hash [48]byte tcsErr := []struct { rtmr int digest []byte wantErr string }{ {rtmr: 1, digest: sha384Hash[:], wantErr: "could not write digest to rmtr1"}, {rtmr: 3, digest: []byte("aaaaaaaa"), wantErr: "the length of the digest must be 48 bytes"}, {rtmr: -1, digest: sha384Hash[:], wantErr: "invalid rtmr index -1. Index can only be a non-negative number"}, } client := fakertmr.CreateRtmrSubsystem(t.TempDir()) for _, tc := range tcsErr { err := ExtendDigest(client, tc.rtmr, tc.digest) if err == nil || !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("ExtendtoRtmrClient(%d, %q) failed: %v, want %q", tc.rtmr, tc.digest, err, tc.wantErr) } } } func TestExtendDigestRtmrOk(t *testing.T) { var sha384Hash [48]byte tcsOk := []struct { rtmr int digest []byte }{ {rtmr: 2, digest: sha384Hash[:]}, {rtmr: 3, digest: sha384Hash[:]}, // Test the same rtmr index with an existing entry. {rtmr: 3, digest: sha384Hash[:]}, } client := fakertmr.CreateRtmrSubsystem(t.TempDir()) for _, tc := range tcsOk { err := ExtendDigest(client, tc.rtmr, tc.digest) if err != nil { t.Fatalf("ExtendtoRtmrClient (%d, %q) failed: %v", tc.rtmr, tc.digest, err) } } } func TestGetDigestErr(t *testing.T) { tcsErr := []struct { rtmr int wantErr string }{ {rtmr: -1, wantErr: "invalid rtmr index -1. Index can only be a non-negative number"}, } client := fakertmr.CreateRtmrSubsystem(t.TempDir()) for _, tc := range tcsErr { _, err := GetDigest(client, tc.rtmr) if err == nil || !strings.Contains(err.Error(), tc.wantErr) { t.Fatalf("GetDigestRtmr(%d) failed: %v, want %q", tc.rtmr, err, tc.wantErr) } } } func TestGetDigestOk(t *testing.T) { var sha384Hash [48]byte tcsOk := []struct { rtmr int digest []byte tcgMap []byte }{ {rtmr: 0, digest: sha384Hash[:], tcgMap: []byte("1,7\n")}, {rtmr: 1, digest: sha384Hash[:], tcgMap: []byte("2-6\n")}, {rtmr: 2, digest: sha384Hash[:], tcgMap: []byte("8-15\n")}, {rtmr: 3, digest: sha384Hash[:], tcgMap: []byte("\n")}, // Test the same rtmr index with an existing entry. {rtmr: 2, digest: sha384Hash[:], tcgMap: []byte("8-15\n")}, } client := fakertmr.CreateRtmrSubsystem(t.TempDir()) for _, tc := range tcsOk { r, err := GetDigest(client, tc.rtmr) if err != nil { t.Fatalf("GetDigestRtmr(%d) failed: %v", tc.rtmr, err) } if r.RtmrIndex != tc.rtmr { t.Fatalf("GetDigestRtmr(%d) failed: got %d, want %d", tc.rtmr, r.RtmrIndex, tc.rtmr) } if !bytes.Equal(r.tcgMap, tc.tcgMap) { t.Fatalf("GetDigestRtmr(%d) failed: got %q, want %q", tc.rtmr, r.tcgMap, tc.tcgMap) } } } func TestGetRtmrDigestAndExtendDigest(t *testing.T) { var sha384Hash [48]byte sha384Hash[0] = 0x01 client := fakertmr.CreateRtmrSubsystem(t.TempDir()) rtmrIndex := 3 // GetDigest digest1, err := GetDigest(client, rtmrIndex) if err != nil { t.Fatalf("GetDigest(%d) failed: %v", rtmrIndex, err) } // ExtendDigest err = ExtendDigest(client, rtmrIndex, sha384Hash[:]) if err != nil { t.Fatalf("ExtendDigest(%d) failed: %v", rtmrIndex, err) } // GetDigest digest2, err := GetDigest(client, rtmrIndex) if err != nil { t.Fatalf("GetDigest(%d) failed: %v", rtmrIndex, err) } if bytes.Equal(digest1.digest, digest2.digest) { t.Fatalf("rtmr%q does not change after an extend %q", rtmrIndex, digest2.digest) } }