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

Confusion on base documentation #304

Open
ParkerRedford opened this issue Jan 17, 2023 · 16 comments
Open

Confusion on base documentation #304

ParkerRedford opened this issue Jan 17, 2023 · 16 comments

Comments

@ParkerRedford
Copy link

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
Copy link
Collaborator

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:

  • create order with the CA
  • ask what challenges to perform (generally a collection of either http or DNS challenges)
  • perform the challenges (e,g, write a file to your webserver path for http validation, or update a DNS TXT record for dns validation). For http challenges this means present the require file name with the required content as specified by the challenge object and it takes the form http:///.well-known/acme-challenge/
  • submit the challenges : this is the step you might be missing and it tells the CA your read for each challenge response to be checked,
  • 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.
  • once all challenges are completed and valid, the order with switch to the Ready state (or Valid)
  • you can then Finalize your order (which involves submitting a CSR for your cert, signed by your CSR key) and Downloading the cert. .Generate does both of these steps I think (from memory) but you can split them out if you want to.

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
Copy link
Author

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
Copy link
Collaborator

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
Copy link
Author

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
Copy link
Collaborator

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
Copy link
Author

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
Copy link
Collaborator

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
Copy link
Author

ParkerRedford commented Jan 23, 2023

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
Copy link

@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
Copy link
Collaborator

@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
Copy link

ColinRaaijmakers commented Mar 1, 2023

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
Copy link

martinguenther commented May 4, 2023

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
Copy link

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

@webprofusion-chrisc
Copy link
Collaborator

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
Copy link

InteXX commented Dec 9, 2023

@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
Copy link

InteXX commented Dec 9, 2023

@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

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

No branches or pull requests

7 participants