Skip to content

Commit

Permalink
[WIP] add support for PBKDF2 for passphrases
Browse files Browse the repository at this point in the history
Passphrase support currently hardcodes the use of Argon2. This adds
support for specifying PBKDF2, for use in environments where FIPS140
compliance is required.
  • Loading branch information
chrisccoulson committed Mar 16, 2024
1 parent 836ff3d commit 9a2d46c
Show file tree
Hide file tree
Showing 11 changed files with 653 additions and 8 deletions.
16 changes: 16 additions & 0 deletions argon2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"math"
"os"
"runtime"
"time"

snapd_testutil "github.com/snapcore/snapd/testutil"

Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
30 changes: 30 additions & 0 deletions internal/pbkdf2/export_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

package pbkdf2

import "time"

func MockTimeExecution(fn func(*Params) time.Duration) (restore func()) {
orig := timeExecution
timeExecution = fn
return func() {
timeExecution = orig
}
}
132 changes: 132 additions & 0 deletions internal/pbkdf2/pbkdf2.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

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
}
Loading

0 comments on commit 9a2d46c

Please sign in to comment.