From 9a2d46cd7bc21f1e7baa62560c3fe51d3b84cab2 Mon Sep 17 00:00:00 2001 From: Chris Coulson Date: Sat, 9 Mar 2024 02:45:20 +0000 Subject: [PATCH] [WIP] add support for PBKDF2 for passphrases Passphrase support currently hardcodes the use of Argon2. This adds support for specifying PBKDF2, for use in environments where FIPS140 compliance is required. --- argon2_test.go | 16 ++++ export_test.go | 19 ++++ go.mod | 6 +- go.sum | 6 ++ internal/pbkdf2/export_test.go | 30 +++++++ internal/pbkdf2/pbkdf2.go | 132 +++++++++++++++++++++++++++ internal/pbkdf2/pbkdf2_test.go | 130 +++++++++++++++++++++++++++ kdf.go | 26 ++++++ keydata.go | 31 +++++-- pbkdf2.go | 107 ++++++++++++++++++++++ pbkdf2_test.go | 158 +++++++++++++++++++++++++++++++++ 11 files changed, 653 insertions(+), 8 deletions(-) create mode 100644 internal/pbkdf2/export_test.go create mode 100644 internal/pbkdf2/pbkdf2.go create mode 100644 internal/pbkdf2/pbkdf2_test.go create mode 100644 kdf.go create mode 100644 pbkdf2.go create mode 100644 pbkdf2_test.go diff --git a/argon2_test.go b/argon2_test.go index 7dd15230..016957c7 100644 --- a/argon2_test.go +++ b/argon2_test.go @@ -23,6 +23,7 @@ import ( "math" "os" "runtime" + "time" snapd_testutil "github.com/snapcore/snapd/testutil" @@ -101,6 +102,21 @@ func (s *argon2Suite) TestKDFParamsExplicitMode(c *C) { }) } +func (s *argon2Suite) TestKDFParamsTargetDuration(c *C) { + var opts Argon2Options + opts.TargetDuration = 1 * time.Second + params, err := opts.KdfParams(32) + c.Assert(err, IsNil) + c.Check(s.kdf.BenchmarkMode, Equals, Argon2id) + + c.Check(params, DeepEquals, &KdfParams{ + Type: "argon2id", + Time: 4, + Memory: 512031, + CPUs: s.cpusAuto, + }) +} + func (s *argon2Suite) TestKDFParamsMemoryLimit(c *C) { var opts Argon2Options opts.MemoryKiB = 32 * 1024 diff --git a/export_test.go b/export_test.go index 86e2cf1c..0a5e6a6f 100644 --- a/export_test.go +++ b/export_test.go @@ -20,18 +20,25 @@ package secboot import ( + "crypto" "io" + "time" "github.com/snapcore/secboot/internal/luks2" "github.com/snapcore/secboot/internal/luksview" ) +const ( + NilHash = nilHash +) + var ( UnmarshalV1KeyPayload = unmarshalV1KeyPayload UnmarshalProtectedKeys = unmarshalProtectedKeys ) type ( + HashAlg = hashAlg KdfParams = kdfParams ProtectedKeys = protectedKeys ) @@ -40,6 +47,10 @@ func (o *Argon2Options) KdfParams(keyLen uint32) (*KdfParams, error) { return o.kdfParams(keyLen) } +func (o *PBKDF2Options) KdfParams(keyLen uint32) (*KdfParams, error) { + return o.kdfParams(keyLen) +} + func MockLUKS2Activate(fn func(string, string, []byte, int) error) (restore func()) { origActivate := luks2Activate luks2Activate = fn @@ -112,6 +123,14 @@ func MockNewLUKSView(fn func(string, luks2.LockMode) (*luksview.View, error)) (r } } +func MockPBKDF2Benchmark(fn func(time.Duration, crypto.Hash) (uint, error)) (restore func()) { + orig := pbkdf2Benchmark + pbkdf2Benchmark = fn + return func() { + pbkdf2Benchmark = orig + } +} + func MockRuntimeNumCPU(n int) (restore func()) { orig := runtimeNumCPU runtimeNumCPU = func() int { diff --git a/go.mod b/go.mod index feb17558..553978ac 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/canonical/go-tpm2 v1.3.0 github.com/canonical/tcglog-parser v0.0.0-20230929123437-16b3d8d08691 github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85 - golang.org/x/crypto v0.9.0 - golang.org/x/sys v0.8.0 + golang.org/x/crypto v0.21.0 + golang.org/x/sys v0.18.0 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.3.0 @@ -22,7 +22,7 @@ require ( github.com/kr/pretty v0.2.2-0.20200810074440-814ac30b4b18 // indirect github.com/kr/text v0.1.0 // indirect github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 // indirect - golang.org/x/net v0.10.0 // indirect + golang.org/x/net v0.21.0 // indirect gopkg.in/retry.v1 v1.0.3 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect ) diff --git a/go.sum b/go.sum index 9caf132b..183950b9 100644 --- a/go.sum +++ b/go.sum @@ -66,10 +66,14 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c/go.mod h1:iQL9McJNjoIa5mjH6nYTCTZXUN6RP+XW3eib7Ya3XcI= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -78,6 +82,8 @@ golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= diff --git a/internal/pbkdf2/export_test.go b/internal/pbkdf2/export_test.go new file mode 100644 index 00000000..7b208082 --- /dev/null +++ b/internal/pbkdf2/export_test.go @@ -0,0 +1,30 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package pbkdf2 + +import "time" + +func MockTimeExecution(fn func(*Params) time.Duration) (restore func()) { + orig := timeExecution + timeExecution = fn + return func() { + timeExecution = orig + } +} diff --git a/internal/pbkdf2/pbkdf2.go b/internal/pbkdf2/pbkdf2.go new file mode 100644 index 00000000..147a06b8 --- /dev/null +++ b/internal/pbkdf2/pbkdf2.go @@ -0,0 +1,132 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package pbkdf2 + +import ( + "crypto" + "errors" + "math" + "time" + + "golang.org/x/crypto/pbkdf2" +) + +const ( + benchmarkPassword = "foo" +) + +var ( + benchmarkSalt = []byte("0123456789abcdefghijklmnopqrstuv") +) + +var timeExecution = func(params *Params) time.Duration { + start := time.Now() + if _, err := Key(benchmarkPassword, benchmarkSalt, params, uint(params.HashAlg.Size())); err != nil { + panic(err) + } + return time.Now().Sub(start) +} + +// Benchmark computes the number of iterations for desired duration +// with the specified digest algorithm. The specified algorithm must +// be available. This benchmark is largely based on that implemented +// by cryptsetup. +// +// When producing keys that are larger than the output size of the +// digest algorithm, PBKDF2 runs the specified number of iterations +// multiple times - eg, to produce a 64-byte key with SHA-256, PBKDF2 +// runs the specified number of iterations twice to produce the key in +// 2 rounds and this takes twice as long as it takes to produce a +// 32-byte key. This runs the benchmark for a single round by selecting a +// key length that is the same size as the output of the digest algorithm, +// which means that if SHA-256 is selected with a target duration of 1 second +// and the result is subsequently used to derive a 64-byte key, it will take 2 +// seconds. This is safer than the alternative which is that all rounds are +// benchmarked (eg, using SHA-256 to produce a 64-byte key) for 1 second, and +// then it's subsequently possible to run a single round in order to produce +// 32-bytes of output key material in 500ms. +func Benchmark(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { + if !hashAlg.Available() { + return 0, errors.New("unavailable digest algorithm") + } + + // Start with 1000 iterations. + iterationsOut := uint(1000) + iterations := iterationsOut + + for i := 1; ; i++ { + if iterations > math.MaxInt { + return 0, errors.New("too many iterations") + } + + // time the key derivation + duration := timeExecution(&Params{Iterations: iterations, HashAlg: hashAlg}) + if duration > 0 { + // calculate the required number of iterations to return, based on the tested + // iterations, measured duration and target duration. + iterationsOut = uint(time.Duration(iterations) * targetDuration / duration) + } + + // scale up the number of iterations to test next + switch { + case i > 10: + return 0, errors.New("insufficient progress") + case duration > 500*time.Millisecond: + return iterationsOut, nil + case duration <= 62*time.Millisecond: + iterations *= 16 + case duration <= 125*time.Millisecond: + iterations *= 8 + case duration <= 250*time.Millisecond: + iterations *= 4 + default: + iterations *= 2 + } + } +} + +// Params are the key derivation parameters for PBKDF2. +type Params struct { + // Iterations are the number of iterations. + Iterations uint + + // HashAlg is the digest algorithm to use. The algorithm + // must be available + HashAlg crypto.Hash +} + +// Key derives a key of the desired length from the supplied passphrase and salt, +// using the supplied parameters. +// +// This will return an error if the key length or number of iterations are less than +// zero, or the supplied digest algorithm is not available. +func Key(passphrase string, salt []byte, params *Params, keyLen uint) ([]byte, error) { + switch { + case params == nil: + return nil, errors.New("nil params") + case params.Iterations > math.MaxInt: + return nil, errors.New("too many iterations") + case !params.HashAlg.Available(): + return nil, errors.New("unavailable digest algorithm") + case keyLen > math.MaxInt: + return nil, errors.New("invalid key length") + } + return pbkdf2.Key([]byte(passphrase), salt, int(params.Iterations), int(keyLen), params.HashAlg.New), nil +} diff --git a/internal/pbkdf2/pbkdf2_test.go b/internal/pbkdf2/pbkdf2_test.go new file mode 100644 index 00000000..32195a32 --- /dev/null +++ b/internal/pbkdf2/pbkdf2_test.go @@ -0,0 +1,130 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package pbkdf2_test + +import ( + "crypto" + "crypto/rand" + _ "crypto/sha256" + _ "crypto/sha512" + "math" + "testing" + "time" + + "golang.org/x/crypto/pbkdf2" + . "gopkg.in/check.v1" + + . "github.com/snapcore/secboot/internal/pbkdf2" +) + +func Test(t *testing.T) { TestingT(t) } + +type pbkdf2Suite struct{} + +func (s *pbkdf2Suite) mockTimeExecution(c *C, expectedHash crypto.Hash) (restore func()) { + return MockTimeExecution(func(params *Params) time.Duration { + c.Check(params.HashAlg, Equals, expectedHash) + // hardcode 1us per iteration + return time.Duration(params.Iterations) * time.Microsecond + }) +} + +var _ = Suite(&pbkdf2Suite{}) + +func (s *pbkdf2Suite) TestBenchmark(c *C) { + restore := s.mockTimeExecution(c, crypto.SHA256) + defer restore() + + iterations, err := Benchmark(250*time.Millisecond, crypto.SHA256) + c.Check(err, IsNil) + c.Check(iterations, Equals, uint(250000)) +} + +func (s *pbkdf2Suite) TestBenchmarkDifferentHash(c *C) { + restore := s.mockTimeExecution(c, crypto.SHA512) + defer restore() + + iterations, err := Benchmark(250*time.Millisecond, crypto.SHA512) + c.Check(err, IsNil) + c.Check(iterations, Equals, uint(250000)) +} + +func (s *pbkdf2Suite) TestBenchmarkDifferentTarget(c *C) { + restore := s.mockTimeExecution(c, crypto.SHA256) + defer restore() + + iterations, err := Benchmark(2*time.Second, crypto.SHA256) + c.Check(err, IsNil) + c.Check(iterations, Equals, uint(2000000)) +} + +func (s *pbkdf2Suite) TestBenchmarkDifferentTarget2(c *C) { + restore := s.mockTimeExecution(c, crypto.SHA256) + defer restore() + + iterations, err := Benchmark(10*time.Millisecond, crypto.SHA256) + c.Check(err, IsNil) + c.Check(iterations, Equals, uint(10000)) +} + +func (s *pbkdf2Suite) TestBenchmarkInvalidHash(c *C) { + _, err := Benchmark(2*time.Second, 0) + c.Check(err, ErrorMatches, `unavailable digest algorithm`) +} + +func (s *pbkdf2Suite) TestKey(c *C) { + salt := make([]byte, 16) + rand.Read(salt) + + key, err := Key("foo", salt, &Params{Iterations: 1000, HashAlg: crypto.SHA256}, 32) + c.Check(err, IsNil) + expectedKey := pbkdf2.Key([]byte("foo"), salt, 1000, 32, crypto.SHA256.New) + c.Check(key, DeepEquals, expectedKey) +} + +func (s *pbkdf2Suite) TestKeyDifferentArgs(c *C) { + salt := make([]byte, 32) + rand.Read(salt) + + key, err := Key("bar", salt, &Params{Iterations: 200000, HashAlg: crypto.SHA512}, 64) + c.Check(err, IsNil) + expectedKey := pbkdf2.Key([]byte("bar"), salt, 200000, 64, crypto.SHA512.New) + c.Check(key, DeepEquals, expectedKey) +} + +func (s *pbkdf2Suite) TestKeyNilParams(c *C) { + _, err := Key("foo", nil, nil, 32) + c.Check(err, ErrorMatches, `nil params`) +} + +func (s *pbkdf2Suite) TestKeyInvalidIterations(c *C) { + _, err := Key("foo", nil, &Params{Iterations: math.MaxUint, HashAlg: crypto.SHA256}, 32) + c.Check(err, ErrorMatches, `too many iterations`) +} + +func (s *pbkdf2Suite) TestKeyInvalidHash(c *C) { + _, err := Key("foo", nil, &Params{Iterations: 1000}, 32) + c.Check(err, ErrorMatches, `unavailable digest algorithm`) +} + +func (s *pbkdf2Suite) TestKeyInvalidKeyLen(c *C) { + _, err := Key("foo", nil, &Params{Iterations: 1000, HashAlg: crypto.SHA256}, math.MaxUint) + c.Check(err, ErrorMatches, `invalid key length`) +} diff --git a/kdf.go b/kdf.go new file mode 100644 index 00000000..70fc9c6d --- /dev/null +++ b/kdf.go @@ -0,0 +1,26 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package secboot + +// KDFOptions is an interface for supplying options for different +// key derivation functions +type KDFOptions interface { + kdfParams(keyLen uint32) (*kdfParams, error) +} diff --git a/keydata.go b/keydata.go index 281f5332..33554f48 100644 --- a/keydata.go +++ b/keydata.go @@ -31,6 +31,7 @@ import ( "hash" "io" + "github.com/snapcore/secboot/internal/pbkdf2" "golang.org/x/crypto/cryptobyte" cryptobyte_asn1 "golang.org/x/crypto/cryptobyte/asn1" "golang.org/x/crypto/hkdf" @@ -159,7 +160,7 @@ type KeyParams struct { // implementation. type KeyWithPassphraseParams struct { KeyParams - KDFOptions *Argon2Options // The passphrase KDF options + KDFOptions KDFOptions // The passphrase KDF options // AuthKeySize is the size of key to derive from the passphrase for // use by the platform implementation. @@ -275,10 +276,11 @@ func (a hashAlg) marshalASN1(b *cryptobyte.Builder) { } type kdfParams struct { - Type string `json:"type"` - Time int `json:"time"` - Memory int `json:"memory"` - CPUs int `json:"cpus"` + Type string `json:"type"` + Time int `json:"time"` + Memory int `json:"memory"` + CPUs int `json:"cpus"` + Hash hashAlg `json:"hash"` } // kdfData corresponds to the arguments to a KDF and matches the @@ -383,6 +385,9 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte, if params.AuthKeySize < 0 { return nil, nil, nil, fmt.Errorf("invalid auth key size (%d bytes)", params.AuthKeySize) } + if params.KDF.Time < 0 { + return nil, nil, nil, fmt.Errorf("invalid KDF time (%d)", params.KDF.Time) + } kdfAlg := d.data.KDFAlg if !hashAlgAvailable(&kdfAlg) { @@ -409,6 +414,13 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte, switch params.KDF.Type { case string(Argon2i), string(Argon2id): + if params.KDF.Memory < 0 { + return nil, nil, nil, fmt.Errorf("invalid argon2 memory (%d)", params.KDF.Memory) + } + if params.KDF.CPUs < 0 { + return nil, nil, nil, fmt.Errorf("invalid argon2 threads (%d)", params.KDF.CPUs) + } + mode := Argon2Mode(params.KDF.Type) costParams := &Argon2CostParams{ Time: uint32(params.KDF.Time), @@ -421,6 +433,15 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte, if len(derived) != params.DerivedKeySize { return nil, nil, nil, errors.New("KDF returned unexpected key length") } + case pbkdf2Type: + pbkdfParams := &pbkdf2.Params{ + Iterations: uint(params.KDF.Time), + HashAlg: crypto.Hash(params.KDF.Hash), + } + derived, err = pbkdf2.Key(passphrase, salt, pbkdfParams, uint(params.DerivedKeySize)) + if err != nil { + return nil, nil, nil, xerrors.Errorf("cannot derive key from passphrase: %w", err) + } default: return nil, nil, nil, fmt.Errorf("unexpected intermediate KDF type \"%s\"", params.KDF.Type) } diff --git a/pbkdf2.go b/pbkdf2.go new file mode 100644 index 00000000..72aa5bb4 --- /dev/null +++ b/pbkdf2.go @@ -0,0 +1,107 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package secboot + +import ( + "crypto" + "errors" + "fmt" + "math" + "time" + + "github.com/snapcore/secboot/internal/pbkdf2" + "golang.org/x/xerrors" +) + +const ( + pbkdf2Type = "pbkdf2" +) + +var ( + pbkdf2Benchmark = pbkdf2.Benchmark +) + +type PBKDF2Options struct { + TargetDuration time.Duration + + ForceIterations uint32 + + HashAlg crypto.Hash +} + +func (o *PBKDF2Options) kdfParams(keyLen uint32) (*kdfParams, error) { + if keyLen > math.MaxInt32 { + return nil, errors.New("invalid key length") + } + + defaultHashAlg := crypto.SHA256 + switch { + case keyLen >= 48 && keyLen < 64: + defaultHashAlg = crypto.SHA384 + case keyLen >= 64: + defaultHashAlg = crypto.SHA512 + } + + switch { + case o.ForceIterations > 0: + // The non-benchmarked path. Ensure that ForceIterations + // fits into an int32 so that it always fits into an int + switch { + case o.ForceIterations > math.MaxInt32: + return nil, fmt.Errorf("invalid iterations count %d", o.ForceIterations) + } + + params := &kdfParams{ + Type: pbkdf2Type, + Time: int(o.ForceIterations), // no limit to the time cost. + Hash: hashAlg(defaultHashAlg), + } + if o.HashAlg != crypto.Hash(0) { + switch o.HashAlg { + case crypto.SHA1, crypto.SHA224, crypto.SHA256, crypto.SHA384, crypto.SHA512: + params.Hash = hashAlg(o.HashAlg) + default: + return nil, errors.New("invalid hash algorithm") + } + } + + return params, nil + default: + targetDuration := 2 * time.Second // the default target duration is 2s. + hashAlg := defaultHashAlg + + if o.TargetDuration != 0 { + targetDuration = o.TargetDuration + } + if o.HashAlg != crypto.Hash(0) { + hashAlg = o.HashAlg + } + + iterations, err := pbkdf2Benchmark(targetDuration, hashAlg) + if err != nil { + return nil, xerrors.Errorf("cannot benchmark KDF: %w", err) + } + + o = &PBKDF2Options{ + ForceIterations: uint32(iterations), + HashAlg: hashAlg} + return o.kdfParams(keyLen) + } +} diff --git a/pbkdf2_test.go b/pbkdf2_test.go new file mode 100644 index 00000000..7ec1479c --- /dev/null +++ b/pbkdf2_test.go @@ -0,0 +1,158 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package secboot_test + +import ( + "crypto" + "time" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/secboot" + "github.com/snapcore/secboot/internal/pbkdf2" +) + +type pbkdf2Suite struct{} + +var _ = Suite(&pbkdf2Suite{}) + +func (s *pbkdf2Suite) checkParams(c *C, opts *PBKDF2Options, keyLen uint32, params *KdfParams) { + c.Check(params.Type, Equals, "pbkdf2") + + expectedHash := crypto.SHA256 + switch { + case keyLen >= 48 && keyLen < 64: + expectedHash = crypto.SHA384 + case keyLen >= 64: + expectedHash = crypto.SHA512 + } + if opts.HashAlg != crypto.Hash(0) { + expectedHash = opts.HashAlg + } + c.Check(crypto.Hash(params.Hash), Equals, expectedHash) + + if opts.ForceIterations != 0 { + c.Check(params.Time, Equals, int(opts.ForceIterations)) + } else { + c.Check(params.Time, Not(Equals), 0) + } + + c.Check(params.Memory, Equals, 0) + c.Check(params.CPUs, Equals, 0) +} + +func (s *pbkdf2Suite) TestKDFParamsDefault(c *C) { + restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { + c.Check(targetDuration, Equals, 2*time.Second) + c.Check(hashAlg, Equals, crypto.SHA256) + return pbkdf2.Benchmark(targetDuration, hashAlg) + }) + defer restore() + + var opts PBKDF2Options + params, err := opts.KdfParams(32) + c.Assert(err, IsNil) + c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Hash, Equals, HashAlg(crypto.SHA256)) + c.Check(params.Memory, Equals, 0) + c.Check(params.CPUs, Equals, 0) +} + +func (s *pbkdf2Suite) TestKDFParamsDefault48(c *C) { + restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { + c.Check(targetDuration, Equals, 2*time.Second) + c.Check(hashAlg, Equals, crypto.SHA384) + return pbkdf2.Benchmark(targetDuration, hashAlg) + }) + defer restore() + + var opts PBKDF2Options + params, err := opts.KdfParams(48) + c.Assert(err, IsNil) + c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Hash, Equals, HashAlg(crypto.SHA384)) + c.Check(params.Memory, Equals, 0) + c.Check(params.CPUs, Equals, 0) +} + +func (s *pbkdf2Suite) TestKDFParamsDefault64(c *C) { + restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { + c.Check(targetDuration, Equals, 2*time.Second) + c.Check(hashAlg, Equals, crypto.SHA512) + return pbkdf2.Benchmark(targetDuration, hashAlg) + }) + defer restore() + + var opts PBKDF2Options + params, err := opts.KdfParams(64) + c.Assert(err, IsNil) + c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Hash, Equals, HashAlg(crypto.SHA512)) + c.Check(params.Memory, Equals, 0) + c.Check(params.CPUs, Equals, 0) +} + +func (s *pbkdf2Suite) TestKDFParamsTargetDuration(c *C) { + restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { + c.Check(targetDuration, Equals, 200*time.Millisecond) + c.Check(hashAlg, Equals, crypto.SHA256) + return pbkdf2.Benchmark(targetDuration, hashAlg) + }) + defer restore() + + var opts PBKDF2Options + opts.TargetDuration = 200 * time.Millisecond + params, err := opts.KdfParams(32) + c.Assert(err, IsNil) + c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Hash, Equals, HashAlg(crypto.SHA256)) + c.Check(params.Memory, Equals, 0) + c.Check(params.CPUs, Equals, 0) +} + +func (s *pbkdf2Suite) TestKDFParamsForceIterations(c *C) { + var opts PBKDF2Options + opts.ForceIterations = 2000 + params, err := opts.KdfParams(32) + c.Assert(err, IsNil) + c.Check(params, DeepEquals, &KdfParams{ + Type: "pbkdf2", + Time: 2000, + Hash: HashAlg(crypto.SHA256), + }) +} + +func (s *pbkdf2Suite) TestKDFParamsCustomHash(c *C) { + restore := MockPBKDF2Benchmark(func(targetDuration time.Duration, hashAlg crypto.Hash) (uint, error) { + c.Check(targetDuration, Equals, 2*time.Second) + c.Check(hashAlg, Equals, crypto.SHA512) + return pbkdf2.Benchmark(targetDuration, hashAlg) + }) + defer restore() + + var opts PBKDF2Options + opts.HashAlg = crypto.SHA512 + params, err := opts.KdfParams(32) + c.Assert(err, IsNil) + c.Check(params.Type, Equals, "pbkdf2") + c.Check(params.Hash, Equals, HashAlg(crypto.SHA512)) + c.Check(params.Memory, Equals, 0) + c.Check(params.CPUs, Equals, 0) +}