]> Cypherpunks repositories - gostls13.git/commitdiff
crypto/internal/fips/drbg: implement CTR_DRBG
authorFilippo Valsorda <filippo@golang.org>
Mon, 4 Nov 2024 13:02:58 +0000 (14:02 +0100)
committerGopher Robot <gobot@golang.org>
Tue, 19 Nov 2024 00:30:16 +0000 (00:30 +0000)
For #69536

Change-Id: I016bb723841acbda50f013db46f9d2dda200e1fd
Reviewed-on: https://go-review.googlesource.com/c/go/+/624977
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Roland Shoemaker <roland@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Auto-Submit: Filippo Valsorda <filippo@golang.org>

src/crypto/internal/fips/aes/ctr.go
src/crypto/internal/fips/drbg/ctrdrbg.go [new file with mode: 0644]
src/crypto/internal/fips/drbg/ctrdrbg_test.go [new file with mode: 0644]
src/go/build/deps_test.go

index a20d3864d5426c265f5add325c419668193838b9..c492b900ead8cf33f432a2fd4c3991e26878861d 100644 (file)
@@ -40,6 +40,18 @@ func (c *CTR) XORKeyStream(dst, src []byte) {
        }
 }
 
+// RoundToBlock is used by CTR_DRBG, which discards the rightmost unused bits at
+// each request. It rounds the offset up to the next block boundary.
+func RoundToBlock(c *CTR) {
+       if remainder := c.offset % BlockSize; remainder != 0 {
+               var carry uint64
+               c.offset, carry = bits.Add64(c.offset, BlockSize-remainder, 0)
+               if carry != 0 {
+                       panic("crypto/aes: counter overflow")
+               }
+       }
+}
+
 // XORKeyStreamAt behaves like XORKeyStream but keeps no state, and instead
 // seeks into the keystream by the given bytes offset from the start (ignoring
 // any XORKetStream calls). This allows for random access into the keystream, up
