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 <filippo@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Reviewed-by: Russ Cox <rsc@golang.org>
Auto-Submit: Filippo Valsorda <filippo@golang.org>
This commit is contained in:
Daniel McCarney 2024-11-20 16:11:06 -05:00 committed by Gopher Robot
parent 97ae1817fb
commit fab2b8b0fa
11 changed files with 188 additions and 60 deletions

3
api/next/61477.txt Normal file
View File

@ -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

View File

@ -0,0 +1,2 @@
A new `crypto/hkdf` package was added based on the pre-existing
`golang.org/x/crypto/hkdf` package. <!-- go.dev/issue/61477 -->

View File

@ -0,0 +1 @@
<!-- This is a new package; covered in 6-stdlib/3-hkdf.md. -->

View File

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

49
src/crypto/hkdf/hkdf.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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.

View File

@ -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;