From: Daniel McCarney Date: Wed, 20 Nov 2024 21:11:06 +0000 (-0500) Subject: crypto/hkdf: init package X-Git-Tag: go1.24rc1~124 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=fab2b8b0fa569959243345dd4dcef651451d24fa;p=gostls13.git crypto/hkdf: init package This commit imports the x/crypto/hkdf package as a public crypto package based on the linked proposal. Since we've already implemented this internal to the FIPS boundary (mod some small changes based on the proposal discussion) this largely defers to that implementation. Updates #61477 Change-Id: Ie3dcee75314dfbe22eec8b31c43c926fe80637bb Reviewed-on: https://go-review.googlesource.com/c/go/+/630296 Reviewed-by: Filippo Valsorda LUCI-TryBot-Result: Go LUCI Reviewed-by: Dmitri Shuralyov Reviewed-by: Russ Cox Auto-Submit: Filippo Valsorda --- diff --git a/api/next/61477.txt b/api/next/61477.txt new file mode 100644 index 0000000000..aeb6acd3ef --- /dev/null +++ b/api/next/61477.txt @@ -0,0 +1,3 @@ +pkg crypto/hkdf, func Expand[$0 hash.Hash](func() $0, []uint8, string, int) ([]uint8, error) #61477 +pkg crypto/hkdf, func Extract[$0 hash.Hash](func() $0, []uint8, []uint8) ([]uint8, error) #61477 +pkg crypto/hkdf, func Key[$0 hash.Hash](func() $0, []uint8, []uint8, string, int) ([]uint8, error) #61477 diff --git a/doc/next/6-stdlib/3-hkdf.md b/doc/next/6-stdlib/3-hkdf.md new file mode 100644 index 0000000000..1914fa1aaf --- /dev/null +++ b/doc/next/6-stdlib/3-hkdf.md @@ -0,0 +1,2 @@ +A new `crypto/hkdf` package was added based on the pre-existing +`golang.org/x/crypto/hkdf` package. diff --git a/doc/next/6-stdlib/99-minor/crypto/hkdf/61477.md b/doc/next/6-stdlib/99-minor/crypto/hkdf/61477.md new file mode 100644 index 0000000000..5b1c75e63e --- /dev/null +++ b/doc/next/6-stdlib/99-minor/crypto/hkdf/61477.md @@ -0,0 +1 @@ + diff --git a/src/crypto/hkdf/example_test.go b/src/crypto/hkdf/example_test.go new file mode 100644 index 0000000000..789f7ae58c --- /dev/null +++ b/src/crypto/hkdf/example_test.go @@ -0,0 +1,53 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package hkdf_test + +import ( + "bytes" + "crypto/hkdf" + "crypto/rand" + "crypto/sha256" + "fmt" +) + +// Usage example that expands one master secret into three other +// cryptographically secure keys. +func Example_usage() { + // Underlying hash function for HMAC. + hash := sha256.New + keyLen := hash().Size() + + // Cryptographically secure master secret. + secret := []byte{0x00, 0x01, 0x02, 0x03} // i.e. NOT this. + + // Non-secret salt, optional (can be nil). + // Recommended: hash-length random value. + salt := make([]byte, hash().Size()) + if _, err := rand.Read(salt); err != nil { + panic(err) + } + + // Non-secret context info, optional (can be nil). + info := "hkdf example" + + // Generate three 128-bit derived keys. + var keys [][]byte + for i := 0; i < 3; i++ { + key, err := hkdf.Key(hash, secret, salt, info, keyLen) + if err != nil { + panic(err) + } + keys = append(keys, key) + } + + for i := range keys { + fmt.Printf("Key #%d: %v\n", i+1, !bytes.Equal(keys[i], make([]byte, 16))) + } + + // Output: + // Key #1: true + // Key #2: true + // Key #3: true +} diff --git a/src/crypto/hkdf/hkdf.go b/src/crypto/hkdf/hkdf.go new file mode 100644 index 0000000000..e33e0acef2 --- /dev/null +++ b/src/crypto/hkdf/hkdf.go @@ -0,0 +1,49 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package hkdf + +import ( + "crypto/internal/fips140/hkdf" + "errors" + "hash" +) + +// Extract generates a pseudorandom key for use with [Expand] from an input +// secret and an optional independent salt. +// +// Only use this function if you need to reuse the extracted key with multiple +// Expand invocations and different context values. Most common scenarios, +// including the generation of multiple keys, should use [Key] instead. +func Extract[H hash.Hash](h func() H, secret, salt []byte) ([]byte, error) { + return hkdf.Extract(h, secret, salt), nil +} + +// Expand derives a key from the given hash, key, and optional context info, +// returning a []byte of length keyLength that can be used as cryptographic key. +// The extraction step is skipped. +// +// The key should have been generated by [Extract], or be a uniformly +// random or pseudorandom cryptographically strong key. See RFC 5869, Section +// 3.3. Most common scenarios will want to use [Key] instead. +func Expand[H hash.Hash](h func() H, pseudorandomKey []byte, info string, keyLength int) ([]byte, error) { + limit := h().Size() * 255 + if keyLength > limit { + return nil, errors.New("hkdf: requested key length too large") + } + + return hkdf.Expand(h, pseudorandomKey, info, keyLength), nil +} + +// Key derives a key from the given hash, secret, salt and context info, +// returning a []byte of length keyLength that can be used as cryptographic key. +// Salt and info can be nil. +func Key[Hash hash.Hash](h func() Hash, secret, salt []byte, info string, keyLength int) ([]byte, error) { + limit := h().Size() * 255 + if keyLength > limit { + return nil, errors.New("hkdf: requested key length too large") + } + + return hkdf.Key(h, secret, salt, info, keyLength), nil +} diff --git a/src/crypto/internal/fips140test/hkdf_test.go b/src/crypto/hkdf/hkdf_test.go similarity index 86% rename from src/crypto/internal/fips140test/hkdf_test.go rename to src/crypto/hkdf/hkdf_test.go index 9ddfe88f4f..201b440289 100644 --- a/src/crypto/internal/fips140test/hkdf_test.go +++ b/src/crypto/hkdf/hkdf_test.go @@ -2,15 +2,12 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -package fipstest_test - -// TODO(fips, #61477): move this to crypto/hkdf once it exists. +package hkdf import ( "bytes" "crypto/internal/boring" "crypto/internal/fips140" - "crypto/internal/fips140/hkdf" "crypto/md5" "crypto/sha1" "crypto/sha256" @@ -301,19 +298,30 @@ var hkdfTests = []hkdfTest{ func TestHKDF(t *testing.T) { for i, tt := range hkdfTests { - prk := hkdf.Extract(tt.hash, tt.master, tt.salt) + prk, err := Extract(tt.hash, tt.master, tt.salt) + if err != nil { + t.Errorf("test %d: PRK extraction failed: %v", i, err) + } if !bytes.Equal(prk, tt.prk) { t.Errorf("test %d: incorrect PRK: have %v, need %v.", i, prk, tt.prk) } - out := hkdf.Key(tt.hash, tt.master, tt.salt, tt.info, len(tt.out)) - if !bytes.Equal(out, tt.out) { - t.Errorf("test %d: incorrect output: have %v, need %v.", i, out, tt.out) + key, err := Key(tt.hash, tt.master, tt.salt, string(tt.info), len(tt.out)) + if err != nil { + t.Errorf("test %d: key derivation failed: %v", i, err) + } + + if !bytes.Equal(key, tt.out) { + t.Errorf("test %d: incorrect output: have %v, need %v.", i, key, tt.out) } - out = hkdf.Expand(tt.hash, prk, tt.info, len(tt.out)) - if !bytes.Equal(out, tt.out) { - t.Errorf("test %d: incorrect output from Expand: have %v, need %v.", i, out, tt.out) + expanded, err := Expand(tt.hash, prk, string(tt.info), len(tt.out)) + if err != nil { + t.Errorf("test %d: key expansion failed: %v", i, err) + } + + if !bytes.Equal(expanded, tt.out) { + t.Errorf("test %d: incorrect output from Expand: have %v, need %v.", i, expanded, tt.out) } } } @@ -321,72 +329,82 @@ func TestHKDF(t *testing.T) { func TestHKDFLimit(t *testing.T) { hash := sha1.New master := []byte{0x00, 0x01, 0x02, 0x03} - info := []byte{} - - // The maximum output bytes should be extractable + info := "" limit := hash().Size() * 255 - hkdf.Key(hash, master, nil, info, limit) - - // Reading one more should panic - defer func() { - if err := recover(); err == nil { - t.Error("expected panic") - } - }() - hkdf.Key(hash, master, nil, info, limit+1) -} - -func TestFIPSServiceIndicator(t *testing.T) { - if boring.Enabled { - t.Skip("in BoringCrypto mode HMAC is not from the Go FIPS module") - } - - fips140.ResetServiceIndicator() - hkdf.Key(sha256.New, []byte("YELLOW SUBMARINE"), nil, nil, 32) - if !fips140.ServiceIndicator() { - t.Error("FIPS service indicator should be set") - } - // Key too short. - fips140.ResetServiceIndicator() - hkdf.Key(sha256.New, []byte("key"), nil, nil, 32) - if fips140.ServiceIndicator() { - t.Error("FIPS service indicator should not be set") + // The maximum output bytes should be extractable + out, err := Key(hash, master, nil, info, limit) + if err != nil || len(out) != limit { + t.Errorf("key derivation failed: %v", err) } - // Salt and info are short, which is ok, but translates to a short HMAC key. - fips140.ResetServiceIndicator() - hkdf.Key(sha256.New, []byte("YELLOW SUBMARINE"), []byte("salt"), []byte("info"), 32) - if !fips140.ServiceIndicator() { - t.Error("FIPS service indicator should be set") + // Reading one more should return an error + _, err = Key(hash, master, nil, info, limit+1) + if err == nil { + t.Error("expected key derivation to fail, but it succeeded") } } func Benchmark16ByteMD5Single(b *testing.B) { - benchmarkHKDFSingle(md5.New, 16, b) + benchmarkHKDF(md5.New, 16, b) } func Benchmark20ByteSHA1Single(b *testing.B) { - benchmarkHKDFSingle(sha1.New, 20, b) + benchmarkHKDF(sha1.New, 20, b) } func Benchmark32ByteSHA256Single(b *testing.B) { - benchmarkHKDFSingle(sha256.New, 32, b) + benchmarkHKDF(sha256.New, 32, b) } func Benchmark64ByteSHA512Single(b *testing.B) { - benchmarkHKDFSingle(sha512.New, 64, b) + benchmarkHKDF(sha512.New, 64, b) } -func benchmarkHKDFSingle(hasher func() hash.Hash, block int, b *testing.B) { +func benchmarkHKDF(hasher func() hash.Hash, block int, b *testing.B) { master := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07} salt := []byte{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17} - info := []byte{0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27} + info := string([]byte{0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27}) b.SetBytes(int64(block)) b.ResetTimer() for i := 0; i < b.N; i++ { - hkdf.Key(hasher, master, salt, info, block) + _, err := Key(hasher, master, salt, info, hasher().Size()) + if err != nil { + b.Errorf("failed to derive key: %v", err) + } + } +} + +func TestFIPSServiceIndicator(t *testing.T) { + if boring.Enabled { + t.Skip("in BoringCrypto mode HMAC is not from the Go FIPS module") + } + + fips140.ResetServiceIndicator() + _, err := Key(sha256.New, []byte("YELLOW SUBMARINE"), nil, "", 32) + if err != nil { + panic(err) + } + if !fips140.ServiceIndicator() { + t.Error("FIPS service indicator should be set") + } + + // Key too short. + fips140.ResetServiceIndicator() + _, err = Key(sha256.New, []byte("key"), nil, "", 32) + if err != nil { + panic(err) + } + if fips140.ServiceIndicator() { + t.Error("FIPS service indicator should not be set") + } + + // Salt and info are short, which is ok, but translates to a short HMAC key. + fips140.ResetServiceIndicator() + _, err = Key(sha256.New, []byte("YELLOW SUBMARINE"), []byte("salt"), "info", 32) + if !fips140.ServiceIndicator() { + t.Error("FIPS service indicator should be set") } } diff --git a/src/crypto/internal/fips140/hkdf/cast.go b/src/crypto/internal/fips140/hkdf/cast.go index 422ca9e309..8ddcadc016 100644 --- a/src/crypto/internal/fips140/hkdf/cast.go +++ b/src/crypto/internal/fips140/hkdf/cast.go @@ -24,7 +24,7 @@ func init() { 0xa6, 0xc1, 0xde, 0x42, 0x4f, 0x2c, 0x99, 0x60, 0x64, 0xdb, 0x66, 0x3e, 0xec, 0xa6, 0x37, 0xff, } - got := Key(sha256.New, input, input, input, len(want)) + got := Key(sha256.New, input, input, string(input), len(want)) if !bytes.Equal(got, want) { return errors.New("unexpected result") } diff --git a/src/crypto/internal/fips140/hkdf/hkdf.go b/src/crypto/internal/fips140/hkdf/hkdf.go index 982775129b..6ddae5c3f2 100644 --- a/src/crypto/internal/fips140/hkdf/hkdf.go +++ b/src/crypto/internal/fips140/hkdf/hkdf.go @@ -19,10 +19,11 @@ func Extract[H fips140.Hash](h func() H, secret, salt []byte) []byte { extractor := hmac.New(h, salt) hmac.MarkAsUsedInHKDF(extractor) extractor.Write(secret) + return extractor.Sum(nil) } -func Expand[H fips140.Hash](h func() H, pseudorandomKey, info []byte, keyLen int) []byte { +func Expand[H fips140.Hash](h func() H, pseudorandomKey []byte, info string, keyLen int) []byte { out := make([]byte, 0, keyLen) expander := hmac.New(h, pseudorandomKey) hmac.MarkAsUsedInHKDF(expander) @@ -38,7 +39,7 @@ func Expand[H fips140.Hash](h func() H, pseudorandomKey, info []byte, keyLen int expander.Reset() } expander.Write(buf) - expander.Write(info) + expander.Write([]byte(info)) expander.Write([]byte{counter}) buf = expander.Sum(buf[:0]) remain := keyLen - len(out) @@ -49,7 +50,7 @@ func Expand[H fips140.Hash](h func() H, pseudorandomKey, info []byte, keyLen int return out } -func Key[H fips140.Hash](h func() H, secret, salt, info []byte, keyLen int) []byte { +func Key[H fips140.Hash](h func() H, secret, salt []byte, info string, keyLen int) []byte { prk := Extract(h, secret, salt) return Expand(h, prk, info, keyLen) } diff --git a/src/crypto/internal/fips140/tls13/tls13.go b/src/crypto/internal/fips140/tls13/tls13.go index f2c8250f3b..009844a507 100644 --- a/src/crypto/internal/fips140/tls13/tls13.go +++ b/src/crypto/internal/fips140/tls13/tls13.go @@ -36,7 +36,7 @@ func ExpandLabel[H fips140.Hash](hash func() H, secret []byte, label string, con hkdfLabel = append(hkdfLabel, label...) hkdfLabel = append(hkdfLabel, byte(len(context))) hkdfLabel = append(hkdfLabel, context...) - return hkdf.Expand(hash, secret, hkdfLabel, length) + return hkdf.Expand(hash, secret, string(hkdfLabel), length) } func extract[H fips140.Hash](hash func() H, newSecret, currentSecret []byte) []byte { diff --git a/src/crypto/internal/hpke/hpke.go b/src/crypto/internal/hpke/hpke.go index 229a9a9162..d8a0cc1ecb 100644 --- a/src/crypto/internal/hpke/hpke.go +++ b/src/crypto/internal/hpke/hpke.go @@ -42,7 +42,7 @@ func (kdf *hkdfKDF) LabeledExpand(suiteID []byte, randomKey []byte, label string labeledInfo = append(labeledInfo, suiteID...) labeledInfo = append(labeledInfo, label...) labeledInfo = append(labeledInfo, info...) - return hkdf.Expand(kdf.hash.New, randomKey, labeledInfo, int(length)) + return hkdf.Expand(kdf.hash.New, randomKey, string(labeledInfo), int(length)) } // dhKEM implements the KEM specified in RFC 9180, Section 4.1. diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index a4003442ae..662bb59439 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -511,7 +511,7 @@ var depsRules = ` crypto/boring < crypto/aes, crypto/des, crypto/hmac, crypto/md5, crypto/rc4, - crypto/sha1, crypto/sha256, crypto/sha512; + crypto/sha1, crypto/sha256, crypto/sha512, crypto/hkdf; crypto/boring, crypto/internal/fips140/edwards25519/field < crypto/ecdh; @@ -530,7 +530,8 @@ var depsRules = ` crypto/sha1, crypto/sha256, crypto/sha512, - golang.org/x/crypto/sha3 + golang.org/x/crypto/sha3, + crypto/hkdf < CRYPTO; CGO, fmt, net !< CRYPTO;