You appear to be well aware of delayed expansion since you are correctly using it most of the time.
However, what you have in your code I usually call nested variables, because there are no real arrays in batch scripting since every array element is nothing but an individual scalar variable (pems[0], pems[1], pems[2], etc.); so we might refer to something like this also as pseudo-arrays.
Anyway, something like echo !pems[%selectedPem%]! can correctly expanded, unless it is placed within a line or block of code in which selectedPem becomes updated before, because then we would require delayed expansion for selectedPem as well. echo !pems[!selectedPem!]! does not work, because it tries to expand variables pems[ and ], and selectedPem is interpreted as a literal string.
Before moving forward, let us first go back to echo !pems[%selectedPem%]!: why is this expandable? Well, the trick is we have two expansion phases here, the normal or immediate expansion (%), followed by the delayed expansion (!). The important thing is the sequence: the inner one of the nested variables (hence the array index) must be expended before the outer one (so the array element). Something like echo %pems[!selectedPem!]% cannot be expanded correctly, because there is (most likely) no variable named pems[!selectedPem!], and after expanding it to an empty string, the two ! disappear and so the delayed expansion phase does never see them.
Now let us go a step further: to expand something similar to echo !pems[%selectedPem%]! inside of a line or block of code that also updates selectedPem, we must avoid immediate expansion. We already learned that delayed expansion cannot be nested, but there are some other expansions:
- The callcommand introduces another expansion phase after delayed expansion, which again handles%-signs; so the full sequence is immediate expansion, delayed expansion and another%-expansion. Dismissing the first one, let us make use of the latter two in a way that the inner one of the nested variables becomes expanded before the outer one, meaning:call echo %%pems[!selectedPem!]%%. To skip the immediate expansion phase we simply double the%signs, which become replaced by a literal one each, so we haveecho %pems[!selectedPem!]%after immediate expansion. The next step is delayed expansion, then the said next%-expansion.
 You should take notice that thecallmethod is quite slow, so heavy usage could drastically reduce the overall performance of your script.
- Expansion of forloop variables happens after immediate expansion, but before delayed expansion. So let us wrap aforloop around that iterates once only and returns the value ofselectedPem, like this:for %%Z in (!selectedPem!) do echo !pems[%%Z]!. This works, becausefordoes not access the file system unless a wild-card*or?appears, which should not be used in variable names or (pseudo-)array indexes anyway.
 Instead of a standardforloop,for /Fcould be used also:for /F %%Z in ("!selectedPem!") do echo !pems[%%Z]!(there is no option string like"delims="required in caseselectedPemis expected to contain just an index number).
 Afor /Lloop could be used too (for numeric indexes, of course):for /L %%Z in (!selectedPem!,1,!selectedPem!) do echo !pems[%%Z]!.
- Implicit expansion established by the set /Acommand, meaning that neither%nor!is necessary to read variables, can be used too, but only if the array element contains a numeric value (note that/Astands for arithmetics). Since this is a feature specific toset /A, this kind of expansion happens during command execution, which in turn happens after delayed expansion. So we can use that like this:set /A "pem=pems[!selectedPem!]" & echo !pem!.
- Just for the sake of completeness, here is one more way: set /A, when executed incmdrather than in batch context, outputs the (last) result on the console; given that this constitutes the index number, we could capture this byfor /Fand expand the array element like this:for /F %%Z in ('set /A "selectedPem"') do echo !pems[%%Z]!.set /Ais executed in a newcmdinstance byfor /F, so this approach is not the fastest one, of course.
There is a great and comprehensive post concerning this topic which you should go through it detail: Arrays, linked lists and other data structures in cmd.exe (batch) script.
For parsing of command lines and batch scripts as well as variable expansion in general, refer to this awesome thread: How does the Windows Command Interpreter (CMD.EXE) parse scripts?
Now let us take a look at your code. There are several issues:
- use cd /Dinstead ofcdin order to change also the drive if necessary; by the way, the interim variablediris not necessary (let me recommend to not use variable names that equal internal or external commands for the sake of readability), simply usecdon the path immediately;
- you missed using delayed expansion in line set pems[%pemI%]=%%~nxp, it should readset pems[!pemI!]=%%~nxpaspemIbecomes updated within the surroundingforloop;
- you either need delayed expansion in line set /a pemI=%pemI%+1too, or you make use of the implicit variable expansion ofset /A, soset /A pemI=pemI+1would work; this can even be more simplified however:set /A pemI+=1, which is totally equivalent;
- I would use case-insensitive comparison particularly when it comes to user input, like your check for Q, which would beif /I "!selectedPem!"=="q";
- now we come to a line that needs what we have learned above: ECHO !pems[%selectedPem%]!needs to becomecall echo %%pems[!selectedPem!]%%; an alternative way usingforis inserted too in a comment (rem);
- then there is another line with the same problem: set privatekey=!pems[%selectedPem%]!needs to becomecall set privatekey=%%pems[!selectedPem!]%%(or the approach based onfortoo);
- once again you missed using delayed expansion, this time in line ECHO You chose %privatekey%, which should readecho You chose !privatekey!;
- finally I improved quotation of your code, in particular that of setcommands, which should better be written likeset "VAR=Value", so any invisible trailing white-spaces do not become part of the value, and the value itself becomes protected if it contains special characters, but the quotation marks do not become part of the value, which could disturb particularly for contatenation;
So here is the fixed code (with all your original rem remarks removed):
@echo off
setlocal EnableDelayedExpansion 
cd /D "%~dp0"
echo Performing initial search of private and public keys...
set "pemI=0"
set "keyI=0"
for %%p in (*.pem) do (
    set "pems[!pemI!]=%%~nxp"
    set /A "pemI+=1"
)
set /A "totalPems=pemI-1"
if defined pems[0] (
    echo PEM Files Found:
    for /L %%p in (0,1,%totalPems%) do (
        echo %%p^) !pems[%%p]!
    )
    set /P selectedPem="Enter a number or Q: "
    if /I "!selectedPem!"=="q" (
        echo Skipping private key selection.
    ) else (
        echo !selectedPem!
        echo %pems[0]%
        call echo %%pems[!selectedPem!]%%
        rem for %%Z in (!selectedPem!) do echo !pems[%%Z]!
        call set "privatekey=%%pems[!selectedPem!]%%"
        rem for %%Z in (!selectedPem!) do set "privatekey=!pems[%%Z]!"
        echo You chose !privatekey!
    )   
)
I have to admit I did not check the logic of your script in detail though.