diff --git a/.goversion b/.goversion index 7a429d6..08002f8 100644 --- a/.goversion +++ b/.goversion @@ -1 +1 @@ -1.24.6 +1.27.1 diff --git a/README.md b/README.md index 79b9e73..3543a1e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,19 @@ care loading the right key in the TPM, as needed. By default, 2048-bit RSA keys are created. AES keys, ECC keys, HMAC keys, and RSA keys of different sizes can also be created if the TPM supports them. +Keys are created under a storage root key (SRK) in the owner hierarchy, so +clearing the TPM invalidates them. All commands that use keys are protected +by HMAC sessions salted with the SRK: auth values are never sent to the TPM +in cleartext, secret parameters are encrypted, and responses are +authenticated. Serialized keys are bound to the SRK that they were created +under. + +The SRK itself is trusted on first use: this package doesn't verify that it +belongs to a genuine TPM (e.g. with the endorsement key's certificate). A +device that impersonates the TPM when a key is created can capture that key's +auth value and parameters. Once a key exists, a different device can't use it, +or complete the sessions that protect it. + ## Example: ```go @@ -106,14 +119,14 @@ func main() { $ go run ./example --sim Key type: RSA-2048 -Saved key: 02e002de0020dd5702c97c8bc5c59cfb5ecfb8c193092738c9a648814206b2257db1616bfa090010c0aa1d491afccbd61604980c3bdcf09a3bb520bf95f067f24a73ebfe201dbcd7fd7b6527419a7279d6bc34eb1438929b16a6cee8b7fe0fa326dbb5cbbce18b428d8fe688da83385b6fb0d0dbe9b7ce389169dfaa218cef452c6a4ec39887f1a889513542105aadac781c37a56056c8950417698f5d890d2c2815378311c242aff2c6cdbb8d293c8eb854dc974287e0525256f2bbb406838b34178a9a5165d3b5e6ed56a1c32349bdf140f91fae960d9c0ec6d571986e94efc02ba8b7516e2f345754323ee0b17536127eae7025e8c4573dcac585d32bff2612cdc04b333e91aa8b027db8cafe269a645e6390228fd2e5413d40663e90e485dc14dc0b2938fb1b7b6649343dc509f0935eee2dbf957d7d0383889e68a5ff9ff6cc6bc944bc2b68645b36c00f4d34f10c5467bbb3253028495b4393c133b294c630095dd90af669800ccc824f38d894193d613687b47d64a3906aa0eff579d758f5216dd68e13066d31f7318bc0f186e5b7b82109585c78c2b79515669674bb657845824aaa1980f41948c68ddcf367e3a6861f6c380e7103b4d0c9efdc2c4d603965039841bccf204d7d117b4e2018d6304a451cd31a7d96a6c4f909ac0ac629941f45735e6aec7d9d6e4bfe9a6e8cdacc588eda02a0314b199a8548b7fed72e3fe160c09c87b4328f57cc5eb0cfa1686b3b2dfc7678238e9e686e37ea95ac993153078dc3f854fbaaf36025e241ab0f9bda9ae643523e3f142c2d6c80df87922daa080bfd295796e27e73e65c5351286475f73e68a365c3c466aa586bd63a53288a857b1311ad57f6c91adcc38b1984a2478f38f57bd45d256bc42bf25a04b13705c30a58f99370ad258e512aeeaa31e8ad465c7b6d3132e1d65f1bb184b66e352118f3e702e35ce0eff9c5368c47da007213143659447b0f5b911948e66d0b5a96c92152a2416d0f39d72db434146417561733638edc412706b44c5b8dec2668011801160001000b000600720000001000100800000000000100c1eb6d8227a6496320fb38146f585c4ec5445ffb2cd98686934e26d4913d47c6ace469f9c6ec421878ca13369a008b609889eb084f9a5b93ed82eebdad82487dbad04ed4f0dd4f7bd2efd7832103b29a4ce23810c53327c9c836d61b27e8bfc63a3b1652be6b71a9f112783b9654eb90e0c7b13d7616a64cbd7c50b9526a9ca629875d06903a21d8d0f37b902739ef4bc1a43e41331daa129cbddd6a5c260775b8a5dc73fc1cbb25a689aaa366b88af8d2f51c2c96871dfb0d6bfb1b2353dae8c0ba9783a7ea36421fa1870fe1f47a98781e296ebd65b723dbe9c6e48c6753d9b3f9502ff8f7ca490cac746a52131cbf4a86985a5c4dad03fb50e42c0d7e46a9 +Saved key: 010022000b3d8e02b755a6bf844c51eb62a9f6aad4d5363aa0f8f504ee31af32ed67e851f202e002de002073f34d6185fb2540c0e25c3c7c1701ba1d577d209b4363d9e86a0f23fd73bb0f001046c597eb180179b1b71973c95409847cfb983ec2bdbeb4c1b3f7f79577ff9e8647412270a3725ec0b3924095e15198754255d366892182e71a77f846b8316c29b83139f547e5c89a4460d222f0d7319e290e166857c09a7b44b0c4e6adac613a69f10d1748f462efe33d158e586ce691497f4e4361106b36201ef36d356f8ebde12de2bd0154f634e5d80ecf272dcf223fea9f21da273079e9eba0fa8061e6e6d4fa3049d45dff89c9deae626da092a6d05320507d5244d96fbb1c7deac0f0a1e8a98545149e6adf12560d3a26ccf19a10bef44e15a2a1460518a6560e0303df0e96f96707a83ab6dbe7cc828cd434de7c95d9bf3b2b355ddb374b477f00c8ba17ef8d4e1b57fc6365149941abaac2d456ea91d7cff8b63266871803bc70658a0dc7b968dabeeddce77fba5c6b2315852ade5011d142ae8bff5d09d9ce7cedaa561c323abb0c8ed0e2918bcbc13b1fdb34e174383630d017a95b0e48abef7b4e944b2fc6fc4e3c84d5d12bba68177afbef818f35a747369829100f6f55986308f3fb619db63291ed75bb435a1acf386ba8ff96f046e3ccc1a87194a2e0c37b7308e774016ed0d9101659e3fb238a9c7e8743b9376843ff549dd7a21933540b31dd7dc1b841400917027f513c27ca619a3ef3532b709bfad32931c6eb54e820e681f29ba669d908ed05ca508d489ced426a7776c7e4822b3322c607314453b5fa5d89b46ce9c653ecb7b50238928bc55983d22519faac2f5fda6afbbc9ebf318d12e8fd5515e3866c3b841212b432ffd1a7445a179c4618b8a962500132b14f52c5b39446fc789ddbe45a07417ea67218a020f509f51bcf0534009dd325daba8ee0ca91337120ba953d71a8dcc0cfe5896c8180175079d4f693ac4137b377e8a8dd86dab16248a446dc49fefaca9484a7122d2baf4e648e7387b2b542c41f3bf47e13e7260aadc3b0b9775406fcebb31e006a79a68f4151ebc767011801160001000b000600720000001000100800000000000100d094d5d76532d5fae8d7b8252007d97f831da2edae73131fa164c4e81b9f9b82618bd7c63d3a2b6b07e97e78b38df3556a98db73e7d1f2f4465061c90a17388abb6cc093d1bfbb216ba8f94be4a116db2f327fa4c3b0d4ce261970c700ce94d4eb0e8972a08c62680487caec35a24a14822f91d1aae80988ca3b4a18a8e15f2b133d7e1de477fec7788c8be1b2ea77a73b869a3ac7457bf4863fcad5a0e7eec80cddd60fe7869827141469ed68054b63babb3976cececf48782d3676b62dcbcf0258bc8a018d8950c0a76fb53a6597a9b1a8893020cbcfc7b0c5ff784e23fbeff66860a6bb862f2c78202d6929dfa7253b282758b6d53ecc30f650a85cc4949b Payload: "Hello world!" -Encrypted with TPM: b8f3d8181fdb13950f76af65f7641c944e346e6b9cb5729426fb5eb0786350e3a77148c051c80386ecd1e3f3a9b51df3f741877872609b65160a56188f2d40981fbd663f896b7939263b0156a2b023cab6c9bbb2dae1283b9e89ad66aab628d3333bc99d0e00227161a39e10ad2ff56a9ff6366e2f17bc720d48641944d62c1cf9ff36bef75661175cd92b0af1b11f0845d4053f0409110009911fe438ca4c6e71f56e2246ceb998d91718d979a7d46ee931345a2e530a327b51add1d52aa34b58a984a54cebfb66a98222e161b6967d29e94b0f680a650b9a05f34fc52430030a1bd9019d224173477c6962e889ee9ab3d33424919dc8fcf985f2b80ff59aea -Signature: 7b13438f77557a3bd5bb7d732e949c526bfd9d3ba0e8a3b782fe7d07559c429943bf0c2048d699f8ab9f7b45ecffa5d32cb5537d1aafe81b14f2f44ab8aa5b7aa94e3d61aed0c3fd19d32c66c0eb58112fe8453a111cca0bbdc3895eb21576b1e2b237ef7825eb9fab8c10fb2d124c1ce8f7ef8f32e8d12519c2da3cd34268336d26b0a15b0db28d8301e57b5a0db7ad25b857753b6d83edac5e38077d5fed3c30a5d09fd34c5ca9c02c7f867ac0209b84c1b815f4b85979f6357d4ff19540176a77aa52a62d8588a8254914ce09cab692573f91a50c73238dfe009d069ba3d595c1eb7c93297c4f0c3aa02b779d880b3065d4ffd93dfd4b870d915ac010b1f1 +Encrypted with TPM: bafc3dd90da5986058712675e4d23b8b3aa9e222c20251e0da405a3effca7ea14d76a87b7c5709399f937b4a475523b618910992425b8c17a00221e327664c9f177d47dbf16eb0c1d4d7c6285e326454157f3251c2f34aa49a4fb693938959d5df57cb7f8a5671104cc80ef50b9752ef9ac78faa64bc89e5e6728ed454078b778be2ab9b29f1513ed030e0364bf1506ba170b08238f7aa9e66bec34ba19e340048c5aaa93ebcdd4deab51c6117bd633b8f62a612cfa11f4291e2d300a7d8caa8e94ccc8a6382f98c4fafde5d1c58854adc05673ab8e85ee80f8db2025c3d00060b333c00c92f615791edd60ca419df86923db3acb420dcaaf867edca2cf0c356 +Signature: c8b19e81c0452f31ca3316b11c04b86b7aab00dc82d4ca5869d9aa2bd733f240b8b67a21e194a365acd1ab36cedf78c0d548e0f52758ab33ad82cea95e15d3a53d1722c7ca3da33451fc285645219f9fa0d6d2d95264233db18d016e31b20730ee194a0d4b2c955b7ed73c350bcebf20708592023547d8855cca279d7b0ac0e481823c37732712de2ce79fff4b6a6e99ad67cac795f4eb9628010846d34fd38d1a1134d964b7d85ddf95f7eda6475f8ad6a6a1695585b0f7bdc8273c949a69315fe0e942e4071f0dba55adab0a1d03dc9e279429da299f08db3e44f432625f474575cc0beceb344209a94991ee61d4b09fb02266d779b4b80c75d28a2c5e64d9 Verify signature: OK Decrypted with TPM: "Hello world!" -Encrypted with rsa.EncryptOAEP: 37057d43cccdf5b0155db7c14c2641cb0e4d58c021a31dcd098f548f3eddfb9217024031ee232cd5c7618996c15e99df69132ff2a7cc4974878ad369d0f26db72758a91f587799ca4826d3826927f4626e937a1566e861919175328a9b6b9d3bcd8ddc7a6e0222cd15dc49a3abf9b423c143fb1b27e9072e57967f5653d6e2d224d14a16812b75584f2e7313cf69cab4cfbf527f8e9480095eb65ca929573e0de8c4dc06e6ad0170694491c9d02bd0a16b299219be317063d2fdc6102e157d109b8369c39d5f1cbb64a42b6dd3627bba4c044869d396269afb6bca503e2a9f3ef65cf3ab5a4d9d463e3488963c370cf3a95094b26ab215db064a7dbfca95d220 +Encrypted with rsa.EncryptOAEP: 316a2510c0b39b777aec5fcc66531371b1f45c03d4fd64cb06a71f77fc4f4c09124183648b14da1c7c17ffd99e2dbe504bfabd810fd81ec0a4346f9bbe1d81c9b500447eb455e38df2fd21c25c597d2f46b7d676d837deab425979a83318a05247390b1ec418fe3dd93b062b9c85d4bcfdd11fcba0ef93ffd0fc6689e357d11d05905f40680c39bb007f3997b8b9bd94e86e6d193ee4426bb97df2e760c01c79d657a2f8bc358b237a1e18fba44da660a22fba4719d74d30bd39704b7c30e4a105e736e38515e4751fd288e007ecfb788b2da4b8eee27715463afb8e6813c0836ffa20f938bd64c6f7d673da14bd25d951ce0cefbfa50151903293fb868dcbc4 Decrypted with TPM: "Hello world!" ``` diff --git a/go.mod b/go.mod index 6f13c48..08a0a43 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/c2FmZQ/tpm -go 1.24.0 +go 1.26.0 -toolchain go1.24.6 +toolchain go1.27.1 require ( github.com/google/go-tpm v0.9.8 - github.com/google/go-tpm-tools v0.4.7 - golang.org/x/crypto v0.48.0 + github.com/google/go-tpm-tools v0.4.10 + golang.org/x/crypto v0.57.0 ) -require golang.org/x/sys v0.41.0 // indirect +require golang.org/x/sys v0.48.0 // indirect diff --git a/go.sum b/go.sum index 3da1865..528ab5f 100644 --- a/go.sum +++ b/go.sum @@ -1,22 +1,24 @@ -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/GoogleCloudPlatform/confidential-space/server v0.0.0-20260915221818-a6c0685fb9fc h1:LPSfe+IaXHrYCNzEVZ4O48UdwNxtiNIEwKgIyU4Aawg= +github.com/GoogleCloudPlatform/confidential-space/server v0.0.0-20260915221818-a6c0685fb9fc/go.mod h1:M77DH8QPPCBBzfdwZPIWpAZdDHJZu0icyq6HTZAEojI= +github.com/google/go-configfs-tsm v0.3.3 h1:8mrlZLYrFFxyc8PFpT1piBUFDEYBVsBjAkFCwqQ2f9Y= +github.com/google/go-configfs-tsm v0.3.3/go.mod h1:in2lmJDGaYEiPOJY4vlq4lGXjkR/GcxN1k7o5oR2qn0= 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-tdx-guest v0.3.2-0.20250814004405-ffb0869e6f4d h1:Ff8goEP/ue2/rZT5qyoRicuySCYDbAXEZS8Cf1fgsUo= +github.com/google/go-tdx-guest v0.3.2-0.20250814004405-ffb0869e6f4d/go.mod h1:uHy3VaNXNXhl0fiPxKqTxieeouqQmW6A0EfLcaeCYBk= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/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/go-tpm-tools v0.4.10 h1:yKN4HVEaYXYR12gwRSCZ/Ej+5HYP8NClIKGAsc4/mC0= +github.com/google/go-tpm-tools v0.4.10/go.mod h1:5m0NX6bdl0DmD9R2cVBQROaKOeSSFL7FPcgeuQsXkLI= 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= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -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= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/tpm.go b/tpm.go index a711254..459f8d2 100644 --- a/tpm.go +++ b/tpm.go @@ -30,12 +30,29 @@ // // By default, 2048-bit RSA keys are created. AES keys, ECC keys, HMAC keys, and // RSA keys of different sizes can also be created if the TPM supports them. +// +// Keys are created under a storage root key (SRK) in the owner hierarchy, so +// clearing the TPM invalidates them. All commands that use keys are protected +// by HMAC sessions salted with the SRK: auth values are never sent to the TPM +// in cleartext, secret parameters are encrypted, and responses are +// authenticated. Serialized keys are bound to the SRK that they were created +// under. +// +// The SRK itself is trusted on first use: this package doesn't verify that it +// belongs to a genuine TPM (e.g. with the endorsement key's certificate). A +// device that impersonates the TPM when a key is created can capture that +// key's auth value and parameters. Once a key exists, a different device can't +// use it, or complete the sessions that protect it. package tpm import ( + "bytes" "crypto" + "crypto/aes" + "crypto/cipher" "crypto/ecdsa" "crypto/elliptic" + "crypto/hkdf" "crypto/rand" "crypto/rsa" "crypto/sha256" @@ -66,6 +83,9 @@ var ( ErrWrongKeyType = errors.New("operation not implemented with this key type") ErrInvalidCurve = errors.New("invalid curve id") ErrDecrypt = errors.New("decryption error") + ErrInvalidKey = errors.New("invalid or unsupported key") + ErrKeyUsage = errors.New("operation not permitted by the key's usage") + ErrWrongTPM = errors.New("key was created with a different TPM or storage root key") ) type KeyType int @@ -88,17 +108,20 @@ func (t KeyType) String() string { // Option is an option that can be passed to New. type Option func(*TPM) -// WithTPM specifies an already open TPM device to use. +// WithTPM specifies an already open TPM device to use. The TPM may be shared +// with other users, so New leaves existing objects alone. func WithTPM(rwc io.ReadWriteCloser) Option { return func(t *TPM) { t.tpm = transport.FromReadWriteCloser(rwc) } } -// WithEndorsementAuth specifies the endorsement passphrase. -func WithEndorsementAuth(pp []byte) Option { +// WithOwnerAuth specifies the owner (storage hierarchy) passphrase. Keys are +// created under a primary key in the owner hierarchy, so clearing the TPM +// (TPM2_Clear) invalidates them. +func WithOwnerAuth(pp []byte) Option { return func(t *TPM) { - t.endorsementAuth = slices.Clone(pp) + t.ownerAuth = slices.Clone(pp) } } @@ -110,6 +133,9 @@ func WithObjectAuth(pp []byte) Option { } // New returns a new TPM that's ready to use. +// +// Without [WithTPM], New opens /dev/tpmrm0, or /dev/tpm0 if the kernel +// resource manager isn't available. func New(opts ...Option) (*TPM, error) { var tpm TPM for _, o := range opts { @@ -118,31 +144,50 @@ func New(opts ...Option) (*TPM, error) { if tpm.tpm == nil { t, err := linuxtpm.Open("/dev/tpmrm0") if errors.Is(err, os.ErrNotExist) { - t, err = linuxtpm.Open("/dev/tpm0") + if t, err = linuxtpm.Open("/dev/tpm0"); err != nil { + return nil, err + } + // Without the resource manager, transient objects + // and sessions left behind by a previous process + // stay loaded. /dev/tpm0 can only be opened by one + // process at a time, so they can't belong to anyone + // else. + if err := flushLeftoverHandles(t); err != nil { + t.Close() + return nil, err + } } if err != nil { return nil, err } tpm.tpm = t } + return &tpm, nil +} - // Flush any existing transient handles. - capResp, err := tpm2.GetCapability{ - Capability: tpm2.TPMCapHandles, - Property: uint32(tpm2.TPMHTTransient) << 24, - PropertyCount: 100, - }.Execute(tpm.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_GetCapability: %w", err) - } - handles, err := capResp.CapabilityData.Data.Handles() - if err != nil { - return nil, fmt.Errorf("TPM2_GetCapability(Handles): %w", err) - } - for _, h := range handles.Handle { - tpm2.FlushContext{FlushHandle: h}.Execute(tpm.tpm) +func flushLeftoverHandles(t transport.TPM) error { + for _, ht := range []tpm2.TPMHT{ + tpm2.TPMHTTransient, + tpm2.TPMHTHMACSession, // loaded sessions + tpm2.TPMHTPolicySession, // saved sessions + } { + capResp, err := tpm2.GetCapability{ + Capability: tpm2.TPMCapHandles, + Property: uint32(ht) << 24, + PropertyCount: 100, + }.Execute(t) + if err != nil { + return fmt.Errorf("TPM2_GetCapability: %w", err) + } + handles, err := capResp.CapabilityData.Data.Handles() + if err != nil { + return fmt.Errorf("TPM2_GetCapability(Handles): %w", err) + } + for _, h := range handles.Handle { + tpm2.FlushContext{FlushHandle: h}.Execute(t) + } } - return &tpm, nil + return nil } var _ io.Closer = (*TPM)(nil) @@ -151,20 +196,40 @@ var _ io.Closer = (*TPM)(nil) // cryptographic keys that are bound to that TPM. The keys can never be used // without the TPM created them. type TPM struct { - mu sync.Mutex - tpm transport.TPMCloser - objectAuth []byte - endorsementAuth []byte - loadedKey string - loadedHandle tpm2.TPMHandle + mu sync.Mutex + tpm transport.TPMCloser + objectAuth []byte + ownerAuth []byte + loadedKey string + loadedHandle tpm2.TPMHandle + srk tpm2.NamedHandle + srkPublic tpm2.TPMTPublic + // Reusable sessions that authorize the use of keys, with parameter + // encryption in both directions, or only for the command. + inOutSession session + inSession session +} + +type session struct { + s tpm2.Session + close func() error } type keyOptions struct { keyType KeyType bits int curve elliptic.Curve + usage keyUsage } +type keyUsage int + +const ( + usageAny keyUsage = iota + usageSign + usageDecrypt +) + // KeyOption is an option that can be passed to CreateKey. type KeyOption func(*keyOptions) @@ -186,6 +251,29 @@ func WithECC(curve elliptic.Curve) KeyOption { } } +// WithSigningOnly restricts a new RSA key to signing. The key can't be used to +// decrypt. ECC keys are always signing-only. +// +// By default, RSA keys can be used for both signing and decryption. The TPM +// requires such dual-use keys to have no fixed scheme, so they can also be +// used with other schemes than the ones that this package uses (e.g. raw RSA +// decryption), by anyone who can use the key directly with the TPM. +func WithSigningOnly() KeyOption { + return func(opts *keyOptions) { + opts.usage = usageSign + } +} + +// WithDecryptionOnly restricts a new RSA key to RSA-OAEP decryption with +// SHA-256. The key can't be used to sign. +// +// See [WithSigningOnly]. +func WithDecryptionOnly() KeyOption { + return func(opts *keyOptions) { + opts.usage = usageDecrypt + } +} + // WithAES indicates that an AES key should be created. func WithAES(bits int) KeyOption { return func(opts *keyOptions) { @@ -210,11 +298,16 @@ func WithHMAC(bits int) KeyOption { func (t *TPM) CreateKey(opts ...KeyOption) (*Key, error) { t.mu.Lock() defer t.mu.Unlock() - b, err := t.createLocked(opts...) - if err != nil { - return nil, err - } - return t.unmarshalLocked(b) + var key *Key + err := t.runLocked(func() error { + b, err := t.createLocked(opts...) + if err != nil { + return err + } + key, err = t.unmarshalLocked(b) + return err + }) + return key, err } func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { @@ -225,18 +318,38 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { for _, o := range opts { o(&opt) } + if opt.keyType == TypeECC && opt.usage == usageAny { + // This package doesn't implement decryption with ECC keys. + opt.usage = usageSign + } + switch { + case opt.usage == usageAny: + case opt.keyType == TypeRSA: + case opt.keyType == TypeECC && opt.usage == usageSign: + default: + return nil, fmt.Errorf("key usage not supported with %s keys", opt.keyType) + } t.flushLocked() - srk, flush, err := t.srk() + srk, err := t.srkLocked() if err != nil { return nil, err } - defer flush() var public tpm2.TPMTPublic switch opt.keyType { case TypeRSA: + rsaScheme := tpm2.TPMTRSAScheme{Scheme: tpm2.TPMAlgNull} + if opt.usage == usageDecrypt { + rsaScheme = tpm2.TPMTRSAScheme{ + Scheme: tpm2.TPMAlgOAEP, + Details: tpm2.NewTPMUAsymScheme( + tpm2.TPMAlgOAEP, + &tpm2.TPMSEncSchemeOAEP{HashAlg: tpm2.TPMAlgSHA256}, + ), + } + } unique := make([]byte, opt.bits/8) if _, err := io.ReadFull(rand.Reader, unique); err != nil { return nil, fmt.Errorf("rand: %w", err) @@ -254,12 +367,13 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { NoDA: false, EncryptedDuplication: false, Restricted: false, - Decrypt: true, - SignEncrypt: true, + Decrypt: opt.usage != usageSign, + SignEncrypt: opt.usage != usageDecrypt, }, Parameters: tpm2.NewTPMUPublicParms( tpm2.TPMAlgRSA, &tpm2.TPMSRSAParms{ + Scheme: rsaScheme, KeyBits: tpm2.TPMKeyBits(opt.bits), }, ), @@ -272,11 +386,6 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { } case TypeECC: - uniqueSize := opt.curve.Params().BitSize / 8 - unique := make([]byte, 2*uniqueSize) - if _, err := io.ReadFull(rand.Reader, unique); err != nil { - return nil, fmt.Errorf("rand: %w", err) - } var curve tpm2.TPMECCCurve var hash tpm2.TPMAlgID switch opt.curve { @@ -295,6 +404,11 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { default: return nil, ErrInvalidCurve } + uniqueSize := opt.curve.Params().BitSize / 8 + unique := make([]byte, 2*uniqueSize) + if _, err := io.ReadFull(rand.Reader, unique); err != nil { + return nil, fmt.Errorf("rand: %w", err) + } public = tpm2.TPMTPublic{ Type: tpm2.TPMAlgECC, NameAlg: hash, @@ -308,7 +422,7 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { NoDA: false, EncryptedDuplication: false, Restricted: false, - Decrypt: true, + Decrypt: false, SignEncrypt: true, }, Parameters: tpm2.NewTPMUPublicParms( @@ -421,7 +535,11 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { } createResp, err := tpm2.Create{ - ParentHandle: srk, + ParentHandle: tpm2.AuthHandle{ + Handle: srk.Handle, + Name: srk.Name, + Auth: t.sessionLocked(nil, tpm2.AESEncryption(128, tpm2.EncryptIn)), + }, InSensitive: tpm2.TPM2BSensitiveCreate{ Sensitive: &tpm2.TPMSSensitiveCreate{ UserAuth: tpm2.TPM2BAuth{ @@ -432,29 +550,106 @@ func (t *TPM) createLocked(opts ...KeyOption) ([]byte, error) { InPublic: tpm2.New2B(public), }.Execute(t.tpm) if err != nil { - return nil, fmt.Errorf("TPM2_Create: %w", err) + return nil, tpmError("TPM2_Create", err) } priv := tpm2.Marshal(createResp.OutPrivate) pub := tpm2.Marshal(createResp.OutPublic) buf := cryptobyte.NewBuilder(nil) - buf.AddUint16(uint16(len(priv))) - buf.AddBytes(priv) - buf.AddUint16(uint16(len(pub))) - buf.AddBytes(pub) - + buf.AddUint8(keyFormatVersion) + buf.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(srk.Name.Buffer) + }) + buf.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(priv) + }) + buf.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { + b.AddBytes(pub) + }) return buf.Bytes() } +// Serialized key format: +// +// version (1) || uint16-prefixed SRK name || uint16-prefixed TPM2B_PRIVATE || +// uint16-prefixed TPM2B_PUBLIC +// +// The SRK name pins the key to the storage root key that it was created +// under. When the key is loaded, the TPM's SRK must have the same name, and +// the commands that use the key are protected by sessions salted with that +// SRK. A different device pretending to be the TPM cannot complete these +// sessions. +const keyFormatVersion = 1 + // Close closes the connections to the TPM. func (t *TPM) Close() error { t.mu.Lock() defer t.mu.Unlock() - t.flushLocked() + t.resetLocked() return t.tpm.Close() } +// resetLocked flushes the loaded key, the sessions, and the SRK. They are +// recreated when needed. +func (t *TPM) resetLocked() { + t.flushLocked() + for _, s := range []*session{&t.inOutSession, &t.inSession} { + if s.s != nil { + s.close() + *s = session{} + } + } + if t.srk.Handle != 0 { + tpm2.FlushContext{FlushHandle: t.srk.Handle}.Execute(t.tpm) + t.srk = tpm2.NamedHandle{} + } +} + +// runLocked runs f, which sends commands to the TPM. The SRK, the sessions, +// and the loaded key are kept across calls. If the TPM no longer has them, +// e.g. after a TPM restart, or because another user of a shared TPM flushed +// them, they are recreated and f is retried once. +func (t *TPM) runLocked(f func() error) error { + err := f() + var cmdErr *commandError + if !errors.As(err, &cmdErr) { + return err + } + var rc tpm2.TPMRC + if !errors.As(cmdErr.err, &rc) { + // The command or the response was lost, or the response + // didn't validate. The sessions may be out of sync with the + // TPM. + t.resetLocked() + return err + } + if !errors.Is(rc, tpm2.TPMRCHandle) && + (rc < tpm2.TPMRCReferenceH0 || rc > tpm2.TPMRCReferenceS6) { + return err + } + t.resetLocked() + return f() +} + +// commandError is an error returned by a TPM command. +type commandError struct { + cmd string + err error +} + +func tpmError(cmd string, err error) error { + return &commandError{cmd: cmd, err: err} +} + +func (e *commandError) Error() string { + return e.cmd + ": " + e.err.Error() +} + +func (e *commandError) Unwrap() error { + return e.err +} + func (t *TPM) flushLocked() { if t.loadedKey != "" { tpm2.FlushContext{FlushHandle: t.loadedHandle}.Execute(t.tpm) @@ -467,7 +662,13 @@ func (t *TPM) flushLocked() { func (t *TPM) UnmarshalKey(b []byte) (*Key, error) { t.mu.Lock() defer t.mu.Unlock() - return t.unmarshalLocked(b) + var key *Key + err := t.runLocked(func() error { + var err error + key, err = t.unmarshalLocked(b) + return err + }) + return key, err } func (t *TPM) unmarshalLocked(in []byte) (*Key, error) { @@ -475,13 +676,13 @@ func (t *TPM) unmarshalLocked(in []byte) (*Key, error) { hashed := sha256.Sum256(in) str := cryptobyte.String(in) - var sz uint16 - var savedPriv []byte - if !str.ReadUint16(&sz) || !str.ReadBytes(&savedPriv, int(sz)) { - return nil, errors.New("parse error") - } - var savedPub []byte - if !str.ReadUint16(&sz) || !str.ReadBytes(&savedPub, int(sz)) { + var version uint8 + var srkName, savedPriv, savedPub cryptobyte.String + if !str.ReadUint8(&version) || version != keyFormatVersion || + !str.ReadUint16LengthPrefixed(&srkName) || + !str.ReadUint16LengthPrefixed(&savedPriv) || + !str.ReadUint16LengthPrefixed(&savedPub) || + !str.Empty() { return nil, errors.New("parse error") } priv, err := tpm2.Unmarshal[tpm2.TPM2BPrivate](savedPriv) @@ -492,16 +693,33 @@ func (t *TPM) unmarshalLocked(in []byte) (*Key, error) { if err != nil { return nil, fmt.Errorf("TPM2_Unmarshal: %w", err) } + pubContents, err := pub.Contents() + if err != nil { + return nil, fmt.Errorf("TPM2_Unmarshal: %w", err) + } + name, err := tpm2.ObjectName(pubContents) + if err != nil { + return nil, fmt.Errorf("TPM2_Unmarshal: %w", err) + } + + srk, err := t.srkLocked() + if err != nil { + return nil, err + } + if !bytes.Equal(srk.Name.Buffer, srkName) { + return nil, ErrWrongTPM + } out := &Key{ t: t, id: hex.EncodeToString(hashed[:]), + name: *name, priv: *priv, pub: *pub, keyb: in, } - if t.loadedKey == out.id { - t.flushLocked() + if err := out.parsePublic(pubContents); err != nil { + return nil, err } if err := out.loadLocked(); err != nil { return nil, err @@ -515,15 +733,18 @@ var _ crypto.Signer = (*Key)(nil) // Key performs cryptographic operations via the TPM. It implements the // [crypto.Signer] and [crypto.Decrypter] interfaces. type Key struct { - t *TPM - id string - priv tpm2.TPM2BPrivate - pub tpm2.TPM2BPublic - keyb []byte - keyType KeyType - bits int - curve elliptic.Curve - publicKey crypto.PublicKey + t *TPM + id string + name tpm2.TPM2BName + priv tpm2.TPM2BPrivate + pub tpm2.TPM2BPublic + keyb []byte + keyType KeyType + bits int + curve elliptic.Curve + publicKey crypto.PublicKey + canSign bool + canDecrypt bool } func (k *Key) loadLocked() error { @@ -532,54 +753,62 @@ func (k *Key) loadLocked() error { } k.t.flushLocked() - srk, flush, err := k.t.srk() + srk, err := k.t.srkLocked() if err != nil { return err } - defer flush() loadResp, err := tpm2.Load{ - ParentHandle: srk, - InPrivate: k.priv, - InPublic: k.pub, + ParentHandle: tpm2.AuthHandle{ + Handle: srk.Handle, + Name: srk.Name, + // The SRK's auth value is empty, its private area is + // encrypted by the SRK, and the name of the loaded + // object is verified below. The commands that use the + // object are authorized with sessions salted with the + // SRK, which also cover the object's name. + Auth: tpm2.PasswordAuth(nil), + }, + InPrivate: k.priv, + InPublic: k.pub, }.Execute(k.t.tpm) if err != nil { - return fmt.Errorf("TPM2_Load: %w", err) + return tpmError("TPM2_Load", err) + } + if !bytes.Equal(loadResp.Name.Buffer, k.name.Buffer) { + tpm2.FlushContext{FlushHandle: loadResp.ObjectHandle}.Execute(k.t.tpm) + return errors.New("TPM2_Load: unexpected object name") } k.t.loadedKey = k.id k.t.loadedHandle = loadResp.ObjectHandle - if k.publicKey == nil { - if err := k.getPublicLocked(); err != nil { - return err - } - } return nil } -func (k *Key) getPublicLocked() error { - readPublicResp, err := tpm2.ReadPublic{ObjectHandle: k.t.loadedHandle}.Execute(k.t.tpm) - if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) - } - outPublic, err := readPublicResp.OutPublic.Contents() - if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) +// parsePublic extracts the key's parameters from its public area. +func (k *Key) parsePublic(outPublic *tpm2.TPMTPublic) error { + // Only accept keys that were generated inside this TPM and that can't + // leave it. Anyone with access to the TPM can create or import keys + // under the SRK, including keys with sensitive data that they know. + attrs := outPublic.ObjectAttributes + if !attrs.FixedTPM || !attrs.FixedParent || !attrs.SensitiveDataOrigin || !attrs.UserWithAuth { + return ErrInvalidKey } - + k.canSign = attrs.SignEncrypt + k.canDecrypt = attrs.Decrypt switch tpm2.TPMAlgID(outPublic.Type) { case tpm2.TPMAlgRSA: rsaParms, err := outPublic.Parameters.RSADetail() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } rsaPubKeyN, err := outPublic.Unique.RSA() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } rsaPubKey, err := tpm2.RSAPub(rsaParms, rsaPubKeyN) if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } k.publicKey = rsaPubKey k.keyType = TypeRSA @@ -588,15 +817,15 @@ func (k *Key) getPublicLocked() error { case tpm2.TPMAlgECC: eccParms, err := outPublic.Parameters.ECCDetail() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } c, err := tpm2.TPMECCCurve(eccParms.CurveID).Curve() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } eccPoint, err := outPublic.Unique.ECC() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } k.publicKey = &ecdsa.PublicKey{ Curve: c, @@ -609,12 +838,12 @@ func (k *Key) getPublicLocked() error { case tpm2.TPMAlgSymCipher: symParms, err := outPublic.Parameters.SymDetail() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } if symParms.Sym.Algorithm == tpm2.TPMAlgAES { bits, err := symParms.Sym.KeyBits.AES() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } k.keyType = TypeAES k.bits = int(*bits) @@ -623,13 +852,13 @@ func (k *Key) getPublicLocked() error { case tpm2.TPMAlgKeyedHash: keyedHashParms, err := outPublic.Parameters.KeyedHashDetail() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } if keyedHashParms.Scheme.Scheme == tpm2.TPMAlgHMAC { k.keyType = TypeHMAC details, err := keyedHashParms.Scheme.Details.HMAC() if err != nil { - return fmt.Errorf("TPM2_ReadPublic: %w", err) + return fmt.Errorf("parsePublic: %w", err) } switch details.HashAlg { case tpm2.TPMAlgSHA384: @@ -643,66 +872,62 @@ func (k *Key) getPublicLocked() error { } } } + if k.keyType == 0 { + return ErrInvalidKey + } return nil } // Marshal returns the serialized version of the key, which can be stored // offline, and later unmarshaled with [TPM.UnmarshalKey]. func (k *Key) Marshal() ([]byte, error) { - return k.keyb, nil + return slices.Clone(k.keyb), nil } // Public returns the public key. func (k *Key) Public() crypto.PublicKey { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil - } return k.publicKey } // Type returns the key type. func (k *Key) Type() KeyType { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return 0 - } return k.keyType } // Bits returns the key size. func (k *Key) Bits() int { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return 0 - } return k.bits } // Curve returns the key curve ID. func (k *Key) Curve() elliptic.Curve { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil - } return k.curve } // HMAC returns the HMAC signature of the message. func (k *Key) HMAC(message []byte) ([]byte, error) { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil, err - } if k.keyType != TypeHMAC { return nil, ErrWrongKeyType } - return k.hmacLocked(message) + return k.run(func() ([]byte, error) { + return k.hmacLocked(message) + }) +} + +// run loads the key and runs f, which uses it. +func (k *Key) run(f func() ([]byte, error)) ([]byte, error) { + k.t.mu.Lock() + defer k.t.mu.Unlock() + var out []byte + err := k.t.runLocked(func() error { + if err := k.loadLocked(); err != nil { + return err + } + var err error + out, err = f() + return err + }) + return out, err } func (k *Key) hmacLocked(message []byte) ([]byte, error) { @@ -718,13 +943,15 @@ func (k *Key) hmacLocked(message []byte) ([]byte, error) { return nil, ErrWrongKeyType } + sess, err := k.t.keySessionLocked(true) + if err != nil { + return nil, err + } resp, err := tpm2.Hmac{ Handle: tpm2.AuthHandle{ Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), + Name: k.name, + Auth: sess, }, Buffer: tpm2.TPM2BMaxBuffer{ Buffer: message, @@ -732,102 +959,118 @@ func (k *Key) hmacLocked(message []byte) ([]byte, error) { HashAlg: hashAlg, }.Execute(k.t.tpm) if err != nil { - return nil, fmt.Errorf("TPM2_HMAC: %w", err) + return nil, tpmError("TPM2_HMAC", err) } return resp.OutHMAC.Buffer, nil } // Sign signs a digest with the key (RSA and ECC) or computes the HMAC -// (HMAC keys). +// (HMAC keys). The digest must be a SHA-256, SHA-384, or SHA-512 hash. func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil, err + if !k.canSign { + return nil, ErrKeyUsage } - - var hashAlg tpm2.TPMSSchemeHash - switch h := opts.HashFunc(); h { - case crypto.SHA1: - hashAlg = tpm2.TPMSSchemeHash{HashAlg: tpm2.TPMAlgSHA1} - case crypto.SHA256: - hashAlg = tpm2.TPMSSchemeHash{HashAlg: tpm2.TPMAlgSHA256} - case crypto.SHA384: - hashAlg = tpm2.TPMSSchemeHash{HashAlg: tpm2.TPMAlgSHA384} - case crypto.SHA512: - hashAlg = tpm2.TPMSSchemeHash{HashAlg: tpm2.TPMAlgSHA512} - default: + h := opts.HashFunc() + if h == crypto.SHA1 { return nil, fmt.Errorf("unexpected hash %v", h) } + tpmHash, err := tpmHashAlg(h) + if err != nil { + return nil, err + } + if len(digest) != h.Size() { + return nil, fmt.Errorf("digest length %d, want %d for %v", len(digest), h.Size(), h) + } + hashAlg := tpm2.TPMSSchemeHash{HashAlg: tpmHash} + var scheme tpm2.TPMTSigScheme switch k.keyType { case TypeRSA: - scheme := tpm2.TPMTSigScheme{ + scheme = tpm2.TPMTSigScheme{ Scheme: tpm2.TPMAlgRSASSA, Details: tpm2.NewTPMUSigScheme(tpm2.TPMAlgRSASSA, &hashAlg), } if pss, ok := opts.(*rsa.PSSOptions); ok { - if pss.SaltLength != 32 { - return nil, fmt.Errorf("unexpected pss salt length %d, want 32", pss.SaltLength) + // The TPM's salt is as long as the digest. + switch pss.SaltLength { + case rsa.PSSSaltLengthAuto, rsa.PSSSaltLengthEqualsHash, h.Size(): + default: + return nil, fmt.Errorf("unexpected pss salt length %d, want %d", pss.SaltLength, h.Size()) } - scheme.Scheme = tpm2.TPMAlgRSAPSS - scheme.Details = tpm2.NewTPMUSigScheme(tpm2.TPMAlgRSAPSS, &hashAlg) - } - resp, err := tpm2.Sign{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), - }, - Digest: tpm2.TPM2BDigest{ - Buffer: digest, - }, - InScheme: scheme, - Validation: tpm2.TPMTTKHashCheck{ - Tag: tpm2.TPMSTHashCheck, - }, - }.Execute(k.t.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_Sign: %w", err) - } - if resp.Signature.SigAlg == tpm2.TPMAlgRSASSA { - sig, err := resp.Signature.Signature.RSASSA() - if err != nil { - return nil, err + scheme = tpm2.TPMTSigScheme{ + Scheme: tpm2.TPMAlgRSAPSS, + Details: tpm2.NewTPMUSigScheme(tpm2.TPMAlgRSAPSS, &hashAlg), } - return sig.Sig.Buffer, nil } - sig, err := resp.Signature.Signature.RSAPSS() + + case TypeECC: + scheme = tpm2.TPMTSigScheme{ + Scheme: tpm2.TPMAlgECDSA, + Details: tpm2.NewTPMUSigScheme(tpm2.TPMAlgECDSA, &hashAlg), + } + + case TypeHMAC: + return k.run(func() ([]byte, error) { + return k.hmacLocked(digest) + }) + + default: + return nil, ErrWrongKeyType + } + + sig, err := k.run(func() ([]byte, error) { + return k.signLocked(digest, scheme) + }) + if err != nil { + return nil, err + } + if scheme.Scheme == tpm2.TPMAlgRSAPSS { + // Older TPMs use the longest possible salt. + if err := rsa.VerifyPSS(k.publicKey.(*rsa.PublicKey), h, digest, sig, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}); err != nil { + return nil, fmt.Errorf("TPM2_Sign: unexpected pss salt length: %w", err) + } + } + return sig, nil +} + +func (k *Key) signLocked(digest []byte, scheme tpm2.TPMTSigScheme) ([]byte, error) { + sess, err := k.t.keySessionLocked(false) + if err != nil { + return nil, err + } + resp, err := tpm2.Sign{ + KeyHandle: tpm2.AuthHandle{ + Handle: k.t.loadedHandle, + Name: k.name, + Auth: sess, + }, + Digest: tpm2.TPM2BDigest{ + Buffer: digest, + }, + InScheme: scheme, + Validation: tpm2.TPMTTKHashCheck{ + Tag: tpm2.TPMSTHashCheck, + }, + }.Execute(k.t.tpm) + if err != nil { + return nil, tpmError("TPM2_Sign", err) + } + switch resp.Signature.SigAlg { + case tpm2.TPMAlgRSASSA: + sig, err := resp.Signature.Signature.RSASSA() if err != nil { return nil, err } return sig.Sig.Buffer, nil - case TypeECC: - resp, err := tpm2.Sign{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), - }, - Digest: tpm2.TPM2BDigest{ - Buffer: digest, - }, - InScheme: tpm2.TPMTSigScheme{ - Scheme: tpm2.TPMAlgECDSA, - Details: tpm2.NewTPMUSigScheme(tpm2.TPMAlgECDSA, &hashAlg), - }, - Validation: tpm2.TPMTTKHashCheck{ - Tag: tpm2.TPMSTHashCheck, - }, - }.Execute(k.t.tpm) + case tpm2.TPMAlgRSAPSS: + sig, err := resp.Signature.Signature.RSAPSS() if err != nil { - return nil, fmt.Errorf("TPM2_Sign: %w", err) + return nil, err } + return sig.Sig.Buffer, nil + + case tpm2.TPMAlgECDSA: sig, err := resp.Signature.Signature.ECDSA() if err != nil { return nil, err @@ -839,11 +1082,8 @@ func (k *Key) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) (signatur }) return b.Bytes() - case TypeHMAC: - return k.hmacLocked(digest) - default: - return nil, ErrWrongKeyType + return nil, fmt.Errorf("TPM2_Sign: unexpected signature algorithm %v", resp.Signature.SigAlg) } } @@ -868,58 +1108,27 @@ func addASN1IntBytes(b *cryptobyte.Builder, bytes []byte) { } // Encrypt encrypts cleartext with the key. +// +// With RSA keys, the cleartext is encrypted with RSA-OAEP (SHA-256) and its +// size is limited by the key size. +// +// With AES keys, the cleartext is encrypted with authenticated encryption +// (AES-GCM) using a single-use data key that is wrapped by the TPM key. There +// is no practical size limit. func (k *Key) Encrypt(cleartext []byte) (ciphertext []byte, err error) { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil, err + if k.keyType == TypeRSA && !k.canDecrypt { + return nil, ErrKeyUsage } switch k.keyType { case TypeRSA: - resp, err := tpm2.RSAEncrypt{ - KeyHandle: k.t.loadedHandle, - Message: tpm2.TPM2BPublicKeyRSA{ - Buffer: cleartext, - }, - InScheme: tpm2.TPMTRSADecrypt{ - Scheme: tpm2.TPMAlgOAEP, - Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgOAEP, &tpm2.TPMSEncSchemeOAEP{HashAlg: tpm2.TPMAlgSHA256}), - }, - }.Execute(k.t.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_RSAEncrypt: %w", err) - } - return resp.OutData.Buffer, nil + // Only the public key is needed. Encrypting in software keeps + // the cleartext off the bus. + return rsa.EncryptOAEP(sha256.New(), rand.Reader, k.publicKey.(*rsa.PublicKey), cleartext, nil) case TypeAES: - iv := make([]byte, 16) - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - return nil, fmt.Errorf("rand: %w", err) - } - resp, err := tpm2.EncryptDecrypt2{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), - }, - Message: tpm2.TPM2BMaxBuffer{ - Buffer: cleartext, - }, - Decrypt: false, - IV: tpm2.TPM2BIV{ - Buffer: iv, - }, - }.Execute(k.t.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_EncryptDecrypt2: %w", err) - } - enc := resp.OutData.Buffer - out := make([]byte, len(iv)+len(enc)) - copy(out, iv) - copy(out[len(iv):], enc) - return out, nil + return k.run(func() ([]byte, error) { + return k.aesEncryptLocked(cleartext) + }) default: return nil, ErrWrongKeyType @@ -927,81 +1136,288 @@ func (k *Key) Encrypt(cleartext []byte) (ciphertext []byte, err error) { } // Decrypt decrypts ciphertext with the key. -func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, _ crypto.DecrypterOpts) (plaintext []byte, err error) { - k.t.mu.Lock() - defer k.t.mu.Unlock() - if err := k.loadLocked(); err != nil { - return nil, err +// +// With RSA keys, only RSA-OAEP is supported. opts may be nil (SHA-256, no +// label) or a [*rsa.OAEPOptions]. The TPM requires OAEP labels to be +// null-terminated, so a non-empty Label must end with a 0x00 byte. +func (k *Key) Decrypt(_ io.Reader, ciphertext []byte, opts crypto.DecrypterOpts) (plaintext []byte, err error) { + if k.keyType == TypeRSA && !k.canDecrypt { + return nil, ErrKeyUsage } switch k.keyType { case TypeRSA: - resp, err := tpm2.RSADecrypt{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), - }, - CipherText: tpm2.TPM2BPublicKeyRSA{ - Buffer: ciphertext, - }, - InScheme: tpm2.TPMTRSADecrypt{ - Scheme: tpm2.TPMAlgOAEP, - Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgOAEP, &tpm2.TPMSEncSchemeOAEP{HashAlg: tpm2.TPMAlgSHA256}), - }, - }.Execute(k.t.tpm) + hashAlg, label, err := oaepParams(opts) if err != nil { - return nil, fmt.Errorf("TPM2_RSADecrypt: %w", err) + return nil, err } - return resp.Message.Buffer, nil + return k.run(func() ([]byte, error) { + return k.rsaDecryptLocked(ciphertext, hashAlg, label) + }) case TypeAES: - if len(ciphertext) < 16 { - return nil, ErrDecrypt - } - resp, err := tpm2.EncryptDecrypt2{ - KeyHandle: tpm2.AuthHandle{ - Handle: k.t.loadedHandle, - Name: tpm2.TPM2BName{ - Buffer: []byte(k.id), - }, - Auth: tpm2.PasswordAuth(k.t.objectAuth), - }, - Message: tpm2.TPM2BMaxBuffer{ - Buffer: ciphertext[16:], - }, - Decrypt: true, - IV: tpm2.TPM2BIV{ - Buffer: ciphertext[:16], - }, - }.Execute(k.t.tpm) - if err != nil { - return nil, fmt.Errorf("TPM2_EncryptDecrypt2: %w", err) - } - return resp.OutData.Buffer, nil + return k.run(func() ([]byte, error) { + return k.aesDecryptLocked(ciphertext) + }) default: return nil, ErrWrongKeyType } } -func (t *TPM) srk() (tpm2.NamedHandle, func(), error) { +func (k *Key) rsaDecryptLocked(ciphertext []byte, hashAlg tpm2.TPMIAlgHash, label []byte) ([]byte, error) { + sess, err := k.t.keySessionLocked(true) + if err != nil { + return nil, err + } + resp, err := tpm2.RSADecrypt{ + KeyHandle: tpm2.AuthHandle{ + Handle: k.t.loadedHandle, + Name: k.name, + Auth: sess, + }, + CipherText: tpm2.TPM2BPublicKeyRSA{ + Buffer: ciphertext, + }, + InScheme: tpm2.TPMTRSADecrypt{ + Scheme: tpm2.TPMAlgOAEP, + Details: tpm2.NewTPMUAsymScheme(tpm2.TPMAlgOAEP, &tpm2.TPMSEncSchemeOAEP{HashAlg: hashAlg}), + }, + Label: tpm2.TPM2BData{ + Buffer: label, + }, + }.Execute(k.t.tpm) + if err != nil { + return nil, tpmError("TPM2_RSADecrypt", err) + } + return resp.Message.Buffer, nil +} + +// oaepParams returns the TPM hash algorithm and label to use for RSA-OAEP +// decryption with the given options. +func oaepParams(opts crypto.DecrypterOpts) (tpm2.TPMIAlgHash, []byte, error) { + if opts == nil { + return tpm2.TPMAlgSHA256, nil, nil + } + o, ok := opts.(*rsa.OAEPOptions) + if !ok { + return 0, nil, fmt.Errorf("unsupported decrypter options %T", opts) + } + if o.MGFHash != 0 && o.MGFHash != o.Hash { + return 0, nil, errors.New("OAEP MGF hash must be the same as the label hash") + } + hashAlg, err := tpmHashAlg(o.Hash) + if err != nil { + return 0, nil, err + } + if len(o.Label) > 0 && o.Label[len(o.Label)-1] != 0 { + return 0, nil, errors.New("OAEP label must be null-terminated") + } + return hashAlg, o.Label, nil +} + +func tpmHashAlg(h crypto.Hash) (tpm2.TPMIAlgHash, error) { + switch h { + case crypto.SHA1: + return tpm2.TPMAlgSHA1, nil + case crypto.SHA256: + return tpm2.TPMAlgSHA256, nil + case crypto.SHA384: + return tpm2.TPMAlgSHA384, nil + case crypto.SHA512: + return tpm2.TPMAlgSHA512, nil + default: + return 0, fmt.Errorf("unexpected hash %v", h) + } +} + +// AES ciphertext format: +// +// version (1) || iv (16) || wrapped data key (32) || AES-GCM ciphertext+tag +// +// A random data key is generated for each message and wrapped (AES-CFB) by the +// TPM key. The message itself is sealed with AES-GCM in software, using a key +// derived from the data key. The data key is never reused, so a fixed nonce is +// safe. The header is authenticated as additional data. +const ( + aesVersion = 1 + aesIVSize = 16 + aesDataKeyLen = 32 + aesHeaderSize = 1 + aesIVSize + aesDataKeyLen +) + +func (k *Key) aesEncryptLocked(cleartext []byte) ([]byte, error) { + header := make([]byte, aesHeaderSize) + header[0] = aesVersion + iv := header[1 : 1+aesIVSize] + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, fmt.Errorf("rand: %w", err) + } + dataKey := make([]byte, aesDataKeyLen) + if _, err := io.ReadFull(rand.Reader, dataKey); err != nil { + return nil, fmt.Errorf("rand: %w", err) + } + wrapped, err := k.tpmAESLocked(dataKey, iv, false) + if err != nil { + return nil, err + } + if len(wrapped) != aesDataKeyLen { + return nil, errors.New("TPM2_EncryptDecrypt2: unexpected output size") + } + copy(header[1+aesIVSize:], wrapped) + + aead, err := newDataKeyAEAD(dataKey) + if err != nil { + return nil, err + } + nonce := make([]byte, aead.NonceSize()) + out := make([]byte, aesHeaderSize, aesHeaderSize+len(cleartext)+aead.Overhead()) + copy(out, header) + return aead.Seal(out, nonce, cleartext, header), nil +} + +func (k *Key) aesDecryptLocked(ciphertext []byte) ([]byte, error) { + if len(ciphertext) < aesHeaderSize || ciphertext[0] != aesVersion { + return nil, ErrDecrypt + } + header := ciphertext[:aesHeaderSize] + iv := header[1 : 1+aesIVSize] + dataKey, err := k.tpmAESLocked(header[1+aesIVSize:], iv, true) + if err != nil { + return nil, err + } + aead, err := newDataKeyAEAD(dataKey) + if err != nil { + return nil, err + } + nonce := make([]byte, aead.NonceSize()) + out, err := aead.Open(nil, nonce, ciphertext[aesHeaderSize:], header) + if err != nil { + return nil, ErrDecrypt + } + return out, nil +} + +func newDataKeyAEAD(dataKey []byte) (cipher.AEAD, error) { + key, err := hkdf.Key(sha256.New, dataKey, nil, "github.com/c2FmZQ/tpm AES-GCM", 32) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return cipher.NewGCM(block) +} + +func (k *Key) tpmAESLocked(in, iv []byte, decrypt bool) ([]byte, error) { + sess, err := k.t.keySessionLocked(true) + if err != nil { + return nil, err + } + resp, err := tpm2.EncryptDecrypt2{ + KeyHandle: tpm2.AuthHandle{ + Handle: k.t.loadedHandle, + Name: k.name, + Auth: sess, + }, + Message: tpm2.TPM2BMaxBuffer{ + Buffer: in, + }, + Decrypt: decrypt, + IV: tpm2.TPM2BIV{ + Buffer: iv, + }, + }.Execute(k.t.tpm) + if err != nil { + return nil, tpmError("TPM2_EncryptDecrypt2", err) + } + return resp.OutData.Buffer, nil +} + +// srkLocked returns the storage root key (SRK), creating it if needed. The SRK +// is the parent of all the keys, and it is used to salt the sessions that +// protect the commands sent to the TPM. It stays loaded until Close. +func (t *TPM) srkLocked() (tpm2.NamedHandle, error) { + if t.srk.Handle != 0 { + return t.srk, nil + } createPrimaryResp, err := tpm2.CreatePrimary{ PrimaryHandle: tpm2.AuthHandle{ - Handle: tpm2.TPMRHEndorsement, - Auth: tpm2.PasswordAuth(t.endorsementAuth), + Handle: tpm2.TPMRHOwner, + // No salt is available yet. The HMAC session keeps + // the owner auth value off the bus. + Auth: tpm2.HMAC(tpm2.TPMAlgSHA256, 16, tpm2.Auth(t.ownerAuth)), }, InPublic: tpm2.New2B(tpm2.RSASRKTemplate), }.Execute(t.tpm) if err != nil { - return tpm2.NamedHandle{}, nil, fmt.Errorf("TPM2_CreatePrimary: %w", err) + return tpm2.NamedHandle{}, tpmError("TPM2_CreatePrimary", err) + } + flush := func() { + tpm2.FlushContext{FlushHandle: createPrimaryResp.ObjectHandle}.Execute(t.tpm) } - srk := tpm2.NamedHandle{ + pub, err := createPrimaryResp.OutPublic.Contents() + if err != nil { + flush() + return tpm2.NamedHandle{}, fmt.Errorf("TPM2_CreatePrimary: %w", err) + } + // The name is computed from the public area, instead of using the + // name from the response. Nothing authenticates the response, so the + // SRK is trusted on first use. See the package documentation. + name, err := tpm2.ObjectName(pub) + if err != nil { + flush() + return tpm2.NamedHandle{}, fmt.Errorf("TPM2_CreatePrimary: %w", err) + } + t.srk = tpm2.NamedHandle{ Handle: createPrimaryResp.ObjectHandle, - Name: createPrimaryResp.Name, + Name: *name, + } + t.srkPublic = *pub + return t.srk, nil +} + +// sessionLocked returns a one-time HMAC session, salted with the SRK, to +// authorize the use of an object with the given auth value. The auth value is +// never sent to the TPM, the TPM's response is authenticated, and opts can +// enable encryption of the first command and/or response parameter. The SRK +// must already be loaded. +// +// Starting a salted session is expensive. Keys are used with the reusable +// sessions from keySessionLocked instead. +func (t *TPM) sessionLocked(auth []byte, opts ...tpm2.AuthOption) tpm2.Session { + opts = append([]tpm2.AuthOption{ + tpm2.Auth(auth), + tpm2.Salted(t.srk.Handle, t.srkPublic), + }, opts...) + return tpm2.HMAC(tpm2.TPMAlgSHA256, 16, opts...) +} + +// keySessionLocked returns a reusable HMAC session, salted with the SRK, to +// authorize the use of a key, like sessionLocked. The command parameter is +// encrypted, and so is the response parameter if encryptResponse is true. +// The TPM rejects sessions that encrypt the response of commands that can't +// encrypt it, like TPM2_Sign. +func (t *TPM) keySessionLocked(encryptResponse bool) (tpm2.Session, error) { + p, dir := &t.inSession, tpm2.EncryptIn + if encryptResponse { + p, dir = &t.inOutSession, tpm2.EncryptInOut + } + if p.s != nil { + return p.s, nil + } + srk, err := t.srkLocked() + if err != nil { + return nil, err + } + s, closer, err := tpm2.HMACSession(t.tpm, tpm2.TPMAlgSHA256, 16, + tpm2.Auth(t.objectAuth), + tpm2.Salted(srk.Handle, t.srkPublic), + tpm2.AESEncryption(128, dir), + ) + if err != nil { + return nil, tpmError("TPM2_StartAuthSession", err) } - return srk, func() { - tpm2.FlushContext{FlushHandle: srk}.Execute(t.tpm) - }, nil + *p = session{s: s, close: closer} + return s, nil } diff --git a/tpm_test.go b/tpm_test.go index 4b0a336..eb14b7d 100644 --- a/tpm_test.go +++ b/tpm_test.go @@ -28,12 +28,22 @@ import ( "crypto" "crypto/ecdsa" "crypto/elliptic" + "crypto/rand" "crypto/rsa" + "crypto/sha1" "crypto/sha256" + _ "crypto/sha512" + "encoding/binary" + "errors" "fmt" + "io" + "slices" "testing" "github.com/google/go-tpm-tools/simulator" + "github.com/google/go-tpm/tpm2" + "github.com/google/go-tpm/tpm2/transport" + "golang.org/x/crypto/cryptobyte" ) func TestRSA(t *testing.T) { @@ -78,6 +88,35 @@ func TestRSA(t *testing.T) { } pub := key.Public() + + // OAEP options are honored. + label := []byte("label\x00") + for _, h := range []crypto.Hash{crypto.SHA1, crypto.SHA256} { + enc, err := rsa.EncryptOAEP(h.New(), rand.Reader, pub.(*rsa.PublicKey), []byte(payload), label) + if err != nil { + t.Fatalf("rsa.EncryptOAEP: %v", err) + } + dec, err := key.Decrypt(nil, enc, &rsa.OAEPOptions{Hash: h, Label: label}) + if err != nil { + t.Fatalf("tpm.Decrypt(%v, label): %v", h, err) + } + if got, want := string(dec), payload; got != want { + t.Fatalf("Decrypt() = %q, want %q", got, want) + } + if _, err := key.Decrypt(nil, enc, &rsa.OAEPOptions{Hash: h, Label: []byte("other\x00")}); err == nil { + t.Fatal("tpm.Decrypt with wrong label should have failed") + } + if _, err := key.Decrypt(nil, enc, &rsa.OAEPOptions{Hash: h}); err == nil { + t.Fatal("tpm.Decrypt without label should have failed") + } + } + if _, err := key.Decrypt(nil, enc, &rsa.OAEPOptions{Hash: crypto.SHA256, Label: []byte("label")}); err == nil { + t.Fatal("tpm.Decrypt with non-null-terminated label should have failed") + } + if _, err := key.Decrypt(nil, enc, &rsa.PKCS1v15DecryptOptions{}); err == nil { + t.Fatal("tpm.Decrypt with PKCS1v15 options should have failed") + } + hashed := sha256.Sum256([]byte(payload)) sig, err := key.Sign(nil, hashed[:], crypto.SHA256) if err != nil { @@ -86,6 +125,13 @@ func TestRSA(t *testing.T) { if err := rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed[:], sig); err != nil { t.Fatalf("VerifyPKCS1v15: %v", err) } + sha1Hashed := sha1.Sum([]byte(payload)) + if _, err := key.Sign(nil, sha1Hashed[:], crypto.SHA1); err == nil { + t.Fatal("Sign() with SHA-1 should have failed") + } + if _, err := key.Sign(nil, hashed[:16], crypto.SHA256); err == nil { + t.Fatal("Sign() with short digest should have failed") + } pssOptions := &rsa.PSSOptions{SaltLength: 32, Hash: crypto.SHA256} sig2, err := key.Sign(nil, hashed[:], pssOptions) if err != nil { @@ -95,11 +141,11 @@ func TestRSA(t *testing.T) { t.Fatalf("VerifyPSS: %v", err) } - tpm.objectAuth = []byte("wrong") + setObjectAuth(tpm, []byte("wrong")) if _, err := key.Decrypt(nil, enc, nil); err == nil { t.Fatal("tpm.Decrypt should have failed") } - tpm.objectAuth = []byte(keyPassphrase) + setObjectAuth(tpm, []byte(keyPassphrase)) } } @@ -194,11 +240,37 @@ func TestAES(t *testing.T) { t.Fatalf("Decrypt() = %q, want %q", got, want) } - tpm.objectAuth = []byte("wrong") + // Large messages are supported. + large := bytes.Repeat([]byte("x"), 100000) + encLarge, err := key.Encrypt(large) + if err != nil { + t.Fatalf("tpm.Encrypt(large): %v", err) + } + decLarge, err := key.Decrypt(nil, encLarge, nil) + if err != nil { + t.Fatalf("tpm.Decrypt(large): %v", err) + } + if !bytes.Equal(decLarge, large) { + t.Fatal("Decrypt(large) mismatch") + } + + // Any modification of the ciphertext must be detected. + for i := range enc { + tampered := slices.Clone(enc) + tampered[i] ^= 0x01 + if _, err := key.Decrypt(nil, tampered, nil); err == nil { + t.Fatalf("tpm.Decrypt should have failed with byte %d modified", i) + } + } + if _, err := key.Decrypt(nil, enc[:len(enc)-1], nil); err == nil { + t.Fatal("tpm.Decrypt should have failed with truncated ciphertext") + } + + setObjectAuth(tpm, []byte("wrong")) if _, err := key.Decrypt(nil, enc, nil); err == nil { t.Fatal("tpm.Decrypt should have failed") } - tpm.objectAuth = []byte(keyPassphrase) + setObjectAuth(tpm, []byte(keyPassphrase)) } } @@ -341,7 +413,12 @@ func TestMarshal(t *testing.T) { if err != nil { t.Fatalf("key.Marshal: %v", err) } - contexts = append(contexts, b) + contexts = append(contexts, slices.Clone(b)) + // The caller owns the returned slice. + clear(b) + if b2, _ := key.Marshal(); !bytes.Equal(b2, contexts[i]) { + t.Fatal("Modifying the output of key.Marshal changed the key") + } payload := []byte(fmt.Sprintf("Payload %d", i)) enc, err := key.Encrypt(payload) @@ -374,3 +451,563 @@ func TestMarshal(t *testing.T) { } } } + +func TestClearInvalidatesKeys(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + key, err := tpm.CreateKey() + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + b, err := key.Marshal() + if err != nil { + t.Fatalf("key.Marshal: %v", err) + } + if _, err := tpm.UnmarshalKey(b); err != nil { + t.Fatalf("tpm.UnmarshalKey: %v", err) + } + + tpm.mu.Lock() + tpm.flushLocked() + _, err = tpm2.Clear{ + AuthHandle: tpm2.AuthHandle{ + Handle: tpm2.TPMRHLockout, + Auth: tpm2.PasswordAuth(nil), + }, + }.Execute(tpm.tpm) + // TPM2_Clear flushed the SRK. + tpm.srk = tpm2.NamedHandle{} + tpm.mu.Unlock() + if err != nil { + t.Fatalf("TPM2_Clear: %v", err) + } + + if _, err := tpm.UnmarshalKey(b); !errors.Is(err, ErrWrongTPM) { + t.Fatalf("tpm.UnmarshalKey after TPM2_Clear: got %v, want %v", err, ErrWrongTPM) + } +} + +func TestWrongTPM(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + key, err := tpm.CreateKey() + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + b, err := key.Marshal() + if err != nil { + t.Fatalf("key.Marshal: %v", err) + } + tpm.Close() + + rwc2, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + other, err := New(WithTPM(rwc2)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer other.Close() + if _, err := other.UnmarshalKey(b); !errors.Is(err, ErrWrongTPM) { + t.Fatalf("tpm.UnmarshalKey on different TPM: got %v, want %v", err, ErrWrongTPM) + } +} + +// recorder records all the bytes exchanged with the TPM. +type recorder struct { + io.ReadWriteCloser + buf bytes.Buffer +} + +func (r *recorder) Read(b []byte) (int, error) { + n, err := r.ReadWriteCloser.Read(b) + r.buf.Write(b[:n]) + return n, err +} + +func (r *recorder) Write(b []byte) (int, error) { + r.buf.Write(b) + return r.ReadWriteCloser.Write(b) +} + +func TestBusConfidentiality(t *testing.T) { + var ( + ownerAuth = []byte("owner-secret-passphrase") + objectAuth = []byte("object-secret-passphrase") + payload = []byte("very-secret-payload-0123456789") + ) + + sim, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + if _, err := (tpm2.HierarchyChangeAuth{ + AuthHandle: tpm2.AuthHandle{ + Handle: tpm2.TPMRHOwner, + Auth: tpm2.PasswordAuth(nil), + }, + NewAuth: tpm2.TPM2BAuth{Buffer: ownerAuth}, + }).Execute(transport.FromReadWriteCloser(sim)); err != nil { + t.Fatalf("TPM2_HierarchyChangeAuth: %v", err) + } + rec := &recorder{ReadWriteCloser: sim} + + tpm, err := New(WithTPM(rec), WithOwnerAuth(ownerAuth), WithObjectAuth(objectAuth)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + for _, opt := range []KeyOption{WithRSA(2048), WithAES(128), WithHMAC(256)} { + key, err := tpm.CreateKey(opt) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + switch key.Type() { + case TypeRSA, TypeAES: + enc, err := key.Encrypt(payload) + if err != nil { + t.Fatalf("key.Encrypt: %v", err) + } + dec, err := key.Decrypt(nil, enc, nil) + if err != nil { + t.Fatalf("key.Decrypt: %v", err) + } + if !bytes.Equal(dec, payload) { + t.Fatalf("key.Decrypt() = %q, want %q", dec, payload) + } + case TypeHMAC: + mac, err := key.HMAC(payload) + if err != nil { + t.Fatalf("key.HMAC: %v", err) + } + if bytes.Contains(rec.buf.Bytes(), mac) { + t.Error("HMAC output seen on the bus") + } + } + } + + for _, secret := range [][]byte{ownerAuth, objectAuth, payload} { + if bytes.Contains(rec.buf.Bytes(), secret) { + t.Errorf("%q seen on the bus", secret) + } + } +} + +func TestRejectKnownSensitiveKey(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + // Create an HMAC key under the SRK with a key value chosen by the + // caller, i.e. a key that isn't secret. + tpm.mu.Lock() + srk, err := tpm.srkLocked() + if err != nil { + tpm.mu.Unlock() + t.Fatalf("srkLocked: %v", err) + } + createResp, err := tpm2.Create{ + ParentHandle: tpm2.AuthHandle{ + Handle: srk.Handle, + Name: srk.Name, + Auth: tpm.sessionLocked(nil), + }, + InSensitive: tpm2.TPM2BSensitiveCreate{ + Sensitive: &tpm2.TPMSSensitiveCreate{ + Data: tpm2.NewTPMUSensitiveCreate(&tpm2.TPM2BSensitiveData{ + Buffer: []byte("attacker-known-key"), + }), + }, + }, + InPublic: tpm2.New2B(tpm2.TPMTPublic{ + Type: tpm2.TPMAlgKeyedHash, + NameAlg: tpm2.TPMAlgSHA256, + ObjectAttributes: tpm2.TPMAObject{ + FixedTPM: true, + FixedParent: true, + UserWithAuth: true, + SignEncrypt: true, + }, + Parameters: tpm2.NewTPMUPublicParms( + tpm2.TPMAlgKeyedHash, + &tpm2.TPMSKeyedHashParms{ + Scheme: tpm2.TPMTKeyedHashScheme{ + Scheme: tpm2.TPMAlgHMAC, + Details: tpm2.NewTPMUSchemeKeyedHash( + tpm2.TPMAlgHMAC, + &tpm2.TPMSSchemeHMAC{HashAlg: tpm2.TPMAlgSHA256}, + ), + }, + }, + ), + }), + }.Execute(tpm.tpm) + tpm.mu.Unlock() + if err != nil { + t.Fatalf("TPM2_Create: %v", err) + } + + var b cryptobyte.Builder + b.AddUint8(keyFormatVersion) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(srk.Name.Buffer) }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(tpm2.Marshal(createResp.OutPrivate)) }) + b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(tpm2.Marshal(createResp.OutPublic)) }) + + if _, err := tpm.UnmarshalKey(b.BytesOrPanic()); !errors.Is(err, ErrInvalidKey) { + t.Fatalf("tpm.UnmarshalKey: got %v, want %v", err, ErrInvalidKey) + } +} + +func TestKeyUsage(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + payload := []byte("Hello World!") + hashed := sha256.Sum256(payload) + + t.Run("RSA signing only", func(t *testing.T) { + key, err := tpm.CreateKey(WithRSA(2048), WithSigningOnly()) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + sig, err := key.Sign(nil, hashed[:], crypto.SHA256) + if err != nil { + t.Fatalf("key.Sign: %v", err) + } + if err := rsa.VerifyPKCS1v15(key.Public().(*rsa.PublicKey), crypto.SHA256, hashed[:], sig); err != nil { + t.Fatalf("VerifyPKCS1v15: %v", err) + } + if _, err := key.Encrypt(payload); !errors.Is(err, ErrKeyUsage) { + t.Fatalf("key.Encrypt: got %v, want %v", err, ErrKeyUsage) + } + enc, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, key.Public().(*rsa.PublicKey), payload, nil) + if err != nil { + t.Fatalf("rsa.EncryptOAEP: %v", err) + } + if _, err := key.Decrypt(nil, enc, nil); !errors.Is(err, ErrKeyUsage) { + t.Fatalf("key.Decrypt: got %v, want %v", err, ErrKeyUsage) + } + // The TPM enforces it too. + key.canDecrypt = true + if _, err := key.Decrypt(nil, enc, nil); err == nil { + t.Fatal("TPM decrypted with a signing-only key") + } + }) + + t.Run("RSA decryption only", func(t *testing.T) { + key, err := tpm.CreateKey(WithRSA(2048), WithDecryptionOnly()) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + enc, err := key.Encrypt(payload) + if err != nil { + t.Fatalf("key.Encrypt: %v", err) + } + dec, err := key.Decrypt(nil, enc, nil) + if err != nil { + t.Fatalf("key.Decrypt: %v", err) + } + if !bytes.Equal(dec, payload) { + t.Fatalf("key.Decrypt() = %q, want %q", dec, payload) + } + if _, err := key.Sign(nil, hashed[:], crypto.SHA256); !errors.Is(err, ErrKeyUsage) { + t.Fatalf("key.Sign: got %v, want %v", err, ErrKeyUsage) + } + // The TPM enforces it too. + key.canSign = true + if _, err := key.Sign(nil, hashed[:], crypto.SHA256); err == nil { + t.Fatal("TPM signed with a decryption-only key") + } + // Only the key's scheme (OAEP) can be used, not raw RSA. + tpm.mu.Lock() + _, err = tpm2.RSADecrypt{ + KeyHandle: tpm2.AuthHandle{ + Handle: tpm.loadedHandle, + Name: key.name, + Auth: tpm.sessionLocked(nil), + }, + CipherText: tpm2.TPM2BPublicKeyRSA{Buffer: enc}, + InScheme: tpm2.TPMTRSADecrypt{Scheme: tpm2.TPMAlgRSAES}, + }.Execute(tpm.tpm) + tpm.mu.Unlock() + if err == nil { + t.Fatal("TPM decrypted with a scheme other than the key's scheme") + } + }) + + t.Run("ECC signing only", func(t *testing.T) { + key, err := tpm.CreateKey(WithECC(elliptic.P256()), WithSigningOnly()) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + sig, err := key.Sign(nil, hashed[:], crypto.SHA256) + if err != nil { + t.Fatalf("key.Sign: %v", err) + } + if !ecdsa.VerifyASN1(key.Public().(*ecdsa.PublicKey), hashed[:], sig) { + t.Fatal("VerifyASN1 failed") + } + }) + + t.Run("ECC default", func(t *testing.T) { + key, err := tpm.CreateKey(WithECC(elliptic.P256())) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + if !key.canSign || key.canDecrypt { + t.Fatalf("canSign = %v, canDecrypt = %v, want signing only", key.canSign, key.canDecrypt) + } + }) + + t.Run("Invalid curve", func(t *testing.T) { + if _, err := tpm.CreateKey(WithECC(nil)); !errors.Is(err, ErrInvalidCurve) { + t.Fatalf("tpm.CreateKey: got %v, want %v", err, ErrInvalidCurve) + } + }) + + t.Run("Unsupported", func(t *testing.T) { + for _, opts := range [][]KeyOption{ + {WithECC(elliptic.P256()), WithDecryptionOnly()}, + {WithAES(128), WithSigningOnly()}, + {WithAES(128), WithDecryptionOnly()}, + {WithHMAC(256), WithDecryptionOnly()}, + } { + if _, err := tpm.CreateKey(opts...); err == nil { + t.Errorf("tpm.CreateKey should have failed") + } + } + }) +} + +func TestNewKeepsOtherObjects(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + other, err := tpm2.CreatePrimary{ + PrimaryHandle: tpm2.TPMRHOwner, + InPublic: tpm2.New2B(tpm2.ECCSRKTemplate), + }.Execute(transport.FromReadWriteCloser(rwc)) + if err != nil { + t.Fatalf("TPM2_CreatePrimary: %v", err) + } + + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + if _, err := (tpm2.ReadPublic{ObjectHandle: other.ObjectHandle}).Execute(tpm.tpm); err != nil { + t.Fatalf("Object created before New is gone: %v", err) + } +} + +// setObjectAuth changes the auth value that tpm uses with keys, as if it had +// been created with a different WithObjectAuth. +func setObjectAuth(tpm *TPM, auth []byte) { + tpm.mu.Lock() + defer tpm.mu.Unlock() + tpm.objectAuth = auth + tpm.resetLocked() +} + +func TestRSAPSS(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + key, err := tpm.CreateKey(WithRSA(2048)) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + pub := key.Public().(*rsa.PublicKey) + + for _, h := range []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512} { + hh := h.New() + hh.Write([]byte("Hello World!")) + digest := hh.Sum(nil) + + for _, saltLength := range []int{rsa.PSSSaltLengthAuto, rsa.PSSSaltLengthEqualsHash, h.Size()} { + sig, err := key.Sign(nil, digest, &rsa.PSSOptions{SaltLength: saltLength, Hash: h}) + if err != nil { + t.Fatalf("Sign(%v, %d): %v", h, saltLength, err) + } + if err := rsa.VerifyPSS(pub, h, digest, sig, &rsa.PSSOptions{SaltLength: h.Size()}); err != nil { + t.Fatalf("VerifyPSS(%v, %d): %v", h, saltLength, err) + } + } + if _, err := key.Sign(nil, digest, &rsa.PSSOptions{SaltLength: h.Size() + 1, Hash: h}); err == nil { + t.Fatalf("Sign(%v) with unsupported salt length should have failed", h) + } + } +} + +// cmdCounter counts the commands sent to the TPM. +type cmdCounter struct { + io.ReadWriteCloser + counts map[tpm2.TPMCC]int +} + +func (c *cmdCounter) Write(b []byte) (int, error) { + if len(b) >= 10 { + c.counts[tpm2.TPMCC(binary.BigEndian.Uint32(b[6:10]))]++ + } + return c.ReadWriteCloser.Write(b) +} + +func TestSessionReuse(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + counter := &cmdCounter{ReadWriteCloser: rwc, counts: make(map[tpm2.TPMCC]int)} + tpm, err := New(WithTPM(counter), WithObjectAuth([]byte("passphrase"))) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + key1, err := tpm.CreateKey(WithRSA(2048)) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + key2, err := tpm.CreateKey(WithAES(128)) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + hashed := sha256.Sum256([]byte("Hello World!")) + use := func() { + if _, err := key1.Sign(nil, hashed[:], crypto.SHA256); err != nil { + t.Fatalf("key1.Sign: %v", err) + } + enc, err := key1.Encrypt([]byte("Hello World!")) + if err != nil { + t.Fatalf("key1.Encrypt: %v", err) + } + if _, err := key1.Decrypt(nil, enc, nil); err != nil { + t.Fatalf("key1.Decrypt: %v", err) + } + if enc, err = key2.Encrypt([]byte("Hello World!")); err != nil { + t.Fatalf("key2.Encrypt: %v", err) + } + if _, err := key2.Decrypt(nil, enc, nil); err != nil { + t.Fatalf("key2.Decrypt: %v", err) + } + } + use() + clear(counter.counts) + for range 5 { + use() + } + if n := counter.counts[tpm2.TPMCCStartAuthSession]; n != 0 { + t.Errorf("TPM2_StartAuthSession called %d times, want 0", n) + } + if n := counter.counts[tpm2.TPMCCCreatePrimary]; n != 0 { + t.Errorf("TPM2_CreatePrimary called %d times, want 0", n) + } +} + +func TestRecoverFlushedHandles(t *testing.T) { + rwc, err := simulator.Get() + if err != nil { + t.Fatalf("simulator.Get: %v", err) + } + tpm, err := New(WithTPM(rwc)) + if err != nil { + t.Fatalf("New: %v", err) + } + defer tpm.Close() + + key, err := tpm.CreateKey(WithRSA(2048)) + if err != nil { + t.Fatalf("tpm.CreateKey: %v", err) + } + hashed := sha256.Sum256([]byte("Hello World!")) + enc, err := key.Encrypt([]byte("Hello World!")) + if err != nil { + t.Fatalf("key.Encrypt: %v", err) + } + if _, err := key.Sign(nil, hashed[:], crypto.SHA256); err != nil { + t.Fatalf("key.Sign: %v", err) + } + if _, err := key.Decrypt(nil, enc, nil); err != nil { + t.Fatalf("key.Decrypt: %v", err) + } + + // Everything that the TPM object loaded disappears, e.g. because + // another user of the TPM flushed it. + raw := transport.FromReadWriteCloser(rwc) + if err := flushLeftoverHandles(raw); err != nil { + t.Fatalf("flushLeftoverHandles: %v", err) + } + for _, ht := range []tpm2.TPMHT{tpm2.TPMHTTransient, tpm2.TPMHTHMACSession, tpm2.TPMHTPolicySession} { + resp, err := tpm2.GetCapability{ + Capability: tpm2.TPMCapHandles, + Property: uint32(ht) << 24, + PropertyCount: 100, + }.Execute(raw) + if err != nil { + t.Fatalf("TPM2_GetCapability: %v", err) + } + handles, err := resp.CapabilityData.Data.Handles() + if err != nil { + t.Fatalf("TPM2_GetCapability(Handles): %v", err) + } + if len(handles.Handle) != 0 { + t.Fatalf("Handles of type %v not flushed: %v", ht, handles.Handle) + } + } + + if _, err := key.Sign(nil, hashed[:], crypto.SHA256); err != nil { + t.Fatalf("key.Sign after flush: %v", err) + } + if err := flushLeftoverHandles(raw); err != nil { + t.Fatalf("flushLeftoverHandles: %v", err) + } + if _, err := key.Decrypt(nil, enc, nil); err != nil { + t.Fatalf("key.Decrypt after flush: %v", err) + } + if err := flushLeftoverHandles(raw); err != nil { + t.Fatalf("flushLeftoverHandles: %v", err) + } + if _, err := tpm.CreateKey(WithECC(elliptic.P256())); err != nil { + t.Fatalf("tpm.CreateKey after flush: %v", err) + } +}