]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/hkdf: init package
authorDaniel McCarney <daniel@binaryparadox.net>
Wed, 20 Nov 2024 21:11:06 +0000 (16:11 -0500)
committerGopher Robot <gobot@golang.org>
Thu, 21 Nov 2024 22:55:17 +0000 (22:55 +0000)
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>

api/next/61477.txt [new file with mode: 0644]
doc/next/6-stdlib/3-hkdf.md [new file with mode: 0644]
doc/next/6-stdlib/99-minor/crypto/hkdf/61477.md [new file with mode: 0644]
src/crypto/hkdf/example_test.go [new file with mode: 0644]
src/crypto/hkdf/hkdf.go [new file with mode: 0644]
src/crypto/hkdf/hkdf_test.go [moved from src/crypto/internal/fips140test/hkdf_test.go with 86% similarity]
src/crypto/internal/fips140/hkdf/cast.go
src/crypto/internal/fips140/hkdf/hkdf.go
src/crypto/internal/fips140/tls13/tls13.go
src/crypto/internal/hpke/hpke.go
src/go/build/deps_test.go

diff --git a/api/next/61477.txt b/api/next/61477.txt
new file mode 100644 (file)
index 0000000..aeb6acd
--- /dev/null
@@ -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 (file)
index 0000000..1914fa1
--- /dev/null
@@ -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 -->
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 (file)
index 0000000..5b1c75e
--- /dev/null
@@ -0,0 +1 @@
+<!-- This is a new package; covered in 6-stdlib/3-hkdf.md. -->
diff --git a/src/crypto/hkdf/example_test.go b/src/crypto/hkdf/example_test.go
new file mode 100644 (file)
index 0000000..789f7ae
--- /dev/null
@@ -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 (file)
index 0000000..e33e0ac
--- /dev/null
@@ -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
+}
similarity index 86%
rename from src/crypto/internal/fips140test/hkdf_test.go
rename to src/crypto/hkdf/hkdf_test.go
index 9ddfe88f4f0de111a3aab3bc20762d3d51f9e053..201b440289bb2d4aef6f2bf1965d19941afaa037 100644 (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)
+               }
+
+               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")
        }
 }
index 422ca9e309f90e80d637b2e5d4f9c9f8723cbdec..8ddcadc0166851d9217674378370a81d58d531ae 100644 (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")
                }
index 982775129b2805714a17884af9569fb6ee67874d..6ddae5c3f2febccd5b7440e26c7a542071e1d2e2 100644 (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)
 }
index f2c8250f3b4b205ded79a335ab1ff1412b0ce54c..009844a5078f673f85734953a04094dce36cbf73 100644 (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 {
index 229a9a916250d4b2f61b2950142e80a3fb75d6c2..d8a0cc1ecbd8084425b546c12bb0be6f7a77e0d9 100644 (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.
index a4003442ae7e397d33e098de1a19957508e8475e..662bb594396f72e61b9a7d32bd41468f1ab507b0 100644 (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;