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/&lt;HASH&gt;/server | K, H, SessionID, cipher algorithm | server IV key, server encryption key, server integrity key |
 | KTS-IFC/&lt;HASH&gt;/initiator | output length bytes, serverN bytes, serverE bytes | generated ciphertext (iutC), derived keying material (dkm) |
 | KTS-IFC/&lt;HASH&gt;/responder | iutN bytes, iutE bytes, iutP bytes, iutQ bytes, iutD bytes, ciphertext (serverC) bytes | derived keying material (dkm) |
+| OneStepNoCounter/&lt;HASH&gt; | 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"},