Skip to content

fix(namecheap): extract leaf + chain by scanning PEM armor#41

Merged
albedosehen merged 1 commit into
mainfrom
fix/namecheap-cert-extraction
May 9, 2026
Merged

fix(namecheap): extract leaf + chain by scanning PEM armor#41
albedosehen merged 1 commit into
mainfrom
fix/namecheap-cert-extraction

Conversation

@albedosehen
Copy link
Copy Markdown
Contributor

What

get_info's cert_pem and chain_pem extractors were looking at the wrong XML locations. Namecheap returns:

<Certificates CertificateReturned="true" ReturnType="INDIVIDUAL">
  <Certificate><![CDATA[LEAF_PEM]]></Certificate>
  <CaCertificates>
    <Certificate Type="INTERMEDIATE">
      <Certificate><![CDATA[INT_PEM]]></Certificate>
    </Certificate>
    ...
  </CaCertificates>
</Certificates>

rota was calling first_text("CertificateReturned") and first_text("CACertificate")CertificateReturned is an ATTRIBUTE on <Certificates> (not an element with text) and CACertificate doesn't exist (the element is <CaCertificates> with nested <Certificate>s). Result: cert_pem + chain_pem both empty, is_issued() always false, polling hangs at await_issuance even when Sectigo has issued the cert and status is active.

Found this by manually extracting the oneiric.dev cert via curl + Python while debugging PR #40's polling behavior. The cert was right there in the response that rota was discarding as 'empty'.

Fix

New ApiResponse::pem_blocks(label) scans the raw response for -----BEGIN <label>-----...-----END <label>----- armor and returns each block in document order. Cuts through the nested-element wrapping by ignoring the XML structure entirely for cert payloads.

get_info now calls pem_blocks("CERTIFICATE"); first block is the leaf, rest are the chain (joined with newlines). The CSR present in the same response is skipped automatically because its label is "CERTIFICATE REQUEST" — BEGIN CERTIFICATE----- (with trailing dashes) doesn't substring-match BEGIN CERTIFICATE REQUEST-----.

Cumulative

This is bug #6 in the rota+Namecheap end-to-end renewal pipeline. Prior PRs in this series:

Verified

  • cargo fmt --all --check clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo test --workspace --locked 111 daemon tests pass (+3 new)

rota's `get_info` was looking for `<CertificateReturned>` element
text and `<CACertificate>` element text. Neither matches Namecheap's
actual `ssl.getInfo&Returncertificate=true` response, which carries
`CertificateReturned` as an ATTRIBUTE on `<Certificates>` and packs
PEMs in nested `<Certificate>` elements:

  <Certificates CertificateReturned="true" ReturnType="INDIVIDUAL">
    <Certificate><![CDATA[LEAF_PEM]]></Certificate>
    <CaCertificates>
      <Certificate Type="INTERMEDIATE">
        <Certificate><![CDATA[INTERMEDIATE_1_PEM]]></Certificate>
      </Certificate>
      ...
    </CaCertificates>
  </Certificates>

Result: cert_pem and chain_pem both empty, `is_issued()` false,
polling never terminates even when status==active. So PR #40's
chain-follow lands on the right SSL ID but `await_issuance` still
hangs at the extraction step. Found by extracting the cert manually
out of band when `getInfo` returned status=active for oneiric.dev's
in-flight order: rota's parser yielded empty strings even though
the PEMs were sitting right there in the response.

Fix: new `ApiResponse::pem_blocks(label)` method scans the raw
response for `-----BEGIN <label>-----`...`-----END <label>-----`
armor and returns each block in document order. `get_info` calls
`pem_blocks("CERTIFICATE")`; first block is the leaf, rest are the
chain (concatenated with newlines). The CSR present in the same
response is safely skipped because its label is "CERTIFICATE
REQUEST" and `BEGIN CERTIFICATE-----` doesn't substring-match
`BEGIN CERTIFICATE REQUEST-----`.

This is the 6th and (hopefully) final layer in the rota+Namecheap
end-to-end renewal pipeline, after PRs #36 (reverted), #37 (CDATA
unwrap), #38 (DnsCname variant), #39 (lowercase HostName), #40
(ReplacedBy chain + Status XML path). Tests: 3 new in xml::tests
covering the leaf+chain extraction, the CSR-skip rule, and the
empty-input edge case. Total daemon test count 111 (was 108).
@albedosehen albedosehen merged commit 8f0beca into main May 9, 2026
1 check passed
@albedosehen albedosehen deleted the fix/namecheap-cert-extraction branch May 9, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant