acvptool: Add support for DRBG Change-Id: Ia9dda0826787aea4d63536524074e343ff6c87d9 Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/38644 Reviewed-by: Adam Langley <agl@google.com> Reviewed-by: Gurleen Grewal <gurleengrewal@google.com> Commit-Queue: Adam Langley <agl@google.com>
diff --git a/util/fipstools/acvp/acvptool/subprocess/drbg.go b/util/fipstools/acvp/acvptool/subprocess/drbg.go new file mode 100644 index 0000000..b7f93f9 --- /dev/null +++ b/util/fipstools/acvp/acvptool/subprocess/drbg.go
@@ -0,0 +1,155 @@ +package subprocess + +import ( + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" +) + +// The following structures reflect the JSON of ACVP DRBG tests. See +// https://usnistgov.github.io/ACVP/artifacts/acvp_sub_drbg.html#rfc.section.4 + +type drbgTestVectorSet struct { + Groups []drbgTestGroup `json:"testGroups"` +} + +type drbgTestGroup struct { + ID uint64 `json:"tgId"` + Mode string `json:"mode"` + UseDerivationFunction bool `json:"derFunc,omitempty"` + PredictionResistance bool `json:"predResistance"` + Reseed bool `json:"reSeed"` + EntropyBits uint64 `json:"entropyInputLen"` + NonceBits uint64 `json:"nonceLen"` + PersonalizationBits uint64 `json:"persoStringLen"` + AdditionalDataBits uint64 `json:"additionalInputLen"` + RetBits uint64 `json:"returnedBitsLen"` + Tests []struct { + ID uint64 `json:"tcId"` + EntropyHex string `json:"entropyInput"` + NonceHex string `json:"nonce"` + PersonalizationHex string `json:"persoString"` + Other []struct { + AdditionalDataHex string `json:"additionalInput"` + EntropyHex string `json:"entropyInput"` + Use string `json:"intendedUse"` + } `json:"otherInput"` + } `json:"tests"` +} + +type drbgTestGroupResponse struct { + ID uint64 `json:"tgId"` + Tests []drbgTestResponse `json:"tests"` +} + +type drbgTestResponse struct { + ID uint64 `json:"tcId"` + OutHex string `json:"returnedBits,omitempty"` +} + +// drbg implements an ACVP algorithm by making requests to the +// subprocess to generate random bits with the given entropy and other paramaters. +type drbg struct { + // algo is the ACVP name for this algorithm and also the command name + // given to the subprocess to generate random bytes. + algo string + modes map[string]bool // the supported underlying primitives for the DRBG + m *Subprocess +} + +func (d *drbg) Process(vectorSet []byte) (interface{}, error) { + var parsed drbgTestVectorSet + if err := json.Unmarshal(vectorSet, &parsed); err != nil { + return nil, err + } + + var ret []drbgTestGroupResponse + // See + // https://usnistgov.github.io/ACVP/artifacts/acvp_sub_drbg.html#rfc.section.4 + // for details about the tests. + for _, group := range parsed.Groups { + response := drbgTestGroupResponse{ + ID: group.ID, + } + + if _, ok := d.modes[group.Mode]; !ok { + return nil, fmt.Errorf("test group %d specifies mode %q, which is not supported for the %s algorithm", group.ID, group.Mode, d.algo) + } + + if group.PredictionResistance { + return nil, fmt.Errorf("Test group %d specifies prediction-resistance mode, which is not supported", group.ID) + } + + if group.Reseed { + return nil, fmt.Errorf("Test group %d requests re-seeding, which is not supported", group.ID) + } + + if group.RetBits%8 != 0 { + return nil, fmt.Errorf("Test group %d requests %d-bit outputs, but fractional-bytes are not supported", group.ID, group.RetBits) + } + + for _, test := range group.Tests { + ent, err := extractField(test.EntropyHex, group.EntropyBits) + if err != nil { + return nil, fmt.Errorf("failed to extract entropy hex from test case %d/%d: %s", group.ID, test.ID, err) + } + + nonce, err := extractField(test.NonceHex, group.NonceBits) + if err != nil { + return nil, fmt.Errorf("failed to extract nonce hex from test case %d/%d: %s", group.ID, test.ID, err) + } + + perso, err := extractField(test.PersonalizationHex, group.PersonalizationBits) + if err != nil { + return nil, fmt.Errorf("failed to extract personalization hex from test case %d/%d: %s", group.ID, test.ID, err) + } + + const numAdditionalInputs = 2 + if len(test.Other) != numAdditionalInputs { + return nil, fmt.Errorf("test case %d/%d provides %d additional inputs, but subprocess only expects %d", group.ID, test.ID, len(test.Other), numAdditionalInputs) + } + + var additionalInputs [numAdditionalInputs][]byte + for i, other := range test.Other { + if other.Use != "generate" { + return nil, fmt.Errorf("other %d from test case %d/%d has use %q, but expected 'generate'", i, group.ID, test.ID, other.Use) + } + additionalInputs[i], err = extractField(other.AdditionalDataHex, group.AdditionalDataBits) + if err != nil { + return nil, fmt.Errorf("failed to extract additional input %d from test case %d/%d: %s", i, group.ID, test.ID, err) + } + } + + outLen := group.RetBits / 8 + var outLenBytes [4]byte + binary.LittleEndian.PutUint32(outLenBytes[:], uint32(outLen)) + result, err := d.m.transact(d.algo+"/"+group.Mode, 1, outLenBytes[:], ent, perso, additionalInputs[0], additionalInputs[1], nonce) + if err != nil { + return nil, fmt.Errorf("DRBG operation failed: %s", err) + } + + if l := uint64(len(result[0])); l != outLen { + return nil, fmt.Errorf("wrong length DRBG result: %d bytes but wanted %d", l, outLenBytes) + } + + // https://usnistgov.github.io/ACVP/artifacts/acvp_sub_drbg.html#rfc.section.4 + response.Tests = append(response.Tests, drbgTestResponse{ + ID: test.ID, + OutHex: hex.EncodeToString(result[0]), + }) + } + + ret = append(ret, response) + } + + return ret, nil +} + +// validate the length and hex of a JSON field in test vectors +func extractField(fieldHex string, bits uint64) ([]byte, error) { + if uint64(len(fieldHex))*4 != bits { + return nil, fmt.Errorf("expected %d bits but have %d-byte hex string", bits, len(fieldHex)) + } + return hex.DecodeString(fieldHex) +}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go index 1f3ff8e..452a422 100644 --- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go +++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -61,6 +61,8 @@ "HMAC-SHA2-256": &hmacPrimitive{"HMAC-SHA2-256", 32, m}, "HMAC-SHA2-384": &hmacPrimitive{"HMAC-SHA2-384", 48, m}, "HMAC-SHA2-512": &hmacPrimitive{"HMAC-SHA2-512", 64, m}, + "ctrDRBG": &drbg{"ctrDRBG", map[string]bool{"AES-128": true, "AES-192": true, "AES-256": true}, m}, + "hmacDRBG": &drbg{"hmacDRBG", map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true}, m}, } return m
diff --git a/util/fipstools/acvp/modulewrapper/modulewrapper.cc b/util/fipstools/acvp/modulewrapper/modulewrapper.cc index e6d8b21..5f3150d 100644 --- a/util/fipstools/acvp/modulewrapper/modulewrapper.cc +++ b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
@@ -28,6 +28,8 @@ #include <openssl/sha.h> #include <openssl/span.h> +#include "../../../../crypto/fipsmodule/rand/internal.h" + static constexpr size_t kMaxArgs = 8; static constexpr size_t kMaxArgLength = (1 << 20); static constexpr size_t kMaxNameLength = 30; @@ -212,6 +214,23 @@ " \"macLen\": [{" " \"min\": 32, \"max\": 512, \"increment\": 8" " }]" + "}," + "{" + " \"algorithm\": \"ctrDRBG\"," + " \"revision\": \"1.0\"," + " \"predResistanceEnabled\": [false]," + " \"reseedImplemented\": false," + " \"capabilities\": [{" + " \"mode\": \"AES-256\"," + " \"derFuncEnabled\": false," + " \"entropyInputLen\": [384]," + " \"nonceLen\": [0]," + " \"persoStringLen\": [{\"min\": 0, \"max\": 384, \"increment\": 16}]," + " \"additionalInputLen\": [" + " {\"min\": 0, \"max\": 384, \"increment\": 16}" + " ]," + " \"returnedBitsLen\": 2048" + " }]" "}" "]"; return WriteReply( @@ -280,6 +299,40 @@ return WriteReply(STDOUT_FILENO, Span<const uint8_t>(digest, digest_len)); } +static bool DRBG(const Span<const uint8_t> args[]) { + const auto out_len_bytes = args[0]; + const auto entropy = args[1]; + const auto personalisation = args[2]; + const auto additional_data1 = args[3]; + const auto additional_data2 = args[4]; + const auto nonce = args[5]; + + uint32_t out_len; + if (out_len_bytes.size() != sizeof(out_len) || + entropy.size() != CTR_DRBG_ENTROPY_LEN || + // nonces are not supported + nonce.size() != 0) { + return false; + } + memcpy(&out_len, out_len_bytes.data(), sizeof(out_len)); + if (out_len > (1 << 24)) { + return false; + } + std::vector<uint8_t> out(out_len); + + CTR_DRBG_STATE drbg; + if (!CTR_DRBG_init(&drbg, entropy.data(), personalisation.data(), + personalisation.size()) || + !CTR_DRBG_generate(&drbg, out.data(), out_len, additional_data1.data(), + additional_data1.size()) || + !CTR_DRBG_generate(&drbg, out.data(), out_len, additional_data2.data(), + additional_data2.size())) { + return false; + } + + return WriteReply(STDOUT_FILENO, Span<const uint8_t>(out)); +} + static constexpr struct { const char name[kMaxNameLength + 1]; uint8_t expected_args; @@ -300,6 +353,7 @@ {"HMAC-SHA2-256", 2, HMAC<EVP_sha256>}, {"HMAC-SHA2-384", 2, HMAC<EVP_sha384>}, {"HMAC-SHA2-512", 2, HMAC<EVP_sha512>}, + {"ctrDRBG/AES-256", 6, DRBG}, }; int main() {