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 9, 2024
1 parent 6399f2c commit 66007e1
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 9 deletions.
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
97 changes: 97 additions & 0 deletions internal/pbkdf2/pbkdf2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// -*- 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"
"time"

"golang.org/x/crypto/pbkdf2"
)

const (
benchmarkPassword = "foo"
)

var (
benchmarkSalt = []byte("0123456789abcdefghijklmnopqrstuv")
)

type BenchmarkParams struct {
TargetDuration time.Duration

HashAlg crypto.Hash
}

func Benchmark(params *BenchmarkParams, keyLen int32) (*Params, error) {
if keyLen < 0 {
return nil, errors.New("invalid key length")
}

outParams := &Params{
Iterations: 1000,
HashAlg: params.HashAlg,
}
iterations := outParams.Iterations

for i := 1; ; i++ {
start := time.Now()
Key(benchmarkPassword, benchmarkSalt, &Params{Iterations: iterations, HashAlg: params.HashAlg}, keyLen)
duration := time.Now().Sub(start)

newIterationsOut := int32(int64(iterations) * int64(params.TargetDuration/duration))
if newIterationsOut < outParams.Iterations {
return outParams, nil
}
outParams.Iterations = newIterationsOut

switch {
case i > 10:
return nil, errors.New("insufficient progress")
case duration > 500*time.Millisecond:
return outParams, 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
}
}
}

type Params struct {
Iterations int32

HashAlg crypto.Hash
}

func Key(passphrase string, salt []byte, params *Params, keyLen int32) []byte {
if params.Iterations < 0 {
panic("invalid iterations")
}
if keyLen < 0 {
panic("invalid key length")
}
return pbkdf2.Key([]byte(passphrase), salt, int(params.Iterations), int(keyLen), params.HashAlg.New)
}
26 changes: 26 additions & 0 deletions kdf.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

package secboot

// KDFOptions is an interface for supplying options for different
// key derivation functions
type KDFOptions interface {
kdfParams(keyLen uint32) (*kdfParams, error)
}
33 changes: 27 additions & 6 deletions keydata.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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"
Expand Down Expand Up @@ -154,7 +155,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.
Expand Down Expand Up @@ -270,10 +271,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
Expand Down Expand Up @@ -378,6 +380,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) {
Expand All @@ -403,7 +408,14 @@ func (d *KeyData) derivePassphraseKeys(passphrase string) (key, iv, auth []byte,
var derived []byte

switch params.KDF.Type {
case "argon2i", "argon2id":
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),
Expand All @@ -416,6 +428,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: int32(params.KDF.Time),
HashAlg: crypto.Hash(params.KDF.Hash),
}
if !pbkdfParams.HashAlg.Available() {
return nil, nil, nil, fmt.Errorf("unavailable pbkdf2 digest algorithm %v", pbkdfParams.HashAlg)
}
derived = pbkdf2.Key(passphrase, salt, pbkdfParams, int32(params.DerivedKeySize))
default:
return nil, nil, nil, fmt.Errorf("unexpected intermediate KDF type \"%s\"", params.KDF.Type)
}
Expand Down
93 changes: 93 additions & 0 deletions pbkdf2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// -*- 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 secboot

import (
"crypto"
"errors"
"fmt"
"math"
"time"

"github.com/snapcore/secboot/internal/pbkdf2"
"golang.org/x/xerrors"
)

const (
pbkdf2Type = "pbkdf2"
)

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")
}

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 int64(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(crypto.SHA256),
}
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:
benchmarkParams := &pbkdf2.BenchmarkParams{
TargetDuration: 2 * time.Second, // the default target duration is 2s.
HashAlg: crypto.SHA256,
}
if o.HashAlg != crypto.Hash(0) {
benchmarkParams.HashAlg = o.HashAlg
}

params, err := pbkdf2.Benchmark(benchmarkParams, int32(keyLen))
if err != nil {
return nil, xerrors.Errorf("cannot benchmark KDF: %w", err)
}

o = &PBKDF2Options{
ForceIterations: uint32(params.Iterations),
HashAlg: params.HashAlg}
return o.kdfParams(keyLen)
}
}

0 comments on commit 66007e1

Please sign in to comment.