diff --git a/src/crypto/internal/fips/drbg/ctrdrbg.go b/src/crypto/internal/fips/drbg/ctrdrbg.go
new file mode 100644 (file)
index 0000000..4e2d7aa
--- /dev/null
@@ -0,0 +1,130 @@
+// 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 drbg
+
+import (
+       "crypto/internal/fips/aes"
+       "crypto/internal/fips/subtle"
+       "internal/byteorder"
+       "math/bits"
+)
+
+// Counter is an SP 800-90A Rev. 1 CTR_DRBG instantiated with AES-256.
+//
+// Per Table 3, it has a security strength of 256 bits, a seed size of 384 bits,
+// a counter length of 128 bits, a reseed interval of 2^48 requests, and a
+// maximum request size of 2^19 bits (2^16 bytes, 64 KiB).
+//
+// We support a narrow range of parameters that fit the needs of our RNG:
+// AES-256, no derivation function, no personalization string, no prediction
+// resistance, and 384-bit additional input.
+type Counter struct {
+       // c is instantiated with K as the key and V as the counter.
+       c aes.CTR
+
+       reseedCounter uint64
+}
+
+const (
+       keySize        = 256 / 8
+       SeedSize       = keySize + aes.BlockSize
+       reseedInterval = 1 << 48
+       maxRequestSize = (1 << 19) / 8
+)
+
+func NewCounter(entropy *[SeedSize]byte) *Counter {
+       // CTR_DRBG_Instantiate_algorithm, per Section 10.2.1.3.1.
+
+       K := make([]byte, keySize)
+       V := make([]byte, aes.BlockSize)
+
+       // V starts at 0, but is incremented in CTR_DRBG_Update before each use,
+       // unlike AES-CTR where it is incremented after each use.
+       V[len(V)-1] = 1
+
+       cipher, err := aes.New(K)
+       if err != nil {
+               panic(err)
+       }
+
+       c := &Counter{}
+       c.c = *aes.NewCTR(cipher, V)
+       c.update(entropy)
+       c.reseedCounter = 1
+       return c
+}
+
+func (c *Counter) update(seed *[SeedSize]byte) {
+       // CTR_DRBG_Update, per Section 10.2.1.2.
+
+       temp := make([]byte, SeedSize)
+       c.c.XORKeyStream(temp, seed[:])
+       K := temp[:keySize]
+       V := temp[keySize:]
+
+       // Again, we pre-increment V, like in NewCounter.
+       increment((*[aes.BlockSize]byte)(V))
+
+       cipher, err := aes.New(K)
+       if err != nil {
+               panic(err)
+       }
+       c.c = *aes.NewCTR(cipher, V)
+}
+
+func increment(v *[aes.BlockSize]byte) {
+       hi := byteorder.BeUint64(v[:8])
+       lo := byteorder.BeUint64(v[8:])
+       lo, c := bits.Add64(lo, 1, 0)
+       hi, _ = bits.Add64(hi, 0, c)
+       byteorder.BePutUint64(v[:8], hi)
+       byteorder.BePutUint64(v[8:], lo)
+}
+
+func (c *Counter) Reseed(entropy, additionalInput *[SeedSize]byte) {
+       // CTR_DRBG_Reseed_algorithm, per Section 10.2.1.4.1.
+       var seed [SeedSize]byte
+       subtle.XORBytes(seed[:], entropy[:], additionalInput[:])
+       c.update(&seed)
+       c.reseedCounter = 1
+}
+
+// Generate produces at most maxRequestSize bytes of random data in out.
+func (c *Counter) Generate(out []byte, additionalInput *[SeedSize]byte) (reseedRequired bool) {
+       // CTR_DRBG_Generate_algorithm, per Section 10.2.1.5.1.
+
+       if len(out) > maxRequestSize {
+               panic("crypto/drbg: internal error: request size exceeds maximum")
+       }
+
+       // Step 1.
+       if c.reseedCounter > reseedInterval {
+               return true
+       }
+
+       // Step 2.
+       if additionalInput != nil {
+               c.update(additionalInput)
+       } else {
+               // If the additional input is null, the first CTR_DRBG_Update is
+               // skipped, but the additional input is replaced with an all-zero string
+               // for the second CTR_DRBG_Update.
+               additionalInput = new([SeedSize]byte)
+       }
+
+       // Steps 3-5.
+       clear(out)
+       c.c.XORKeyStream(out, out)
+       aes.RoundToBlock(&c.c)
+
+       // Step 6.
+       c.update(additionalInput)
+
+       // Step 7.
+       c.reseedCounter++
+
+       // Step 8.
+       return false
+}
diff --git a/src/crypto/internal/fips/drbg/ctrdrbg_test.go b/src/crypto/internal/fips/drbg/ctrdrbg_test.go
new file mode 100644 (file)
index 0000000..ca124cc
--- /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 drbg_test
+
+import (
+       "bytes"
+       "crypto/internal/fips/drbg"
+       "crypto/internal/fips/subtle"
+       "encoding/hex"
+       "testing"
+)
+
+func TestCounter(t *testing.T) {
+       // https://github.com/usnistgov/ACVP-Server/blob/fb44dce/gen-val/json-files/ctrDRBG-1.0/prompt.json#L4447-L4482
+
+       entropyInput := decodeHex("9FCBB4CCC0135C484BDED061DA9FD70748682FE84166B97FF53F9AA1909B2E95D3D529C0F453B3AC575D12AA441CC5CD")
+       persoString := decodeHex("2C9FED0B39556CDBE699EBCA2A0EC7EECB287E8744475050C572FA8AE9ED0A4A7D6F1CABF1C4278532FB20AF7D64BD32")
+       reseedEntropy := decodeHex("913C0DA19B010EDDD55A7A4F3F713EEF5B1534D34360A7EC376AE71A6B340043CC7726F762CB853453F399B3A645062A")
+       reseedAdditional := decodeHex("2D9D4EC141A22E6CD2F6EE4F6719CF6BDF95CFE50B8D5EA6C87D38B4B872706FFF80B0380BB90E9C42D11D6526E56C29")
+       additional1 := decodeHex("A642F06D327828F3E84564A3E37D60C157073B95864CA07981B0189668A0D978CD5DC68F06801CEFF0DC839A312B028E")
+       additional2 := decodeHex("9DB14BABFA9107C88BA92073C0B4A65E89147EA06D74B894142979482F452915B35B5636F9B8A951759735ADE7C8D5D1")
+       returnedBits := decodeHex("F10C645683FF0131254052ED4C698122B46B563654C29D728AC191CA4AAEFE649EEFE4C6FC33B25BB739294DD5CF578099F856C98D98000CBF971F1E6EA900822FF8C110118F6520471744D3F8A3F5C7D568494240E57F5488AF9C9F9F4E7322F56CCD843C0DBFCE9170C02E205389420527F23EDB3369D9FCC5E34901B5BA4EB71B973FC7982FFE0899FF7FE53EE0C4F51A3EF93EF9C6D4D279DD7536F8776BE94AAA05E89EF6E6AEE8832B4B42FFCA5FB91EC0273F9EF945865512889B0C5EE141D1B38DF827D2A694835561628C6F9B093A01A835F07ADBB9E03FEBF93389E8F3B86E1E0ABF1F9958FA286AD995289C2F606D1A9043A166C1AFE8D00769C712650819C9068A4BD22717C98338395A7BA6E95B5178BFBF4EFB0F05A91713BA8BF2127A6BA1EDFA6D1CAB05C03EE0D2AFE1DA4EB8F2C579EC872FF4B602027EF4BDCF2F4B01423F8E600A13D7CACB6AB83263BA58F907694AF614A6724FD0E4C627A0D91DDC6716C697FACE6F4808A4F37B731DE4E0CD4766CEADAAAF47992505299C72AC1A6E9A8335B8D7E501B3841188D0DA4DE5267674444DC2B0CF9F010756FA865A25CA3F1B24C34E845B2259926B6A867A7684DE68A6137C4FB0F47A2E54AE9E6455BEBA0B0A9629644FE9E378EE95386443BA977124FFD1192E9F460684C7B09FA99F5F93F04F56FD7955E042187887CE696F1934017E458B16B5C9")
+
+       // We don't support personalization strings, but the pre-generated JSON
+       // vectors always use them, so just pre-mix them.
+       var seed [drbg.SeedSize]byte
+       subtle.XORBytes(seed[:], entropyInput, persoString)
+       c := drbg.NewCounter(&seed)
+
+       c.Reseed((*[48]byte)(reseedEntropy), (*[48]byte)(reseedAdditional))
+
+       buf := make([]byte, len(returnedBits))
+       c.Generate(buf, (*[48]byte)(additional1))
+
+       c.Generate(buf, (*[48]byte)(additional2))
+       if !bytes.Equal(buf, returnedBits) {
+               t.Errorf("unexpected output:\n%x\n%x", buf, returnedBits)
+       }
+}
+
+func decodeHex(s string) []byte {
+       b, err := hex.DecodeString(s)
+       if err != nil {
+               panic(err)
+       }
+       return b
+}
index abff1be2ada4e6df8e18ad16413d9ab0f59e34d4..3367ff614441a20199681707ef0cd849306ee8c6 100644 (file)
@@ -452,6 +452,7 @@ var depsRules = `
        < crypto/internal/fips/alias
        < crypto/internal/fips/subtle
        < crypto/internal/fips/aes
+       < crypto/internal/fips/drbg
        < crypto/internal/fips/sha256
        < crypto/internal/fips/sha512
        < crypto/internal/fips/sha3