FlorianUekermann / rustls-acme

Apache License 2.0
136 stars 27 forks source link

Testing in challenging #29

Closed casey closed 1 year ago

casey commented 2 years ago

I'm not sure what the solution is, but writing tests against a server that uses rustls-acme is rather challenging. Our dev and CI machines don't have public-facing domains, which precludes requesting certs from let's encrypt. Also, even if we could, this would slow down the tests.

One possible solution would be to allow injecting a cert into an AcmeConfig, which would be used in testing instead of contacting Let's Encrypt for a cert.

FlorianUekermann commented 2 years ago

Yes, that's a valid concern (and a common one from my experience with similar libraries).

Technically there is already a way to inject certs. You can set a certificate cache, which yields a cert you create, which never expires. You could even use the existing DirCache for this. You can look up the hash for the filename in your production cache directory if you want to use the same config.

However, it may be worth including such a cache implementation in the library. The cert could be generated on the fly. Maybe even with a signature from a known CA cert that you can add on the test client side. The CA cert has some downsides if users are careless though. I've seen test flags accidentally slip into production deployments in "fast paced" work environments. I can't prevent that anyway, but I don't really want to be the guy whose repo contains the CA cert that can be used to sign certs for those vulnerable deployments.

Would a TestCache implementation that just generates a cert work for you? Or would you prefer having more control what cert is returned from the cache? Or am I missing a reason why even more direct control may be useful.

casey commented 2 years ago

However, it may be worth including such a cache implementation in the library.

That would definitely be nice. Figuring out what to put in the cache directory, in terms of format and filename, is confusing. I did it once for a previous project, and it worked, but now I'm doing it again for another project, and I totally forgot the details, like how to figure out what filename to use.

The cert could be generated on the fly. Maybe even with a signature from a known CA cert that you can add on the test client side. The CA cert has some downsides if users are careless though.

I'm not sure I understand the security implication here. Let's say that you had a master test CA cert whose private key was public. A user then accidentally deploys a test instance to production, which uses a cert that's signed with the master test CA cert. Is that deployment then vulnerable? Clients wouldn't be able to connect to it, because they don't have the master CA cert, but is there a security vulnerability? Anyone could create a new cert signed by the master test CA cert, but because clients don't have that cert, they couldn't do anything with it.

If need be, the CA cert could be generated on the fly. That way there's no single CA cert that can leak.

Would a TestCache implementation that just generates a cert work for you? Or would you prefer having more control what cert is returned from the cache? Or am I missing a reason why even more direct control may be useful.

A TestCache implementation would be great for unit tests, but for integration tests I'd need to write the cert to a directory with the correct filename, to get the app to pick it up:

A function like this would be great:

// Using a struct to be specific with what's returned
struct TestCert {
  filename: String,
  ca: Cert,
  end_entity: Cert,
}

impl TestCert {
  fn new(domains: &[&str]) -> Self {
    # stuff
  }
}
FlorianUekermann commented 2 years ago

I just want to quickly clarify to the security related concern I mentioned. More on the rest later.

I'm not sure I understand the security implication here. Let's say that you had a master test CA cert whose private key was public. A user then accidentally deploys a test instance to production, which uses a cert that's signed with the master test CA cert. Is that deployment then vulnerable?

You are correct, the server deployment wouldn't be vulnerable. But a test client which accepts the test CA cert could be, so if some kind of client were to be deployed or otherwise released with support for the test cert, it is potentially vulnerable. Given that several well-known companies have done similar things intentionally, I'm sure it happens accidentally all the time.

FlorianUekermann commented 1 year ago

Sorry for the long silence (first I was a bit busy and then wasn't able to get a public IP on my dev system for a few weeks, which makes working on this library no fun). Let's get something merged asap.

I just pushed a commit to the "testcache" branch. Have a look at src/caches/test.rs. You basically just do config.cache(TestCache::new()) to configure a cache which has it's own CA certificate and issues a cert on loads operations.

You can get the CA certificate by calling TestCache::ca_pem(), so you can write it to file or whatever you like. The certificate is different every time you construct a new TestCache. But it implements Clone so you can reuse it within the same process.

I guess it would be nice to be able to supply your own CA and KeyPair, but I'm not quite sure what format would make sense from a user perspective, so I have not implemented that. Maybe TestCache::from_pem(key_and_cert: String)?, where the pem looks like this:

-----BEGIN PRIVATE KEY-----
<snip>
-----END PRIVATE KEY-----

-----BEGIN CERTIFICATE-----
<snip>
-----END CERTIFICATE-----

Or simply: TestCache::from_ca_cert(rcgen::Certificate). That would make my life easier, but requires that the user writes a few more lines of rcgen code, which doesn't feel very ergonomic. Opinions?

casey commented 1 year ago

I think that for my purposes, config.cache(TestCache::new()) and TestCache::ca_pem() would work fine, and I wouldn't need to supply my own key/certificate. The HTTPS part of the project I'm working on hasn't changed, so I probably won't wind up using the test cache unless I wind up going back and modifying the HTTPS code, in which case I'd want to add tests to make sure I wasn't breaking anything.

FlorianUekermann commented 1 year ago

merged into main