Allow creating x25519 key pairs from JS (#5846)

This commit is contained in:
Amaury Chamayou 2023-11-28 17:23:47 +00:00 коммит произвёл GitHub
Родитель cd069ab47b
Коммит 3882284f14
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
11 изменённых файлов: 182 добавлений и 63 удалений

Просмотреть файл

@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [5.0.0-dev8]
NOTE: UNRELEASED
- `ccf.crypto.generateEddsaKeyPair`, `pubEddsaPemToJwk` and `eddsaPemToJwk` now support `x25519` as well as `curve25519` (#5846).
[5.0.0-dev8]: https://github.com/microsoft/CCF/releases/tag/ccf-5.0.0-dev8
- `POST /recovery/members/{memberId}:recover` is now authenticated by COSE Sign1, making it consistent with the other `POST` endpoints in governance, and avoiding a potential denial of service where un-authenticated and un-authorised clients could submit invalid shares repeatedly. The `submit_recovery_share.sh` script has been amended accordingly, and now takes a `--member-id-privk` and `--member-id-cert` (#5821).
## [5.0.0-dev7]
[5.0.0-dev7]: https://github.com/microsoft/CCF/releases/tag/ccf-5.0.0-dev7

Просмотреть файл

@ -25,7 +25,8 @@ namespace crypto
/// The SECP256K1 curve
SECP256K1,
/// The CURVE25519 curve
CURVE25519
CURVE25519,
X25519
};
DECLARE_JSON_ENUM(
@ -34,7 +35,8 @@ namespace crypto
{CurveID::SECP384R1, "Secp384R1"},
{CurveID::SECP256R1, "Secp256R1"},
{CurveID::SECP256K1, "Secp256K1"},
{CurveID::CURVE25519, "Curve25519"}});
{CurveID::CURVE25519, "Curve25519"},
{CurveID::X25519, "X25519"}});
static constexpr CurveID service_identity_curve_choice = CurveID::SECP384R1;
// SNIPPET_END: supported_curves

Просмотреть файл

