diff --git a/CHANGELOG.md b/CHANGELOG.md index 19fcc9a4bab..525b507895b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,35 @@ 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). +## [6.0.0-dev6] + +[6.0.0-dev6]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev6 + +### Added + +- Added a `ccf::any_cert_auth_policy` (C++), or `any_cert` (JS/TS), implementing TLS client certificate authentication, but without checking for the presence of the certificate in the governance user or member tables. This enables applications wanting to do so to perform user management in application space, using application tables (#6608). + +## [6.0.0-dev5] + +[6.0.0-dev5]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev5 + +### Added + +- Updated `ccf::cose::edit::set_unprotected_header()` API, to allow removing the unprotected header altogether (#6607). +- Updated `ccf.cose.verify_receipt()` to support checking the claim_digest against a reference value (#6607). + +## [6.0.0-dev4] + +[6.0.0-dev4]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev4 + +### Added + +- `ccf.cose.verify_receipt()` to support verifiying [draft COSE receipts](https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/) (#6603). + +### Removed + +- Remove SECP256K1 support as a part of the migration to Azure Linux (#6592). + ## [6.0.0-dev3] [6.0.0-dev3]: https://github.com/microsoft/CCF/releases/tag/6.0.0-dev3 @@ -12,7 +41,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Changed - Set VMPL value when creating SNP attestations, and check VMPL value is in guest range when verifiying attestation, since recent [updates allow host-initiated attestations](https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/programmer-references/56860.pdf) (#6583). -- Added ccf::cose::edit::set_unprotected_header() API, to allow easy injection of proofs in signatures, and of receipts in signed statements (#6586). +- Added `ccf::cose::edit::set_unprotected_header()` API, to allow easy injection of proofs in signatures, and of receipts in signed statements (#6586). ## [6.0.0-dev2] diff --git a/doc/build_apps/api.rst b/doc/build_apps/api.rst index 808ec6394ec..0ec3229e031 100644 --- a/doc/build_apps/api.rst +++ b/doc/build_apps/api.rst @@ -63,6 +63,9 @@ Policies .. doxygenvariable:: ccf::member_cert_auth_policy :project: CCF +.. doxygenvariable:: ccf::any_cert_auth_policy + :project: CCF + .. doxygenvariable:: ccf::member_cose_sign1_auth_policy :project: CCF @@ -86,6 +89,10 @@ Identities :project: CCF :members: +.. doxygenstruct:: ccf::AnyCertAuthnIdentity + :project: CCF + :members: + .. doxygenstruct:: ccf::UserCOSESign1AuthnIdentity :project: CCF :members: diff --git a/doc/build_apps/js_app_bundle.rst b/doc/build_apps/js_app_bundle.rst index 50484646b9d..f4ba215e07b 100644 --- a/doc/build_apps/js_app_bundle.rst +++ b/doc/build_apps/js_app_bundle.rst @@ -70,6 +70,7 @@ Each endpoint object contains the following information: - ``"user_cert"`` - ``"member_cert"`` + - ``"any_cert"`` - ``"jwt"`` - ``"user_cose_sign1"`` - ``"no_auth"`` diff --git a/include/ccf/common_auth_policies.h b/include/ccf/common_auth_policies.h index 81557157d22..c2edb9c7b84 100644 --- a/include/ccf/common_auth_policies.h +++ b/include/ccf/common_auth_policies.h @@ -31,6 +31,11 @@ namespace ccf static std::shared_ptr member_cert_auth_policy = std::make_shared(); + /** Authenticate using TLS session identity, but do not check + * the certificate against any table, and let the application decide */ + static std::shared_ptr any_cert_auth_policy = + std::make_shared(); + /** Authenticate using JWT, validating the token using the * @c public:ccf.gov.jwt.public_signing_keys_metadata table */ static std::shared_ptr jwt_auth_policy = diff --git a/include/ccf/crypto/cose.h b/include/ccf/crypto/cose.h index 3777130b2df..a06677b8cfa 100644 --- a/include/ccf/crypto/cose.h +++ b/include/ccf/crypto/cose.h @@ -16,34 +16,43 @@ namespace ccf::cose::edit struct AtKey { - /// @brief The key at which to insert the value. + /// @brief The sub-key at which to insert the value. int64_t key; }; using Type = std::variant; } + namespace desc + { + struct Empty + {}; + + struct Value + { + /// @brief The type of position at which to insert the value. + pos::Type position; + /// @brief The top-level key at which to insert the value. + int64_t key; + /// @brief The value to insert in the unprotected header. + const std::vector& value; + }; + + using Type = std::variant; + } + /** - * Set the unprotected header of a COSE_Sign1 message, to a map containing - * @p key and depending on the value of @p position, either an array - * containing - * @p value, or a map with key @p subkey and value @p value. + * Set the unprotected header of a COSE_Sign1 message, according to a + * descriptor. * - * Useful to add a proof to a signature to turn it into a receipt, or to + * Useful to add a proof to a signature to turn it into a receipt, to * add a receipt to a signed statement to turn it into a transparent - * statement. + * statement, or simply to strip the unprotected header from a COSE Sign1. * * @param cose_input The COSE_Sign1 message to edit. - * @param key The key at which to insert either an array or a map. - * @param position Either InArray or AtKey, to determine whether to insert an - * array or a map. - * @param value The value to insert either in the array or the map. - * - * @return The COSE_Sign1 message with the new unprotected header. + * @param descriptor An object describing whether and how to set the + * unprotected header. */ std::vector set_unprotected_header( - const std::span& cose_input, - int64_t key, - pos::Type position, - const std::vector value); + const std::span& cose_input, const desc::Type& descriptor); } \ No newline at end of file diff --git a/include/ccf/crypto/curve.h b/include/ccf/crypto/curve.h index 088dd2c6946..6d0755acef9 100644 --- a/include/ccf/crypto/curve.h +++ b/include/ccf/crypto/curve.h @@ -22,8 +22,6 @@ namespace ccf::crypto SECP384R1, /// The SECP256R1 curve SECP256R1, - /// The SECP256K1 curve - SECP256K1, /// The CURVE25519 curve CURVE25519, X25519 @@ -34,7 +32,6 @@ namespace ccf::crypto {{CurveID::NONE, "None"}, {CurveID::SECP384R1, "Secp384R1"}, {CurveID::SECP256R1, "Secp256R1"}, - {CurveID::SECP256K1, "Secp256K1"}, {CurveID::CURVE25519, "Curve25519"}, {CurveID::X25519, "X25519"}}); @@ -50,8 +47,6 @@ namespace ccf::crypto return MDType::SHA384; case CurveID::SECP256R1: return MDType::SHA256; - case CurveID::SECP256K1: - return MDType::SHA256; default: { throw std::logic_error(fmt::format("Unhandled CurveID: {}", ec)); diff --git a/include/ccf/crypto/jwk.h b/include/ccf/crypto/jwk.h index 926740a4eb4..1b4886cb1a2 100644 --- a/include/ccf/crypto/jwk.h +++ b/include/ccf/crypto/jwk.h @@ -38,16 +38,12 @@ namespace ccf::crypto enum class JsonWebKeyECCurve { P256 = 0, - P256K1 = 1, - P384 = 2, - P521 = 3 + P384 = 1, + P521 = 2 }; DECLARE_JSON_ENUM( JsonWebKeyECCurve, {{JsonWebKeyECCurve::P256, "P-256"}, - {JsonWebKeyECCurve::P256K1, - "secp256k1"}, // As per - // https://www.rfc-editor.org/rfc/rfc8812#name-jose-and-cose-secp256k1-cur {JsonWebKeyECCurve::P384, "P-384"}, {JsonWebKeyECCurve::P521, "P-521"}}); @@ -59,8 +55,6 @@ namespace ccf::crypto return JsonWebKeyECCurve::P384; case CurveID::SECP256R1: return JsonWebKeyECCurve::P256; - case CurveID::SECP256K1: - return JsonWebKeyECCurve::P256K1; default: throw std::logic_error(fmt::format("Unknown curve {}", curve_id)); } @@ -74,8 +68,6 @@ namespace ccf::crypto return CurveID::SECP384R1; case JsonWebKeyECCurve::P256: return CurveID::SECP256R1; - case JsonWebKeyECCurve::P256K1: - return CurveID::SECP256K1; default: throw std::logic_error(fmt::format("Unknown JWK curve {}", jwk_curve)); } diff --git a/include/ccf/endpoint.h b/include/ccf/endpoint.h index 7e97cdf0c0d..6be3112cdd7 100644 --- a/include/ccf/endpoint.h +++ b/include/ccf/endpoint.h @@ -226,6 +226,7 @@ namespace ccf::endpoints * * @see ccf::empty_auth_policy * @see ccf::user_cert_auth_policy + * @see ccf::any_cert_auth_policy */ AuthnPolicies authn_policies; }; diff --git a/include/ccf/endpoints/authentication/cert_auth.h b/include/ccf/endpoints/authentication/cert_auth.h index 6624cc01c05..844716db7f3 100644 --- a/include/ccf/endpoints/authentication/cert_auth.h +++ b/include/ccf/endpoints/authentication/cert_auth.h @@ -114,4 +114,38 @@ namespace ccf return SECURITY_SCHEME_NAME; }; }; + + struct AnyCertAuthnIdentity : public AuthnIdentity + { + // Certificate as a vector of DER-encoded bytes + std::vector cert; + }; + + class AnyCertAuthnPolicy : public AuthnPolicy + { + protected: + std::unique_ptr validity_periods; + + public: + static constexpr auto SECURITY_SCHEME_NAME = "any_cert"; + + AnyCertAuthnPolicy(); + virtual ~AnyCertAuthnPolicy(); + + std::unique_ptr authenticate( + ccf::kv::ReadOnlyTx& tx, + const std::shared_ptr& ctx, + std::string& error_reason) override; + + std::optional get_openapi_security_schema() + const override + { + return get_cert_based_security_schema(); + } + + virtual std::string get_security_scheme_name() override + { + return SECURITY_SCHEME_NAME; + }; + }; } diff --git a/include/ccf/endpoints/authentication/js.h b/include/ccf/endpoints/authentication/js.h index 2b843902094..2a853d4ccbe 100644 --- a/include/ccf/endpoints/authentication/js.h +++ b/include/ccf/endpoints/authentication/js.h @@ -24,6 +24,10 @@ namespace ccf ccf::MemberCertAuthnPolicy::SECURITY_SCHEME_NAME, ccf::member_cert_auth_policy); + policies.emplace( + ccf::AnyCertAuthnPolicy::SECURITY_SCHEME_NAME, + ccf::any_cert_auth_policy); + policies.emplace( ccf::JwtAuthnPolicy::SECURITY_SCHEME_NAME, ccf::jwt_auth_policy); @@ -62,6 +66,10 @@ namespace ccf { return ccf::MemberCertAuthnPolicy::SECURITY_SCHEME_NAME; } + else if constexpr (std::is_same_v) + { + return ccf::AnyCertAuthnPolicy::SECURITY_SCHEME_NAME; + } else if constexpr (std::is_same_v) { return ccf::JwtAuthnPolicy::SECURITY_SCHEME_NAME; diff --git a/js/ccf-app/src/endpoints.ts b/js/ccf-app/src/endpoints.ts index 60fe22687b3..cbdf5322ba0 100644 --- a/js/ccf-app/src/endpoints.ts +++ b/js/ccf-app/src/endpoints.ts @@ -117,7 +117,18 @@ export interface EmptyAuthnIdentity extends AuthnIdentityCommon { policy: "no_auth"; } -interface UserMemberAuthnIdentityCommon extends AuthnIdentityCommon { +interface CertAuthnIdentityCommon extends AuthnIdentityCommon { + /** + * PEM-encoded certificate. + */ + cert: string; +} + +export interface AnyCertAuthnIdentity extends CertAuthnIdentityCommon { + policy: "any_cert"; +} + +interface UserMemberAuthnIdentityCommon extends CertAuthnIdentityCommon { /** * User/member ID. */ @@ -127,11 +138,6 @@ interface UserMemberAuthnIdentityCommon extends AuthnIdentityCommon { * User/member data object. */ data: any; - - /** - * PEM-encoded user/member certificate. - */ - cert: string; } export interface UserCertAuthnIdentity extends UserMemberAuthnIdentityCommon { @@ -193,6 +199,7 @@ export interface AllOfAuthnIdentity extends AuthnIdentityCommon { user_cert?: UserCertAuthnIdentity; member_cert?: MemberCertAuthnIdentity; + any_cert?: AnyCertAuthnIdentity; user_cose_sign1?: UserCOSESign1AuthnIdentity; member_cose_sign1?: MemberCOSESign1AuthnIdentity; jwt?: JwtAuthnIdentity; @@ -207,6 +214,7 @@ export type AuthnIdentity = | EmptyAuthnIdentity | UserCertAuthnIdentity | MemberCertAuthnIdentity + | AnyCertAuthnIdentity | JwtAuthnIdentity | MemberCOSESign1AuthnIdentity | UserCOSESign1AuthnIdentity diff --git a/js/ccf-app/src/global.ts b/js/ccf-app/src/global.ts index cf03dfab2f6..c60439f9f23 100644 --- a/js/ccf-app/src/global.ts +++ b/js/ccf-app/src/global.ts @@ -386,7 +386,7 @@ export interface CCFCrypto { /** * Generate an ECDSA key pair. * - * @param curve The name of the curve, one of "secp256r1", "secp256k1", "secp384r1". + * @param curve The name of the curve, one of "secp256r1", "secp384r1". */ generateEcdsaKeyPair(curve: string): CryptoKeyPair; diff --git a/js/ccf-app/test/polyfill.test.ts b/js/ccf-app/test/polyfill.test.ts index 5b3cf0259f8..60c9d72f681 100644 --- a/js/ccf-app/test/polyfill.test.ts +++ b/js/ccf-app/test/polyfill.test.ts @@ -89,13 +89,6 @@ describe("polyfill", function () { assert.isTrue(pair.privateKey.startsWith("-----BEGIN PRIVATE KEY-----")); }); }); - describe("generateEcdsaKeyPair/secp256k1", function () { - it("generates a random ECDSA P256K1 key pair", function () { - const pair = ccf.crypto.generateEcdsaKeyPair("secp256k1"); - assert.isTrue(pair.publicKey.startsWith("-----BEGIN PUBLIC KEY-----")); - assert.isTrue(pair.privateKey.startsWith("-----BEGIN PRIVATE KEY-----")); - }); - }); describe("generateEcdsaKeyPair/secp384r1", function () { it("generates a random ECDSA P384R1 key pair", function () { const pair = ccf.crypto.generateEcdsaKeyPair("secp384r1"); @@ -586,7 +579,7 @@ describe("polyfill", function () { describe("pemToJwk and jwkToPem", function () { it("EC", function () { const my_kid = "my_kid"; - const curves = ["secp256r1", "secp256k1", "secp384r1"]; + const curves = ["secp256r1", "secp384r1"]; for (const curve of curves) { const pair = ccf.crypto.generateEcdsaKeyPair(curve); { diff --git a/python/pyproject.toml b/python/pyproject.toml index b8fadadf3d8..81e6f2ad6af 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ccf" -version = "6.0.0-dev3" +version = "6.0.0-dev6" authors = [ { name="CCF Team", email="CCF-Sec@microsoft.com" }, ] diff --git a/python/src/ccf/cose.py b/python/src/ccf/cose.py index 48b3898140a..fb9c2713b6b 100644 --- a/python/src/ccf/cose.py +++ b/python/src/ccf/cose.py @@ -9,6 +9,7 @@ import base64 import cbor2 import json +from hashlib import sha256 from datetime import datetime import pycose.headers # type: ignore from pycose.keys.ec2 import EC2Key # type: ignore @@ -24,6 +25,8 @@ from cryptography.x509 import load_pem_x509_certificate from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.x509.base import CertificatePublicKeyTypes +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat Pem = str @@ -37,6 +40,17 @@ "encrypted_recovery_share", ] + GOV_MSG_TYPES_WITH_PROPOSAL_ID +# See https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/ +# should move to a pycose.header value after RFC publication + +COSE_PHDR_VDP_LABEL = 396 +COSE_RECEIPT_INCLUSION_PROOF_LABEL = -1 + +# See https://datatracker.ietf.org/doc/draft-birkholz-cose-receipts-ccf-profile/ + +CCF_PROOF_LEAF_LABEL = 1 +CCF_PROOF_PATH_LABEL = 2 + def from_cryptography_eckey_obj(ext_key) -> EC2Key: """ @@ -187,6 +201,50 @@ def validate_cose_sign1(pubkey, cose_sign1, payload=None): raise ValueError("signature is invalid") +def verify_receipt( + receipt_bytes: bytes, key: CertificatePublicKeyTypes, claim_digest: bytes +): + """ + Verify a COSE Sign1 receipt as defined in https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs/, + using the CCF tree algorithm defined in https://datatracker.ietf.org/doc/draft-birkholz-cose-receipts-ccf-profile/ + """ + # Extract the expected KID from the public key used for verification, + # and check it against the value set in the COSE header before using + # it to verify the proofs. + expected_kid = sha256( + key.public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) + ).digest() + receipt = Sign1Message.decode(receipt_bytes) + cose_key = from_cryptography_eckey_obj(key) + assert receipt.phdr[pycose.headers.KID] == expected_kid + receipt.key = cose_key + + assert COSE_PHDR_VDP_LABEL in receipt.uhdr, "Verifiable data proof is required" + proof = receipt.uhdr[COSE_PHDR_VDP_LABEL] + assert COSE_RECEIPT_INCLUSION_PROOF_LABEL in proof, "Inclusion proof is required" + inclusion_proofs = proof[COSE_RECEIPT_INCLUSION_PROOF_LABEL] + assert inclusion_proofs, "At least one inclusion proof is required" + for inclusion_proof in inclusion_proofs: + assert isinstance(inclusion_proof, bytes), "Inclusion proof must be bstr" + proof = cbor2.loads(inclusion_proof) + assert CCF_PROOF_LEAF_LABEL in proof, "Leaf must be present" + leaf = proof[CCF_PROOF_LEAF_LABEL] + accumulator = sha256( + leaf[0] + sha256(leaf[1].encode()).digest() + leaf[2] + ).digest() + assert CCF_PROOF_PATH_LABEL in proof, "Path must be present" + path = proof[CCF_PROOF_PATH_LABEL] + for left, digest in path: + if left: + accumulator = sha256(digest + accumulator).digest() + else: + accumulator = sha256(accumulator + digest).digest() + if not receipt.verify_signature(accumulator): + raise ValueError("Signature verification failed") + if claim_digest != leaf[2]: + raise ValueError(f"Claim digest mismatch: {leaf[2]!r} != {claim_digest!r}") + + _SIGN_DESCRIPTION = """Create and sign a COSE Sign1 message for CCF governance Note that this tool writes binary COSE Sign1 to standard output. diff --git a/samples/apps/logging/js/app.json b/samples/apps/logging/js/app.json index ecd7b61a6de..63791f55a1f 100644 --- a/samples/apps/logging/js/app.json +++ b/samples/apps/logging/js/app.json @@ -24,6 +24,7 @@ }, "user_cert", "member_cert", + "any_cert", "jwt", "user_cose_sign1", "no_auth" diff --git a/samples/apps/logging/js/src/logging.js b/samples/apps/logging/js/src/logging.js index c4b30907e28..278f73ca675 100644 --- a/samples/apps/logging/js/src/logging.js +++ b/samples/apps/logging/js/src/logging.js @@ -436,6 +436,11 @@ function describe_member_cert_ident(lines, obj) { lines.push(`The caller's cert is:\n${obj.cert}`); } +function describe_any_cert_ident(lines, obj) { + lines.push("Any TLS cert"); + lines.push(`The caller's cert is:\n${obj.cert}`); +} + function describe_jwt_ident(lines, obj) { lines.push("JWT"); lines.push( @@ -468,6 +473,7 @@ export function multi_auth(request) { const describers = { user_cert: describe_user_cert_ident, member_cert: describe_member_cert_ident, + any_cert: describe_any_cert_ident, jwt: describe_jwt_ident, user_cose_sign1: describe_cose_ident, no_auth: describe_noauth_ident, diff --git a/samples/apps/logging/logging.cpp b/samples/apps/logging/logging.cpp index 8a3fe63dcc0..3a97f29109d 100644 --- a/samples/apps/logging/logging.cpp +++ b/samples/apps/logging/logging.cpp @@ -289,6 +289,17 @@ namespace loggingapp return response; } + else if ( + auto any_cert_ident = + dynamic_cast(caller.get())) + { + auto response = std::string("Any TLS cert"); + auto caller_cert = ccf::crypto::cert_der_to_pem(any_cert_ident->cert); + + response += + fmt::format("\nThe caller's cert is:\n{}", caller_cert.str()); + return response; + } else if ( auto jwt_ident = dynamic_cast(caller.get())) @@ -1168,6 +1179,7 @@ namespace loggingapp user_cert_jwt_and_sig_auth_policy, ccf::user_cert_auth_policy, ccf::member_cert_auth_policy, + ccf::any_cert_auth_policy, ccf::jwt_auth_policy, ccf::user_cose_sign1_auth_policy, ccf::empty_auth_policy}) @@ -2065,11 +2077,13 @@ namespace loggingapp return; } - size_t vdp = 396; + int64_t vdp = 396; auto inclusion_proof = ccf::cose::edit::pos::AtKey{-1}; - auto cose_receipt = ccf::cose::edit::set_unprotected_header( - *signature, vdp, inclusion_proof, *proof); + ccf::cose::edit::desc::Value desc{inclusion_proof, vdp, *proof}; + + auto cose_receipt = + ccf::cose::edit::set_unprotected_header(*signature, desc); ctx.rpc_ctx->set_response_status(HTTP_STATUS_OK); ctx.rpc_ctx->set_response_header( diff --git a/src/crypto/cose.cpp b/src/crypto/cose.cpp index f4ad8c67ade..ca3b166b7a2 100644 --- a/src/crypto/cose.cpp +++ b/src/crypto/cose.cpp @@ -12,10 +12,7 @@ namespace ccf::cose::edit { std::vector set_unprotected_header( - const std::span& cose_input, - int64_t key, - pos::Type pos, - const std::vector value) + const std::span& cose_input, const desc::Type& descriptor) { UsefulBufC buf{cose_input.data(), cose_input.size()}; @@ -83,16 +80,31 @@ namespace ccf::cose::edit throw std::logic_error("Failed to parse COSE_Sign1"); } - // Maximum expected size of the additional map, sub-map is the - // worst-case scenario - const size_t additional_map_size = QCBOR_HEAD_BUFFER_SIZE + // map - QCBOR_HEAD_BUFFER_SIZE + // key - sizeof(key) + // key - QCBOR_HEAD_BUFFER_SIZE + // submap - QCBOR_HEAD_BUFFER_SIZE + // subkey - sizeof(pos::AtKey::key) + // subkey - QCBOR_HEAD_BUFFER_SIZE + // value - value.size(); // value + size_t additional_map_size = 0; + + if (std::holds_alternative(descriptor)) + { + // Nothing to do + } + else if (std::holds_alternative(descriptor)) + { + auto& [pos, key, value] = std::get(descriptor); + + // Maximum expected size of the additional map, sub-map is the + // worst-case scenario + additional_map_size = QCBOR_HEAD_BUFFER_SIZE + // map + QCBOR_HEAD_BUFFER_SIZE + // key + sizeof(key) + // key + QCBOR_HEAD_BUFFER_SIZE + // submap + QCBOR_HEAD_BUFFER_SIZE + // subkey + sizeof(pos::AtKey::key) + // subkey + QCBOR_HEAD_BUFFER_SIZE + // value + value.size(); // value + } + else + { + throw std::logic_error("Invalid COSE_Sign1 edit descriptor"); + } // We add one extra QCBOR_HEAD_BUFFER_SIZE, because we parse and re-encode // the protected header bstr, which involves variable integer encoding, just @@ -108,24 +120,37 @@ namespace ccf::cose::edit QCBOREncode_AddBytes(&ectx, phdr); QCBOREncode_OpenMap(&ectx); - if (std::holds_alternative(pos)) + if (std::holds_alternative(descriptor)) { - QCBOREncode_OpenArrayInMapN(&ectx, key); - QCBOREncode_AddBytes(&ectx, {value.data(), value.size()}); - QCBOREncode_CloseArray(&ectx); + // Nothing to do } - else if (std::holds_alternative(pos)) + else if (std::holds_alternative(descriptor)) { - QCBOREncode_OpenMapInMapN(&ectx, key); - auto subkey = std::get(pos).key; - QCBOREncode_OpenArrayInMapN(&ectx, subkey); - QCBOREncode_AddBytes(&ectx, {value.data(), value.size()}); - QCBOREncode_CloseArray(&ectx); - QCBOREncode_CloseMap(&ectx); + auto& [pos, key, value] = std::get(descriptor); + + if (std::holds_alternative(pos)) + { + QCBOREncode_OpenArrayInMapN(&ectx, key); + QCBOREncode_AddBytes(&ectx, {value.data(), value.size()}); + QCBOREncode_CloseArray(&ectx); + } + else if (std::holds_alternative(pos)) + { + QCBOREncode_OpenMapInMapN(&ectx, key); + auto subkey = std::get(pos).key; + QCBOREncode_OpenArrayInMapN(&ectx, subkey); + QCBOREncode_AddBytes(&ectx, {value.data(), value.size()}); + QCBOREncode_CloseArray(&ectx); + QCBOREncode_CloseMap(&ectx); + } + else + { + throw std::logic_error("Invalid COSE_Sign1 edit operation"); + } } else { - throw std::logic_error("Invalid COSE_Sign1 edit operation"); + throw std::logic_error("Invalid COSE_Sign1 edit descriptor"); } QCBOREncode_CloseMap(&ectx); diff --git a/src/crypto/openssl/public_key.cpp b/src/crypto/openssl/public_key.cpp index 6e1afaefef2..f10b847548d 100644 --- a/src/crypto/openssl/public_key.cpp +++ b/src/crypto/openssl/public_key.cpp @@ -138,8 +138,6 @@ namespace ccf::crypto return CurveID::SECP384R1; case NID_X9_62_prime256v1: return CurveID::SECP256R1; - case NID_secp256k1: - return CurveID::SECP256K1; default: throw std::runtime_error(fmt::format("Unknown OpenSSL curve {}", nid)); } @@ -163,10 +161,6 @@ namespace ccf::crypto { return NID_X9_62_prime256v1; } - else if (gname == SN_secp256k1) - { - return NID_secp256k1; - } else { throw std::runtime_error(fmt::format("Unknown OpenSSL group {}", gname)); @@ -187,8 +181,6 @@ namespace ccf::crypto return NID_secp384r1; case CurveID::SECP256R1: return NID_X9_62_prime256v1; - case CurveID::SECP256K1: - return NID_secp256k1; default: throw std::logic_error( fmt::format("unsupported OpenSSL CurveID {}", gid)); @@ -287,23 +279,29 @@ namespace ccf::crypto Unique_PKEY key_from_raw_ec_point(const std::vector& raw, int nid) { #if defined(OPENSSL_VERSION_MAJOR) && OPENSSL_VERSION_MAJOR >= 3 - const unsigned char* pp = raw.data(); - EVP_PKEY* pkey = NULL; - OSSL_PARAM params[2]; + const auto curve_name = (char*)OSSL_EC_curve_nid2name(nid); + + OSSL_PARAM params[3]; params[0] = OSSL_PARAM_construct_utf8_string( - OSSL_PKEY_PARAM_GROUP_NAME, (char*)OSSL_EC_curve_nid2name(nid), 0); - params[1] = OSSL_PARAM_construct_end(); + OSSL_PKEY_PARAM_GROUP_NAME, curve_name, 0); + params[1] = OSSL_PARAM_construct_octet_string( + OSSL_PKEY_PARAM_PUB_KEY, (void*)raw.data(), raw.size()); + params[2] = OSSL_PARAM_construct_end(); - Unique_EVP_PKEY_CTX pctx("EC"); - EVP_PKEY_fromdata_init(pctx); - EVP_PKEY_fromdata( - pctx, &pkey, OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS, params); + auto pctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL); + CHECK1(EVP_PKEY_fromdata_init(pctx)); + + EVP_PKEY* pkey = NULL; + CHECK1(EVP_PKEY_fromdata(pctx, &pkey, EVP_PKEY_PUBLIC_KEY, params)); - pkey = d2i_PublicKey(EVP_PKEY_EC, &pkey, &pp, raw.size()); if (pkey == NULL) { EVP_PKEY_free(pkey); - throw std::logic_error("Error loading public key"); + + throw std::logic_error(fmt::format( + "Error loading public key. Curve: {}, err: {}", + curve_name, + OpenSSL::error_string(ERR_get_error()))); } Unique_PKEY pk(pkey); diff --git a/src/crypto/test/bench.cpp b/src/crypto/test/bench.cpp index 7297fbf62d3..996345e77b6 100644 --- a/src/crypto/test/bench.cpp +++ b/src/crypto/test/bench.cpp @@ -154,9 +154,6 @@ namespace CREATE_KEYPAIRS auto create_256r1 = benchmark_create; PICOBENCH(create_256r1).iterations({1000}).samples(10); - auto create_256k1 = benchmark_create; - PICOBENCH(create_256k1).iterations({1000}).samples(10); - auto create_384r1 = benchmark_create; PICOBENCH(create_384r1).iterations({1000}).samples(10); } @@ -193,22 +190,6 @@ namespace SIGN_SECP256R1 PICOBENCH(sign_256r1_ossl_100k).PICO_SUFFIX(CurveID::SECP256R1); } -PICOBENCH_SUITE("sign secp256k1"); -namespace SIGN_SECP256K1 -{ - auto sign_256k1_ossl_1byte = - benchmark_sign; - PICOBENCH(sign_256k1_ossl_1byte).PICO_SUFFIX(CurveID::SECP256K1); - - auto sign_256k1_ossl_1k = - benchmark_sign; - PICOBENCH(sign_256k1_ossl_1k).PICO_SUFFIX(CurveID::SECP256K1); - - auto sign_256k1_ossl_100k = - benchmark_sign; - PICOBENCH(sign_256k1_ossl_100k).PICO_SUFFIX(CurveID::SECP256K1); -} - PICOBENCH_SUITE("verify secp384r1"); namespace SECP384R1 { @@ -253,28 +234,6 @@ namespace SECP256R1 PICOBENCH(verify_256r1_ossl_100k).PICO_SUFFIX(CurveID::SECP256R1); } -PICOBENCH_SUITE("verify secp256k1"); -namespace SECP256K1 -{ - auto verify_256k1_ossl_1byte = - benchmark_verify; - PICOBENCH(verify_256k1_ossl_1byte).PICO_SUFFIX(CurveID::SECP256K1); - - auto verify_256k1_ossl_1k = benchmark_verify< - KeyPair_OpenSSL, - PublicKey_OpenSSL, - CurveID::SECP256K1, - 1024>; - PICOBENCH(verify_256k1_ossl_1k).PICO_SUFFIX(CurveID::SECP256K1); - - auto verify_256k1_ossl_100k = benchmark_verify< - KeyPair_OpenSSL, - PublicKey_OpenSSL, - CurveID::SECP256K1, - 102400>; - PICOBENCH(verify_256k1_ossl_100k).PICO_SUFFIX(CurveID::SECP256K1); -} - PICOBENCH_SUITE("sign RSA-2048"); namespace SIGN_RSA2048 { diff --git a/src/crypto/test/cose.cpp b/src/crypto/test/cose.cpp index c3304d0cb7b..5457ba652cf 100644 --- a/src/crypto/test/cose.cpp +++ b/src/crypto/test/cose.cpp @@ -111,12 +111,18 @@ TEST_CASE("Verification and payload invariant") { for (const auto& position : positions) { - auto csp_set = - ccf::cose::edit::set_unprotected_header(csp, key, position, value); + ccf::cose::edit::desc::Value desc{position, key, value}; + auto csp_set = ccf::cose::edit::set_unprotected_header(csp, desc); signer.verify(csp_set); } } + + { + auto csp_set_empty = ccf::cose::edit::set_unprotected_header( + csp, ccf::cose::edit::desc::Empty{}); + signer.verify(csp_set_empty); + } } } @@ -132,14 +138,23 @@ TEST_CASE("Idempotence") { for (const auto& position : positions) { - auto csp_set_once = - ccf::cose::edit::set_unprotected_header(csp, key, position, value); + ccf::cose::edit::desc::Value desc{position, key, value}; + auto csp_set_once = ccf::cose::edit::set_unprotected_header(csp, desc); - auto csp_set_twice = ccf::cose::edit::set_unprotected_header( - csp_set_once, key, position, value); + auto csp_set_twice = + ccf::cose::edit::set_unprotected_header(csp_set_once, desc); REQUIRE(csp_set_once == csp_set_twice); } } + + { + auto csp_set_empty = ccf::cose::edit::set_unprotected_header( + csp, ccf::cose::edit::desc::Empty{}); + auto csp_set_twice_empty = ccf::cose::edit::set_unprotected_header( + csp_set_empty, ccf::cose::edit::desc::Empty{}); + + REQUIRE(csp_set_empty == csp_set_twice_empty); + } } } @@ -155,8 +170,8 @@ TEST_CASE("Check unprotected header") { for (const auto& position : positions) { - auto csp_set = - ccf::cose::edit::set_unprotected_header(csp, key, position, value); + ccf::cose::edit::desc::Value desc{position, key, value}; + auto csp_set = ccf::cose::edit::set_unprotected_header(csp, desc); std::vector ref(1024); { @@ -215,5 +230,48 @@ TEST_CASE("Check unprotected header") REQUIRE(err == QCBOR_SUCCESS); } } + + { + auto csp_set_empty = ccf::cose::edit::set_unprotected_header( + csp, ccf::cose::edit::desc::Empty{}); + + std::vector ref(1024); + { + // Create expected reference value for the unprotected header + UsefulBuf ref_buf{ref.data(), ref.size()}; + QCBOREncodeContext ctx; + QCBOREncode_Init(&ctx, ref_buf); + QCBOREncode_OpenMap(&ctx); + QCBOREncode_CloseMap(&ctx); + UsefulBufC ref_buf_c; + QCBOREncode_Finish(&ctx, &ref_buf_c); + ref.resize(ref_buf_c.len); + ref.shrink_to_fit(); + } + + size_t uhdr_start, uhdr_end; + QCBORError err; + QCBORItem item; + QCBORDecodeContext ctx; + UsefulBufC buf{csp_set_empty.data(), csp_set_empty.size()}; + QCBORDecode_Init(&ctx, buf, QCBOR_DECODE_MODE_NORMAL); + QCBORDecode_EnterArray(&ctx, nullptr); + QCBORDecode_GetNthTagOfLast(&ctx, 0); + // Protected header + QCBORDecode_VGetNextConsume(&ctx, &item); + // Unprotected header + QCBORDecode_PartialFinish(&ctx, &uhdr_start); + QCBORDecode_VGetNextConsume(&ctx, &item); + QCBORDecode_PartialFinish(&ctx, &uhdr_end); + std::vector uhdr{ + csp_set_empty.data() + uhdr_start, csp_set_empty.data() + uhdr_end}; + REQUIRE(uhdr == ref); + // Payload + QCBORDecode_VGetNextConsume(&ctx, &item); + // Signature + QCBORDecode_VGetNextConsume(&ctx, &item); + QCBORDecode_ExitArray(&ctx); + err = QCBORDecode_Finish(&ctx); + } } } \ No newline at end of file diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index c5c7b1c0afb..584013d1de7 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -179,9 +179,9 @@ void corrupt(T& buf) } static constexpr CurveID supported_curves[] = { - CurveID::SECP384R1, CurveID::SECP256R1, CurveID::SECP256K1}; + CurveID::SECP384R1, CurveID::SECP256R1}; -static constexpr char const* labels[] = {"secp384r1", "secp256r1", "secp256k1"}; +static constexpr char const* labels[] = {"secp384r1", "secp256r1"}; ccf::crypto::Pem generate_self_signed_cert( const KeyPairPtr& kp, const std::string& name) @@ -1045,7 +1045,7 @@ TEST_CASE("PEM to JWK and back") INFO("EC"); { - auto curves = {CurveID::SECP384R1, CurveID::SECP256R1, CurveID::SECP256K1}; + auto curves = {CurveID::SECP384R1, CurveID::SECP256R1}; for (auto const& curve : curves) { diff --git a/src/endpoints/authentication/cert_auth.cpp b/src/endpoints/authentication/cert_auth.cpp index af23aaa89ea..b5eafa69fd8 100644 --- a/src/endpoints/authentication/cert_auth.cpp +++ b/src/endpoints/authentication/cert_auth.cpp @@ -117,6 +117,7 @@ namespace ccf if (!validity_periods->is_cert_valid_now(caller_cert, error_reason)) { + // Error is set by the call when necessary return nullptr; } @@ -203,4 +204,33 @@ namespace ccf error_reason = "Could not find matching node certificate"; return nullptr; } + + AnyCertAuthnPolicy::AnyCertAuthnPolicy() : + validity_periods(std::make_unique()) + {} + + AnyCertAuthnPolicy::~AnyCertAuthnPolicy() = default; + + std::unique_ptr AnyCertAuthnPolicy::authenticate( + ccf::kv::ReadOnlyTx& tx, + const std::shared_ptr& ctx, + std::string& error_reason) + { + const auto& caller_cert = ctx->get_session_context()->caller_cert; + if (caller_cert.empty()) + { + error_reason = "No caller certificate"; + return nullptr; + } + + if (!validity_periods->is_cert_valid_now(caller_cert, error_reason)) + { + // Error is set by the call when necessary + return nullptr; + } + + auto identity = std::make_unique(); + identity->cert = caller_cert; + return identity; + } } diff --git a/src/js/extensions/ccf/crypto.cpp b/src/js/extensions/ccf/crypto.cpp index 80f6fd5d4e6..25f2b2ba6d8 100644 --- a/src/js/extensions/ccf/crypto.cpp +++ b/src/js/extensions/ccf/crypto.cpp @@ -132,10 +132,6 @@ namespace ccf::js::extensions { cid = ccf::crypto::CurveID::SECP256R1; } - else if (curve == "secp256k1") - { - cid = ccf::crypto::CurveID::SECP256K1; - } else if (curve == "secp384r1") { cid = ccf::crypto::CurveID::SECP384R1; @@ -143,8 +139,7 @@ namespace ccf::js::extensions else { return JS_ThrowRangeError( - ctx, - "Unsupported curve id, supported: secp256r1, secp256k1, secp384r1"); + ctx, "Unsupported curve id, supported: secp256r1, secp384r1"); } try diff --git a/src/js/extensions/ccf/request.cpp b/src/js/extensions/ccf/request.cpp index b1181b5b26a..cb0439a5f91 100644 --- a/src/js/extensions/ccf/request.cpp +++ b/src/js/extensions/ccf/request.cpp @@ -3,6 +3,7 @@ #include "ccf/js/extensions/ccf/request.h" +#include "ccf/crypto/verifier.h" #include "ccf/endpoints/authentication/all_of_auth.h" #include "ccf/endpoints/authentication/cert_auth.h" #include "ccf/endpoints/authentication/cose_auth.h" @@ -164,6 +165,21 @@ namespace ccf::js::extensions return caller; } + // For any cert, instead of an id-based lookup for the PEM cert and + // potential associated data, we directly retrieve the cert bytes as + // DER from the identity object, as provided by the session, and + // convert them to PEM. + if ( + auto any_cert_ident = + dynamic_cast(ident.get())) + { + auto policy_name = ccf::get_policy_name_from_ident(any_cert_ident); + caller.set("policy", ctx.new_string(policy_name)); + auto pem_cert = ccf::crypto::cert_der_to_pem(any_cert_ident->cert); + caller.set("cert", ctx.new_string(pem_cert.str())); + return caller; + } + char const* policy_name = nullptr; std::string id; bool is_member = false; diff --git a/src/node/historical_queries.h b/src/node/historical_queries.h index 5c987ce37f2..06d822ae357 100644 --- a/src/node/historical_queries.h +++ b/src/node/historical_queries.h @@ -265,14 +265,14 @@ namespace ccf::historical auto new_it = new_seqnos.begin(); while (new_it != new_seqnos.end()) { - if (*new_it == prev_it->first) + if (prev_it != my_stores.end() && *new_it == prev_it->first) { // Asking for a seqno which was also requested previously - do // nothing and advance to compare next entries ++new_it; ++prev_it; } - else if (*new_it > prev_it->first) + else if (prev_it != my_stores.end() && *new_it > prev_it->first) { // No longer looking for a seqno which was previously requested. // Remove it from my_stores diff --git a/tests/e2e_logging.py b/tests/e2e_logging.py index ce9918e38ad..6519a9e7208 100644 --- a/tests/e2e_logging.py +++ b/tests/e2e_logging.py @@ -665,6 +665,13 @@ def require_new_response(r): assert r.body.text().startswith("Member TLS cert"), r.body.text() require_new_response(r) + # Create a keypair that is not a user + network.create_user("not_a_user", args.participants_curve, record=False) + with primary.client("not_a_user") as c: + r = c.post("/app/multi_auth") + assert r.body.text().startswith("Any TLS cert"), r.body.text() + require_new_response(r) + LOG.info("Authenticate via JWT token") jwt_issuer = infra.jwt_issuer.JwtIssuer() jwt_issuer.register(network) @@ -1015,6 +1022,18 @@ def test_cose_signature_schema(network, args): def test_cose_receipt_schema(network, args): primary, _ = network.find_nodes() + # Make sure the last transaction does not contain application claims + member = network.consortium.get_any_active_member() + r = member.update_ack_state_digest(primary) + with primary.client() as client: + client.wait_for_commit(r) + + service_cert_path = os.path.join(network.common_dir, "service_cert.pem") + service_cert = load_pem_x509_certificate( + open(service_cert_path, "rb").read(), default_backend() + ) + service_key = service_cert.public_key() + with primary.client("user0") as client: r = client.get("/commit") assert r.status_code == http.HTTPStatus.OK @@ -1031,8 +1050,10 @@ def test_cose_receipt_schema(network, args): headers={infra.clients.CCF_TX_ID_HEADER: txid}, log_capture=[], # Do not emit raw binary to stdout ) + if r.status_code == http.HTTPStatus.OK: cbor_proof = r.body.data() + ccf.cose.verify_receipt(cbor_proof, service_key, b"\0" * 32) cbor_proof_filename = os.path.join( network.common_dir, f"receipt_{txid}.cose" ) diff --git a/tests/npm-app/app.json b/tests/npm-app/app.json index efbb5460fca..5179677d65d 100644 --- a/tests/npm-app/app.json +++ b/tests/npm-app/app.json @@ -1585,6 +1585,7 @@ }, "user_cert", "member_cert", + "any_cert", "jwt", "user_cose_sign1", "no_auth" diff --git a/tests/npm-app/src/endpoints/auth.ts b/tests/npm-app/src/endpoints/auth.ts index 52b0f6fa9c6..3d340b37e29 100644 --- a/tests/npm-app/src/endpoints/auth.ts +++ b/tests/npm-app/src/endpoints/auth.ts @@ -36,6 +36,8 @@ export function checkMultiAuth(request: ccfapp.Request): ccfapp.Response { describe_user_cert_ident(lines, request.caller); } else if (request.caller.policy === "member_cert") { describe_member_cert_ident(lines, request.caller); + } else if (request.caller.policy === "any_cert") { + describe_any_cert_ident(lines, request.caller); } else if (request.caller.policy === "jwt") { describe_jwt_ident(lines, request.caller); } else if (request.caller.policy === "user_cose_sign1") { @@ -73,6 +75,14 @@ function describe_member_cert_ident( lines.push(`The caller's cert is:\n${obj.cert}`); } +function describe_any_cert_ident( + lines: Lines, + obj: ccfapp.AnyCertAuthnIdentity, +) { + lines.push("Any TLS cert"); + lines.push(`The caller's cert is:\n${obj.cert}`); +} + function describe_jwt_ident(lines: Lines, obj: ccfapp.JwtAuthnIdentity) { lines.push("JWT"); lines.push( diff --git a/tests/npm_tests.py b/tests/npm_tests.py index 86880cd39b7..49351a13c73 100644 --- a/tests/npm_tests.py +++ b/tests/npm_tests.py @@ -44,7 +44,7 @@ def generate_and_verify_jwk(client): assert r.status_code != http.HTTPStatus.OK # Elliptic curve - curves = [ec.SECP256R1, ec.SECP256K1, ec.SECP384R1] + curves = [ec.SECP256R1, ec.SECP384R1] for curve in curves: priv_pem, pub_pem = infra.crypto.generate_ec_keypair(curve) # Private @@ -305,12 +305,6 @@ def test_npm_app(network, args): r.body.json()["privateKey"], r.body.json()["publicKey"] ) - r = c.post("/app/generateEcdsaKeyPair", {"curve": "secp256k1"}) - 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"] - ) - r = c.post("/app/generateEcdsaKeyPair", {"curve": "secp384r1"}) assert r.status_code == http.HTTPStatus.OK, r.status_code assert infra.crypto.check_key_pair_pem( @@ -475,7 +469,7 @@ def test_npm_app(network, args): pass # Test ECDSA signing + verification - curves = [ec.SECP256R1, ec.SECP256K1, ec.SECP384R1] + curves = [ec.SECP256R1, ec.SECP384R1] for curve in curves: key_priv_pem, key_pub_pem = infra.crypto.generate_ec_keypair(curve) algorithm = {"name": "ECDSA", "hash": "SHA-256"} @@ -577,7 +571,7 @@ def test_npm_app(network, args): assert r.status_code == http.HTTPStatus.OK, r.status_code assert r.body.json() is False, r.body - curves = [ec.SECP256R1, ec.SECP256K1, ec.SECP384R1] + curves = [ec.SECP256R1, ec.SECP384R1] for curve in curves: key_priv_pem, key_pub_pem = infra.crypto.generate_ec_keypair(curve) algorithm = {"name": "ECDSA", "hash": "SHA-256"}