pax_global_header00006660000000000000000000000064151540047700014515gustar00rootroot0000000000000052 comment=f0467d2eb8801ceff05258da4f08140716fb165a age-plugin-tpm-1.0.1/000077500000000000000000000000001515400477000143425ustar00rootroot00000000000000age-plugin-tpm-1.0.1/.github/000077500000000000000000000000001515400477000157025ustar00rootroot00000000000000age-plugin-tpm-1.0.1/.github/workflows/000077500000000000000000000000001515400477000177375ustar00rootroot00000000000000age-plugin-tpm-1.0.1/.github/workflows/build.yml000066400000000000000000000041141515400477000215610ustar00rootroot00000000000000name: Build and upload binaries on: release: types: [published] push: pull_request: permissions: contents: read jobs: build: name: Build binaries runs-on: ubuntu-latest strategy: matrix: include: - {PLATFORM: linux-amd64, GOOS: linux, GOARCH: amd64} - {PLATFORM: linux-armv6, GOOS: linux, GOARCH: arm, GOARM: 6} - {PLATFORM: linux-arm64, GOOS: linux, GOARCH: arm64} steps: - name: Install Go uses: actions/setup-go@v5 with: go-version: 1.x - name: Checkout repository uses: actions/checkout@v2 with: fetch-depth: 0 - name: Build binary run: | cp LICENSE "$RUNNER_TEMP/LICENSE" echo -e "\n---\n" >> "$RUNNER_TEMP/LICENSE" curl -L "https://go.dev/LICENSE?m=text" >> "$RUNNER_TEMP/LICENSE" VERSION="$(git describe --tags)" DIR="$(mktemp -d)" mkdir "$DIR/age-plugin-tpm" cp "$RUNNER_TEMP/LICENSE" "$DIR/age-plugin-tpm" go build -o "$DIR/age-plugin-tpm" -ldflags "-X main.version=$VERSION" -trimpath ./cmd/... tar -cvzf "age-plugin-tpm-$VERSION-$GOOS-$GOARCH.tar.gz" -C "$DIR" age-plugin-tpm env: CGO_ENABLED: 0 GOOS: ${{ matrix.GOOS }} GOARCH: ${{ matrix.GOARCH }} GOARM: ${{ matrix.GOARM }} - name: Upload workflow artifacts uses: actions/upload-artifact@v4 with: name: age-plugin-tpm-binaries-${{ matrix.PLATFORM }} path: age-plugin-tpm-* upload: name: Upload release binaries if: github.event_name == 'release' needs: build permissions: contents: write runs-on: ubuntu-latest steps: - name: Download workflow artifacts uses: actions/download-artifact@v4 with: pattern: age-plugin-tpm-binaries-* merge-multiple: true - name: Upload release artifacts run: gh release upload "$GITHUB_REF_NAME" age-plugin-tpm-* env: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} age-plugin-tpm-1.0.1/.github/workflows/test.yml000066400000000000000000000016001515400477000214360ustar00rootroot00000000000000name: Go tests on: [push, pull_request] permissions: contents: read jobs: test: name: Test strategy: fail-fast: false matrix: go: [1.x] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go ${{ matrix.go }} uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - name: Checkout repository uses: actions/checkout@v2 with: fetch-depth: 0 - name: add stefanberger/swtpm-jammy run: sudo add-apt-repository ppa:stefanberger/swtpm-jammy - name: Install swtpm and age run: sudo apt-get install -y swtpm-tools age - name: Run tests run: go test -race ./... - name: Run go vet run: go vet ./... - name: staticcheck uses: dominikh/staticcheck-action@v1.3.1 with: install-go: false age-plugin-tpm-1.0.1/LICENSE000066400000000000000000000021011515400477000153410ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2023 age-plugin-tpm Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. age-plugin-tpm-1.0.1/Makefile000066400000000000000000000003121515400477000157760ustar00rootroot00000000000000 all: build build: age-plugin-tpm age-plugin-tpm: go build -o age-plugin-tpm ./cmd/age-plugin-tpm .PHONY: age-plugin-tpm test: go test ./... check: staticcheck ./... go vet ./... .PHONY: test age-plugin-tpm-1.0.1/README.md000066400000000000000000000051131515400477000156210ustar00rootroot00000000000000TPM plugin for age clients ========================== `age-plugin-tpm` is a plugin for [age](https://age-encryption.org/v1) clients like [`age`](https://age-encryption.org) and [`rage`](https://str4d.xyz/rage), which enables files to be encrypted to age identities sealed by the TPM. # Features * Keys created on the TPM, sealed outside of it * PIN support * TPM session encryption ## Installation The simplest way of installing this plugin is by running the follow go command. `go install github.com/foxboron/age-plugin-tpm/cmd/age-plugin-tpm@latest` Alternatively download the [pre-built binaries](https://github.com/foxboron/age-plugin-tpm/releases). # Usage ```bash # Create identity $ age-plugin-tpm --generate -o age-identity.txt $ age-plugin-tpm -y age-identity.txt > age-recipient.txt # Encrypt / Decrypt something $ echo 'Hack The Planet!' | age -R age-recipient.txt -o test-decrypt.txt $ age --decrypt -i age-identity.txt -o - test-decrypt.txt Hack The Planet! ``` You can add `--pin` when calling `--generate` to require a PIN when encrypting or decrypting. ### When used non-interactively If you want to use a `--pin` non-interactively, you can use the `AGE_TPM_PIN` environment variable. Please be aware that environment variables are not secure, and can be read from `/proc/$PID/environ`. ```bash # Create identity $ AGE_TPM_PIN=1234 age-plugin-tpm --generate --pin -o age-identity.txt $ age-plugin-tpm -y age-identity.txt > age-recipient.txt # Encrypt / Decrypt something $ echo 'Hack The Planet!' | age -R age-recipient.txt -o test-decrypt.txt $ AGE_TPM_PIN=1234 age --decrypt -i age-identity.txt -o - test-decrypt.txt Hack The Planet! ``` ## Commands An age identity can be created with: ``` $ age-plugin-tpm --generate -o - | tee age-identity.txt # Created: 2025-12-25 01:54:45.690315451 +0100 CET m=+0.011592629 # Recipient: age1tag1q096edfp3ty6n36fj5kyq0yuesp7rdcmm7sjswzdcrekh6ash8n3uys987t AGE-PLUGIN-TPM-1QGQQQKQQYVQQKQQZQPEQQQQQZQQPJQQTQQPSQYQQYR96EDFP3TY6N36FJ5KYQ0YUESP7RDCMM7SJSWZDCREKH6ASH8N3UQPQYE4FZAPQXA3HRLELET3KX2EDSWDRF2ET4DWMTN0AWMKHUPQ8EK8SQLSQYQYMY5ZVWQYDY5D7WZ0W6KEXDWNUAP00DEVQ76AJ7HVV85TWU0DFCQQS0DA7N7E8GN55U6E4G8ECFFNRTP7XJTHD440N3CZW6STXNWQGA89WF3NF3PEDPUAPC8AW5XNZW68E4QG7X85G2CM5TZDKAP2UZ9EEAAC5LQ0R9PJEX5280SG0U47HA09EAFQ6VSVX65HCGRGNQQ3QQZL5H2W3M34CMSTWMRXLR90YRDZPZKWGZK7H7E079KLCCSSVRLFMQYEY547R ``` To display the recipient of a given identity: ``` $ age-plugin-tpm -y age-identity.txt age1tag1q096edfp3ty6n36fj5kyq0yuesp7rdcmm7sjswzdcrekh6ash8n3uys987t ``` ## License Licensed under the MIT license. See [LICENSE](LICENSE) or http://opensource.org/licenses/MIT age-plugin-tpm-1.0.1/cmd/000077500000000000000000000000001515400477000151055ustar00rootroot00000000000000age-plugin-tpm-1.0.1/cmd/age-plugin-tpm/000077500000000000000000000000001515400477000177335ustar00rootroot00000000000000age-plugin-tpm-1.0.1/cmd/age-plugin-tpm/main.go000066400000000000000000000152021515400477000212060ustar00rootroot00000000000000package main import ( "bytes" "fmt" "io" "log" "os" "filippo.io/age" page "filippo.io/age/plugin" "filippo.io/age/tag" "github.com/foxboron/age-plugin-tpm/plugin" "github.com/google/go-tpm/tpm2/transport" "github.com/spf13/cobra" "golang.org/x/term" ) type PluginOptions struct { AgePlugin string Convert bool Generate bool Decrypt bool Encrypt bool OutputFile string LogFile string OldStyleRecipient bool PIN bool } var example = ` $ age-plugin-tpm --generate -o age-identity.txt # Created: 2023-07-10 22:13:57.864450969 +0200 CEST m=+0.475252114 # Recipient: age1tpm1qt92lcdxj75rjz9e4t9nud7fv6t2cfn8rhzdfnc0z2rnfgv3cqwrqgme4dq AGE-PLUGIN-TPM-1QYQQQKQQYVQQKQQZQPEQQQQQZQQPJQQTQQPSQYQQYR92LCDXJ75RJZ9E4T9NUD7[...] $ echo "Hello World" | age -r "age1tpm1syqqqpqrtxsnkkqlmu505zzrq439hetls4qwwmyhsv8dgjhksvtewvx29lxs7s68qy" > secret.age $ age --decrypt -i age-identity.txt -o - secret.age Hello World` var ( version = "dev" pluginOptions = PluginOptions{} rootCmd = &cobra.Command{ Use: "age-plugin-tpm", Long: "age-plugin-tpm is a tool to generate age compatible identities backed by a TPM.", Example: example, Version: version, RunE: RunPlugin, } ) func SetLogger() { var w io.Writer if pluginOptions.LogFile != "" { w, _ = os.Open(pluginOptions.LogFile) } else if os.Getenv("AGEDEBUG") != "" { w = os.Stderr } else { w = io.Discard } plugin.SetLogger(w) } func clearLine(out io.Writer) { const ( CUI = "\033[" // Control Sequence Introducer CPL = CUI + "F" // Cursor Previous Line EL = CUI + "K" // Erase in Line ) fmt.Fprintf(out, "\r\n"+CPL+EL) } func GetPin(prompt string) ([]byte, error) { fmt.Fprintf(os.Stderr, "%s ", prompt) return term.ReadPassword(int(os.Stdin.Fd())) } func RunCli(cmd *cobra.Command, tpm transport.TPMCloser, in io.Reader, out io.Writer) error { var pin []byte var err error switch { case pluginOptions.Generate: if pluginOptions.PIN { if s := os.Getenv("AGE_TPM_PIN"); s != "" { pin = []byte(s) } else { pin, err = GetPin("Enter pin for key:") if err != nil { return err } clearLine(os.Stdin) confirm, err := GetPin("Confirm pin:") if err != nil { return err } if !bytes.Equal(pin, confirm) { return fmt.Errorf("pins didn't match") } } } if pluginOptions.OutputFile != "" && pluginOptions.OutputFile != "-" { f, err := os.OpenFile(pluginOptions.OutputFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) if err != nil { return err } defer f.Close() out = f } var rcp fmt.Stringer identity, recipient, err := plugin.CreateIdentity(tpm, pin) if err != nil { return err } rcp = recipient if pluginOptions.OldStyleRecipient { rcp = identity.TPMRecipient() } if err = plugin.MarshalIdentity(identity, rcp, out); err != nil { return err } case pluginOptions.Convert: if pluginOptions.OutputFile != "" && pluginOptions.OutputFile != "-" { f, err := os.OpenFile(pluginOptions.OutputFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) if err != nil { return err } defer f.Close() out = f } identity, err := plugin.ParseIdentity(in) if err != nil { return err } var recipient fmt.Stringer if pluginOptions.OldStyleRecipient { recipient = identity.TPMRecipient() } else { recipient, err = identity.Recipient() if err != nil { return err } } return plugin.MarshalRecipient(recipient, out) default: return cmd.Help() } return nil } func RunPlugin(cmd *cobra.Command, args []string) error { switch pluginOptions.AgePlugin { case "recipient-v1": plugin.Log.Println("Got recipient-v1") p, err := page.New("tpm") if err != nil { return err } p.HandleRecipient(func(data []byte) (age.Recipient, error) { if p != nil { if err := p.DisplayMessage("The recipient was created with a previous version of age-plugin-tpm, please convert or create a new recipient."); err != nil { return nil, fmt.Errorf("failed displaying message: %v", err) } } return tag.NewClassicRecipient(data) }) if exitCode := p.RecipientV1(); exitCode != 0 { return fmt.Errorf("age-plugin exited with code %d", exitCode) } case "identity-v1": tpm, err := plugin.NewTPM("") if err != nil { return err } defer tpm.Close() plugin.Log.Println("Got identity-v1") p, err := page.New("tpm") if err != nil { return err } p.HandleIdentity(func(data []byte) (age.Identity, error) { i, err := plugin.DecodeIdentity(page.EncodeIdentity("tpm", data)) if err != nil { return nil, err } // Set callbacks for TPM and PIN access i.Callbacks(p, tpm, func() ([]byte, error) { var pin []byte if s := os.Getenv("AGE_TPM_PIN"); s != "" { pin = []byte(s) } else { ps, err := p.RequestValue("Please enter the PIN for the key:", true) if err != nil { return nil, err } pin = []byte(ps) } return pin, nil }, ) return i, nil }) os.Exit(p.Main()) default: tpm, err := plugin.NewTPM("") if err != nil { return err } defer tpm.Close() in := os.Stdin if inFile := cmd.Flags().Arg(0); inFile != "" && inFile != "-" { f, err := os.Open(inFile) if err != nil { return fmt.Errorf("failed to open input file %q: %v", inFile, err) } defer f.Close() in = f } return RunCli(cmd, tpm, in, os.Stdout) } return nil } func pluginFlags(cmd *cobra.Command, opts *PluginOptions) { flags := cmd.Flags() flags.SortFlags = false flags.BoolVarP(&pluginOptions.Convert, "convert", "y", false, "Convert identities to recipients.") flags.StringVarP(&pluginOptions.OutputFile, "output", "o", "", "Write the result to the file.") flags.BoolVarP(&pluginOptions.Generate, "generate", "g", false, "Generate a identity.") flags.BoolVarP(&pluginOptions.PIN, "pin", "p", false, "Include a pin with the key. Alternatively export AGE_TPM_PIN.") // Debug or logging stuff flags.StringVar(&pluginOptions.LogFile, "log-file", "", "Logging file for debug output") // Old style recipient flags.BoolVar(&pluginOptions.OldStyleRecipient, "tpm-recipient", false, "Use the old-style tpm recipient instead of the new p256tag recipient.") // Hidden commands flags.BoolVar(&pluginOptions.Decrypt, "decrypt", false, "wip") flags.BoolVar(&pluginOptions.Encrypt, "encrypt", false, "wip") flags.StringVar(&pluginOptions.AgePlugin, "age-plugin", "", "internal use") flags.MarkHidden("decrypt") flags.MarkHidden("encrypt") flags.MarkHidden("age-plugin") } func main() { SetLogger() pluginFlags(rootCmd, &pluginOptions) if err := rootCmd.Execute(); err != nil { log.Fatal(err) } } age-plugin-tpm-1.0.1/cmd/age-plugin-tpm/main_test.go000066400000000000000000000027241515400477000222520ustar00rootroot00000000000000package main import ( "bytes" "os" "testing" "filippo.io/age" page "filippo.io/age/plugin" "github.com/foxboron/age-plugin-tpm/plugin" "github.com/google/go-tpm/tpm2/transport/simulator" "github.com/spf13/cobra" ) func TestEncryptDecrypt(t *testing.T) { var identity *plugin.Identity var stanzas []*age.Stanza SetLogger() tpm, err := simulator.OpenSimulator() if err != nil { t.Fatalf("failed opening tpm: %v", err) } defer tpm.Close() p, err := page.New("tpm") if err != nil { t.Fatalf("%v", err) } t.Run("Create keys", func(t *testing.T) { var generatedKey bytes.Buffer pluginOptions = PluginOptions{ Generate: true, } if err := RunCli(&cobra.Command{}, tpm, os.Stdin, &generatedKey); err != nil { t.Fatalf("Failed generating keys") } i, err := plugin.ParseIdentity(&generatedKey) if err != nil { t.Fatalf("%v", err) } identity = i }) identity.Callbacks(p, tpm, func() ([]byte, error) { return nil, nil }) identity.Unwrap(nil) fileKey := []byte("test") t.Run("Encrypt", func(t *testing.T) { recipient, err := identity.Recipient() if err != nil { t.Fatalf("failed getting recipient") } stanzas, err = recipient.Wrap(fileKey) if err != nil { t.Fatal("failed wrapping filekey") } }) t.Run("Decrypt", func(t *testing.T) { f, err := identity.Unwrap(stanzas) if err != nil { t.Fatalf("failed unwrapping filekey: %v", err) } if !bytes.Equal(fileKey, f) { t.Fatal("filekey are not equal") } }) } age-plugin-tpm-1.0.1/cmd/age-plugin-tpm/scripts_test.go000066400000000000000000000015551515400477000230160ustar00rootroot00000000000000package main import ( "os" "testing" "filippo.io/age" "filippo.io/age/plugin" "filippo.io/age/tag" "github.com/rogpeppe/go-internal/testscript" ) func TestMain(m *testing.M) { testscript.Main(m, map[string]func(){ "age-plugin-tpm": func() { main() }, "age-plugin-tag": func() { p, _ := plugin.New("tag") p.HandleRecipient(func(data []byte) (age.Recipient, error) { // TODO: Remove // Backwards compat waiting for new release return tag.NewClassicRecipient(data) }) p.HandleIdentity(func(data []byte) (age.Identity, error) { // TODO: We should not touch this return nil, nil }) os.Exit(p.Main()) }, }) } func TestPlugin(t *testing.T) { testscript.Run(t, testscript.Params{ Dir: "testdata/script", Setup: func(e *testscript.Env) error { e.Vars = append(e.Vars, "_AGE_TPM_SIMULATOR=1") return nil }, }) } age-plugin-tpm-1.0.1/cmd/age-plugin-tpm/testdata/000077500000000000000000000000001515400477000215445ustar00rootroot00000000000000age-plugin-tpm-1.0.1/cmd/age-plugin-tpm/testdata/script/000077500000000000000000000000001515400477000230505ustar00rootroot00000000000000age-plugin-tpm-1.0.1/cmd/age-plugin-tpm/testdata/script/oldencryption.txt000066400000000000000000000022761515400477000265110ustar00rootroot00000000000000# Try old encryption scheme exec age --decrypt -i ./identity -o - encrypted stdout 'Hello Old Encryption' stderr 're-encrypt' # Encrypt with old recipient exec age --encrypt -R ./recipient -o old-recipient-out input stderr 'please convert' exec age --decrypt -i ./identity -o - old-recipient-out stdout 'Hello World' -- input -- Hello World -- identity -- AGE-PLUGIN-TPM-1QYQQQKQQYVQQKQQZQPEQQQQQZQQPJQQTQQPSQYQQYZVAVTU4L9CNF9XTXG4Q53L0X7M023HDQPJP4KSN43H6KNYRD3TR7QPQPKSLWWASGJGJZJDDSGUS9WPR2UGGF7VTXQDP4QNWMUZZWW33PMLQQLSQYQ2UTHJ7M64D5DMSYP3U4YYHH56AQW2FEF535SPLK4SE70S5U68FWQQS89R9UL8ZE3ST8XJWSDZTVQMZ8M5T0QRP3ZAF9YS0XN0XUW5WKUFNDEMTLESC8XXJ6TAR3VJ49Y8A70FVE5HH6D8K25M5XREZDNUNXWZSZ0KSLPSVNR2GPDJ7PML2324UNU6E8TUD22K78VCLUTUNSS -- recipient -- age1tpm1q2vavtu4l9cnf9xtxg4q53l0x7m023hdqpjp4ksn43h6knyrd3tr78cwcjw -- encrypted -- -----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHRwbS1lY2MgWURybkl3IEFrV0tRVFpq YXVyRW8vQVV0amNxczV2SUpJWW1JWmZvbFlIa3BkeUZUV1NyCklmTXRQWkJoUjVj ZlBVQjZ6ZXVPTTlLN25XYTJQOHdxeUtHS2pYblNtSkUKLS0tIHNwUGpYeUIzYmV2 MXQralk1V0xRcnhDLzFtVXBnNE44Q2d6VXZWeWorVVUKWrE651o8KKRpWWH11lha ACKbtqsXbBPwooZ25UKGxR/Iqoe+uIYJANebVJS5oP81N3IbXh8= -----END AGE ENCRYPTED FILE----- age-plugin-tpm-1.0.1/cmd/age-plugin-tpm/testdata/script/tests.txt000066400000000000000000000026631515400477000247620ustar00rootroot00000000000000# Use the plugin with age exec age-plugin-tpm --generate -o age-identity.txt stdin age-identity.txt exec age-plugin-tpm -y -o age-recipient.txt stdin input.txt exec age -R ./age-recipient.txt -o encrypted.txt stdin encrypted.txt exec age --decrypt -i ./age-identity.txt -o - stdout 'Hello World' exec rm age-identity.txt age-recipient.txt encrypted.txt # Create an old-style identity and decrypt exec age-plugin-tpm --generate --tpm-recipient -o age-identity.txt stdin age-identity.txt exec age-plugin-tpm -y -o age-recipient.txt stdin input.txt exec age -R ./age-recipient.txt -o encrypted.txt stdin encrypted.txt exec age --decrypt -i ./age-identity.txt -o - stdout 'Hello World' # Encrypt to multiple recipients and decrypt exec age-plugin-tpm -g -o identity.txt exec age-plugin-tpm -y identity.txt -o recipient.txt exec age-plugin-tpm -g -o identity2.txt exec age-plugin-tpm -y identity2.txt -o recipient2.txt exec cat recipient.txt recipient2.txt stdin stdout exec tee recipients.txt exec cat recipient2.txt recipient.txt stdin stdout exec tee recipients-swapped.txt stdin input.txt exec age -R recipients.txt -o secret.age stdin secret.age exec age -i identity.txt -d stdout 'Hello World' stdin input.txt exec age -R recipients-swapped.txt -o secret.age stdin secret.age exec age -i identity.txt -d stdout 'Hello World' exec rm identity.txt identity2.txt recipient.txt recipient2.txt recipients.txt recipients-swapped.txt -- input.txt -- Hello World age-plugin-tpm-1.0.1/go.mod000066400000000000000000000010241515400477000154450ustar00rootroot00000000000000module github.com/foxboron/age-plugin-tpm go 1.25.0 require ( filippo.io/age v1.3.0 filippo.io/hpke v0.4.0 filippo.io/nistec v0.0.4 github.com/google/go-tpm v0.9.7 github.com/google/go-tpm-tools v0.4.7 github.com/rogpeppe/go-internal v1.14.1 github.com/spf13/cobra v1.10.2 golang.org/x/crypto v0.46.0 golang.org/x/term v0.38.0 ) require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/tools v0.39.0 // indirect ) age-plugin-tpm-1.0.1/go.sum000066400000000000000000000100371515400477000154760ustar00rootroot00000000000000c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M= c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo= filippo.io/age v1.3.0 h1:Pd+kiLApjp+OlJ6bBvmioX2QABosEeTxXgl0rpKwqF0= filippo.io/age v1.3.0/go.mod h1:EZorDTYUxt836i3zdori5IJX/v2Lj6kWFU0cfh6C0D4= filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A= filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY= filippo.io/nistec v0.0.4 h1:F14ZHT5htWlMnQVPndX9ro9arf56cBhQxq4LnDI491s= filippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc h1:SG12DWUUM5igxm+//YX5Yq4vhdoRnOG9HkCodkOn+YU= github.com/google/go-configfs-tsm v0.3.3-0.20240919001351-b4b5b84fdcbc/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= github.com/google/go-sev-guest v0.14.0 h1:dCb4F3YrHTtrDX3cYIPTifEDz7XagZmXQioxRBW4wOo= github.com/google/go-sev-guest v0.14.0/go.mod h1:SK9vW+uyfuzYdVN0m8BShL3OQCtXZe/JPF7ZkpD3760= github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843 h1:+MoPobRN9HrDhGyn6HnF5NYo4uMBKaiFqAtf/D/OB4A= github.com/google/go-tdx-guest v0.3.2-0.20241009005452-097ee70d0843/go.mod h1:g/n8sKITIT9xRivBUbizo34DTsUm2nN2uU3A662h09g= github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA= github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm-tools v0.4.7 h1:J3ycC8umYxM9A4eF73EofRZu4BxY0jjQnUnkhIBbvws= github.com/google/go-tpm-tools v0.4.7/go.mod h1:gSyXTZHe3fgbzb6WEGd90QucmsnT1SRdlye82gH8QjQ= github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= age-plugin-tpm-1.0.1/plugin/000077500000000000000000000000001515400477000156405ustar00rootroot00000000000000age-plugin-tpm-1.0.1/plugin/crypto.go000066400000000000000000000065041515400477000175140ustar00rootroot00000000000000package plugin import ( "crypto/cipher" "crypto/ecdh" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "io" "math/big" "filippo.io/nistec" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" ) // Functions that deals with the encryption/decryption of the filekey we get from age // Currently the sender does not utilize the TPM for any crypto operations, // but the decryption of the filekey for the identity itself does. const p256Label = "age-encryption.org/v1/tpm-p256" // Key Dreivative function for age-plugin-tpm // Sets up a hkdf instance with a salt that contains the shared key and the public key // Returns an chacha20poly1305 AEAD instance func kdf(sharedKey, publicKey *ecdh.PublicKey, shared []byte) (cipher.AEAD, error) { sharedKeyB := sharedKey.Bytes() publicKeyB := publicKey.Bytes() // We use the concatinated bytes of the shared key and the public key for the // key derivative functions. salt := make([]byte, 0, len(sharedKeyB)+len(publicKeyB)) salt = append(salt, sharedKeyB...) salt = append(salt, publicKeyB...) h := hkdf.New(sha256.New, shared, salt, []byte(p256Label)) wrappingKey := make([]byte, chacha20poly1305.KeySize) if _, err := io.ReadFull(h, wrappingKey); err != nil { return nil, err } return chacha20poly1305.New(wrappingKey) } // Unwraps a key using the standard kdf function. func UnwrapKey(sessionKey, publicKey *ecdh.PublicKey, shared, fileKey []byte) ([]byte, error) { nonce := make([]byte, chacha20poly1305.NonceSize) aead, err := kdf(sessionKey, publicKey, shared) if err != nil { return nil, err } return aead.Open(nil, nonce, fileKey, nil) } // Wraps a key using the standard kdf function. func WrapKey(sessionKey, publicKey *ecdh.PublicKey, shared, fileKey []byte) ([]byte, error) { nonce := make([]byte, chacha20poly1305.NonceSize) aead, err := kdf(sessionKey, publicKey, shared) if err != nil { return nil, err } return aead.Seal(nil, nonce, fileKey, nil), nil } // Wraps the file key in a session key // Returns the sealed filekey, the session pubkey bytes, error func EncryptFileKey(fileKey []byte, pubkey *ecdh.PublicKey) ([]byte, []byte, error) { // Create the session key we'll be passing to the stanza sessionKey, _ := ecdh.P256().GenerateKey(rand.Reader) sessionPubKey := sessionKey.PublicKey() // Do ECDH for the shared secret shared, err := sessionKey.ECDH(pubkey) if err != nil { return nil, nil, err } // Wrap the filekey with our aead instance b, err := WrapKey(sessionPubKey, pubkey, shared, fileKey) if err != nil { return nil, nil, err } // Return the bytes, and the marshalled compressed bytes of the session public // key. return b, MarshalCompressedEC(sessionPubKey), nil } // Unmarshal a compressed ec key func UnmarshalCompressedEC(b []byte) (*big.Int, *big.Int, *ecdh.PublicKey, error) { x, y := elliptic.UnmarshalCompressed(elliptic.P256(), b) ec := ecdsa.PublicKey{ Curve: elliptic.P256(), X: x, Y: y, } key, err := ec.ECDH() return x, y, key, err } // Marshal a compressed EC key func MarshalCompressedEC(pk *ecdh.PublicKey) []byte { point, err := nistec.NewP256Point().SetBytes(pk.Bytes()) if err != nil { panic("invalid compressed ec point") } return point.BytesCompressed() } func xyECC(p []byte) ([]byte, []byte) { if p[0] != 4 { panic("p256 key is not a p256 key") } return p[1 : 32+1], p[1+32:] } age-plugin-tpm-1.0.1/plugin/crypto_test.go000066400000000000000000000041261515400477000205510ustar00rootroot00000000000000package plugin import ( "bytes" "errors" "fmt" "io" "testing" "github.com/google/go-tpm/tpm2/transport/simulator" ) func TestEncryptionDecryption(t *testing.T) { tpm, err := simulator.OpenSimulator() if err != nil { t.Fatalf("failed opening tpm: %v", err) } defer tpm.Close() SetLogger(io.Discard) cases := []struct { msg string filekey []byte pin []byte decryptpin []byte shouldfail bool }{ { msg: "test encryption/decrypt - no pin", filekey: []byte("this is a test filekey"), }, { msg: "test encryption/decrypt - pin", filekey: []byte("this is a test filekey"), pin: []byte("123"), decryptpin: []byte("123"), }, { msg: "test encryption/decrypt - no pin for decryption", filekey: []byte("this is a test filekey"), pin: []byte("123"), shouldfail: true, }, { msg: "test encryption/decrypt - no pin for key, pin for decryption", filekey: []byte("this is a test filekey"), pin: []byte(""), decryptpin: []byte("123"), shouldfail: false, }, } for n, c := range cases { t.Run(fmt.Sprintf("case %d, %s", n, c.msg), func(t *testing.T) { identity, _, err1 := CreateIdentity(tpm, c.pin) identity.Callbacks(nil, tpm, func() ([]byte, error) { return c.decryptpin, nil }) // Ensure we can always use both recipients recipient := identity.TPMRecipient() stanzas, err2 := recipient.Wrap(c.filekey) unwrappedFileKey, err3 := identity.Unwrap(stanzas) rrecipient, err := identity.Recipient() if err != nil { t.Fatal(err) } stanzas, err22 := rrecipient.Wrap(c.filekey) unwrappedFileKey2, err33 := identity.Unwrap(stanzas) if errors.Join(err, err1, err2, err3, err22, err33) != nil { if c.shouldfail { return } t.Fatalf("failed test: %v", err) } if c.shouldfail { t.Fatalf("test should be failing") } if !bytes.Equal(c.filekey, unwrappedFileKey) { t.Fatalf("filkeys are not the same") } if !bytes.Equal(c.filekey, unwrappedFileKey2) { t.Fatalf("filkeys are not the same") } }) } } age-plugin-tpm-1.0.1/plugin/identity.go000066400000000000000000000113641515400477000200250ustar00rootroot00000000000000package plugin import ( "bufio" "bytes" "crypto/ecdh" "encoding/binary" "errors" "fmt" "io" "strings" "time" "filippo.io/age" "filippo.io/age/plugin" "filippo.io/age/tag" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" ) // We need to know if the TPM handle has a pin set type PINStatus uint8 const ( NoPIN PINStatus = iota HasPIN ) func (p PINStatus) String() string { switch p { case NoPIN: return "NoPIN" case HasPIN: return "HasPIN" } return "Not a PINStatus" } // Identity is the base Identity file for serialziation/deserialization type Identity struct { Version uint8 PIN PINStatus Private tpm2.TPM2BPrivate Public tpm2.TPM2BPublic SRKName *tpm2.TPM2BName // Private fields for implementation details publickey *ecdh.PublicKey p *plugin.Plugin tpm transport.TPMCloser pin func() ([]byte, error) } var _ age.Identity = &Identity{} func (i *Identity) checktpm() bool { // We need to check if we have passed a hw device // TODO: Figure out a better relationship between identities // identity -> TPM enabled identity -> ( TPMTagIdentity || TPMIdentity ) return i.tpm == nil } func (i *Identity) Callbacks(plugin *plugin.Plugin, tpm transport.TPMCloser, pin func() ([]byte, error)) { i.p = plugin i.tpm = tpm i.pin = pin } func (i *Identity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { if i.checktpm() { panic("missing tpm or age.Plugin access") } var resp age.Identity for _, stanza := range stanzas { switch stanza.Type { case "p256tag": resp = NewTPMTagIdentity(i.tpm, i.pin, i) case "tpm-ecc": if i.p != nil { if err := i.p.DisplayMessage("The file was encrypted with a previous version of age-plugin-tpm. Please re-encrypt the file!"); err != nil { return nil, fmt.Errorf("failed displaying message: %v", err) } } resp = NewTPMIdentity(i.tpm, i.pin, i) default: continue } fileKey, err := resp.Unwrap([]*age.Stanza{stanza}) if errors.Is(err, age.ErrIncorrectIdentity) { continue } return fileKey, err } return nil, age.ErrIncorrectIdentity } func (i *Identity) Publickey() *ecdh.PublicKey { return i.publickey } func (i *Identity) Serialize() []any { return []interface{}{ &i.Version, &i.PIN, } } func (i *Identity) TPMRecipient() *TPMRecipient { return NewTPMRecipient(i.publickey) } func (i *Identity) Recipient() (*tag.Recipient, error) { return NewTagRecipient(i.publickey) } func (i *Identity) HasPIN() bool { return i.PIN == HasPIN } func (i *Identity) String() string { var b bytes.Buffer for _, v := range i.Serialize() { binary.Write(&b, binary.BigEndian, v) } var pub []byte pub = append(pub, tpm2.Marshal(i.Public)...) pub = append(pub, tpm2.Marshal(i.Private)...) if i.Version > 1 { pub = append(pub, tpm2.Marshal(i.SRKName)...) } b.Write(pub) return plugin.EncodeIdentity(PluginName, b.Bytes()) } func DecodeIdentity(s string) (*Identity, error) { var key Identity name, b, err := plugin.ParseIdentity(s) if err != nil { return nil, err } if name != PluginName { return nil, fmt.Errorf("invalid hrp") } r := bytes.NewBuffer(b) for _, f := range key.Serialize() { if err := binary.Read(r, binary.BigEndian, f); err != nil { return nil, err } } public, err := tpm2.Unmarshal[tpm2.TPM2BPublic](r.Bytes()) if err != nil { return nil, fmt.Errorf("failed parsing TPMTPublic: %v", err) } r.Next(len(public.Bytes()) + 2) private, err := tpm2.Unmarshal[tpm2.TPM2BPrivate](r.Bytes()) if err != nil { return nil, fmt.Errorf("failed parsing TPMTPrivate: %v", err) } r.Next(len(private.Buffer) + 2) key.Public = *public key.Private = *private // Parse out the public key early ecdhKey, err := PublicToECDH(*public) if err != nil { return nil, err } key.publickey = ecdhKey if key.Version > 1 { name, err := tpm2.Unmarshal[tpm2.TPM2BName](r.Bytes()) if err != nil { return nil, err } key.SRKName = name } return &key, nil } func ParseIdentity(f io.Reader) (*Identity, error) { // Same parser as age const privateKeySizeLimit = 1 << 24 // 16 MiB scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit)) var n int for scanner.Scan() { n++ line := scanner.Text() if strings.HasPrefix(line, "#") || line == "" { continue } identity, err := DecodeIdentity(line) if err != nil { return nil, fmt.Errorf("error at line %d: %v", n, err) } return identity, nil } return nil, fmt.Errorf("no identities found") } var marshalTemplate = ` # Created: %s ` func MarshalIdentity(i *Identity, recipient fmt.Stringer, w io.Writer) error { s := fmt.Sprintf(marshalTemplate, time.Now()) s = strings.TrimSpace(s) fmt.Fprintf(w, "%s\n", s) fmt.Fprintf(w, "# Recipient: %s\n", recipient.String()) fmt.Fprintf(w, "\n%s\n", i.String()) return nil } age-plugin-tpm-1.0.1/plugin/identity_test.go000066400000000000000000000045541515400477000210670ustar00rootroot00000000000000package plugin import ( "errors" "io" "reflect" "testing" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport/simulator" ) func mustPublic(data []byte) tpm2.TPM2BPublic { return tpm2.BytesAs2B[tpm2.TPMTPublic](data) } func mustPrivate(data []byte) tpm2.TPM2BPrivate { return tpm2.TPM2BPrivate{ Buffer: data, } } func mustSRK(data []byte) *tpm2.TPM2BName { return &tpm2.TPM2BName{ Buffer: data, } } var data = []struct { key string t *Identity }{ { key: "AGE-PLUGIN-TPM-1QYQQQPNSW43XC6TRQQRHQUNFWESHGEGN0E0FM", t: &Identity{ Version: 1, Public: mustPublic([]byte("public")), Private: mustPrivate([]byte("private")), }, }, { key: "AGE-PLUGIN-TPM-1QYQSQPNSW43XC6TRQQRHQUNFWESHGEGWKR32R", t: &Identity{ Version: 1, PIN: HasPIN, Public: mustPublic([]byte("public")), Private: mustPrivate([]byte("private")), }, }, { key: "AGE-PLUGIN-TPM-1QGQQQPNSW43XC6TRQQRHQUNFWESHGEGQQDEHY6CUT4TFU", t: &Identity{ Version: 2, Public: mustPublic([]byte("public")), Private: mustPrivate([]byte("private")), SRKName: mustSRK([]byte("srk")), }, }, { key: "AGE-PLUGIN-TPM-1QGQSQPNSW43XC6TRQQRHQUNFWESHGEGQQDEHY6CHRM9KS", t: &Identity{ Version: 2, PIN: HasPIN, Public: mustPublic([]byte("public")), Private: mustPrivate([]byte("private")), SRKName: mustSRK([]byte("srk")), }, }, } func TestIdentityIdentityGeneration(t *testing.T) { for _, d := range data { k := d.t.String() if !reflect.DeepEqual(k, d.key) { t.Fatalf("no the same. Got %v expected %v", k, d.key) } } } // func TestIdentityDecode(t *testing.T) { // for _, d := range data { // k, err := DecodeIdentity(d.key) // if err != nil { // t.Fatalf("failed to decode key: %v", err) // } // if !reflect.DeepEqual(k, d.t) { // t.Fatalf("no the same") // } // } // } func TestIdentityCreateEncodeDecode(t *testing.T) { tpm, err := simulator.OpenSimulator() if err != nil { t.Fatalf("failed opening tpm: %v", err) } defer tpm.Close() SetLogger(io.Discard) identity, _, err1 := CreateIdentity(tpm, nil) identity.Callbacks(nil, tpm, func() ([]byte, error) { return nil, nil }) k := identity.String() identity2, err2 := DecodeIdentity(k) if err = errors.Join(err1, err2); err != nil { t.Fatalf("failed test: %v", err) } if identity2.String() != k { t.Fatalf("failed to parse identityes") } } age-plugin-tpm-1.0.1/plugin/logging.go000066400000000000000000000002111515400477000176070ustar00rootroot00000000000000package plugin import ( "io" "log" ) var ( Log *log.Logger ) func SetLogger(w io.Writer) { Log = log.New(w, "", log.Lshortfile) } age-plugin-tpm-1.0.1/plugin/plugin.go000066400000000000000000000134461515400477000174750ustar00rootroot00000000000000package plugin import ( "bytes" "encoding/base64" "fmt" "filippo.io/age/tag" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" ) const ( PluginName = "tpm" ) func getSharedSRK(tpm transport.TPMCloser) (*tpm2.AuthHandle, *tpm2.TPMTPublic, error) { const SRK_HANDLE tpm2.TPMIDHObject = 0x81000001 srk := tpm2.ReadPublic{ ObjectHandle: SRK_HANDLE, } var rsp *tpm2.ReadPublicResponse rsp, err := srk.Execute(tpm) if err != nil { return nil, nil, fmt.Errorf("failed to acquire primary key: %v", err) } srkPublic, err := rsp.OutPublic.Contents() if err != nil { return nil, nil, fmt.Errorf("failed getting srk public content: %v", err) } return &tpm2.AuthHandle{ Handle: SRK_HANDLE, Name: rsp.Name, Auth: tpm2.PasswordAuth(nil), }, srkPublic, nil } func createTransientSRK(tpm transport.TPMCloser) (*tpm2.AuthHandle, *tpm2.TPMTPublic, error) { srk := tpm2.CreatePrimary{ PrimaryHandle: tpm2.TPMRHOwner, InSensitive: tpm2.TPM2BSensitiveCreate{ Sensitive: &tpm2.TPMSSensitiveCreate{ UserAuth: tpm2.TPM2BAuth{ Buffer: []byte(nil), }, }, }, InPublic: tpm2.New2B(tpm2.ECCSRKTemplate), } var rsp *tpm2.CreatePrimaryResponse rsp, err := srk.Execute(tpm) if err != nil { return nil, nil, fmt.Errorf("failed creating primary key: %v", err) } srkPublic, err := rsp.OutPublic.Contents() if err != nil { return nil, nil, fmt.Errorf("failed getting srk public content: %v", err) } return &tpm2.AuthHandle{ Handle: rsp.ObjectHandle, Name: rsp.Name, Auth: tpm2.PasswordAuth(nil), }, srkPublic, nil } // Creates a new identity. It initializes a new SRK parent in the TPM and // returns the identity and the corresponding recipient. // Note: It does not load the identity key into the TPM. func CreateIdentity(tpm transport.TPMCloser, pin []byte) (*Identity, *tag.Recipient, error) { srkHandle, srkPublic, err := getSharedSRK(tpm) if err != nil { Log.Printf("failed to acquire shared SRK, falling back to creating transient SRK: %v\n", err) srkHandle, srkPublic, err = createTransientSRK(tpm) if err != nil { return nil, nil, fmt.Errorf("failed to create transient SRK (and no shared SRK could be acquired): %v", err) } } defer FlushHandle(tpm, srkHandle) eccKey := tpm2.Create{ ParentHandle: srkHandle, InPublic: tpm2.New2B(tpm2.TPMTPublic{ Type: tpm2.TPMAlgECC, NameAlg: tpm2.TPMAlgSHA256, ObjectAttributes: tpm2.TPMAObject{ FixedTPM: true, FixedParent: true, SensitiveDataOrigin: true, UserWithAuth: true, Decrypt: true, }, Parameters: tpm2.NewTPMUPublicParms( tpm2.TPMAlgECC, &tpm2.TPMSECCParms{ CurveID: tpm2.TPMECCNistP256, Scheme: tpm2.TPMTECCScheme{ Scheme: tpm2.TPMAlgECDH, Details: tpm2.NewTPMUAsymScheme( tpm2.TPMAlgECDH, &tpm2.TPMSKeySchemeECDH{ HashAlg: tpm2.TPMAlgSHA256, }, ), }, }, ), }), } pinstatus := NoPIN if !bytes.Equal(pin, []byte("")) { eccKey.InSensitive = tpm2.TPM2BSensitiveCreate{ Sensitive: &tpm2.TPMSSensitiveCreate{ UserAuth: tpm2.TPM2BAuth{ Buffer: pin, }, }, } pinstatus = HasPIN } var eccRsp *tpm2.CreateResponse eccRsp, err = eccKey.Execute(tpm, tpm2.HMAC(tpm2.TPMAlgSHA256, 16, tpm2.AESEncryption(128, tpm2.EncryptIn), tpm2.Salted(srkHandle.Handle, *srkPublic))) if err != nil { return nil, nil, fmt.Errorf("failed creating TPM key: %v", err) } ecdhKey, err := PublicToECDH(eccRsp.OutPublic) if err != nil { return nil, nil, err } identity := &Identity{ Version: 2, PIN: pinstatus, Private: eccRsp.OutPrivate, Public: eccRsp.OutPublic, SRKName: &srkHandle.Name, publickey: ecdhKey, } recipient, err := identity.Recipient() if err != nil { return nil, nil, fmt.Errorf("failed getting recipient: %v", err) } return identity, recipient, nil } func LoadIdentity(tpm transport.TPMCloser, identity *Identity) (*tpm2.AuthHandle, error) { srkHandle, _, err := AcquireIdentitySRK(tpm, identity) if err != nil { return nil, err } defer FlushHandle(tpm, srkHandle) return LoadIdentityWithParent(tpm, *srkHandle, identity) } func AcquireIdentitySRK(tpm transport.TPMCloser, identity *Identity) (*tpm2.AuthHandle, *tpm2.TPMTPublic, error) { // Try to use the shared persistent SRK for newer identities if identity.Version > 1 { srkHandle, srkPublic, err := getSharedSRK(tpm) if err == nil && bytes.Equal(srkHandle.Name.Buffer, identity.SRKName.Buffer) { return srkHandle, srkPublic, nil } } // Otherwise fall back to trying to create a transient SRK srkHandle, srkPublic, err := createTransientSRK(tpm) if err != nil { return nil, nil, fmt.Errorf("failed to create transient SRK while trying to acquire identity SRK: %v", err) } // We didn't store the SRK name for identity version 1, so just assume that this SRK is the right one if identity.Version == 1 || bytes.Equal(srkHandle.Name.Buffer, identity.SRKName.Buffer) { return srkHandle, srkPublic, nil } return nil, nil, fmt.Errorf("unable to acquire SRK matching name specified by identity") } func LoadIdentityWithParent(tpm transport.TPMCloser, parent tpm2.AuthHandle, identity *Identity) (*tpm2.AuthHandle, error) { loadBlobCmd := tpm2.Load{ ParentHandle: parent, InPrivate: identity.Private, InPublic: identity.Public, } loadBlobRsp, err := loadBlobCmd.Execute(tpm) if err != nil { return nil, fmt.Errorf("failed getting handle: %v", err) } // Return a AuthHandle with a nil PasswordAuth return &tpm2.AuthHandle{ Handle: loadBlobRsp.ObjectHandle, Name: loadBlobRsp.Name, Auth: tpm2.PasswordAuth(nil), }, nil } func b64Decode(s string) ([]byte, error) { return base64.RawStdEncoding.Strict().DecodeString(s) } func b64Encode(s []byte) string { return base64.RawStdEncoding.Strict().EncodeToString(s) } age-plugin-tpm-1.0.1/plugin/recipient.go000066400000000000000000000037471515400477000201640ustar00rootroot00000000000000package plugin import ( "crypto/ecdh" "crypto/sha256" "fmt" "io" "filippo.io/age" "filippo.io/age/plugin" "filippo.io/age/tag" "filippo.io/nistec" "github.com/google/go-tpm/tpm2" ) func NewTagRecipientFromBytes(s []byte) (*tag.Recipient, error) { ecdhKey, err := PublicToECDH(tpm2.BytesAs2B[tpm2.TPMTPublic](s)) if err != nil { return nil, err } return NewTagRecipient(ecdhKey) } func NewTagRecipient(ecc *ecdh.PublicKey) (*tag.Recipient, error) { return tag.NewClassicRecipient(MarshalCompressedEC(ecc)) } type TPMRecipient struct { Pubkey *ecdh.PublicKey tag []byte } func (r *TPMRecipient) Tag() []byte { return r.tag } func (r *TPMRecipient) Bytes() []byte { p, err := nistec.NewP256Point().SetBytes(r.Pubkey.Bytes()) if err != nil { panic("internal error: invalid P-256 public key") } return p.BytesCompressed() } func (r *TPMRecipient) String() string { return plugin.EncodeRecipient(PluginName, r.Bytes()) } func (r *TPMRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { wrapped, sessionKey, err := EncryptFileKey(fileKey, r.Pubkey) if err != nil { return nil, err } return []*age.Stanza{{ Type: "tpm-ecc", Args: []string{b64Encode(r.Tag()), b64Encode(sessionKey)}, Body: wrapped, }}, nil } func NewTPMRecipient(ecc *ecdh.PublicKey) *TPMRecipient { sum := sha256.Sum256(ecc.Bytes()) return &TPMRecipient{ Pubkey: ecc, tag: sum[:4], } } func ParseTPMRecipient(s string) (*TPMRecipient, error) { name, b, err := plugin.ParseRecipient(s) if err != nil { return nil, fmt.Errorf("failed to decode recipient: %v", err) } if name != PluginName { return nil, fmt.Errorf("invalid plugin for type %s", name) } p, err := nistec.NewP256Point().SetBytes(b) if err != nil { return nil, err } pubkey, err := ecdh.P256().NewPublicKey(p.Bytes()) if err != nil { return nil, err } return NewTPMRecipient(pubkey), nil } func MarshalRecipient(recipient fmt.Stringer, w io.Writer) error { fmt.Fprintf(w, "%s\n", recipient.String()) return nil } age-plugin-tpm-1.0.1/plugin/recipient_test.go000066400000000000000000000023471515400477000212160ustar00rootroot00000000000000package plugin import ( "crypto/ecdh" "crypto/ecdsa" "crypto/elliptic" "math/big" "reflect" "strings" "testing" ) func bigInt(s string) *big.Int { ret := big.NewInt(0) ret.SetString(s, 10) return ret } func mustECDH(e *ecdsa.PublicKey) *ecdh.PublicKey { ret, _ := e.ECDH() return ret } var cases = []struct { pubKey *TPMRecipient recipient string }{{ pubKey: NewTPMRecipient(mustECDH( &ecdsa.PublicKey{ Curve: elliptic.P256(), X: bigInt("89354244803538158909979995955747079783816134516555582017998279936143319776423"), Y: bigInt("44449113766368004535934930895165275911452797542884597880018495457858036318074"), }, )), recipient: "age1tpm1qtzcedwcyuemjkynrvucs5wyhue4h528vv7s2z9k8xvr78ky6c72wff0tz2", }} func TestDecodeRecipient(t *testing.T) { for _, c := range cases { pubkey, err := ParseTPMRecipient(c.recipient) if err != nil { t.Fatalf("failed decoding recipient: %v", err) } if !reflect.DeepEqual(pubkey, c.pubKey) { t.Fatalf("Did not parse the correct key") } } } func TestEncodeRecipient(t *testing.T) { for _, c := range cases { if !strings.EqualFold(c.pubKey.String(), c.recipient) { t.Fatalf("did not get the recipient back. expected %v, got %v", c.recipient, s) } } } age-plugin-tpm-1.0.1/plugin/tagidentity.go000066400000000000000000000045451515400477000205240ustar00rootroot00000000000000package plugin import ( "crypto/subtle" "errors" "fmt" "filippo.io/age" "filippo.io/age/tag" "filippo.io/hpke" "github.com/google/go-tpm/tpm2/transport" ) // TPMTagIdentity implements the p256tag identity handler type TPMTagIdentity struct { tpm transport.TPMCloser identity *Identity pin func() ([]byte, error) } func NewTPMTagIdentity(tpm transport.TPMCloser, pin func() ([]byte, error), identity *Identity) *TPMTagIdentity { return &TPMTagIdentity{tpm, identity, pin} } var _ age.Identity = &TPMTagIdentity{} func (t *TPMTagIdentity) unwrap(block *age.Stanza) ([]byte, error) { if len(block.Args) < 2 || block.Type != "p256tag" { return nil, age.ErrIncorrectIdentity } tag, err := b64Decode(block.Args[0]) if err != nil { return nil, fmt.Errorf("failed base64 decode session key: %v", err) } sessionKey, err := b64Decode(block.Args[1]) if err != nil { return nil, fmt.Errorf("failed base64 decode session key: %v", err) } rsp, err := t.identity.Recipient() if err != nil { return nil, fmt.Errorf("unwrap: failed to get recipient from identity %v", err) } itag, err := rsp.Tag(sessionKey) if err != nil { return nil, fmt.Errorf("unwrap: failed to get tag from recipient: %v", err) } // Check if we are dealing with the correct key if subtle.ConstantTimeCompare(tag, itag) != 1 { return nil, age.ErrIncorrectIdentity } var pin []byte if t.identity.HasPIN() { pin, err = t.pin() if err != nil { return nil, fmt.Errorf("failed to get pin: %v", err) } } k, err := hpke.NewDHKEMPrivateKey(NewTPMKeyExchange(t.tpm, pin, t.identity)) if err != nil { return nil, fmt.Errorf("failed to unwrap file key: %v", err) } r, err := hpke.NewRecipient(sessionKey, k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("age-encryption.org/p256tag")) if err != nil { return nil, fmt.Errorf("failed to unwrap file key: %v", err) } return r.Open(nil, block.Body) } func (t *TPMTagIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) { for _, s := range stanzas { fileKey, err := t.unwrap(s) if errors.Is(err, age.ErrIncorrectIdentity) { continue } if err != nil { return nil, err } return fileKey, nil } return nil, age.ErrIncorrectIdentity } func (t *TPMTagIdentity) Recipient() *tag.Recipient { resp, err := NewTagRecipient(t.identity.publickey) if err != nil { panic("this is unexpected") } return resp } age-plugin-tpm-1.0.1/plugin/tpm.go000066400000000000000000000034421515400477000167720ustar00rootroot00000000000000package plugin import ( "crypto/ecdh" "io" "os" "sync" sim "github.com/google/go-tpm-tools/simulator" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" "github.com/google/go-tpm/tpm2/transport/linuxtpm" "github.com/google/go-tpm/tpmutil" ) var ( once sync.Once s transport.TPMCloser ) // TPM represents a connection to a TPM simulator. type TPMCloser struct { transport io.ReadWriteCloser } // Send implements the TPM interface. func (t *TPMCloser) Send(input []byte) ([]byte, error) { return tpmutil.RunCommandRaw(t.transport, input) } // Close implements the TPM interface. func (t *TPMCloser) Close() error { return t.transport.Close() } func GetFixedSim() (transport.TPMCloser, error) { var ss *sim.Simulator var err error once.Do(func() { ss, err = sim.GetWithFixedSeedInsecure(123456) s = &TPMCloser{ss} }) return s, err } // Setup a NewTPMDevice func NewTPM(tpmPath string) (transport.TPMCloser, error) { if os.Getenv("_AGE_TPM_SIMULATOR") != "" { return GetFixedSim() } // If we don't pass a path to OpenTPM then we have the tpmrm0 and tpm0 fallbacks if tpmPath != "" { return linuxtpm.Open(tpmPath) } return linuxtpm.Open("/dev/tpmrm0") } // shadow the unexported interface from go-tpm type handle interface { HandleValue() uint32 KnownName() *tpm2.TPM2BName } // Helper to flush handles func FlushHandle(tpm transport.TPM, h handle) { flushSrk := tpm2.FlushContext{FlushHandle: h} flushSrk.Execute(tpm) } func PublicToECDH(b tpm2.TPM2BPublic) (*ecdh.PublicKey, error) { pub, err := b.Contents() if err != nil { return nil, err } parameters, err := pub.Parameters.ECCDetail() if err != nil { return nil, err } eccdeets, err := pub.Unique.ECC() if err != nil { return nil, err } return tpm2.ECDHPub(parameters, eccdeets) } age-plugin-tpm-1.0.1/plugin/tpmidentity.go000066400000000000000000000041661515400477000205500ustar00rootroot00000000000000package plugin import ( "crypto/ecdh" "crypto/sha256" "crypto/subtle" "errors" "fmt" "filippo.io/age" "filippo.io/nistec" "github.com/google/go-tpm/tpm2/transport" ) // TPMIdentity implements the tpm identity handler type TPMIdentity struct { tpm transport.TPMCloser identity *Identity pin func() ([]byte, error) } func NewTPMIdentity(tpm transport.TPMCloser, pin func() ([]byte, error), identity *Identity) *TPMIdentity { return &TPMIdentity{tpm, identity, pin} } var _ age.Identity = &TPMIdentity{} func (t *TPMIdentity) unwrap(block *age.Stanza) ([]byte, error) { if len(block.Args) < 2 || block.Type != "tpm-ecc" { return nil, age.ErrIncorrectIdentity } tag, err := b64Decode(block.Args[0]) if err != nil { return nil, fmt.Errorf("failed base64 decode session key: %v", err) } sessionKey, err := b64Decode(block.Args[1]) if err != nil { return nil, fmt.Errorf("failed base64 decode session key: %v", err) } // The tpm-ecc recipient uses the checksum of the key, with the first 4 bytes // as the tag sum := sha256.Sum256(t.identity.publickey.Bytes()) // Check if we are dealing with the correct key if subtle.ConstantTimeCompare(tag, sum[:4]) != 1 { return nil, age.ErrIncorrectIdentity } var pin []byte if t.identity.HasPIN() { pin, err = t.pin() if err != nil { return nil, fmt.Errorf("failed to get pin: %v", err) } } p, err := nistec.NewP256Point().SetBytes(sessionKey) if err != nil { return nil, err } sessionKeyECDH, err := ecdh.P256().NewPublicKey(p.Bytes()) if err != nil { return nil, err } exchange := NewTPMKeyExchange(t.tpm, pin, t.identity) sharedSecret, err := exchange.ECDH(sessionKeyECDH) if err != nil { return nil, err } // Unwrap the key with the kdf/chacha20 return UnwrapKey(sessionKeyECDH, t.identity.publickey, sharedSecret, block.Body) } func (t *TPMIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { for _, s := range stanzas { fileKey, err := t.unwrap(s) if errors.Is(err, age.ErrIncorrectIdentity) { continue } if err != nil { return nil, err } return fileKey, nil } return nil, age.ErrIncorrectIdentity } age-plugin-tpm-1.0.1/plugin/tpmkeyexchange.go000066400000000000000000000044741515400477000212140ustar00rootroot00000000000000package plugin import ( "fmt" "filippo.io/hpke/crypto/ecdh" "filippo.io/nistec" "github.com/google/go-tpm/tpm2" "github.com/google/go-tpm/tpm2/transport" ) type TPMKeyExchange struct { tpm transport.TPMCloser i *Identity pin []byte } var _ ecdh.KeyExchanger = &TPMKeyExchange{} func NewTPMKeyExchange(tpm transport.TPMCloser, pin []byte, i *Identity) *TPMKeyExchange { return &TPMKeyExchange{ tpm, i, pin, } } func (t *TPMKeyExchange) PublicKey() *ecdh.PublicKey { return t.i.Publickey() } func (t *TPMKeyExchange) Curve() ecdh.Curve { // TODO: We can derive this from the TPM key. But this is never going to change. return ecdh.P256() } func (t *TPMKeyExchange) ECDH(remoteKey *ecdh.PublicKey) ([]byte, error) { // We'll be using the SRK for the session encryption, and we need it as the // parent for our application key. Make sure it's created and available. srkHandle, srkPublic, err := AcquireIdentitySRK(t.tpm, t.i) if err != nil { return nil, err } defer FlushHandle(t.tpm, srkHandle) // We load the identity into the TPM, using the SRK parent. handle, err := LoadIdentityWithParent(t.tpm, *srkHandle, t.i) if err != nil { return nil, err } defer FlushHandle(t.tpm, handle.Handle) // Add the AuthSession for the handle handle.Auth = tpm2.PasswordAuth(t.pin) p, err := nistec.NewP256Point().SetBytes(remoteKey.Bytes()) if err != nil { return nil, err } // Get X/Y points from sessionkey x, y := xyECC(p.Bytes()) // ECDHZGen command for the TPM, turns the sesion key into something we understand. ecdh := tpm2.ECDHZGen{ KeyHandle: *handle, InPoint: tpm2.New2B( tpm2.TPMSECCPoint{ X: tpm2.TPM2BECCParameter{Buffer: x}, Y: tpm2.TPM2BECCParameter{Buffer: y}, }, ), } // Execute the ECDHZGen command, we also add session encryption. // In this case the session encryption only encrypts the private part going out of the TPM, which is the shared // session key we are using in our kdf. ecdhRsp, err := ecdh.Execute(t.tpm, tpm2.HMAC(tpm2.TPMAlgSHA256, 16, tpm2.AESEncryption(128, tpm2.EncryptOut), tpm2.Salted(srkHandle.Handle, *srkPublic))) if err != nil { return nil, fmt.Errorf("failed ecdhzgen: %v", err) } shared, err := ecdhRsp.OutPoint.Contents() if err != nil { return nil, fmt.Errorf("failed getting ecdh point: %v", err) } return shared.X.Buffer, nil } age-plugin-tpm-1.0.1/plugin/tpmkeyexchange_test.go000066400000000000000000000001111515400477000222330ustar00rootroot00000000000000package plugin import "testing" func TestKeyExchange(t *testing.T) { }