@ -82,10 +82,13 @@ namespace crypto
enum class JsonWebKeyEdDSACurve
{
ED25519 = 0
ED25519 = 0,
X25519 = 1
};
DECLARE_JSON_ENUM(
JsonWebKeyEdDSACurve, {{JsonWebKeyEdDSACurve::ED25519, "Ed25519"}});
JsonWebKeyEdDSACurve,
{{JsonWebKeyEdDSACurve::ED25519, "Ed25519"},
{JsonWebKeyEdDSACurve::X25519, "X25519"}});
static JsonWebKeyEdDSACurve curve_id_to_jwk_eddsa_curve(CurveID curve_id)
{
@ -93,6 +96,8 @@ namespace crypto
{
case CurveID::CURVE25519:
return JsonWebKeyEdDSACurve::ED25519;
case CurveID::X25519:
return JsonWebKeyEdDSACurve::X25519;
default:
throw std::logic_error(fmt::format("Unknown EdDSA curve {}", curve_id));
}

Просмотреть файл

@ -371,7 +371,7 @@ export interface CCFCrypto {
/**
* Generate an EdDSA key pair.
*
* @param curve The name of the curve. Currently only "curve25519" is supported.
* @param curve The name of the curve. Only "curve25519" and "x25519" are supported.
*/
generateEddsaKeyPair(curve: string): CryptoKeyPair;
@ -453,7 +453,7 @@ export interface CCFCrypto {
/**
* Converts an EdDSA public key as PEM to JSON Web Key (JWK) object.
* Currently only Curve25519 is supported.
* Only Curve25519 and X25519 are supported.
*
* @param pem EdDSA public key as PEM
* @param kid Key identifier (optional)
@ -462,7 +462,7 @@ export interface CCFCrypto {
/**
* Converts an EdDSA private key as PEM to JSON Web Key (JWK) object.
* Currently only Curve25519 is supported.
* Only Curve25519 and X25519 are supported.
*
* @param pem EdDSA private key as PEM
* @param kid Key identifier (optional)

Просмотреть файл

@ -252,19 +252,33 @@ class CCFPolyfill implements CCF {
return ecdsaKeyPair;
},
generateEddsaKeyPair(curve: string): CryptoKeyPair {
// `type` is always "ed25519" because currently only "curve25519" is supported for `curve`.
const type = "ed25519";
const ecdsaKeyPair = jscrypto.generateKeyPairSync(type, {
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
return ecdsaKeyPair;
if (curve === "curve25519") {
return jscrypto.generateKeyPairSync("ed25519", {
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
} else {
if (curve !== "x25519")
throw new Error(
"Unsupported curve for EdDSA key pair generation: " + curve,
);
return jscrypto.generateKeyPairSync("x25519", {
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
}
},
wrapKey(
key: ArrayBuffer,

Просмотреть файл

@ -76,6 +76,13 @@ describe("polyfill", function () {
assert.isTrue(pair.privateKey.startsWith("-----BEGIN PRIVATE KEY-----"));
});
});
describe("generateEddsaKeyPair/X25519", function () {
it("generates a random EdDSA X25519 key pair", function () {
const pair = ccf.crypto.generateEddsaKeyPair("x25519");
assert.isTrue(pair.publicKey.startsWith("-----BEGIN PUBLIC KEY-----"));
assert.isTrue(pair.privateKey.startsWith("-----BEGIN PRIVATE KEY-----"));
});
});
describe("wrapKey", function () {
it("performs RSA-OAEP wrapping correctly", function () {
const key = ccf.crypto.generateAesKey(128);
@ -608,7 +615,7 @@ describe("polyfill", function () {
assert.equal(pem, pair.privateKey);
}
});
it("EdDSA", function () {
it("Ed25119", function () {
const my_kid = "my_kid";
const pair = ccf.crypto.generateEddsaKeyPair("curve25519");
{
@ -640,6 +647,38 @@ describe("polyfill", function () {
assert.equal(pem, pair.privateKey);
}
});
it("X25119", function () {
const my_kid = "my_kid";
const pair = ccf.crypto.generateEddsaKeyPair("x25519");
{
const jwk = ccf.crypto.pubEddsaPemToJwk(pair.publicKey);
assert.equal(jwk.kty, "OKP");
assert.notEqual(jwk.kid, my_kid);
const pem = ccf.crypto.pubEddsaJwkToPem(jwk);
assert.equal(pem, pair.publicKey);
}
{
const jwk = ccf.crypto.pubEddsaPemToJwk(pair.publicKey, my_kid);
assert.equal(jwk.kty, "OKP");
assert.equal(jwk.kid, my_kid);
const pem = ccf.crypto.pubEddsaJwkToPem(jwk);
assert.equal(pem, pair.publicKey);
}
{
const jwk = ccf.crypto.eddsaPemToJwk(pair.privateKey);
assert.equal(jwk.kty, "OKP");
assert.notEqual(jwk.kid, my_kid);
const pem = ccf.crypto.eddsaJwkToPem(jwk);
assert.equal(pem, pair.privateKey);
}
{
const jwk = ccf.crypto.eddsaPemToJwk(pair.privateKey, my_kid);
assert.equal(jwk.kty, "OKP");
assert.equal(jwk.kid, my_kid);
const pem = ccf.crypto.eddsaJwkToPem(jwk);
assert.equal(pem, pair.privateKey);
}
});
});
describe("kv", function () {
it("basic", function () {

Просмотреть файл

@ -34,16 +34,26 @@ namespace crypto
"Cannot construct EdDSA key pair from non-OKP JWK");
}
if (jwk.crv != JsonWebKeyEdDSACurve::ED25519)
int curve = 0;
if (jwk.crv == JsonWebKeyEdDSACurve::ED25519)
{
curve = EVP_PKEY_ED25519;
}
else if (jwk.crv == JsonWebKeyEdDSACurve::X25519)
{
curve = EVP_PKEY_X25519;
}
else
{
throw std::logic_error(
"Cannot construct EdDSA key pair from non-Ed25519 JWK");
"Cannot construct EdDSA key pair from JWK that is neither Ed25519 nor "
"X25519");
}
auto d_raw = raw_from_b64url(jwk.d);
OpenSSL::CHECKNULL(
key = EVP_PKEY_new_raw_private_key(
EVP_PKEY_ED25519, nullptr, d_raw.data(), d_raw.size()));
curve, nullptr, d_raw.data(), d_raw.size()));
}
Pem EdDSAKeyPair_OpenSSL::private_key_pem() const

Просмотреть файл

@ -28,15 +28,25 @@ namespace crypto
"Cannot construct EdDSA public key from non-OKP JWK");
}
if (jwk.crv != JsonWebKeyEdDSACurve::ED25519)
int curve = 0;
if (jwk.crv == JsonWebKeyEdDSACurve::ED25519)
{
curve = EVP_PKEY_ED25519;
}
else if (jwk.crv == JsonWebKeyEdDSACurve::X25519)
{
curve = EVP_PKEY_X25519;
}
else
{
throw std::logic_error(
"Cannot construct EdDSA public key from non-Ed25519 JWK");
"Cannot construct EdDSA key pair from JWK that is neither Ed25519 nor "
"X25519");
}
auto x_raw = raw_from_b64url(jwk.x);
key = EVP_PKEY_new_raw_public_key(
EVP_PKEY_ED25519, nullptr, x_raw.data(), x_raw.size());
key =
EVP_PKEY_new_raw_public_key(curve, nullptr, x_raw.data(), x_raw.size());
if (key == nullptr)
{
throw std::logic_error("Error constructing EdDSA public key from JWK");
@ -83,6 +93,8 @@ namespace crypto
{
case CurveID::CURVE25519:
return EVP_PKEY_ED25519;
case CurveID::X25519:
return EVP_PKEY_X25519;
default:
throw std::logic_error(
fmt::format("unsupported OpenSSL CurveID {}", gid));
@ -97,6 +109,8 @@ namespace crypto
{
case NID_ED25519:
return CurveID::CURVE25519;
case NID_X25519:
return CurveID::X25519;
default:
throw std::runtime_error(fmt::format("Unknown OpenSSL curve {}", nid));
}

Просмотреть файл

@ -186,10 +186,14 @@ namespace ccf::js
{
cid = crypto::CurveID::CURVE25519;
}
else if (curve == "x25519")
{
cid = crypto::CurveID::X25519;
}
else
{
return JS_ThrowRangeError(
ctx, "Unsupported curve id, supported: curve25519");
ctx, "Unsupported curve id, supported: curve25519, x25519");
}
try

Просмотреть файл

@ -16,7 +16,7 @@ from cryptography.x509 import (
load_pem_x509_certificate,
load_der_x509_certificate,
)
from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding, ed25519
from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding, ed25519, x25519
from cryptography.hazmat.primitives.asymmetric.utils import (
decode_dss_signature,
encode_dss_signature,
@ -100,9 +100,14 @@ def generate_ec_keypair(curve: ec.EllipticCurve = ec.SECP256R1) -> Tuple[str, st
return priv_pem, pub_pem
def generate_eddsa_keypair() -> Tuple[str, str]:
# Currently only Curve25519 is supported
priv = ed25519.Ed25519PrivateKey.generate()
def generate_eddsa_keypair(curve: str) -> Tuple[str, str]:
key_class = {
"curve25519": ed25519.Ed25519PrivateKey,
"x25519": x25519.X25519PrivateKey,
}
if curve not in key_class:
raise ValueError(f"Unsupported curve: {curve}")
priv = key_class[curve].generate()
pub = priv.public_key()
priv_pem = priv.private_bytes(
Encoding.PEM, PrivateFormat.PKCS8, NoEncryption()

Просмотреть файл

@ -19,6 +19,9 @@ import openapi_spec_validator
from jwcrypto import jwk
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
import hmac
import random
@ -117,37 +120,38 @@ def generate_and_verify_jwk(client):
assert body["pem"] == pub_pem
# EdDSA
priv_pem, pub_pem = infra.crypto.generate_eddsa_keypair()
# Note: x25519 is not supported by jwcrypto just yet
for curve in ["curve25519"]:
priv_pem, pub_pem = infra.crypto.generate_eddsa_keypair(curve)
# Private
ref_priv_jwk = jwk.JWK.from_pem(priv_pem.encode()).export_private(as_dict=True)
r = client.post(
"/app/eddsaPemToJwk", body={"pem": priv_pem, "kid": ref_priv_jwk["kid"]}
)
body = r.body.json()
assert r.status_code == http.HTTPStatus.OK
assert body["kty"] == "OKP"
assert body == ref_priv_jwk, f"{body} != {ref_priv_jwk}"
# Private
ref_priv_jwk = jwk.JWK.from_pem(priv_pem.encode()).export(as_dict=True)
r = client.post(
"/app/eddsaPemToJwk", body={"pem": priv_pem, "kid": ref_priv_jwk["kid"]}
)
body = r.body.json()
assert r.status_code == http.HTTPStatus.OK
assert body["kty"] == "OKP"
assert body == ref_priv_jwk, f"{body} != {ref_priv_jwk}"
r = client.post("/app/eddsaJwkToPem", body={"jwk": body})
body = r.body.json()
assert r.status_code == http.HTTPStatus.OK
assert body["pem"] == priv_pem
r = client.post("/app/eddsaJwkToPem", body={"jwk": body})
body = r.body.json()
assert r.status_code == http.HTTPStatus.OK
assert body["pem"] == priv_pem
# Public
ref_pub_jwk = jwk.JWK.from_pem(pub_pem.encode()).export(as_dict=True)
r = client.post(
"/app/pubEddsaPemToJwk", body={"pem": pub_pem, "kid": ref_pub_jwk["kid"]}
)
body = r.body.json()
assert r.status_code == http.HTTPStatus.OK
assert body["kty"] == "OKP"
assert body == ref_pub_jwk, f"{body} != {ref_pub_jwk}"
# Public
ref_pub_jwk = jwk.JWK.from_pem(pub_pem.encode()).export(as_dict=True)
r = client.post(
"/app/pubEddsaPemToJwk", body={"pem": pub_pem, "kid": ref_pub_jwk["kid"]}
)
body = r.body.json()
assert r.status_code == http.HTTPStatus.OK
assert body["kty"] == "OKP"
assert body == ref_pub_jwk, f"{body} != {ref_pub_jwk}"
r = client.post("/app/pubEddsaJwkToPem", body={"jwk": body})
body = r.body.json()
assert r.status_code == http.HTTPStatus.OK
assert body["pem"] == pub_pem
r = client.post("/app/pubEddsaJwkToPem", body={"jwk": body})
body = r.body.json()
assert r.status_code == http.HTTPStatus.OK
assert body["pem"] == pub_pem
@reqs.description("Test module import")
@ -540,6 +544,18 @@ def test_npm_app(network, args):
r.body.json()["privateKey"], r.body.json()["publicKey"]
)
private_key = load_pem_private_key(r.body.json()["privateKey"].encode(), None)
assert isinstance(private_key, Ed25519PrivateKey)
r = c.post("/app/generateEddsaKeyPair", {"curve": "x25519"})
assert r.status_code == http.HTTPStatus.OK, r.status_code
assert infra.crypto.check_key_pair_pem(
r.body.json()["privateKey"], r.body.json()["publicKey"]
)
private_key = load_pem_private_key(r.body.json()["privateKey"].encode(), None)
assert isinstance(private_key, X25519PrivateKey)
aes_key_to_wrap = infra.crypto.generate_aes_key(256)
wrapping_key_priv_pem, wrapping_key_pub_pem = infra.crypto.generate_rsa_keypair(
2048
@ -719,7 +735,7 @@ def test_npm_app(network, args):
pass
# Test EDDSA signing + verification
key_priv_pem, key_pub_pem = infra.crypto.generate_eddsa_keypair()
key_priv_pem, key_pub_pem = infra.crypto.generate_eddsa_keypair("curve25519")
algorithm = {"name": "EdDSA"}
r = c.post(
"/app/sign",
@ -799,7 +815,7 @@ def test_npm_app(network, args):
assert r.status_code == http.HTTPStatus.OK, r.status_code
assert r.body.json() is True, r.body
key_priv_pem, key_pub_pem = infra.crypto.generate_eddsa_keypair()
key_priv_pem, key_pub_pem = infra.crypto.generate_eddsa_keypair("curve25519")
algorithm = {"name": "EdDSA"}
signature = infra.crypto.sign(algorithm, key_priv_pem, data)
r = c.post(