From 74643ed7e9fc4628f94d02d49b55ed47113de570 Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Fri, 21 Aug 2026 14:32:35 +0900 Subject: [PATCH] scep: answer unprocessable pkiMessages with a CertRep - send_pki_failure() sends a signed CertRep with pkiStatus 2 and a failInfo, built from the request's transactionID and senderNonce, carrying no certificate and no envelope target. - handle_pki_op's mandatory-attribute guard also rejects a missing messageType, so a message omitting any of the three RFC 8894 section 3.2.1 attributes gets the same HTTP 400. - An undecryptable pkcsPKIEnvelope and an unrecognized messageType answer through send_pki_failure instead, clearing keep_alive and logging a failed send through a new send_rc. The de-envelop branch keeps its own error in rc and picks failInfo 0 (badAlg) when the content cipher is unsupported and 2 (badRequest) otherwise; the messageType branch returns WOLFCERT_ERR_PROTOCOL. - handle_enroll's three rejections and handle_get_cert_initial's unknown transactionID call send_pki_failure too; handle_get_cert_initial drops its signer certificate parameters and the env_target locals they fed. - check_required_attrs gains a round for a message carrying no messageType, and requires the reply's senderNonce. - check_malformed_dispatch POSTs three hand-built pkiMessages - an undecryptable payload, an unrecognized messageType, and an envelope whose content cipher OID is unassigned - and requires a CertRep with pkiStatus 2, the expected failInfo, the echoed transactionID, a fresh senderNonce per round, and no enveloped messageData. A further request over a keep-alive socket requires the reply to arrive and the connection to close. - Both raw-POST helpers and their calls compile only where wolfSSL has the AES-128-CBC cipher they envelop with. Issue: F-8033 --- src/scep/scep_server.c | 72 +++++---- tests/integration/test_scep_roundtrip.c | 194 +++++++++++++++++++++++- 2 files changed, 234 insertions(+), 32 deletions(-) diff --git a/src/scep/scep_server.c b/src/scep/scep_server.c index ce0889b..3458bf7 100644 --- a/src/scep/scep_server.c +++ b/src/scep/scep_server.c @@ -651,6 +651,18 @@ static int issue_and_reply(WolfCertServer* s, int fd, return rc; } +/* Answer a rejected pkiMessage with a signed CertRep carrying pkiStatus + * FAILURE and failInfo, per RFC 8894 section 3.2.1. */ +static int send_pki_failure(WolfCertServer* s, int fd, + const uint8_t* tid, size_t tid_len, + const uint8_t* snonce, size_t snonce_len, + const char* fail_info) +{ + /* A FAILURE CertRep carries no messageData, hence no envelope target. */ + return send_cert_rep(s, fd, NULL, 0, NULL, 0, + tid, tid_len, snonce, snonce_len, "2", fail_info); +} + /* Handle messageType=19 (PKCSReq) or 17 (RenewalReq) freshly arrived. */ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, const WolfCertBuffer* csr, @@ -668,18 +680,16 @@ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, csr->data, csr->len, s->heap) != WOLFCERT_OK) { /* Report the failure as a CertRep, then close the connection. */ s->keep_alive = 0; - return send_cert_rep(s, fd, NULL, 0, env_target, env_target_len, - tid, tid_len, snonce, snonce_len, - "2", "2" /* badRequest */); + return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + "2" /* badRequest */); } if (check_challenge(csr->data, csr->len, s->cfg_challenge, s->heap) != WOLFCERT_OK) { /* Report the failure as a CertRep, then close the connection. */ s->keep_alive = 0; - return send_cert_rep(s, fd, NULL, 0, env_target, env_target_len, - tid, tid_len, snonce, snonce_len, - "2", "2" /* badRequest */); + return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + "2" /* badRequest */); } if (s->cfg.scep_require_approval) { @@ -694,9 +704,8 @@ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, if (add != WOLFCERT_OK) { /* Queue full - fail rather than silently losing requests. */ - return send_cert_rep(s, fd, NULL, 0, env_target, env_target_len, - tid, tid_len, snonce, snonce_len, - "2", "2" /* badRequest */); + return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + "2" /* badRequest */); } } @@ -716,18 +725,13 @@ static int handle_enroll(WolfCertServer* s, int fd, const char* mt, * to be pending forever. */ static int handle_get_cert_initial(WolfCertServer* s, int fd, const uint8_t* tid, size_t tid_len, - const uint8_t* snonce, size_t snonce_len, - const uint8_t* signer_cert, size_t signer_cert_len) + const uint8_t* snonce, size_t snonce_len) { ScepPriv* p = (ScepPriv*)s->priv; ScepPending* e = pending_find(p, tid, tid_len); if (e == NULL) { - const uint8_t* env_target = signer_cert ? signer_cert : s->ca.cert_der; - size_t env_target_len = signer_cert ? signer_cert_len : s->ca.cert_der_len; - - return send_cert_rep(s, fd, NULL, 0, env_target, env_target_len, - tid, tid_len, snonce, snonce_len, - "2", "4" /* badCertId: no such transaction */); + return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + "4" /* badCertId: no such transaction */); } /* Approve on first poll. A production implementation would hold @@ -778,6 +782,7 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) uint8_t* signer_cert = NULL; size_t signer_cert_len = 0; WolfCertBuffer csr = { 0 }; + int send_rc = WOLFCERT_OK; int rc = wolfcert_scep_parse_pki_message(req->body, req->body_len, &env, &tid, &tid_len, &snonce, &snonce_len, &rnonce, &rnonce_len, @@ -787,25 +792,33 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) goto out; } - if (tid == NULL || tid_len == 0 || snonce == NULL || snonce_len == 0) { + /* RFC 8894 section 3.2.1 requires all three in every message, so one that + * omits any of them is not a pkiMessage a CertRep could answer. */ + if (tid == NULL || tid_len == 0 || snonce == NULL || snonce_len == 0 || + mt == NULL || mt[0] == '\0') { s->keep_alive = 0; send_text(s, fd, 400, "Bad Message", "text/plain", ""); rc = WOLFCERT_ERR_PROTOCOL; goto out; } - if (mt == NULL) { - send_text(s, fd, 400, "Bad Message", "text/plain", ""); - goto out; - } - rc = wolfcert_scep_deenvelop(s->ca.cert_der, s->ca.cert_der_len, s->ca.key_der, s->ca.key_der_len, env.data, env.len, &csr, s->heap); if (rc != WOLFCERT_OK && strcmp(mt, "20") != 0) { /* Decryption matters for 19/17 (CSR inside); for 20 the payload * is IssuerAndSubject which the server matches by txid anyway. */ - send_text(s, fd, 400, "Cannot Decrypt", "text/plain", ""); + const char* fail_info = rc == WOLFCERT_ERR_UNSUPPORTED + ? "0" /* badAlg */ + : "2" /* badRequest */; + + /* Closed by the non-OK return; the flag is for the header. */ + s->keep_alive = 0; + send_rc = send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + fail_info); + if (send_rc != WOLFCERT_OK) + WOLFCERT_LOG_DBG("scep", "CertRep send failed: %d", send_rc); + goto out; } @@ -814,11 +827,16 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) tid, tid_len, snonce, snonce_len); } else if (strcmp(mt, "20") == 0) { - rc = handle_get_cert_initial(s, fd, tid, tid_len, snonce, snonce_len, - signer_cert, signer_cert_len); + rc = handle_get_cert_initial(s, fd, tid, tid_len, snonce, snonce_len); } else { - send_text(s, fd, 400, "Bad Message", "text/plain", ""); + /* Closed by the non-OK return; the flag is for the header. */ + s->keep_alive = 0; + send_rc = send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, + "2" /* badRequest */); + if (send_rc != WOLFCERT_OK) + WOLFCERT_LOG_DBG("scep", "CertRep send failed: %d", send_rc); + rc = WOLFCERT_ERR_PROTOCOL; } diff --git a/tests/integration/test_scep_roundtrip.c b/tests/integration/test_scep_roundtrip.c index 8e09eb9..a31a933 100644 --- a/tests/integration/test_scep_roundtrip.c +++ b/tests/integration/test_scep_roundtrip.c @@ -797,6 +797,7 @@ static int check_getnextca_ca_id(const uint8_t* ca_der_buf, size_t ca_der_len) return 0; } +#if defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_128) /* RFC 8894 section 3.2.1 requires transactionID and a fresh senderNonce in every * pkiMessage; the client always sends both, so POST hand-built ones instead. */ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, @@ -830,8 +831,9 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, /* Each round omits one required attribute; the last is the control that * proves this raw-POST harness reaches the issuance path at all. */ - for (i = 0; rc == WOLFCERT_OK && i < 5; ++i) { - WolfCertScepAttrs a = { .message_type = "19" }; + for (i = 0; rc == WOLFCERT_OK && i < 7; ++i) { + WolfCertScepAttrs a = { .message_type = i == 4 ? NULL : + i == 5 ? "" : "19" }; WolfCertBuffer msg = { 0 }; WolfCertBuffer renv = { 0 }; uint8_t *r_tid = NULL, *r_sn = NULL, *r_rn = NULL, *r_sc = NULL; @@ -856,7 +858,11 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, a.transaction_id = tid; a.transaction_id_len = sizeof(tid); a.sender_nonce = snonce; a.sender_nonce_len = 0; } - else { /* control: both present */ + else if (i == 4 || i == 5) { /* no / zero-length messageType */ + a.transaction_id = tid; a.transaction_id_len = sizeof(tid); + a.sender_nonce = snonce; a.sender_nonce_len = sizeof(snonce); + } + else { /* control: all three present */ a.transaction_id = tid; a.transaction_id_len = sizeof(tid); a.sender_nonce = snonce; a.sender_nonce_len = sizeof(snonce); } @@ -873,8 +879,8 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, msg.data, msg.len, 0, &rsp, &rsp_len); wolfcert_buffer_free(&msg); - if (i < 4) { - /* Nothing to echo, so no conforming CertRep exists. */ + if (i < 6) { + /* An attribute that is absent or empty is not a pkiMessage. */ ok = (st == 400); } else { @@ -886,6 +892,7 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, ok = ok && r_tid != NULL && r_tidl == sizeof(tid) && memcmp(r_tid, tid, sizeof(tid)) == 0 && r_st != NULL && strcmp(r_st, "0") == 0 && renv.len > 0 && + r_sn != NULL && r_snl == sizeof(snonce) && r_rn != NULL && r_rnl == sizeof(snonce) && memcmp(r_rn, snonce, sizeof(snonce)) == 0; @@ -937,6 +944,174 @@ static int check_required_attrs(WolfCertServer* s, const WolfCertKeyCfg* kcfg, return rc; } +/* handle_pki_op's dispatch failures answer with a signed CertRep FAILURE, not + * a bare HTTP error. The client cannot produce these messages, so POST + * hand-built ones. Owns and frees everything it makes. */ +static int check_malformed_dispatch(uint16_t port, const WolfCertKeyCfg* kcfg, + const uint8_t* ca_der_buf, size_t ca_der_len) +{ + static const uint8_t junk[4] = { 0x04, 0x02, 0xAB, 0xCD }; + static const char* const msg_type[3] = { "19", "99", "19" }; + static const char* const want_fi[3] = { "2", "2", "0" }; + + WolfCertCertMeta meta = { .subject_dn = "CN=scep-dispatch" }; + WolfCertKey* key = NULL; + WolfCertBuffer csr = { 0 }; + WolfCertBuffer kder = { 0 }; + WolfCertBuffer env = { 0 }; + uint8_t* signer = NULL; + size_t signer_len = 0; + uint8_t tid[16], snonce[16]; + uint8_t prev_sn[16]; + uint8_t* bad_env = NULL; + int have_prev = 0; + char url[160]; + size_t i; + int rc; + + memset(tid, 0x33, sizeof(tid)); + memset(snonce, 0x44, sizeof(snonce)); + snprintf(url, sizeof(url), + "http://127.0.0.1:%u/scep?operation=PKIOperation", port); + + rc = wolfcert_key_generate(kcfg, &key); + if (rc == WOLFCERT_OK) + rc = wolfcert_csr_build(key, &meta, &csr); + if (rc == WOLFCERT_OK) + rc = wolfcert_key_to_der(key, &kder); + if (rc == WOLFCERT_OK) + rc = wolfcert_scep_self_signed_rsa((RsaKey*)key->impl, csr.data, + csr.len, &signer, &signer_len, NULL); + if (rc == WOLFCERT_OK) + rc = wolfcert_scep_envelop(ca_der_buf, ca_der_len, csr.data, csr.len, + AES128CBCb, &env, NULL); + + /* The last round needs a cipher the CA cannot run: copy the envelope and + * point its algorithm at an unassigned OID under the same arc. */ + if (rc == WOLFCERT_OK) { + static const uint8_t aes128_cbc[] = + { 0x06,0x09,0x60,0x86,0x48,0x01,0x65,0x03,0x04,0x01,0x02 }; + uint8_t* at = NULL; + + bad_env = (uint8_t*)WOLFCERT_XMALLOC(env.len, NULL); + if (bad_env == NULL) + rc = WOLFCERT_ERR_MEMORY; + if (rc == WOLFCERT_OK) { + memcpy(bad_env, env.data, env.len); + at = (uint8_t*)memmem(bad_env, env.len, aes128_cbc, + sizeof(aes128_cbc)); + if (at == NULL) + rc = -1; + else + at[sizeof(aes128_cbc) - 1] = 0x63; + } + } + + for (i = 0; rc == WOLFCERT_OK && i < 3; ++i) { + WolfCertScepAttrs a = { + .transaction_id = tid, .transaction_id_len = sizeof(tid), + .sender_nonce = snonce, .sender_nonce_len = sizeof(snonce), + .message_type = msg_type[i], + }; + /* Round 1 needs an envelope the CA can open, or it trips the + * decrypt branch first; round 2 needs the unrunnable one. */ + const uint8_t* content = i == 1 ? env.data : + i == 2 ? bad_env : junk; + size_t content_len = i >= 1 ? env.len : sizeof(junk); + WolfCertBuffer msg = { 0 }; + WolfCertBuffer renv = { 0 }; + uint8_t *r_tid = NULL, *r_sn = NULL, *r_rn = NULL, *r_sc = NULL; + size_t r_tidl = 0, r_snl = 0, r_rnl = 0, r_scl = 0; + char *r_mt = NULL, *r_st = NULL, *r_fi = NULL; + WolfCertHttpResponse resp = { 0 }; + + rc = wolfcert_scep_build_pki_message(content, content_len, + signer, signer_len, kder.data, kder.len, + SHA256h, &a, &msg, NULL); + if (rc == WOLFCERT_OK) { + WolfCertHttpRequest req = { + .method = "POST", + .url = url, + .content_type = "application/x-pki-message", + .body = msg.data, + .body_len = msg.len, + }; + int ok = wolfcert_http_request(&req, &resp) == WOLFCERT_OK && + resp.status_code == 200 && resp.body != NULL; + + ok = ok && wolfcert_scep_parse_pki_message(resp.body, + resp.body_len, &renv, &r_tid, &r_tidl, &r_sn, + &r_snl, &r_rn, &r_rnl, &r_mt, &r_st, &r_sc, + &r_scl, &r_fi, NULL) == WOLFCERT_OK; + + ok = ok && r_mt != NULL && strcmp(r_mt, "3") == 0 && + r_st != NULL && strcmp(r_st, "2") == 0 && + r_fi != NULL && strcmp(r_fi, want_fi[i]) == 0 && + r_tid != NULL && r_tidl == sizeof(tid) && + memcmp(r_tid, tid, sizeof(tid)) == 0 && + r_rn != NULL && r_rnl == sizeof(snonce) && + memcmp(r_rn, snonce, sizeof(snonce)) == 0 && + renv.len == 0; + + /* Each reply carries a fresh senderNonce of its own. */ + ok = ok && r_sn != NULL && r_snl == sizeof(prev_sn) && + (!have_prev || + memcmp(r_sn, prev_sn, sizeof(prev_sn)) != 0); + if (ok) { + memcpy(prev_sn, r_sn, sizeof(prev_sn)); + have_prev = 1; + } + + WOLFCERT_XFREE(r_tid, NULL); WOLFCERT_XFREE(r_sn, NULL); + WOLFCERT_XFREE(r_rn, NULL); WOLFCERT_XFREE(r_sc, NULL); + WOLFCERT_XFREE(r_mt, NULL); WOLFCERT_XFREE(r_st, NULL); + WOLFCERT_XFREE(r_fi, NULL); + wolfcert_buffer_free(&renv); + wolfcert_http_response_free(&resp); + if (!ok) + rc = -1; + } + + wolfcert_buffer_free(&msg); + } + + /* Ask to keep the connection alive: the CertRep must come back and the + * server must then hang up. Holding the socket open instead times the + * read out, which raw_http_req reports as -1 rather than 200. */ + if (rc == WOLFCERT_OK) { + WolfCertScepAttrs a = { + .transaction_id = tid, .transaction_id_len = sizeof(tid), + .sender_nonce = snonce, .sender_nonce_len = sizeof(snonce), + .message_type = "99", + }; + WolfCertBuffer msg = { 0 }; + + rc = wolfcert_scep_build_pki_message(env.data, env.len, signer, + signer_len, kder.data, kder.len, SHA256h, &a, &msg, NULL); + if (rc == WOLFCERT_OK) { + if (raw_http_req(port, "POST", "/scep?operation=PKIOperation", + "application/x-pki-message", + msg.data, msg.len, 1, NULL, NULL) != 200) { + fprintf(stderr, "FAIL %s:%d dispatch failure did not answer " + "and close\n", __FILE__, __LINE__); + rc = -1; + } + wolfcert_buffer_free(&msg); + } + } + + WOLFCERT_XFREE(bad_env, NULL); + WOLFCERT_XFREE(signer, NULL); + wolfcert_buffer_free(&env); + wolfcert_buffer_free(&kder); + wolfcert_buffer_free(&csr); + wolfcert_key_free(key); + + return rc; +} + +#endif /* HAVE_AES_CBC && WOLFSSL_AES_128 */ + int main(void) { REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); @@ -1098,9 +1273,18 @@ int main(void) REQUIRE(raw_http_status(wolfcert_server_port(s), "/scep?operation=PKIOperation&message=QUJD", "XYZ") == 400); /* body freed */ +#if defined(HAVE_AES_CBC) && defined(WOLFSSL_AES_128) REQUIRE(check_required_attrs(s, &kcfg, ca_der->buffer, ca_der->length) == WOLFCERT_OK); + REQUIRE(check_malformed_dispatch(wolfcert_server_port(s), &kcfg, + ca_der->buffer, ca_der->length) + == WOLFCERT_OK); +#else + printf("SKIP required-attrs and malformed-dispatch " + "(wolfSSL built without AES-128-CBC)\n"); +#endif + #ifdef WOLFCERT_HAVE_ED25519 /* Ed25519 signer must be rejected cleanly (RFC 8894 requires RSA). */ WolfCertKeyCfg edcfg = { .type = WOLFCERT_KEY_ED25519, .param = 0,