diff --git a/argon2_test.go b/argon2_test.go
index 6c7cb813..603ed225 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/crypt_test.go b/crypt_test.go
index 0ce1a756..fbc16c72 100644
--- a/crypt_test.go
+++ b/crypt_test.go
@@ -409,9 +409,11 @@ func (s *cryptSuite) SetUpTest(c *C) {
activated: make(map[string]string)}
s.AddCleanup(s.luks2.enableMocks())
+}
- origKdf := SetArgon2KDF(&testutil.MockArgon2KDF{})
- s.AddCleanup(func() { SetArgon2KDF(origKdf) })
+func (s *cryptSuite) TearDownTest(c *C) {
+ s.keyDataTestBase.TearDownTest(c)
+ s.KeyringTestBase.TearDownTest(c)
}
func (s *cryptSuite) addMockToken(path string, token luks2.Token) int {
diff --git a/export_test.go b/export_test.go
index a284fddf..22383c5d 100644
--- a/export_test.go
+++ b/export_test.go
@@ -20,12 +20,18 @@
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
@@ -36,10 +42,18 @@ type (
ProtectedKeys = protectedKeys
)
+func KDFOptionsKdfParams(o KDFOptions, keyLen uint32) (*KdfParams, error) {
+ return o.kdfParams(keyLen)
+}
+
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 +126,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 49198257..60e4edcb 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,7 @@ require (
github.com/canonical/tcglog-parser v0.0.0-20240820013904-60cf7cbc7c5d
github.com/intel-go/cpuid v0.0.0-20220614022739-219e067757cb
github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85
- golang.org/x/crypto v0.9.0
+ golang.org/x/crypto v0.21.0
golang.org/x/sys v0.19.0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
@@ -24,6 +24,6 @@ require (
github.com/kr/text v0.1.0 // indirect
github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
- golang.org/x/net v0.10.0 // indirect
+ golang.org/x/net v0.21.0 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
)
diff --git a/go.sum b/go.sum
index 468a6c4c..477557e5 100644
--- a/go.sum
+++ b/go.sum
@@ -45,14 +45,14 @@ github.com/snapcore/snapd v0.0.0-20220714152900-4a1f4c93fc85/go.mod h1:Ab4TsNgVa
go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
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/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
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=
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..727a8eb5
--- /dev/null
+++ b/internal/pbkdf2/pbkdf2.go
@@ -0,0 +1,145 @@
+// -*- 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++ {
+ // 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.
+ newIterationsOut := (int64(iterations) * int64(targetDuration)) / int64(duration)
+ if newIterationsOut > math.MaxInt {
+ return 0, errors.New("iteration count result overflow")
+ }
+ iterationsOut = uint(newIterationsOut)
+ }
+
+ // scale up the number of iterations to test next
+ var scale uint
+ switch {
+ case i > 10:
+ return 0, errors.New("insufficient progress")
+ case duration > 500*time.Millisecond:
+ return iterationsOut, nil
+ case duration <= 62*time.Millisecond:
+ scale = 16
+ case duration <= 125*time.Millisecond:
+ scale = 8
+ case duration <= 250*time.Millisecond:
+ scale = 4
+ default:
+ scale = 2
+ }
+ if math.MaxInt/scale < iterations {
+ // It's only possible to hit this on 32-bit platforms. On
+ // 64-bit platforms, we'll always hit the "insufficient progress"
+ // branch first.
+ return 0, errors.New("test iteration count overflow")
+ }
+ iterations *= scale
+ if int64(iterations) > math.MaxInt64/int64(targetDuration) {
+ return 0, errors.New("iteration count result will overflow")
+ }
+ }
+}
+
+// Params are the key derivation parameters for PBKDF2.
+type Params struct {
+ // Iterations are the number of iterations. This can't be
+ // greater than math.MaxInt.
+ 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 greater
+// than the maximum value of a signed integer, 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..7c7a4165
--- /dev/null
+++ b/internal/pbkdf2/pbkdf2_test.go
@@ -0,0 +1,140 @@
+// -*- 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) TestBenchmarkOverflow(c *C) {
+ restore := MockTimeExecution(func(params *Params) time.Duration {
+ return 50 * time.Millisecond
+ })
+ defer restore()
+
+ _, err := Benchmark(450*time.Second, crypto.SHA256)
+ c.Check(err, ErrorMatches, `iteration count result will overflow`)
+}
+
+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 33c196ff..14dabfc3 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.
@@ -279,10 +280,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
@@ -387,6 +389,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) {
@@ -413,6 +418,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),
@@ -425,6 +437,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/keydata_luks_test.go b/keydata_luks_test.go
index 54ce2411..b10c71db 100644
--- a/keydata_luks_test.go
+++ b/keydata_luks_test.go
@@ -52,6 +52,11 @@ func (s *keyDataLuksSuite) SetUpTest(c *C) {
s.AddCleanup(s.luks2.enableMocks())
}
+func (s *keyDataLuksSuite) TearDownTest(c *C) {
+ s.keyDataTestBase.TearDownTest(c)
+ s.BaseTest.TearDownTest(c)
+}
+
var _ = Suite(&keyDataLuksSuite{})
func (s *keyDataLuksSuite) checkKeyDataJSONFromLUKSToken(c *C, path string, id int, keyslot int, name string, priority int, creationParams *KeyParams, nmodels int) {
diff --git a/keydata_test.go b/keydata_test.go
index 5ac68ab6..05bb39b6 100644
--- a/keydata_test.go
+++ b/keydata_test.go
@@ -39,6 +39,7 @@ import (
"time"
. "github.com/snapcore/secboot"
+ "github.com/snapcore/secboot/internal/pbkdf2"
"github.com/snapcore/secboot/internal/testutil"
snapd_testutil "github.com/snapcore/snapd/testutil"
@@ -231,6 +232,8 @@ func toHash(c *C, v interface{}) crypto.Hash {
str, ok := v.(string)
c.Assert(ok, testutil.IsTrue)
switch str {
+ case "null":
+ return crypto.Hash(0)
case "sha1":
return crypto.SHA1
case "sha224":
@@ -248,8 +251,11 @@ func toHash(c *C, v interface{}) crypto.Hash {
}
type keyDataTestBase struct {
- handler *mockPlatformKeyDataHandler
- mockPlatformName string
+ handler *mockPlatformKeyDataHandler
+ mockPlatformName string
+ origArgon2KDF Argon2KDF
+ restorePBKDF2Benchmark func()
+ expectedPBKDF2Hash crypto.Hash
}
func (s *keyDataTestBase) SetUpSuite(c *C) {
@@ -261,6 +267,19 @@ func (s *keyDataTestBase) SetUpSuite(c *C) {
func (s *keyDataTestBase) SetUpTest(c *C) {
s.handler.state = mockPlatformDeviceStateOK
s.handler.passphraseSupport = false
+ s.origArgon2KDF = SetArgon2KDF(&testutil.MockArgon2KDF{})
+ s.restorePBKDF2Benchmark = MockPBKDF2Benchmark(func(duration time.Duration, hashAlg crypto.Hash) (uint, error) {
+ c.Check(hashAlg, Equals, s.expectedPBKDF2Hash)
+ return uint(duration / time.Microsecond), nil
+ })
+}
+
+func (s *keyDataTestBase) TearDownTest(c *C) {
+ if s.restorePBKDF2Benchmark != nil {
+ s.restorePBKDF2Benchmark()
+ s.restorePBKDF2Benchmark = nil
+ }
+ SetArgon2KDF(s.origArgon2KDF)
}
func (s *keyDataTestBase) TearDownSuite(c *C) {
@@ -313,7 +332,7 @@ func (s *keyDataTestBase) mockProtectKeys(c *C, primaryKey PrimaryKey, kdfAlg cr
return out, unlockKey
}
-func (s *keyDataTestBase) mockProtectKeysWithPassphrase(c *C, primaryKey PrimaryKey, kdfOptions *Argon2Options, authKeySize int, KDFAlg crypto.Hash, modelAuthHash crypto.Hash) (out *KeyWithPassphraseParams, unlockKey DiskUnlockKey) {
+func (s *keyDataTestBase) mockProtectKeysWithPassphrase(c *C, primaryKey PrimaryKey, kdfOptions KDFOptions, authKeySize int, KDFAlg crypto.Hash, modelAuthHash crypto.Hash) (out *KeyWithPassphraseParams, unlockKey DiskUnlockKey) {
kp, unlockKey := s.mockProtectKeys(c, primaryKey, KDFAlg, modelAuthHash)
expectedHandle, ok := kp.Handle.(*mockPlatformKeyDataHandle)
@@ -328,6 +347,14 @@ func (s *keyDataTestBase) mockProtectKeysWithPassphrase(c *C, primaryKey Primary
kdfOptions = &defaultOptions
}
+ switch opt := kdfOptions.(type) {
+ case *PBKDF2Options:
+ s.expectedPBKDF2Hash = opt.HashAlg
+ if opt.HashAlg == crypto.Hash(0) {
+ s.expectedPBKDF2Hash = crypto.SHA256
+ }
+ }
+
kpp := &KeyWithPassphraseParams{
KeyParams: *kp,
KDFOptions: kdfOptions,
@@ -428,13 +455,13 @@ func (s *keyDataTestBase) checkKeyDataJSONFromReaderAuthModeNone(c *C, r io.Read
s.checkKeyDataJSONDecodedAuthModeNone(c, j, creationParams, nmodels)
}
-func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[string]interface{}, creationParams *KeyWithPassphraseParams, nmodels int, passphrase string, kdfOpts *Argon2Options) {
+func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[string]interface{}, creationParams *KeyWithPassphraseParams, nmodels int, passphrase string, kdfOpts KDFOptions) {
if kdfOpts == nil {
var def Argon2Options
kdfOpts = &def
}
- kdfParams, err := kdfOpts.KdfParams(0)
+ kdfParams, err := KDFOptionsKdfParams(kdfOpts, 0)
c.Assert(err, IsNil)
s.checkKeyDataJSONCommon(c, j, &creationParams.KeyParams, nmodels)
@@ -461,15 +488,15 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[
k, ok := p["kdf"].(map[string]interface{})
c.Check(ok, testutil.IsTrue)
- str, ok := k["type"].(string)
- c.Check(ok, testutil.IsTrue)
- c.Check(str, Equals, "argon2id")
-
- str, ok = k["salt"].(string)
+ str, ok := k["salt"].(string)
c.Check(ok, testutil.IsTrue)
salt, err := base64.StdEncoding.DecodeString(str)
c.Check(err, IsNil)
+ str, ok = k["type"].(string)
+ c.Check(ok, testutil.IsTrue)
+ c.Check(str, Equals, string(kdfParams.Type))
+
time, ok := k["time"].(float64)
c.Check(ok, testutil.IsTrue)
c.Check(time, Equals, float64(kdfParams.Time))
@@ -482,6 +509,10 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[
c.Check(ok, testutil.IsTrue)
c.Check(cpus, Equals, float64(kdfParams.CPUs))
+ h := toHash(c, k["hash"])
+ c.Check(ok, testutil.IsTrue)
+ c.Check(h, Equals, crypto.Hash(kdfParams.Hash))
+
str, ok = j["encrypted_payload"].(string)
c.Check(ok, testutil.IsTrue)
encryptedPayload, err := base64.StdEncoding.DecodeString(str)
@@ -509,13 +540,23 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[
asnsalt, err := builder.Bytes()
c.Assert(err, IsNil)
- var kdf testutil.MockArgon2KDF
- costParams := &Argon2CostParams{
- Time: uint32(kdfParams.Time),
- MemoryKiB: uint32(kdfParams.Memory),
- Threads: uint8(kdfParams.CPUs),
+ var derived []byte
+ switch o := kdfOpts.(type) {
+ case *Argon2Options:
+ _ = o
+ var kdf testutil.MockArgon2KDF
+ costParams := &Argon2CostParams{
+ Time: uint32(kdfParams.Time),
+ MemoryKiB: uint32(kdfParams.Memory),
+ Threads: uint8(kdfParams.CPUs),
+ }
+ derived, _ = kdf.Derive(passphrase, asnsalt, Argon2Mode(kdfParams.Type), costParams, uint32(derivedKeySize))
+ case *PBKDF2Options:
+ _ = o
+ var err error
+ derived, err = pbkdf2.Key(passphrase, asnsalt, &pbkdf2.Params{Iterations: uint(kdfParams.Time), HashAlg: crypto.Hash(kdfParams.Hash)}, uint(derivedKeySize))
+ c.Assert(err, IsNil)
}
- derived, _ := kdf.Derive(passphrase, asnsalt, Argon2Mode(kdfParams.Type), costParams, uint32(derivedKeySize))
key := make([]byte, int(encryptionKeySize))
@@ -536,7 +577,7 @@ func (s *keyDataTestBase) checkKeyDataJSONDecodedAuthModePassphrase(c *C, j map[
c.Check(payload, DeepEquals, creationParams.EncryptedPayload)
}
-func (s *keyDataTestBase) checkKeyDataJSONFromReaderAuthModePassphrase(c *C, r io.Reader, creationParams *KeyWithPassphraseParams, nmodels int, passphrase string, kdfOpts *Argon2Options) {
+func (s *keyDataTestBase) checkKeyDataJSONFromReaderAuthModePassphrase(c *C, r io.Reader, creationParams *KeyWithPassphraseParams, nmodels int, passphrase string, kdfOpts KDFOptions) {
var j map[string]interface{}
d := json.NewDecoder(r)
@@ -553,9 +594,11 @@ type keyDataSuite struct {
func (s *keyDataSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.keyDataTestBase.SetUpTest(c)
+}
- origKdf := SetArgon2KDF(&testutil.MockArgon2KDF{})
- s.AddCleanup(func() { SetArgon2KDF(origKdf) })
+func (s *keyDataSuite) TearDownTest(c *C) {
+ s.BaseTest.TearDownTest(c)
+ s.keyDataTestBase.TearDownTest(c)
}
var _ = Suite(&keyDataSuite{})
@@ -567,7 +610,7 @@ func (s *keyDataSuite) checkKeyDataJSONAuthModeNone(c *C, keyData *KeyData, crea
s.checkKeyDataJSONFromReaderAuthModeNone(c, w.Reader(), creationParams, nmodels)
}
-func (s *keyDataSuite) checkKeyDataJSONAuthModePassphrase(c *C, keyData *KeyData, creationParams *KeyWithPassphraseParams, nmodels int, passphrase string, kdfOpts *Argon2Options) {
+func (s *keyDataSuite) checkKeyDataJSONAuthModePassphrase(c *C, keyData *KeyData, creationParams *KeyWithPassphraseParams, nmodels int, passphrase string, kdfOpts KDFOptions) {
w := makeMockKeyDataWriter()
c.Check(keyData.WriteAtomic(w), IsNil)
@@ -803,6 +846,20 @@ func (s *keyDataSuite) TestRecoverKeysWithPassphrase2(c *C) {
s.testRecoverKeysWithPassphrase(c, "1234")
}
+func (s *keyDataSuite) TestRecoverKeysWithPassphrasePBKDF2(c *C) {
+ s.handler.passphraseSupport = true
+
+ primaryKey := s.newPrimaryKey(c, 32)
+ protected, unlockKey := s.mockProtectKeysWithPassphrase(c, primaryKey, &PBKDF2Options{}, 32, crypto.SHA256, crypto.SHA256)
+ keyData, err := NewKeyDataWithPassphrase(protected, "passphrase")
+ c.Assert(err, IsNil)
+
+ recoveredUnlockKey, recoveredPrimaryKey, err := keyData.RecoverKeysWithPassphrase("passphrase")
+ c.Check(err, IsNil)
+ c.Check(recoveredUnlockKey, DeepEquals, unlockKey)
+ c.Check(recoveredPrimaryKey, DeepEquals, primaryKey)
+}
+
type testRecoverKeysWithPassphraseErrorHandlingData struct {
kdfType string
errMsg string
@@ -1023,7 +1080,7 @@ func (s *keyDataSuite) TestChangePassphraseWithoutInitial(c *C) {
type testChangePassphraseData struct {
passphrase1 string
passphrase2 string
- kdfOptions *Argon2Options
+ kdfOptions KDFOptions
}
func (s *keyDataSuite) testChangePassphrase(c *C, data *testChangePassphraseData) {
@@ -1074,6 +1131,13 @@ func (s *keyDataSuite) TestChangePassphraseForceIterations(c *C) {
kdfOptions: &Argon2Options{ForceIterations: 3, MemoryKiB: 32 * 1024}})
}
+func (s *keyDataSuite) TestChangePassphrasePBKDF2(c *C) {
+ s.testChangePassphrase(c, &testChangePassphraseData{
+ passphrase1: "12345678",
+ passphrase2: "87654321",
+ kdfOptions: &PBKDF2Options{}})
+}
+
func (s *keyDataSuite) TestChangePassphraseWrongPassphrase(c *C) {
s.handler.passphraseSupport = true
diff --git a/pbkdf2.go b/pbkdf2.go
new file mode 100644
index 00000000..8b911d9e
--- /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..0ae0c007
--- /dev/null
+++ b/pbkdf2_test.go
@@ -0,0 +1,137 @@
+// -*- 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) 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.Logf("benchmarking (%d)", targetDuration)
+ if targetDuration != 200*time.Millisecond {
+ panic("")
+ }
+ 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)
+}