Skip to content

Commit fab2b8b

Browse files
cpugopherbot
authored andcommitted
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 <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]> Reviewed-by: Russ Cox <[email protected]> Auto-Submit: Filippo Valsorda <[email protected]>
1 parent 97ae181 commit fab2b8b

File tree

11 files changed

+188
-60
lines changed

11 files changed

+188
-60
lines changed

api/next/61477.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pkg crypto/hkdf, func Expand[$0 hash.Hash](func() $0, []uint8, string, int) ([]uint8, error) #61477
2+
pkg crypto/hkdf, func Extract[$0 hash.Hash](func() $0, []uint8, []uint8) ([]uint8, error) #61477
3+
pkg crypto/hkdf, func Key[$0 hash.Hash](func() $0, []uint8, []uint8, string, int) ([]uint8, error) #61477

doc/next/6-stdlib/3-hkdf.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
A new `crypto/hkdf` package was added based on the pre-existing
2+
`golang.org/x/crypto/hkdf` package. <!-- go.dev/issue/61477 -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<!-- This is a new package; covered in 6-stdlib/3-hkdf.md. -->

src/crypto/hkdf/example_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Copyright 2014 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package hkdf_test
6+
7+
import (
8+
"bytes"
9+
"crypto/hkdf"
10+
"crypto/rand"
11+
"crypto/sha256"
12+
"fmt"
13+
)
14+
15+
// Usage example that expands one master secret into three other
16+
// cryptographically secure keys.
17+
func Example_usage() {
18+
// Underlying hash function for HMAC.
19+
hash := sha256.New
20+
keyLen := hash().Size()
21+
22+
// Cryptographically secure master secret.
23+
secret := []byte{0x00, 0x01, 0x02, 0x03} // i.e. NOT this.
24+
25+
// Non-secret salt, optional (can be nil).
26+
// Recommended: hash-length random value.
27+
salt := make([]byte, hash().Size())
28+
if _, err := rand.Read(salt); err != nil {
29+
panic(err)
30+
}
31+
32+
// Non-secret context info, optional (can be nil).
33+
info := "hkdf example"
34+
35+
// Generate three 128-bit derived keys.
36+
var keys [][]byte
37+
for i := 0; i < 3; i++ {
38+
key, err := hkdf.Key(hash, secret, salt, info, keyLen)
39+
if err != nil {
40+
panic(err)
41+
}
42+
keys = append(keys, key)
43+
}
44+
45+
for i := range keys {
46+
fmt.Printf("Key #%d: %v\n", i+1, !bytes.Equal(keys[i], make([]byte, 16)))
47+
}
48+
49+
// Output:
50+
// Key #1: true
51+
// Key #2: true
52+
// Key #3: true
53+
}

src/crypto/hkdf/hkdf.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2024 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package hkdf
6+
7+
import (
8+
"crypto/internal/fips140/hkdf"
9+
"errors"
10+
"hash"
11+
)
12+
13+
// Extract generates a pseudorandom key for use with [Expand] from an input
14+
// secret and an optional independent salt.
15+
//
16+
// Only use this function if you need to reuse the extracted key with multiple
17+
// Expand invocations and different context values. Most common scenarios,
18+
// including the generation of multiple keys, should use [Key] instead.
19+
func Extract[H hash.Hash](h func() H, secret, salt []byte) ([]byte, error) {
20+
return hkdf.Extract(h, secret, salt), nil
21+
}
22+
23+
// Expand derives a key from the given hash, key, and optional context info,
24+
// returning a []byte of length keyLength that can be used as cryptographic key.
25+
// The extraction step is skipped.
26+
//
27+
// The key should have been generated by [Extract], or be a uniformly
28+
// random or pseudorandom cryptographically strong key. See RFC 5869, Section
29+
// 3.3. Most common scenarios will want to use [Key] instead.
30+
func Expand[H hash.Hash](h func() H, pseudorandomKey []byte, info string, keyLength int) ([]byte, error) {
31+
limit := h().Size() * 255
32+
if keyLength > limit {
33+
return nil, errors.New("hkdf: requested key length too large")
34+
}
35+
36+
return hkdf.Expand(h, pseudorandomKey, info, keyLength), nil
37+
}
38+
39+
// Key derives a key from the given hash, secret, salt and context info,
40+
// returning a []byte of length keyLength that can be used as cryptographic key.
41+
// Salt and info can be nil.
42+
func Key[Hash hash.Hash](h func() Hash, secret, salt []byte, info string, keyLength int) ([]byte, error) {
43+
limit := h().Size() * 255
44+
if keyLength > limit {
45+
return nil, errors.New("hkdf: requested key length too large")
46+
}
47+
48+
return hkdf.Key(h, secret, salt, info, keyLength), nil
49+
}

