Skip to content
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

HTTP challenge with DNS fallback #100

Open
oalders opened this issue Jan 22, 2025 · 3 comments
Open

HTTP challenge with DNS fallback #100

oalders opened this issue Jan 22, 2025 · 3 comments
Assignees

Comments

@oalders
Copy link

oalders commented Jan 22, 2025

In dealing with wildcard domains, it's handy to do the HTTP challenges first and then fall back to DNS for any names (ie wildcards) which failed the HTTP challenge. I could not find a way to do this with the current API, so I have been doing the following. I realize that this is bad for poking at the internals, but as a stopgap it does the trick. Am I missing something?

Please note: I can't really take credit for this. It was quite late at night and I had claude.ai generate most of it in order to rest my brain.

package Crypt::LE::Batch;

use strict;
use warnings;

use parent 'Crypt::LE';

use Crypt::LE qw( OK );

sub accept_challenge {
    my ( $self, $cb, $params, $type ) = @_;

    # For non-dns challenges, use parent behavior
    return $self->SUPER::accept_challenge( $cb, $params, $type )
        unless $type eq 'dns';

    # Store original state
    my $full_challenges        = $self->{challenges};
    my $full_domains           = $self->{loaded_domains};
    my $full_active_challenges = $self->{active_challenges};

    # Filter to only keep wildcard domains
    my $wildcard_challenges = {};
    my @wildcard_domains;
    foreach my $domain (@$full_domains) {
        next unless $domain =~ /^\*\./;    # only wildcards
        if ( exists $full_challenges->{$domain} ) {
            $wildcard_challenges->{$domain} = $full_challenges->{$domain};
            push @wildcard_domains, $domain;
        }
    }

    # Temporarily replace with filtered set
    $self->{challenges}        = $wildcard_challenges;
    $self->{loaded_domains}    = \@wildcard_domains;
    $self->{active_challenges} = {};    # Start fresh for DNS challenges

    # Let parent handle the actual validation with filtered domains
    my $status = $self->SUPER::accept_challenge( $cb, $params, $type );

    if ( $status == OK ) {

        # Merge the active challenges back
        $self->{active_challenges} = {
            %{ $full_active_challenges || {} },
            %{ $self->{active_challenges} || {} }
        };
    }

    # Restore remaining original state
    $self->{challenges}     = $full_challenges;
    $self->{loaded_domains} = $full_domains;

    return $status;
}

# Override verify_challenge to only verify wildcards for DNS
sub verify_challenge {
    my ( $self, $cb, $params, $type ) = @_;

    return $self->SUPER::verify_challenge( $cb, $params, $type )
        unless $type && $type eq 'dns';

    # Store original state
    my $full_domains = $self->{loaded_domains};

    # Filter to only verify wildcard domains
    my @wildcard_domains = grep { /^\*\./ } @$full_domains;
    $self->{loaded_domains} = \@wildcard_domains;

    # Let parent handle verification
    my $status = $self->SUPER::verify_challenge( $cb, $params, $type );

    # Restore original state
    $self->{loaded_domains} = $full_domains;

    return $status;
}

1;
@do-know do-know self-assigned this Jan 30, 2025
@do-know
Copy link
Owner

do-know commented Jan 30, 2025

I'll see if Claude got it right :) Does that code work for you? Could you plase also elaborate a bit on what calls for this specific workflow? Usually it is either HTTP or DNS "for all", so I'd like to understand better in which scenario it is expected that even though DNS verification is necessary (such as for the wildcard), the system might need to retry on HTTP for non-wildcard names.

@oalders
Copy link
Author

oalders commented Jan 30, 2025

The code does work for me, but the package name that Claude came up with is not right. I've got a branch going at https://github.com/oalders/Crypt-LE/tree/challenge-with-fallback

The background is that I'm implementing a custom cert provisioner for cPanel. There will be many cases where one or more domains on the cert will contain wildcards.

So, if a cert has wildcard and non-wildcard domains I can end up in a weird state where not all domain challenges will pass using HTTP, but if I switch to DNS there are cases where the wildcard domain needs a challenge TXT record on the same domain as a bare domain. (I think that was the case). For that case you could normally just create two TXT records on the same domain, but cPanel doesn't seem to have a way to do that with the Perl APIs. If you give it two records for the same domain the second will clobber the first. So, I was ending up with a mixed bag where HTTP might cover most cases and DNS might also cover most cases, but neither covered all cases.

As far as I can tell this kind of HTTP => DNS fallback is similar to how cPanel has implemented its Let's Encrypt provisioning.

I ended up using your work because of the EAB support (🙏🏻) and the out of the box ZeroSSL support was a nice touch as well.

So, I thank you for your work and if this looks like something that you might be able to use, I'm happy to rework it to your standards.

@oalders
Copy link
Author

oalders commented Jan 30, 2025

Found some notes:

ACME appears to want to have the wildcard *.thing.example.com in a record which has the same name as the challenge record for thing.example.com So, you need to create two different TXT records at the same host.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants