fszlin / certes

A client implementation for the Automated Certificate Management Environment (ACME) protocol
MIT License
543 stars 119 forks source link

Confusion on base documentation #304

Open ParkerRedford opened 1 year ago

ParkerRedford commented 1 year ago

I'm trying to follow the instructions in the documentation for the http challenge, but it fails at "await order.Generate". The error says "Order's status ("pending") is not acceptable for finalization".

I do have the endpoint ".well-known/acme-challenge/" open for file downloads. However, the docs doesn't say how the file should be formed nor how the file should be returned.

The httpChallenge.Validate() function does hit the endpoint with only the token value.

On my server, the file name is saved as "token.key" and the endpoint returns the file name as "token.key" as well, so the the endpoint does search for the key pair.

I don't know what I am missing to get this to work. The only thing I can think of is that the validation is being hit too soon, but I don't know if that's the case.

webprofusion-chrisc commented 1 year ago

Some familiarity with ACME is required and expected in order for you to use this library to implement to ACME workflow. It may be worth reading through https://www.rfc-editor.org/rfc/rfc8555.html to understand what's expected to happen but the general flow is:

Do you need to implement your own ACME workflow or could you use an established ACME client tool (https://certifytheweb.com etc) to achieve the same result?

ParkerRedford commented 1 year ago

I did managed to get the challenge validated by using a while loop to wait it out. I still get the same error message when I get to .Generate I must be missing the connection between the download and the challenge.

I'm trying to implement my own ACME workflow using YARP because LettuceEncrypt doesn't work very well. I don't know why Microsoft insists on using LettuceEncrypt.

webprofusion-chrisc commented 1 year ago

I'd imagine the author of that having been a member of their team has some considerable weight.

Ok, so once you have one challenge per identifier validated (so if your cert has domain.com and www.domain.com on it then you'd have two challenges to complete) you can move on to polling the Order until it's status is valid or ready, if you jump straight to Generate without checking the order first then it may not be ready yet (it can take a few seconds to transition from pending/valid to ready).

ParkerRedford commented 1 year ago

I don't see how to validate the order. It won't allow me to wait for the status change. The status shows WaitingForActivation then gives me an error Can not find issuer 'C=US,O=(STAGING) Internet Security Research Group,CN=(STAGING) Pretend Pear X1' for certificate 'C=US,O=(STAGING) Internet Security Research Group,CN=(STAGING) Bogus Broccoli X2'

That error happens after I created the pfxBuilder, so it's not coming from the generate function.

webprofusion-chrisc commented 1 year ago

WaitingForActivation means you forgot to await an async task and the object you have is the task, not the result.

The "pretend pear" issue is that you are testing with Let's Encrypt staging and Certes by default has never heard of this (fake) root certificate. To override that you need to use pfxBuilder.AddIssuers(<bytes of the root cert you want to trust>). To do that you can use:

 using (TextReader textReader = new StringReader(certAsPemString))
  {
      var pemReader = new PemReader(textReader);

      var pemObj = pemReader.ReadPemObject();

      var certBytes = pemObj.Content;
      _issuerCertCache.Add(certBytes);
  }

where certAsPemString is the root cert in PEM format. You can grab them from https://github.com/letsencrypt/website/tree/master/static/certs/staging

ParkerRedford commented 1 year ago

I don't see an async task; I just see order.Resource().Result.Status

Other than that, it worked. I have successfully created the pfx file. The thing was I had to create a poll for validating the challenge. I was hitting .Generate too early, which the docs never mentioned.

webprofusion-chrisc commented 1 year ago

Cool, it's best practise to make your method async and await the task (order.Resource() is a task) rather than accessing .Result directly but if it works for you that's all that matters.

ParkerRedford commented 1 year ago

I am getting The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot. The issuer is R3 and the subject is the domain name that I used.

The certutil command did give me some warnings though No key provider information, Cannot find the certificate and private key for decryption., Private key is NOT plain text exportable. I am not sure those are imported.

HaroldH76 commented 1 year ago

@webprofusion-chrisc do you have an example of the polling?

poll the challenge/order status to see how the CA checks for your challenges are going. This is necessary because challenges may take several seconds or even minutes to complete.

    var order = await acme.NewOrder(new[] { "mytest.test.com" });
    var authz = (await order.Authorizations()).First();
    var dnsChallenge = await authz.Dns();
    var dnsTxt = acme.AccountKey.DnsTxt(dnsChallenge.Token);

    Console.WriteLine(dnsTxt);

    var challengeResult = await dnsChallenge.Validate();

    // Polling?
webprofusion-chrisc commented 1 year ago

@HaroldH76 I use a loop to fetch the latest version of the challenge status and test the status to see if it's valid. There could be other better ways: https://github.com/webprofusion/certify/blob/development/src/Certify.Providers/ACME/Certes/CertesACMEProvider.cs#L1103

ColinRaaijmakers commented 1 year ago

I have written an extension method for my http challenge:

Extension method:

public static async Task<Challenge> ValidateWithRetryAsync(this IChallengeContext httpChallenge)
{
    var result = await httpChallenge.Validate();

    var attempts = 5;
    while (attempts > 0 && result.Status == ChallengeStatus.Pending || result.Status == ChallengeStatus.Processing)
    {
        Thread.Sleep(1000);

        attempts--;

        result = await httpChallenge.Resource();
    }

    return result;
}

Usage:

var authorizationContext = acme.Authorization(new Uri(**snip**));
var httpChallengeContext = await authorizationContext.Http();

var challengeResult = await httpChallengeContext.ValidateWithRetryAsync();
martinguenther commented 1 year ago

The link @webprofusion-chrisc posted seams to be broken. This should be the code he is referencing:

var attempts = 20;
while (attempts > 0 && (res.Status != AuthorizationStatus.Valid && res.Status != AuthorizationStatus.Invalid))
{
    res = await authz.Resource();

    attempts--;

    // if status is not yet valid or invalid, wait a sec and try again
    if (res.Status != AuthorizationStatus.Valid && res.Status != AuthorizationStatus.Invalid)
    {
        await Task.Delay(1000);
    }
}
jingliancui commented 1 year ago

Sorry, can't we make a callback to the validate method to check the status?

webprofusion-chrisc commented 1 year ago

The ACME certificate authority (such as let's Encrypt) is remote API, it doesn't have a connection to your machine to notify you of changes, you have to poll the authorization status to see if it is still pending (still being validated) or if it has failed or succeeded.

InteXX commented 9 months ago

@webprofusion-chrisc

it can take a few seconds to transition from pending/valid to ready

What's the difference between the Pending and Processing states? Also: I see no Ready state in v3.0.4.

Could you comment?

InteXX commented 9 months ago

@webprofusion-chrisc

What's the difference between the Pending and Processing states?

I found the answer here:

https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.6