src/crypto/internal/fips140test/hkdf_test.go renamed to src/crypto/hkdf/hkdf_test.go

+70-52
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
package fipstest_test
6-
7-
// TODO(fips, #61477): move this to crypto/hkdf once it exists.
5+
package hkdf
86

97
import (
108
"bytes"
119
"crypto/internal/boring"
1210
"crypto/internal/fips140"
13-
"crypto/internal/fips140/hkdf"
1411
"crypto/md5"
1512
"crypto/sha1"
1613
"crypto/sha256"
@@ -301,92 +298,113 @@ var hkdfTests = []hkdfTest{
301298

302299
func TestHKDF(t *testing.T) {
303300
for i, tt := range hkdfTests {
304-
prk := hkdf.Extract(tt.hash, tt.master, tt.salt)
301+
prk, err := Extract(tt.hash, tt.master, tt.salt)
302+
if err != nil {
303+
t.Errorf("test %d: PRK extraction failed: %v", i, err)
304+
}
305305
if !bytes.Equal(prk, tt.prk) {
306306
t.Errorf("test %d: incorrect PRK: have %v, need %v.", i, prk, tt.prk)
307307
}
308308

309-
out := hkdf.Key(tt.hash, tt.master, tt.salt, tt.info, len(tt.out))
310-
if !bytes.Equal(out, tt.out) {
311-
t.Errorf("test %d: incorrect output: have %v, need %v.", i, out, tt.out)
309+
key, err := Key(tt.hash, tt.master, tt.salt, string(tt.info), len(tt.out))
310+
if err != nil {
311+
t.Errorf("test %d: key derivation failed: %v", i, err)
312+
}
313+
314+
if !bytes.Equal(key, tt.out) {
315+
t.Errorf("test %d: incorrect output: have %v, need %v.", i, key, tt.out)
312316
}
313317

314-
out = hkdf.Expand(tt.hash, prk, tt.info, len(tt.out))
315-
if !bytes.Equal(out, tt.out) {
316-
t.Errorf("test %d: incorrect output from Expand: have %v, need %v.", i, out, tt.out)
318+
expanded, err := Expand(tt.hash, prk, string(tt.info), len(tt.out))
319+
if err != nil {
320+
t.Errorf("test %d: key expansion failed: %v", i, err)
321+
}
322+
323+
if !bytes.Equal(expanded, tt.out) {
324+
t.Errorf("test %d: incorrect output from Expand: have %v, need %v.", i, expanded, tt.out)
317325
}
318326
}
319327
}
320328

321329
func TestHKDFLimit(t *testing.T) {
322330
hash := sha1.New
323331
master := []byte{0x00, 0x01, 0x02, 0x03}
324-
info := []byte{}
325-
326-
// The maximum output bytes should be extractable
332+
info := ""
327333
limit := hash().Size() * 255
328-
hkdf.Key(hash, master, nil, info, limit)
329-
330-
// Reading one more should panic
331-
defer func() {
332-
if err := recover(); err == nil {
333-
t.Error("expected panic")
334-
}
335-
}()
336-
hkdf.Key(hash, master, nil, info, limit+1)
337-
}
338-
339-
func TestFIPSServiceIndicator(t *testing.T) {
340-
if boring.Enabled {
341-
t.Skip("in BoringCrypto mode HMAC is not from the Go FIPS module")
342-
}
343-
344-
fips140.ResetServiceIndicator()
345-
hkdf.Key(sha256.New, []byte("YELLOW SUBMARINE"), nil, nil, 32)
346-
if !fips140.ServiceIndicator() {
347-
t.Error("FIPS service indicator should be set")
348-
}
349334

350-
// Key too short.
351-
fips140.ResetServiceIndicator()
352-
hkdf.Key(sha256.New, []byte("key"), nil, nil, 32)
353-
if fips140.ServiceIndicator() {
354-
t.Error("FIPS service indicator should not be set")
335+
// The maximum output bytes should be extractable
336+
out, err := Key(hash, master, nil, info, limit)
337+
if err != nil || len(out) != limit {
338+
t.Errorf("key derivation failed: %v", err)
355339
}
356340

357-
// Salt and info are short, which is ok, but translates to a short HMAC key.
358-
fips140.ResetServiceIndicator()
359-
hkdf.Key(sha256.New, []byte("YELLOW SUBMARINE"), []byte("salt"), []byte("info"), 32)
360-
if !fips140.ServiceIndicator() {
361-
t.Error("FIPS service indicator should be set")
341+
// Reading one more should return an error
342+
_, err = Key(hash, master, nil, info, limit+1)
343+
if err == nil {
344+
t.Error("expected key derivation to fail, but it succeeded")
362345
}
363346
}
364347

365348
func Benchmark16ByteMD5Single(b *testing.B) {
366-
benchmarkHKDFSingle(md5.New, 16, b)
349+
benchmarkHKDF(md5.New, 16, b)
367350
}
368351

369352
func Benchmark20ByteSHA1Single(b *testing.B) {
370-
benchmarkHKDFSingle(sha1.New, 20, b)
353+
benchmarkHKDF(sha1.New, 20, b)
371354
}
372355

373356
func Benchmark32ByteSHA256Single(b *testing.B) {
374-
benchmarkHKDFSingle(sha256.New, 32, b)
357+
benchmarkHKDF(sha256.New, 32, b)
375358
}
376359

377360
func Benchmark64ByteSHA512Single(b *testing.B) {
378-
benchmarkHKDFSingle(sha512.New, 64, b)
361+
benchmarkHKDF(sha512.New, 64, b)
379362
}
380363

381-
func benchmarkHKDFSingle(hasher func() hash.Hash, block int, b *testing.B) {
364+
func benchmarkHKDF(hasher func() hash.Hash, block int, b *testing.B) {
382365
master := []byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}
383366
salt := []byte{0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17}
384-
info := []byte{0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27}
367+
info := string([]byte{0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27})
385368

386369
b.SetBytes(int64(block))
387370
b.ResetTimer()
388371

389372
for i := 0; i < b.N; i++ {
390-
hkdf.Key(hasher, master, salt, info, block)
373+
_, err := Key(hasher, master, salt, info, hasher().Size())
374+
if err != nil {
375+
b.Errorf("failed to derive key: %v", err)
376+
}
377+
}
378+
}
379+
380+
func TestFIPSServiceIndicator(t *testing.T) {
381+
if boring.Enabled {
382+
t.Skip("in BoringCrypto mode HMAC is not from the Go FIPS module")
383+
}
384+
385+
fips140.ResetServiceIndicator()
386+
_, err := Key(sha256.New, []byte("YELLOW SUBMARINE"), nil, "", 32)
387+
if err != nil {
388+
panic(err)
389+
}
390+
if !fips140.ServiceIndicator() {
391+
t.Error("FIPS service indicator should be set")
392+
}
393+
394+
// Key too short.
395+
fips140.ResetServiceIndicator()
396+
_, err = Key(sha256.New, []byte("key"), nil, "", 32)
397+
if err != nil {
398+
panic(err)
399+
}
400+
if fips140.ServiceIndicator() {
401+
t.Error("FIPS service indicator should not be set")
402+
}
403+
404+
// Salt and info are short, which is ok, but translates to a short HMAC key.
405+
fips140.ResetServiceIndicator()
406+
_, err = Key(sha256.New, []byte("YELLOW SUBMARINE"), []byte("salt"), "info", 32)
407+
if !fips140.ServiceIndicator() {
408+
t.Error("FIPS service indicator should be set")
391409
}
392410
}

src/crypto/internal/fips140/hkdf/cast.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func init() {
2424
0xa6, 0xc1, 0xde, 0x42, 0x4f, 0x2c, 0x99, 0x60,
2525
0x64, 0xdb, 0x66, 0x3e, 0xec, 0xa6, 0x37, 0xff,
2626
}
27-
got := Key(sha256.New, input, input, input, len(want))
27+
got := Key(sha256.New, input, input, string(input), len(want))
2828
if !bytes.Equal(got, want) {
2929
return errors.New("unexpected result")
3030
}

src/crypto/internal/fips140/hkdf/hkdf.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ func Extract[H fips140.Hash](h func() H, secret, salt []byte) []byte {
1919
extractor := hmac.New(h, salt)
2020
hmac.MarkAsUsedInHKDF(extractor)
2121
extractor.Write(secret)
22+
2223
return extractor.Sum(nil)
2324
}
2425

25-
func Expand[H fips140.Hash](h func() H, pseudorandomKey, info []byte, keyLen int) []byte {
26+
func Expand[H fips140.Hash](h func() H, pseudorandomKey []byte, info string, keyLen int) []byte {
2627
out := make([]byte, 0, keyLen)
2728
expander := hmac.New(h, pseudorandomKey)
2829
hmac.MarkAsUsedInHKDF(expander)
@@ -38,7 +39,7 @@ func Expand[H fips140.Hash](h func() H, pseudorandomKey, info []byte, keyLen int
3839
expander.Reset()
3940
}
4041
expander.Write(buf)
41-
expander.Write(info)
42+
expander.Write([]byte(info))
4243
expander.Write([]byte{counter})
4344
buf = expander.Sum(buf[:0])
4445
remain := keyLen - len(out)
@@ -49,7 +50,7 @@ func Expand[H fips140.Hash](h func() H, pseudorandomKey, info []byte, keyLen int
4950
return out
5051
}
5152

52-
func Key[H fips140.Hash](h func() H, secret, salt, info []byte, keyLen int) []byte {
53+
func Key[H fips140.Hash](h func() H, secret, salt []byte, info string, keyLen int) []byte {
5354
prk := Extract(h, secret, salt)
5455
return Expand(h, prk, info, keyLen)
5556
}

src/crypto/internal/fips140/tls13/tls13.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func ExpandLabel[H fips140.Hash](hash func() H, secret []byte, label string, con
3636
hkdfLabel = append(hkdfLabel, label...)
3737
hkdfLabel = append(hkdfLabel, byte(len(context)))
3838
hkdfLabel = append(hkdfLabel, context...)
39-
return hkdf.Expand(hash, secret, hkdfLabel, length)
39+
return hkdf.Expand(hash, secret, string(hkdfLabel), length)
4040
}
4141

4242
func extract[H fips140.Hash](hash func() H, newSecret, currentSecret []byte) []byte {

src/crypto/internal/hpke/hpke.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func (kdf *hkdfKDF) LabeledExpand(suiteID []byte, randomKey []byte, label string
4242
labeledInfo = append(labeledInfo, suiteID...)
4343
labeledInfo = append(labeledInfo, label...)
4444
labeledInfo = append(labeledInfo, info...)
45-
return hkdf.Expand(kdf.hash.New, randomKey, labeledInfo, int(length))
45+
return hkdf.Expand(kdf.hash.New, randomKey, string(labeledInfo), int(length))
4646
}
4747

4848
// dhKEM implements the KEM specified in RFC 9180, Section 4.1.

src/go/build/deps_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ var depsRules = `
511511
512512
crypto/boring
513513
< crypto/aes, crypto/des, crypto/hmac, crypto/md5, crypto/rc4,
514-
crypto/sha1, crypto/sha256, crypto/sha512;
514+
crypto/sha1, crypto/sha256, crypto/sha512, crypto/hkdf;
515515
516516
crypto/boring, crypto/internal/fips140/edwards25519/field
517517
< crypto/ecdh;
@@ -530,7 +530,8 @@ var depsRules = `
530530
crypto/sha1,
531531
crypto/sha256,
532532
crypto/sha512,
533-
golang.org/x/crypto/sha3
533+
golang.org/x/crypto/sha3,
534+
crypto/hkdf
534535
< CRYPTO;
535536
536537
CGO, fmt, net !< CRYPTO;

0 commit comments

Comments
 (0)