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) } - 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) + if !bytes.Equal(key, tt.out) { + t.Errorf("test %d: incorrect output: have %v, need %v.", i, key, 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,19 +329,52 @@ func TestHKDF(t *testing.T) { func TestHKDFLimit(t *testing.T) { hash := sha1.New master := []byte{0x00, 0x01, 0x02, 0x03} - info := []byte{} + info := "" + limit := hash().Size() * 255 // The maximum output bytes should be extractable - limit := hash().Size() * 255 - hkdf.Key(hash, master, nil, info, limit) + out, err := Key(hash, master, nil, info, limit) + if err != nil || len(out) != limit { + t.Errorf("key derivation failed: %v", err) + } - // Reading one more should panic - defer func() { - if err := recover(); err == nil { - t.Error("expected panic") + // 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) { + benchmarkHKDF(md5.New, 16, b) +} + +func Benchmark20ByteSHA1Single(b *testing.B) { + benchmarkHKDF(sha1.New, 20, b) +} + +func Benchmark32ByteSHA256Single(b *testing.B) { + benchmarkHKDF(sha256.New, 32, b) +} + +func Benchmark64ByteSHA512Single(b *testing.B) { + benchmarkHKDF(sha512.New, 64, 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 := string([]byte{0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27}) + + b.SetBytes(int64(block)) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + _, err := Key(hasher, master, salt, info, hasher().Size()) + if err != nil { + b.Errorf("failed to derive key: %v", err) } - }() - hkdf.Key(hash, master, nil, info, limit+1) + } } func TestFIPSServiceIndicator(t *testing.T) { @@ -342,51 +383,28 @@ func TestFIPSServiceIndicator(t *testing.T) { } fips140.ResetServiceIndicator() - hkdf.Key(sha256.New, []byte("YELLOW SUBMARINE"), nil, nil, 32) + _, 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() - hkdf.Key(sha256.New, []byte("key"), nil, nil, 32) + _, 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() - hkdf.Key(sha256.New, []byte("YELLOW SUBMARINE"), []byte("salt"), []byte("info"), 32) + _, err = Key(sha256.New, []byte("YELLOW SUBMARINE"), []byte("salt"), "info", 32) if !fips140.ServiceIndicator() { t.Error("FIPS service indicator should be set") } } - -func Benchmark16ByteMD5Single(b *testing.B) { - benchmarkHKDFSingle(md5.New, 16, b) -} - -func Benchmark20ByteSHA1Single(b *testing.B) { - benchmarkHKDFSingle(sha1.New, 20, b) -} - -func Benchmark32ByteSHA256Single(b *testing.B) { - benchmarkHKDFSingle(sha256.New, 32, b) -} - -func Benchmark64ByteSHA512Single(b *testing.B) { - benchmarkHKDFSingle(sha512.New, 64, b) -} - -func benchmarkHKDFSingle(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} - - b.SetBytes(int64(block)) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - hkdf.Key(hasher, master, salt, info, block) - } -} 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;