acvp: add support for finite-field Diffie–Hellman.
This involves adding a new function |DH_compute_key_hashed| that
combines the FFDH with the output hashing inside the FIPS module. This
new function uses the padded FFDH output, as newly specified in SP
800-56Ar3.
Change-Id: Iafcb7e276f16d39bf7d25d3b2f163b5cd6f67883
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/44504
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/crypto/fipsmodule/dh/dh.c b/crypto/fipsmodule/dh/dh.c
index 8194caa..6bc1e53 100644
--- a/crypto/fipsmodule/dh/dh.c
+++ b/crypto/fipsmodule/dh/dh.c
@@ -60,6 +60,7 @@
#include <openssl/bn.h>
#include <openssl/err.h>
+#include <openssl/digest.h>
#include <openssl/mem.h>
#include <openssl/thread.h>
@@ -384,56 +385,118 @@
return ok;
}
-int DH_compute_key(unsigned char *out, const BIGNUM *peers_key, DH *dh) {
- BN_CTX *ctx = NULL;
- BIGNUM *shared_key;
- int ret = -1;
- int check_result;
-
+static int dh_compute_key(DH *dh, BIGNUM *out_shared_key,
+ const BIGNUM *peers_key, BN_CTX *ctx) {
if (BN_num_bits(dh->p) > OPENSSL_DH_MAX_MODULUS_BITS) {
OPENSSL_PUT_ERROR(DH, DH_R_MODULUS_TOO_LARGE);
- goto err;
- }
-
- ctx = BN_CTX_new();
- if (ctx == NULL) {
- goto err;
- }
- BN_CTX_start(ctx);
- shared_key = BN_CTX_get(ctx);
- if (shared_key == NULL) {
- goto err;
+ return 0;
}
if (dh->priv_key == NULL) {
OPENSSL_PUT_ERROR(DH, DH_R_NO_PRIVATE_VALUE);
- goto err;
+ return 0;
}
- if (!BN_MONT_CTX_set_locked(&dh->method_mont_p, &dh->method_mont_p_lock,
+ int check_result;
+ if (!DH_check_pub_key(dh, peers_key, &check_result) || check_result) {
+ OPENSSL_PUT_ERROR(DH, DH_R_INVALID_PUBKEY);
+ return 0;
+ }
+
+ int ret = 0;
+ BN_CTX_start(ctx);
+ BIGNUM *p_minus_1 = BN_CTX_get(ctx);
+
+ if (!p_minus_1 ||
+ !BN_MONT_CTX_set_locked(&dh->method_mont_p, &dh->method_mont_p_lock,
dh->p, ctx)) {
goto err;
}
- if (!DH_check_pub_key(dh, peers_key, &check_result) || check_result) {
- OPENSSL_PUT_ERROR(DH, DH_R_INVALID_PUBKEY);
- goto err;
- }
-
- if (!BN_mod_exp_mont_consttime(shared_key, peers_key, dh->priv_key, dh->p,
- ctx, dh->method_mont_p)) {
+ if (!BN_mod_exp_mont_consttime(out_shared_key, peers_key, dh->priv_key, dh->p,
+ ctx, dh->method_mont_p) ||
+ !BN_copy(p_minus_1, dh->p) ||
+ !BN_sub_word(p_minus_1, 1)) {
OPENSSL_PUT_ERROR(DH, ERR_R_BN_LIB);
goto err;
}
- ret = BN_bn2bin(shared_key, out);
-
-err:
- if (ctx != NULL) {
- BN_CTX_end(ctx);
- BN_CTX_free(ctx);
+ // This performs the check required by SP 800-56Ar3 section 5.7.1.1 step two.
+ if (BN_cmp_word(out_shared_key, 1) <= 0 ||
+ BN_cmp(out_shared_key, p_minus_1) == 0) {
+ OPENSSL_PUT_ERROR(DH, DH_R_INVALID_PUBKEY);
+ goto err;
}
+ ret = 1;
+
+ err:
+ BN_CTX_end(ctx);
+ return ret;
+}
+
+int DH_compute_key(unsigned char *out, const BIGNUM *peers_key, DH *dh) {
+ BN_CTX *ctx = BN_CTX_new();
+ if (ctx == NULL) {
+ return -1;
+ }
+ BN_CTX_start(ctx);
+
+ int ret = -1;
+ BIGNUM *shared_key = BN_CTX_get(ctx);
+ if (shared_key && dh_compute_key(dh, shared_key, peers_key, ctx)) {
+ ret = BN_bn2bin(shared_key, out);
+ }
+
+ BN_CTX_end(ctx);
+ BN_CTX_free(ctx);
+ return ret;
+}
+
+int DH_compute_key_hashed(DH *dh, uint8_t *out, size_t *out_len,
+ size_t max_out_len, const BIGNUM *peers_key,
+ const EVP_MD *digest) {
+ *out_len = (size_t)-1;
+
+ const size_t digest_len = EVP_MD_size(digest);
+ if (digest_len > max_out_len) {
+ return 0;
+ }
+
+ BN_CTX *ctx = BN_CTX_new();
+ if (ctx == NULL) {
+ return 0;
+ }
+ BN_CTX_start(ctx);
+
+ int ret = 0;
+ BIGNUM *shared_key = BN_CTX_get(ctx);
+ const size_t p_len = BN_num_bytes(dh->p);
+ uint8_t *shared_bytes = OPENSSL_malloc(p_len);
+ unsigned out_len_unsigned;
+ if (!shared_key ||
+ !shared_bytes ||
+ !dh_compute_key(dh, shared_key, peers_key, ctx) ||
+ // |DH_compute_key| doesn't pad the output. SP 800-56A is ambiguous about
+ // whether the output should be padded prior to revision three. But
+ // revision three, section C.1, awkwardly specifies padding to the length
+ // of p.
+ //
+ // Also, padded output avoids side-channels, so is always strongly
+ // advisable.
+ !BN_bn2bin_padded(shared_bytes, p_len, shared_key) ||
+ !EVP_Digest(shared_bytes, p_len, out, &out_len_unsigned, digest, NULL) ||
+ out_len_unsigned != digest_len) {
+ goto err;
+ }
+
+ *out_len = digest_len;
+ ret = 1;
+
+ err:
+ BN_CTX_end(ctx);
+ BN_CTX_free(ctx);
+ OPENSSL_free(shared_bytes);
return ret;
}
diff --git a/include/openssl/dh.h b/include/openssl/dh.h
index ef3c481..52d831d 100644
--- a/include/openssl/dh.h
+++ b/include/openssl/dh.h
@@ -178,6 +178,19 @@
OPENSSL_EXPORT int DH_compute_key(uint8_t *out, const BIGNUM *peers_key,
DH *dh);
+// DH_compute_key_hashed calculates the shared key between |dh| and |peers_key|
+// and hashes it with the given |digest|. If the hash output is less than
+// |max_out_len| bytes then it writes the hash output to |out| and sets
+// |*out_len| to the number of bytes written. Otherwise it signals an error. It
+// returns one on success or zero on error.
+//
+// NOTE: this follows the usual BoringSSL return-value convention, but that's
+// different from |DH_compute_key|, above.
+OPENSSL_EXPORT int DH_compute_key_hashed(DH *dh, uint8_t *out, size_t *out_len,
+ size_t max_out_len,
+ const BIGNUM *peers_key,
+ const EVP_MD *digest);
+
// Utility functions.
diff --git a/util/fipstools/acvp/acvptool/subprocess/kasdh.go b/util/fipstools/acvp/acvptool/subprocess/kasdh.go
new file mode 100644
index 0000000..8251b7f
--- /dev/null
+++ b/util/fipstools/acvp/acvptool/subprocess/kasdh.go
@@ -0,0 +1,179 @@
+// Copyright (c) 2020, Google Inc.
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+package subprocess
+
+import (
+ "bytes"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+)
+
+type kasDHVectorSet struct {
+ Groups []kasDHTestGroup `json:"testGroups"`
+}
+
+type kasDHTestGroup struct {
+ ID uint64 `json:"tgId"`
+ Type string `json:"testType"`
+ Role string `json:"kasRole"`
+ Mode string `json:"kasMode"`
+ Hash string `json:"hashAlg"`
+ Scheme string `json:"scheme"`
+ PHex string `json:"p"`
+ QHex string `json:"q"`
+ GHex string `json:"g"`
+ Tests []kasDHTest `json:"tests"`
+}
+
+type kasDHTest struct {
+ ID uint64 `json:"tcId"`
+ PeerPublicHex string `json:"ephemeralPublicServer"`
+ PrivateKeyHex string `json:"ephemeralPrivateIut"`
+ PublicKeyHex string `json:"ephemeralPublicIut"`
+ ResultHex string `json:"hashZIut"`
+}
+
+type kasDHTestGroupResponse struct {
+ ID uint64 `json:"tgId"`
+ Tests []kasDHTestResponse `json:"tests"`
+}
+
+type kasDHTestResponse struct {
+ ID uint64 `json:"tcId"`
+ LocalPublicHex string `json:"ephemeralPublicIut,omitempty"`
+ ResultHex string `json:"hashZIut,omitempty"`
+ Passed *bool `json:"testPassed,omitempty"`
+}
+
+type kasDH struct{}
+
+func (k *kasDH) Process(vectorSet []byte, m Transactable) (interface{}, error) {
+ var parsed kasDHVectorSet
+ if err := json.Unmarshal(vectorSet, &parsed); err != nil {
+ return nil, err
+ }
+
+ // See https://usnistgov.github.io/ACVP/draft-fussell-acvp-kas-ffc.html
+ var ret []kasDHTestGroupResponse
+ for _, group := range parsed.Groups {
+ response := kasDHTestGroupResponse{
+ ID: group.ID,
+ }
+
+ var privateKeyGiven bool
+ switch group.Type {
+ case "AFT":
+ privateKeyGiven = false
+ case "VAL":
+ privateKeyGiven = true
+ default:
+ return nil, fmt.Errorf("unknown test type %q", group.Type)
+ }
+
+ switch group.Hash {
+ case "SHA2-224", "SHA2-256", "SHA2-384", "SHA2-512":
+ break
+ default:
+ return nil, fmt.Errorf("unknown hash function %q", group.Hash)
+ }
+
+ switch group.Role {
+ case "initiator", "responder":
+ break
+ default:
+ return nil, fmt.Errorf("unknown role %q", group.Role)
+ }
+
+ if group.Scheme != "dhEphem" {
+ return nil, fmt.Errorf("unknown scheme %q", group.Scheme)
+ }
+
+ p, err := hex.DecodeString(group.PHex)
+ if err != nil {
+ return nil, err
+ }
+
+ q, err := hex.DecodeString(group.QHex)
+ if err != nil {
+ return nil, err
+ }
+
+ g, err := hex.DecodeString(group.GHex)
+ if err != nil {
+ return nil, err
+ }
+
+ method := "FFDH/" + group.Hash
+
+ for _, test := range group.Tests {
+ if len(test.PeerPublicHex) == 0 {
+ return nil, fmt.Errorf("%d/%d is missing peer's key", group.ID, test.ID)
+ }
+
+ peerPublic, err := hex.DecodeString(test.PeerPublicHex)
+ if err != nil {
+ return nil, err
+ }
+
+ if (len(test.PrivateKeyHex) != 0) != privateKeyGiven {
+ return nil, fmt.Errorf("%d/%d incorrect private key presence", group.ID, test.ID)
+ }
+
+ if privateKeyGiven {
+ privateKey, err := hex.DecodeString(test.PrivateKeyHex)
+ if err != nil {
+ return nil, err
+ }
+
+ publicKey, err := hex.DecodeString(test.PublicKeyHex)
+ if err != nil {
+ return nil, err
+ }
+
+ expectedOutput, err := hex.DecodeString(test.ResultHex)
+ if err != nil {
+ return nil, err
+ }
+
+ result, err := m.Transact(method, 2, p, q, g, peerPublic, privateKey, publicKey)
+ if err != nil {
+ return nil, err
+ }
+
+ ok := bytes.Equal(result[1], expectedOutput)
+ response.Tests = append(response.Tests, kasDHTestResponse{
+ ID: test.ID,
+ Passed: &ok,
+ })
+ } else {
+ result, err := m.Transact(method, 2, p, q, g, peerPublic, nil, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ response.Tests = append(response.Tests, kasDHTestResponse{
+ ID: test.ID,
+ LocalPublicHex: hex.EncodeToString(result[0]),
+ ResultHex: hex.EncodeToString(result[1]),
+ })
+ }
+ }
+
+ ret = append(ret, response)
+ }
+
+ return ret, nil
+}
diff --git a/util/fipstools/acvp/acvptool/subprocess/subprocess.go b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
index e3f11cc..c0d48b7 100644
--- a/util/fipstools/acvp/acvptool/subprocess/subprocess.go
+++ b/util/fipstools/acvp/acvptool/subprocess/subprocess.go
@@ -98,6 +98,7 @@
"RSA": &rsa{},
"kdf-components": &tlsKDF{},
"KAS-ECC-SSC": &kas{},
+ "KAS-FFC": &kasDH{},
}
m.primitives["ECDSA"] = &ecdsa{"ECDSA", map[string]bool{"P-224": true, "P-256": true, "P-384": true, "P-521": true}, m.primitives}
diff --git a/util/fipstools/acvp/acvptool/subprocess/tlskdf.go b/util/fipstools/acvp/acvptool/subprocess/tlskdf.go
index 96a0a5c..4a8f7ee 100644
--- a/util/fipstools/acvp/acvptool/subprocess/tlskdf.go
+++ b/util/fipstools/acvp/acvptool/subprocess/tlskdf.go
@@ -35,8 +35,8 @@
}
type tlsKDFTest struct {
- ID uint64 `json:"tcId"`
- PMSHex string `json:"preMasterSecret"`
+ ID uint64 `json:"tcId"`
+ PMSHex string `json:"preMasterSecret"`
// ClientHelloRandomHex and ServerHelloRandomHex are used for deriving the
// master secret. ClientRandomHex and ServerRandomHex are used for deriving the
// key block. Having different values for these is not possible in a TLS
diff --git a/util/fipstools/acvp/modulewrapper/modulewrapper.cc b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
index 37db281..2c9fd36 100644
--- a/util/fipstools/acvp/modulewrapper/modulewrapper.cc
+++ b/util/fipstools/acvp/modulewrapper/modulewrapper.cc
@@ -29,6 +29,7 @@
#include <openssl/bn.h>
#include <openssl/cipher.h>
#include <openssl/cmac.h>
+#include <openssl/dh.h>
#include <openssl/digest.h>
#include <openssl/ec.h>
#include <openssl/ec_key.h>
@@ -737,6 +738,35 @@
"P-384",
"P-521"
]
+ },
+ {
+ "algorithm": "KAS-FFC",
+ "revision": "1.0",
+ "mode": "Component",
+ "function": [
+ "keyPairGen"
+ ],
+ "scheme": {
+ "dhEphem": {
+ "kasRole": [
+ "initiator"
+ ],
+ "noKdfNoKc": {
+ "parameterSet": {
+ "fb": {
+ "hashAlg": [
+ "SHA2-256"
+ ]
+ },
+ "fc": {
+ "hashAlg": [
+ "SHA2-256"
+ ]
+ }
+ }
+ }
+ }
+ }
}
])";
return WriteReply(
@@ -1678,6 +1708,55 @@
output);
}
+template<const EVP_MD* (*HashFunc)()>
+static bool FFDH(const Span<const uint8_t> args[]) {
+ bssl::UniquePtr<BIGNUM> p(BytesToBIGNUM(args[0]));
+ bssl::UniquePtr<BIGNUM> q(BytesToBIGNUM(args[1]));
+ bssl::UniquePtr<BIGNUM> g(BytesToBIGNUM(args[2]));
+ bssl::UniquePtr<BIGNUM> their_pub(BytesToBIGNUM(args[3]));
+ const Span<const uint8_t> private_key_span = args[4];
+ const Span<const uint8_t> public_key_span = args[5];
+
+ bssl::UniquePtr<DH> dh(DH_new());
+ if (!DH_set0_pqg(dh.get(), p.get(), q.get(), g.get())) {
+ fprintf(stderr, "DH_set0_pqg failed.\n");
+ return 0;
+ }
+
+ // DH_set0_pqg took ownership of these values.
+ p.release();
+ q.release();
+ g.release();
+
+ if (!private_key_span.empty()) {
+ bssl::UniquePtr<BIGNUM> private_key(BytesToBIGNUM(private_key_span));
+ bssl::UniquePtr<BIGNUM> public_key(BytesToBIGNUM(public_key_span));
+
+ if (!DH_set0_key(dh.get(), public_key.get(), private_key.get())) {
+ fprintf(stderr, "DH_set0_key failed.\n");
+ return 0;
+ }
+
+ // DH_set0_key took ownership of these values.
+ public_key.release();
+ private_key.release();
+ } else if (!DH_generate_key(dh.get())) {
+ fprintf(stderr, "DH_generate_key failed.\n");
+ return false;
+ }
+
+ uint8_t digest[EVP_MAX_MD_SIZE];
+ size_t digest_len;
+ if (!DH_compute_key_hashed(dh.get(), digest, &digest_len, sizeof(digest),
+ their_pub.get(), HashFunc())) {
+ fprintf(stderr, "DH_compute_key_hashed failed.\n");
+ return false;
+ }
+
+ return WriteReply(STDOUT_FILENO, BIGNUMBytes(DH_get0_pub_key(dh.get())),
+ Span<const uint8_t>(digest, digest_len));
+}
+
static constexpr struct {
const char name[kMaxNameLength + 1];
uint8_t expected_args;
@@ -1748,6 +1827,7 @@
{"ECDH/P-256", 3, ECDH<NID_X9_62_prime256v1>},
{"ECDH/P-384", 3, ECDH<NID_secp384r1>},
{"ECDH/P-521", 3, ECDH<NID_secp521r1>},
+ {"FFDH/SHA2-256", 6, FFDH<EVP_sha256>},
};
int main() {