-
Notifications
You must be signed in to change notification settings - Fork 3
scep: send a CertRep from every pkiMessage rejection path #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -651,6 +651,24 @@ 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) | ||
| { | ||
| if (tid == NULL || tid_len == 0) { | ||
| s->keep_alive = 0; | ||
| send_text(s, fd, 400, "Bad Request", "text/plain", ""); | ||
| return WOLFCERT_ERR_PROTOCOL; | ||
| } | ||
|
|
||
| /* A FAILURE CertRep carries no messageData, hence no envelope target. */ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The parser takes the transactionID attribute's inner value whatever its tag, so a peer can send an OCTET STRING or a UTF8String of arbitrary bytes and we echo them back inside a PrintableString - The encoder is pre-existing; what changes here is reachability. Before, these three cases got a text/plain 400 and the transactionID was never re-encoded; now it is, on a message that has passed nothing but the CMS signature check. If these paths stay as CertReps, worth validating the recovered transactionID against the PrintableString repertoire and taking the 400 fallback when it doesn't conform. |
||
| 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 +686,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 +710,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 +731,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 +788,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, | ||
|
|
@@ -788,7 +799,11 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) | |
| } | ||
|
|
||
| if (mt == NULL) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The three branches do the same thing but spell the bookkeeping three ways: And here it does the opposite of what |
||
| send_text(s, fd, 400, "Bad Message", "text/plain", ""); | ||
| s->keep_alive = 0; | ||
| rc = send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, | ||
| "2" /* badRequest */); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The commit message says the three branches use failInfo "2, 0 and 2". All three pass |
||
| if (rc == WOLFCERT_OK) | ||
| rc = WOLFCERT_ERR_PROTOCOL; | ||
| goto out; | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We support exactly one content-encryption OID (
|
||
|
|
@@ -798,7 +813,11 @@ static int handle_pki_op(WolfCertServer* s, int fd, const ScepRequest* req) | |
| 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", ""); | ||
| s->keep_alive = 0; | ||
| send_rc = send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, | ||
| "2" /* badRequest */); | ||
| if (send_rc != WOLFCERT_OK) | ||
| rc = send_rc; | ||
| goto out; | ||
| } | ||
|
|
||
|
|
@@ -807,12 +826,14 @@ 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", ""); | ||
| rc = WOLFCERT_ERR_PROTOCOL; | ||
| s->keep_alive = 0; | ||
| rc = send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len, | ||
| "2" /* badRequest */); | ||
| if (rc == WOLFCERT_OK) | ||
| rc = WOLFCERT_ERR_PROTOCOL; | ||
| } | ||
|
|
||
| out: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -670,6 +670,119 @@ static int check_getnextca_ca_id(const uint8_t* ca_der_buf, size_t ca_der_len) | |
| return 0; | ||
| } | ||
|
|
||
| /* 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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[4] = { NULL, "19", "99", NULL }; | ||
|
|
||
| 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]; | ||
| 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only unguarded AES-128 use in the file. Only round 2 needs a decryptable envelope and the server de-envelops whatever OID arrives, so a file-level |
||
| AES128CBCb, &env, NULL); | ||
|
|
||
| for (i = 0; rc == WOLFCERT_OK && i < 4; ++i) { | ||
| /* The last round omits the transactionID, which no CertRep can echo. */ | ||
| WolfCertScepAttrs a = { | ||
| .transaction_id = i == 3 ? NULL : tid, | ||
| .transaction_id_len = i == 3 ? 0 : sizeof(tid), | ||
| .sender_nonce = snonce, .sender_nonce_len = sizeof(snonce), | ||
| .message_type = msg_type[i], | ||
| }; | ||
| /* Round 2 needs an envelope the CA can open, or it trips the | ||
| * decrypt branch first. */ | ||
| const uint8_t* content = i == 2 ? env.data : junk; | ||
| size_t content_len = i == 2 ? 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; | ||
|
|
||
| if (i == 3) { | ||
| ok = ok && resp.status_code == 400 && resp.body_len == 0; | ||
| } | ||
| else { | ||
| ok = 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, "2") == 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; | ||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
|
||
| WOLFCERT_XFREE(signer, NULL); | ||
| wolfcert_buffer_free(&env); | ||
| wolfcert_buffer_free(&kder); | ||
| wolfcert_buffer_free(&csr); | ||
| wolfcert_key_free(key); | ||
|
|
||
| return rc; | ||
| } | ||
|
|
||
| int main(void) | ||
| { | ||
| REQUIRE(wolfcert_init(NULL) == WOLFCERT_OK); | ||
|
|
@@ -789,6 +902,10 @@ int main(void) | |
| /* The CA identifier belongs on GetNextCACert as well (RFC 8894 4.6.1). */ | ||
| REQUIRE(check_getnextca_ca_id(ca_der->buffer, ca_der->length) == 0); | ||
|
|
||
| REQUIRE(check_malformed_dispatch(wolfcert_server_port(s), &kcfg, | ||
| ca_der->buffer, ca_der->length) | ||
| == WOLFCERT_OK); | ||
|
|
||
| /* One-shot SCEP over https:// must refuse to run unverified, the same rule | ||
| * the session open applies: verify_server is the only peer-verification | ||
| * switch, so leaving it off would complete a silent anonymous handshake. */ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The helper clears
keep_aliveon its 400 fallback but leaves it to the caller on the CertRep path, and the callers are split: handle_pki_op (:802, :816, :832) and handle_enroll (:688, :696) set it, while handle_enroll's queue-full and handle_get_cert_initial's badCertId branches deliberately do not. Every combination is coherent today, but split ownership means a future caller that returns non-OK without the assignment emitsConnection: keep-aliveon a socket we then close.The parse-failure branch five lines up (:796) already does exactly that, as do issue_and_reply()'s "Bad CSR" 400 and the 500 sites. All pre-existing, but this PR introduces the invariant and leaves its siblings out of it. Setting
keep_alive = 0inside send_text() for status >= 400 would make it unforgettable. Note also that nothing asserts the header - WolfCertHttpResponse exposes none - so a branch answering with a CertRep and keep_alive still 1 would pass the suite.