util/fipstools: KDA OneStepNoCounter mode support
Extends the acvptool KDA subprocess code to support OneStepNoCounter
mode in addition to HKDF mode, based on the NIST specification:
https://pages.nist.gov/ACVP/draft-hammett-acvp-kas-kdf-onestepnocounter.html
Change-Id: Ie1936ee95304df349a3459e0c21139f88a2f9399
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/75928
Commit-Queue: Bob Beck <bbe@google.com>
Reviewed-by: Adam Langley <agl@google.com>
Reviewed-by: Bob Beck <bbe@google.com>
diff --git a/util/fipstools/acvp/ACVP.md b/util/fipstools/acvp/ACVP.md
index 5fd2866..c93f79c 100644
--- a/util/fipstools/acvp/ACVP.md
+++ b/util/fipstools/acvp/ACVP.md
@@ -148,6 +148,7 @@
| SSHKDF/<HASH>/server | K, H, SessionID, cipher algorithm | server IV key, server encryption key, server integrity key |
| KTS-IFC/<HASH>/initiator | output length bytes, serverN bytes, serverE bytes | generated ciphertext (iutC), derived keying material (dkm) |
| KTS-IFC/<HASH>/responder | iutN bytes, iutE bytes, iutP bytes, iutQ bytes, iutD bytes, ciphertext (serverC) bytes | derived keying material (dkm) |
+| OneStepNoCounter/<HASH> | key, info, salt, output length bytes | derived key |
¹ The iterated tests would result in excessive numbers of round trips if the module wrapper handled only basic operations. Thus some ACVP logic is pushed down for these tests so that the inner loop can be handled locally. Either read the NIST documentation ([block-ciphers](https://pages.nist.gov/ACVP/draft-celi-acvp-symmetric.html#name-monte-carlo-tests-for-block) [hashes](https://pages.nist.gov/ACVP/draft-celi-acvp-sha.html#name-monte-carlo-tests-for-sha-1)) to understand the iteration count and return values or, probably more fruitfully, see how these functions are handled in the `modulewrapper` directory.
diff --git a/util/fipstools/acvp/acvptool/subprocess/kda.go b/util/fipstools/acvp/acvptool/subprocess/kda.go
index 9841786..c93a80b 100644
--- a/util/fipstools/acvp/acvptool/subprocess/kda.go
+++ b/util/fipstools/acvp/acvptool/subprocess/kda.go
@@ -23,6 +23,7 @@
// The following structures reflect the JSON of ACVP KAS KDF tests. See
// https://pages.nist.gov/ACVP/draft-hammett-acvp-kas-kdf-hkdf.html
+// https://pages.nist.gov/ACVP/draft-hammett-acvp-kas-kdf-onestepnocounter.html
type multiModeKda struct {
modes map[string]primitive
@@ -42,7 +43,28 @@
return mode.Process(vectorSet, m)
}
+type kdaPartyInfo struct {
+ IDHex string `json:"partyId"`
+ ExtraHex string `json:"ephemeralData"`
+}
+
+func (p *kdaPartyInfo) data() ([]byte, error) {
+ ret, err := hex.DecodeString(p.IDHex)
+ if err != nil {
+ return nil, err
+ }
+ if len(p.ExtraHex) > 0 {
+ extra, err := hex.DecodeString(p.ExtraHex)
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, extra...)
+ }
+ return ret, nil
+}
+
type hkdfTestVectorSet struct {
+ Mode string `json:"mode"`
Groups []hkdfTestGroup `json:"testGroups"`
}
@@ -56,8 +78,8 @@
type hkdfTest struct {
ID uint64 `json:"tcId"`
Params hkdfParameters `json:"kdfParameter"`
- PartyU hkdfPartyInfo `json:"fixedInfoPartyU"`
- PartyV hkdfPartyInfo `json:"fixedInfoPartyV"`
+ PartyU kdaPartyInfo `json:"fixedInfoPartyU"`
+ PartyV kdaPartyInfo `json:"fixedInfoPartyV"`
ExpectedHex string `json:"dkm"`
}
@@ -99,28 +121,6 @@
return key, salt, nil
}
-type hkdfPartyInfo struct {
- IDHex string `json:"partyId"`
- ExtraHex string `json:"ephemeralData"`
-}
-
-func (p *hkdfPartyInfo) data() ([]byte, error) {
- ret, err := hex.DecodeString(p.IDHex)
- if err != nil {
- return nil, err
- }
-
- if len(p.ExtraHex) > 0 {
- extra, err := hex.DecodeString(p.ExtraHex)
- if err != nil {
- return nil, err
- }
- ret = append(ret, extra...)
- }
-
- return ret, nil
-}
-
type hkdfTestGroupResponse struct {
ID uint64 `json:"tgId"`
Tests []hkdfTestResponse `json:"tests"`
@@ -140,6 +140,10 @@
return nil, err
}
+ if parsed.Mode != "HKDF" {
+ return nil, fmt.Errorf("unexpected KDA mode %q", parsed.Mode)
+ }
+
var respGroups []hkdfTestGroupResponse
for _, group := range parsed.Groups {
group := group
@@ -216,3 +220,171 @@
return respGroups, nil
}
+
+type oneStepTestVectorSet struct {
+ Mode string `json:"mode"`
+ Groups []oneStepTestGroup `json:"testGroups"`
+}
+
+type oneStepTestGroup struct {
+ ID uint64 `json:"tgId"`
+ Type string `json:"testType"` // AFT or VAL
+ Config oneStepConfiguration `json:"kdfConfiguration"`
+ Tests []oneStepTest `json:"tests"`
+}
+
+type oneStepConfiguration struct {
+ Type string `json:"kdfType"`
+ SaltMethod string `json:"saltMethod"`
+ FixedInfoPattern string `json:"fixedInfoPattern"`
+ FixedInputEncoding string `json:"fixedInfoEncoding"`
+ AuxFunction string `json:"auxFunction"`
+ OutputBits uint32 `json:"l"`
+}
+
+func (c *oneStepConfiguration) extract() (outBytes uint32, auxFunction string, err error) {
+ if c.Type != "oneStepNoCounter" ||
+ c.FixedInfoPattern != "uPartyInfo||vPartyInfo" ||
+ c.FixedInputEncoding != "concatenation" ||
+ c.OutputBits%8 != 0 {
+ return 0, "", fmt.Errorf("KDA not configured for OneStepNoCounter: %#v", c)
+ }
+ return c.OutputBits / 8, c.AuxFunction, nil
+}
+
+type oneStepTest struct {
+ ID uint64 `json:"tcId"`
+ Params oneStepTestParameters `json:"kdfParameter"`
+ FixedInfoPartyU kdaPartyInfo `json:"fixedInfoPartyU"`
+ FixedInfoPartyV kdaPartyInfo `json:"fixedInfoPartyV"`
+ DerivedKeyHex string `json:"dkm,omitempty"` // For VAL tests only.
+}
+
+type oneStepTestParameters struct {
+ KdfType string `json:"kdfType"`
+ SaltHex string `json:"salt"`
+ ZHex string `json:"z"`
+ OutputBits uint32 `json:"l"`
+}
+
+func (p oneStepTestParameters) extract() (key []byte, salt []byte, outLen uint32, err error) {
+ if p.KdfType != "oneStepNoCounter" ||
+ p.OutputBits%8 != 0 {
+ return nil, nil, 0, fmt.Errorf("KDA not configured for OneStepNoCounter: %#v", p)
+ }
+ outLen = p.OutputBits / 8
+ salt, err = hex.DecodeString(p.SaltHex)
+ if err != nil {
+ return
+ }
+ key, err = hex.DecodeString(p.ZHex)
+ if err != nil {
+ return
+ }
+ return
+}
+
+type oneStepTestGroupResponse struct {
+ ID uint64 `json:"tgId"`
+ Tests []oneStepTestResponse `json:"tests"`
+}
+
+type oneStepTestResponse struct {
+ ID uint64 `json:"tcId"`
+ KeyOut string `json:"dkm,omitempty"` // For AFT
+ Passed *bool `json:"testPassed,omitempty"` // For VAL
+}
+
+type oneStepNoCounter struct{}
+
+func (k oneStepNoCounter) Process(vectorSet []byte, m Transactable) (any, error) {
+ var parsed oneStepTestVectorSet
+ if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+ return nil, err
+ }
+
+ if parsed.Mode != "OneStepNoCounter" {
+ return nil, fmt.Errorf("unexpected KDA mode %q", parsed.Mode)
+ }
+
+ var respGroups []oneStepTestGroupResponse
+ for _, group := range parsed.Groups {
+ group := group
+
+ groupResp := oneStepTestGroupResponse{ID: group.ID}
+ outBytes, hashName, err := group.Config.extract()
+ if err != nil {
+ return nil, err
+ }
+
+ var isValidationTest bool
+ switch group.Type {
+ case "VAL":
+ isValidationTest = true
+ case "AFT":
+ isValidationTest = false
+ default:
+ return nil, fmt.Errorf("unknown test type %q", group.Type)
+ }
+
+ for _, test := range group.Tests {
+ test := test
+ testResp := oneStepTestResponse{ID: test.ID}
+
+ key, salt, paramsOutBytes, err := test.Params.extract()
+ if err != nil {
+ return nil, err
+ }
+ if paramsOutBytes != outBytes {
+ return nil, fmt.Errorf("test %d in group %d: output length mismatch: %d != %d", test.ID, group.ID, paramsOutBytes, outBytes)
+ }
+
+ uData, err := test.FixedInfoPartyU.data()
+ if err != nil {
+ return nil, err
+ }
+ vData, err := test.FixedInfoPartyV.data()
+ if err != nil {
+ return nil, err
+ }
+
+ info := make([]byte, 0, len(uData)+len(vData))
+ info = append(info, uData...)
+ info = append(info, vData...)
+ var expected []byte
+ if isValidationTest {
+ expected, err = hex.DecodeString(test.DerivedKeyHex)
+ if err != nil {
+ return nil, fmt.Errorf("test %d in group %d: invalid DerivedKeyHex: %w", test.ID, group.ID, err)
+ }
+ }
+
+ cmd := "OneStepNoCounter/" + hashName
+ m.TransactAsync(cmd, 1, [][]byte{key, info, salt, uint32le(outBytes)}, func(result [][]byte) error {
+ if len(result[0]) != int(outBytes) {
+ return fmt.Errorf("OneStepNoCounter operation resulted in %d bytes but wanted %d", len(result[0]), outBytes)
+ }
+
+ if isValidationTest {
+ passed := bytes.Equal(expected, result[0])
+ testResp.Passed = &passed
+ } else {
+ testResp.KeyOut = hex.EncodeToString(result[0])
+ }
+
+ groupResp.Tests = append(groupResp.Tests, testResp)
+ return nil
+ })
+ }
+
+ m.Barrier(func() {
+ respGroups = append(respGroups, groupResp)
+ })
+ }
+
+ if err := m.Flush(); err != nil {
+ return nil, err
+ }
+
+ return respGroups, nil
+}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index 6a7bf30..f550cf0 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -138,7 +138,7 @@
"ctrDRBG": &drbg{"ctrDRBG", map[string]bool{"AES-128": true, "AES-192": true, "AES-256": true}},
"hmacDRBG": &drbg{"hmacDRBG", map[string]bool{"SHA-1": true, "SHA2-224": true, "SHA2-256": true, "SHA2-384": true, "SHA2-512": true, "SHA2-512/224": true, "SHA2-512/256": true, "SHA3-224": true, "SHA3-256": true, "SHA3-384": true, "SHA3-512": true}},
"KDF": &kdfPrimitive{},
- "KDA": &multiModeKda{modes: map[string]primitive{"HKDF": &hkdf{}}},
+ "KDA": &multiModeKda{modes: map[string]primitive{"HKDF": &hkdf{}, "OneStepNoCounter": &oneStepNoCounter{}}},
"TLS-v1.2": &tlsKDF{},
"TLS-v1.3": &tls13{},
"CMAC-AES": &keyedMACPrimitive{"CMAC-AES"},