From: Filippo Valsorda Date: Mon, 4 Nov 2024 13:02:58 +0000 (+0100) Subject: crypto/internal/fips/drbg: implement CTR_DRBG X-Git-Tag: go1.24rc1~299 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=e67037fb27e63dab4af4ef462ba724f0f2691d2a;p=gostls13.git crypto/internal/fips/drbg: implement CTR_DRBG For #69536 Change-Id: I016bb723841acbda50f013db46f9d2dda200e1fd Reviewed-on: https://go-review.googlesource.com/c/go/+/624977 LUCI-TryBot-Result: Go LUCI Reviewed-by: Roland Shoemaker Reviewed-by: Dmitri Shuralyov Auto-Submit: Filippo Valsorda --- diff --git a/src/crypto/internal/fips/aes/ctr.go b/src/crypto/internal/fips/aes/ctr.go index a20d3864d5..c492b900ea 100644 --- a/src/crypto/internal/fips/aes/ctr.go +++ b/src/crypto/internal/fips/aes/ctr.go @@ -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 index 0000000000..4e2d7aa13b --- /dev/null +++ b/src/crypto/internal/fips/drbg/ctrdrbg.go @@ -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 index 0000000000..ca124cc325 --- /dev/null +++ b/src/crypto/internal/fips/drbg/ctrdrbg_test.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 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 +} diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go index abff1be2ad..3367ff6144 100644 --- a/src/go/build/deps_test.go +++ b/src/go/build/deps_test.go @@ -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