Oh JSON.
For those unfamiliar with the reason here, it’s that JSON parsers cannot be relied upon to treat numbers properly. Is 4723476276172647362476274672164762476438 a valid JSON number? Yes, of course it is. What will a JSON parser due with it? Silently truncate it to a 64-bit or 63-bit integer, or a float, probably or if you’re very lucky emit an error (a good JSON decoder written in a sane language like Common Lisp would of course just return the number, but few of us are so lucky).
So the only way to reliably get large integers into and out of JSON is to encode them as something else. Base64-encoded big-endian bytes is not a terrible choice. Silently doing the wrong thing is the root of many security errors, so it not wrong to treat every number in the protocol this way. Of course, then one loses the readability of JSON.
JSON is better than XML, but it really isn’t great. Canonical S-expressions would have been far preferable, but for whatever reason the world didn’t go that way.
I feel like not understanding why JSON won out is being intentionally obtuse. JSON can easily be hand written, edited, and read for most data. Canonical S-expressions are not as easy to read and much harder to write by hand; having to prefix every atom with a length makes is very tedious to write by hand. If you have a JSON object you want to hand edit, you can just type... for an Canonical S-expression, you have to count how many characters you are typing/deleting, and then update the prefix.
You might not think the ability to hand generate, read, and edit is important, but I am pretty sure that is a big reason JSON has won in the end.
Oh, and the Ruby JSON parser handles that large number just fine.
You are going way out of your way to try to come up with ways to rationalize why JSON was a success. The ugly truth is far simpler than what you're trying to sell: it was valid JavaScript. JavaScript WebApps could parse JSON with a call to eval(). No deserialization madness like XML, no need to import a parser. Just fetch a file, pass it to eval(), and you're done.
So of course, ACME is based around a format whose entire reason d'etre is being written and read by hand.
It's weird.
I didn’t feel like my comment was the right place to shill for an alternative, but rather to complain about JSON. But since you raise it.
> JSON can easily be hand written, edited, and read for most data.
So can canonical S-expressions!
> Canonical S-expressions are not as easy to read and much harder to write by hand; having to prefix every atom with a length makes is very tedious to write by hand.
Which is why the advanced representation exists. I contend that this:
(urn:ietf:params:acme:error:malformed
(detail "Some of the identifiers requested were rejected")
(subproblems ((urn:ietf:params:acme:error:malformed
(detail "Invalid underscore in DNS name \"_example.org\"")
(identifier (dns _example.org)))
(urn:ietf:params:acme:error:rejectedIdentifier
(detail "This CA will not issue for \"example.net\"")
(identifier (dns example.net))))))
is far easier to read than this (the first JSON in RFC 8555): {
"type": "urn:ietf:params:acme:error:malformed",
"detail": "Some of the identifiers requested were rejected",
"subproblems": [
{
"type": "urn:ietf:params:acme:error:malformed",
"detail": "Invalid underscore in DNS name \"_example.org\"",
"identifier": {
"type": "dns",
"value": "_example.org"
}
},
{
"type": "urn:ietf:params:acme:error:rejectedIdentifier",
"detail": "This CA will not issue for \"example.net\"",
"identifier": {
"type": "dns",
"value": "example.net"
}
}
]
}
> for an Canonical S-expression, you have to count how many characters you are typing/deleting, and then update the prefix.As you can see, no you do not.
You don't do that, any more than you read or write machine code in binary. You read and write regular S-expressions (or assembly code) and you translate that into and out of canonical S expressions (or machine code) with a tool (an assembler/disassembler).
Of course, if you're doing this in JS and have reasons to think the resulting number may be larger than the precision of a double, you have a huge problem either way. Just as you would if you were writing this in C and thought the number may be larger than what can fit in a long long. But that's true regardless of how you represent it in JSON.
The DER encoding used in the TLS certificates uses the big endian binary format. OpenSSL API wants the big endian binary too.
The format used by this protocol is a simple one.
It's almost exactly the format that is needed to use these numbers, except JSON can't store binary data directly. Converting binary to base 64 is a simple operation (just bit twiddling, no division), and it's easier than converting arbitrarily large numbers between base 2 and base 10. The 17-bit value happens to be an easy one, but other values may need thousands of bits.
It would be silly for the sender and recipient to need to use a BigNum library when the sender has the bytes and the recipient wants the bytes, and neither has use for a decimal number.
json.Number is (almost) my “favorite” arbitrary decimal: https://github.com/ncruces/decimal?tab=readme-ov-file#decima...
I'm half joking, but I'm not sure why S-expressions would be better here. There are LISPs that don't do arbitrary precision math.
Yup, and if you’re using JSON in Go you really do need to be using Number exclusively. Anything else will lead to pain.
> I'm half joking, but I'm not sure why S-expressions would be better here. There are LISPs that don't do arbitrary precision math.
Sure, but I’m referring specifically to https://www.ietf.org/archive/id/draft-rivest-sexp-13.html, which only has lists and bytes, and so number are always just strings and it’s up to the program to interpret them.
Now, S-expressions as used for programming languages such as Lisp do have numbers, but again Lisp has bignums. As for parsers of Lisp S-expressions written in other languages: if they want to comply with the standard, they need to support bignums.
JSON could easily be extended to support them - but there’s no standards body with the authority to make a change like that. So we’re probably stuck with json as-is forever. I really hope something better comes along that we can all agree on before I die of old age.
While we’re at it, I’d also love a way to embed binary data in json. And a canonical way to represent dates. And comments. And I’d like a sane, consistent way to express sum types. And sets and maps (with non string keys) - which JavaScript also natively supports. Sigh.
JSON is a victim of its success: it has become too big to fail, and too big to improve.
For RSA-4096, the modulus is 4096 bits = 512 bytes in binary, which (for my test key) is 684 characters in base64 or 1233 characters in decimal. So the base64 version is much smaller.
Base64 is also more efficient to deal with. An RSA implementation will typically work with the numbers in binary form, so for the base64 encoding you just need to convert the bytes, which is a simple O(n) transformation. Converting the number between binary and decimal, on the other hand, is O(n^2) if done naively, or O(some complicated expression bigger than n log n) if done optimally.
Besides computational complexity, there's also implementation complexity. Base conversion is an algorithm that you normally don't have to implement as part of an RSA implementation. You might argue that it's not hard to find some library to do base conversion for you. Some programming languages even have built-in bigint types. But you typically want to avoid using general-purpose bigint implementations for cryptography. You want to stick to cryptographic libraries, which typically aim to make all operations constant-time to avoid timing side channels. Indeed, the apparent ease-of-use of decimal would arguably be a bad thing since it would encourage implementors to just use a standard bigint type to carry the values around.
You could argue that the same concern applies to base64, but it should be relatively safe to use a naive implementation of base64, since it's going to be a straightforward linear scan over the bytes with less room for timing side channels (though not none).
Why you wouldn't just use the hexadecimal that everyone else seems to use I don't know. There seems to be a rather arbitrary cutoff where people prefer base64 to hexadecimal.
Python 3.13.3 (main, May 21 2025, 07:49:52) [GCC 14.2.0] on linux
Type "help", "copyright", "credits" or "license" for more
information.
>>> import json
>>>
json.loads('47234762761726473624762746721647624764380000000000000000000000000000000000000000000')
47234762761726473624762746721647624764380000000000000000000000000000000000000000000 >> import json, decimal
>> j = "47234762761726473624762746721647624764380000000000000000000000000000000000000000000"
>> json.loads(j, parse_float=decimal.Decimal, parse_int=decimal.Decimal)
Decimal('47234762761726473624762746721647624764380000000000000000000000000000000000000000000')
This way you avoid this problem: >> import json
>> j = "0.47234762761726473624762746721647624764380000000000000000000000000000000000000000000"
>> json.loads(j)
0.47234762761726473
And instead can get: >> import json, decimal
>> j = "0.47234762761726473624762746721647624764380000000000000000000000000000000000000000000"
>> json.loads(j, parse_float=decimal.Decimal, parse_int=decimal.Decimal)
Decimal('0.47234762761726473624762746721647624764380000000000000000000000000000000000000000000')But yea, as a Clojure guy sexprs or EDN would be much better.
Probably there are types not every parser/language can accept, but at least it could throw a meaningful error instead of guessing or even truncating the value.
Haskell’s Aeson library is one of the few exceptions I’ve seen, since it only parses numbers to ‘Scientific’s (essentially a kind of bigint for rationals.) This makes the API very safe, but also incredibly annoying to use if you want to just munge some integers, since you’re forced to handle the error case of the unbounded values not fitting in your fixed-size integer values.
Most programmers likely simply either don’t consider that case, or don’t want to have to deal with it, so bad JSON libraries are the default.
There's an element of "worse is better" here [1]. JSON overtook XML exactly because it's so simple and solves for the social element of communication between disparate projects with wildly different philosophies, like UNIX byte-oriented I/O streams, or like the C calling conventions.
---
[0] https://ecma-international.org/publications-and-standards/st...
The problem with your solution is that it’s also not portable for the same reason (it’s not part of the standard), and the reason that it wasn’t done that way in the first place is because it wouldn’t map to those initial js types!
FYI, you can easily work around this by using replacer and revivers that are part of the standards for stringify and parse and treat numbers differently. But again, the json isn’t portable to places without those replacer/revivers.
I.e, the real problem is treating something that looks like json as json by using standards compliant json parsers - not the apparent structure of the format itself. You could fix this problem in an instant by calling it something other than JSON, but people will see it and still use a JSON parser because it looks like JSON, not because it is JSON.
Kinda blows my mind that the accepted behavior is to just overflow and not raise an exception.
I try to stick to strings for anything that's not a 32 bit int now.
In practice, "alg:none" is a headache and everyone involved should be ashamed.
JSON is better than XML, but it really isn’t great.
JSON doesn't even support comments, c'mon. I mean, it's handy for some things, but I don't know if I'd say "JSON is better than XML" in any universal sense. I still go by the old saw "use the right tool for the job at hand". In some cases maybe it's JSON. In others XML. In others S-Exprs encoded in EBCDIC or something. Whatever works...It's a shame JSON parsers usually default to performance rather than correctness, by using bignums for numbers.
> This specification allows implementations to set limits on the range and precision of numbers accepted
JSON is a terrible interoperability standard.
That sentence has four negations and I honestly can't figure out what it means.
JSON is still hack garbage compared to XML from the turn of the millennia. Like most dominant tech standards, JSON took hold purely because many developers are intellectually lazy and it was easier to slam some sloppy JSON together than to understand XML.
XML with XSD, XPath and XQuery is simply a glorious combination.
The salt here is deserved! JSON Web Signatures are a gnarly format, and the ACME API is pretty enthusiastic about being RESTful.
It’s not what I’d design. I think a lot of that came via the IETF wanting to use other IETF standards, and a dash of design-by-committee.
A few libraries (for JWS, JSON and HTTP) go a long way to making it more pleasant but those libraries themselves aren’t always that nice, especially in C.
I’m working on an interactive client and accompanying documentation to help here too, because the RFC language is a bit dense and often refers to other documents too.
They are??
As someone who wallows in ASN.1, Kerberos, and PKI, I don't find JWS so "gnarly". Even if you're open-coding a JSON Web Signature it will be easier than to open-code S/MIME, CMS, Kerberos, etc. Can you explain what is so gnarly about JWS?
Mind you, there are problems with JWT. Mainly that HTTP user-agents don't know how to fetch the darned things because there is not standard for how to find out how to fetch the darned things, when you should honor a request for them, etc.
https://auth0.com/blog/critical-vulnerabilities-in-json-web-...
Implementations can be written securely, but it's too easy to make mistakes.
Yeah, there's worse stuff from the 90s around, but JOSE and ACME is newer than that - we could have done better!
Alas, it's not changing now.
I think ASN.1 has some warts, but I think a lot of the problems with DER are actually in creaky old tools. People seem way happier with Protobuf, for example: I think that's largely down to tooling.
Just because ASN.1 and friends are exceptionally bad, it does not mean that Json Web * cannot be bad also.
"Somehow, a couple of weeks ago, I found this other site which claimed to be better than LE and which used relatively simple HTTP requests without a bunch of funny data types."
"This is when the fine print finally appeared. This service only lets you mint 90 day certificates on the free tier. Also, you can only do three of them. Then you're done. 270 days for one domain or 3 domains for 90 days, and then you're screwed. Isn't that great? "
She don't mention what this "other site" is.
Without looking at it, are you sure about that?
I once used to know what REST meant. Are you doing REST as in HATEOAS or as in "we expose some http endpoints"?
ACME models everything as JSON objects, each of which is identified by URL. You can GET them, and they link to other objects with Location and Link headers.
To quote from the blog post:
> Dig around in the headers of the response, looking for one named "Location". Don't follow it like a redirection. Why would you ever follow a Location header in a HTTP header, right? Nope, that's your user account's identifier! Yes, you are a URL now.
I don't know if it's the pure ideal of HATEOS, but it's about as close as I've seen in use.
It has the classic failing though: it’s used by scripts which know exactly what they want to do (get a cert), so the clients still hardcode the actions they need. It just adds a layer of indirection as they need to keep track of URLs.
I would have preferred if it was just an RPC-over-HTTP/JSON with fixed endpoints and numeric object IDs.
I know it isn't a skill issue because of who the author is. So I can only imagine it is some sort of personal opinion that they dislike ACME as a concept or the tooling around ACME in general.
We've been using LE for a while (since 2019 I think) for handful of sites, and the best nonsense client _for us_ was https://github.com/do-know/Crypt-LE/releases.
Then this year we've done another piece of work this time against the Sectigo ACME server and le64 wasn't quite good enough.
So we ended up trying:-
- https://github.com/certbot/certbot on GitHub Actions, it was fine but didn't quite like the locked down environment
- https://github.com/go-acme/lego huge binary, cli was interestingly designed and the maintainer was quite rude when raising an issue
- https://github.com/rmbolger/Posh-ACME our favourite, but we ended up going with certbot on GHA once we fixed the weird issues around permissions
Edit* Re-read it. The tone isn't aimed at the ACME or the clients. It's the spec itself. ACME idea good, ACME implementation bad.
> ACME idea good, ACME implementation bad.
Maybe I'm misreading but it sounds like you're on a similar page to the author.
As they said at the top of the article:
> Many of the existing clients are also scary code, and I was not about to run any of them on my machines. They haven't earned the right to run with privileges for my private keys and/or ability to frob the web server (as root!) with their careless ways.
This might seem harsh but when I think it's a pretty fair perspective to have when running security-sensitive processes.
And a lot of the complaints ultimately boil down to not liking JWS. And I'm not really sure what she would have preferred there. ASN.1, which is even more complicated? Some bespoke format where implementations can't make use of existing libraries?
She doesn't "trust" tooling that basically the entire Internet including major security-conscious organizations are using, essentially letting perfect get in the way of good.
I think if she were a less capable engineer she would just set that shit up using the easiest way possible and forget about it like everyone else, and nothing bad would happen. Download nginx proxy manager, click click click, boom I have a wilcard cert, who cares?
I mean, this is her https site, which seems to just be a blog? What type of risk is she mitigating here?
Essentially the author is so skilled that she's letting perfect get in the way of good.
I haven't thought about certificates for years because it's not worth my time. I don't really care about the tooling, it's not my problem, and it's never caused a security issue. Put your shit behind a load balancer and you don't even need to run any ACME software on your own server.
The older posts on the same website provided a bit more context for me to understand today's post better:
- "Why I still have an old-school cert on my https site" - January 3, 2023 - https://rachelbythebay.com/w/2023/01/03/ssl/
- "Another look at the steps for issuing a cert" - January 4, 2023 - https://rachelbythebay.com/w/2023/01/04/cert/
Sadly, security is a cat and mouse game, which means it's always evolving and you're forced to keep up - and it's inherent by the nature of the field, so we can't really blame anyone (unlike, say, being forced to integrate with the latest Google services to be allowed on the Play Store). At least you get to write your own ACME client if you want to. You don't have to use certbot, and there's no TPM-like behaviour locking you out of your own stuff.
It's not just about not understanding, it's that more complex stuff is inherently more prone to security vulnerabilities, however well you think you reviewed its code.
There are a number of shell-based ACME clients whose prerequisites are: OpenSSL and cURL. You're probably already relying on OpenSSL and cURL for a bunch of things already.
If you can read shell code you can step through the logic and understand what they're doing. Some of them (e.g., acme.sh) often run as a service user (e.g., default install from FreeBSD ports) so the code runs unprivileged: just add a sudo (or doas) config to allow it to restart Apache/nginx.
Honest question:
* Do you understand OS syscalls in detail?
* Do you understand how your BIOS initializes your hardware?
* Do you understand how modern filesystems work?
* Do you understand the finer details of HTTP or TCP?
Because... I don't. But I know enough about them that I'm quite convinced each of them is a lot more difficult to understand than ACME. And all of them and a lot more stuff are required if you want to run a web server.
I also wrote up a digested description of the issuance flow here: https://www.arnavion.dev/blog/2019-06-01-how-does-acme-v2-wo... It's not a replacement for reading the RFCs, but it presents the information in the sequence that you would follow for issuance, so think of it like an index to the RFC sections.
Please don't post shallow dismissals, especially of other people's work. A good critical comment teaches us something.
I know ACME alone is not insurmountably complex, but it is another brick in the wall.
Perhaps the author wasn't looking hard enough. It could probably be ported with little effort.
This client really wants the easy case where the client lives on the machine which owns the name and is running the web server, and then it uses OpenBSD-specific partitioning so that elements of the client can't easily taint one another if they're defective
But, the ACME protocol would allow actual air gapping - the protocol doesn't care whether the machine which needs a certificate, the machine running an ACME client, and the machine controlling the name are three separate machines, that's fine, which means if we do not use this OpenBSD all-in-one client we can have a web server which literally doesn't do ACME at all, an ACME client machine which has no permission to serve web pages or anything like that, and name servers which also know nothing about ACME and yet the whole system works.
That's more effort than "I just install OpenBSD" but it's how this was designed to deliver security rather than putting all our trust in OpenBSD to be bug-free.
Most software in the OpenBSD base system lacks features on purpose. Their dev team frequently rejects patches and feature requests without compelling reasons to exist. Less features means less places for things to go wrong means less chance of security bugs.
It exists so their simple webserver (also in the base system) has ACME support working out of the box. No third party software to install, no bullshit to configure, everything just works as part of a super compact OS. Which to this day still fits on a single CD-ROM.
Most of all no stupid Rust compiler needed so it works on i386 (Rust cannot self-host on i386 because it's so bloated it runs out of memory, which is why Rust tools are not included in i386).
If your needs exceed this or you adore complexity then feel free to look elsewhere.
Man page: https://man.openbsd.org/man1/acme-client.1
Source: https://github.com/openbsd/src/tree/master/usr.sbin/acme-cli...
Kind of makes me wonder what kind of stack her website is running on that something like a lightweight ACME library (https://github.com/jmccl/acme-lw comes to mind, but there's a C++ library for ESP32s that should be even more lightweight) loading in the certificates isn't doing the job.
The problem is, SSL is a fucking hot, ossified mess. Many of the noted core issues, especially the weirdnesses around encoding and bitfields, are due to historical baggage of ASN.1/X.509. It's not fun to deal with it, at all... the math alone is bad enough, but the old abstractions to store all the various things for the math are simply constrained by the technological capabilities of the late '80s.
There would have been a chance to at least partially reduce the mess with the introduction of LetsEncrypt - basically, have the protocol transmit all of the required math values in a decent form and get an x.509 cert back - and HTTP/2, but that wasn't done because it would have required redeveloping a bunch of stuff from scratch whereas one can build an ACME CA with, essentially, a few lines of shell script, OpenSSL and six crates of high proof alcohol to drink away one's frustrations of dealing with OpenSSL, and integrate this with all software and libraries that exist there.
ASN.1 and X509 aren't all that bad. It's a comprehensively documented binary format that's efficient and used everywhere, even if it's hidden away in binary protocols you don't look at every day.
Unlike what most people seem to think, ACME isn't something invented just for Let's Encrypt. Let's Encrypt was certainly the first high-profile CA to implement the protocol, but various CAs (free and paid) have their own ACME servers and have had them for ages now. It's a generic protocol for certificate authorities to securely do domain validation and certificate provisioning that Let's Encrypt implemented first.
The unnecessarily complex parts of the protocol when writing a from-the-ground-up client are complex because ACME didn't reinvent the wheel, and reused existing standard protocols instead. Unfortunately, that means having to deal with JWS, but on the other hand, it means most people don't need to write their own ACME-JWS-replacement-protocol parsers. All the other parts are complex because the problem ACME is solving is actually quite complex.
The author wrote [another post](https://rachelbythebay.com/w/2023/01/03/ssl/) about the time they fell for the lies of a CA that promised an "easier" solution. That solution is pretty much ACME, but with more manual steps (like registering an account, entering domain names).
I personally think that for this (and for many other protocols, to be honest) XML would've been a better fit as its parsers are more resilient against weird data, but these days talking about XML will make people look at you like you're proposing COBOL. Hell, I even exchanging raw, binary ASN.1 messages would probably have gone over pretty well, as you need ASN.1 to generate the CSR and request the certificate anyway. But, people chose "modern" JSON instead, so now we're base64 encoding values that JSON parsers will inevitably fuck up instead.
An argument for this is that it makes it theoretically possible for devices that have no knowledge of anything about PKI since the year 2000, and/or no additional programmability, to use Let's Encrypt certs (obtained on their behalf by an external client application). I have, in fact, subsequently gotten something like that to work as a consultant.
This is bad advice - making a 4096 bit key slows down visitors of your website and only gives you 2048 bits of security (if someone can break a 2048 bit RSA key they'll break the LetsEncrypt intermediate cert and can MITM your site). You should use a 2048 bit leaf certificate here
If the server was using a key exchange that did not support forward secrecy then yes. But:
% echo | openssl s_client -connect rachelbythebay.com:443 2>/dev/null | grep Cipher
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Cipher : ECDHE-RSA-AES256-GCM-SHA384
^ they're using ECDHE (elliptic curve diffie hellman), which is providing forward secrecy.Basically forward secrecy is where both the sender and receiver throw away the key after the data is decrypted. That way the key is not available for an attacker to get access to later. If the attacker can find some way other than access to the key to decrypt the data then forward secrecy has no benefit.
My favourite client is probably https://github.com/acmesh-official/acme.sh
If you use a DNS service provider that supports it, you can use the DNS-01 challenge to get a certificate - that means that you can have the acme.sh running on a completely different server which should help if you're twitchy about running a complex script on it. It's also got the advantage of allowing you to get certificates for internal/non-routable addresses.
Personally, I find that tls-alpn-01 is even nicer than dns-01. You can run a web server (or reverse proxy) that listens to port 443, and nothing else, and have it automatically obtain and renew TLS certificates, with the challenges being sent via TLS ALPN over the same port you're already listening on. Several web servers and reverse proxies have support for it built in, so you just configure your domain name and the email address you want to use for your Let's Encrypt account, and you get working TLS.
I assume certbot is the client she’s alluding to that misinterprets one of the factors in the protocol as hex vs decimal and somehow things still work, which is incredibly worrisome.
Then I discovered the web-root approach people mention here and it made a huge difference. Now I have the HTTP snippet in my server set to serve up ACME challenges from a static directory and push everything else to HTTPS, and the ACME client just needs write permission to that directory. I can dynamically include that snippet in all of the sites my server handles and be done.
If I really felt like it, I could even write a wrapper function so the ACME client doesn’t even need restart permissions on the web-server (for me, probably too much to bother with, but for someone like Rachel perhaps worthwhile).
I run it in "webroot" mode on NgINX servers so it's just a matter of including the relevant config file in your HTTP sections (likely before redirecting to HTTPS) so that "/.well-known/acme-challenge/" works correctly. Then when you do run certbot, it can put the challenge file into the webroot and NgINX will automatically serve it. This allows certbot to do its thing without needing to do anything with NgINX.
Pinned to an old version and looking for a replacement right now.
If anyone else ran into that it's just a matter of adding
--server letsencryptPersonally I like https://github.com/dehydrated-io/dehydrated. Same concept as acme.sh but only 2500 lines of shell and 54 open issues. You do have to roll your own hook script though.
Curiously, first commits for both acme.sh and dehydrated were in December 2015. Maybe they both took a security class at uni that fall.
But yeah, can definitely recommend DNS-01 over HTTP-01, since it doesn't involve implicitly messing with your server settings, and makes it much easier to have a single locked server with all the ACME secrets, and then distribute the certs to the open-to-the-internet web servers.
+1 for acme.sh, it's beautiful.
tiny-acme.py is 200 lines, easy to audit and incorporate parts into your own infrastructure. It works well for the tiny work it does but it does support anything more modern.
For instance, Whatsapp can not open HTTP links anymore.
The problem is that mere existence of HTTP is a vulnerability. Users following any insecure link to anywhere allow MITM attackers to inject any content, and redirect to any URL.
These can be targeted attacks against vulnerabilities in the browser. These can be turning browsers into a botnet like the Great Cannon. These can be redirects, popunders, or other sneaky tab manipulation for opening phishing pages for other domains (unrelated to yours) that do have important content.
Your server probably won't even be contacted during such attack. Insecure URLs to your site are the vulnerability. Don't spread URLs that disable network-level security.
I see no other reason to serve content over HTTPS.
Meanwhile, ECDSA is so complex to write that most people will get it wrong and end up with a security hole that makes the NSA happy.
There are private keys and hash functions involved. But base64url and json aren't the worst web crimes to have been inflicted upon us. It's not _that_ bad, is it?
But "the rest" of ACME also include X.509 certificates and PKCS#10 Certificate Signing Requests, which are in turn based on ASN.1 (you're fortunate enough you only need DER encoding) and RSA parameters. ASN.1 and X.509 are devilishly complex if you don't let openssl do everything for you and even if you do. The first few paragraphs are all about making the correct CSR and dealing with RSA, and encoding bigints the right way (which is slightly different between DER and JWK to make things more fun).
Besides that I don't know much about the ACME spec, but the post mentions a couple of other things :
So far, we have (at least): RSA keys, SHA256 digests, RSA signing, base64 but not really base64, string concatenation, JSON inside JSON, Location headers used as identities instead of a target with a 301 response, HEAD requests to get a single value buried as a header, making one request (nonce) to make ANY OTHER request, and there's more to come.
This does sound quite complex. I'm just not sure how much simpler ACME could be. Overturning the clusterfuck that is ASN.1, X.509 and the various PKCS#* standards has been a lost cause for decades now. JOSE is something I would rather do without, but if you're writing an IETF RFC, you're only other option is CMS[1], which is even worse. You can try to offer a new signature format, but that would be shut down for being "simpler and cleaner than JOSE, but JOSE just has some warts that need to be fixed or avoided"[2].
I think the things you're left with that could have been simplified and accepted as a standard are the APIs themselves, like getting a nonce with a HEAD request and storing identifiers in a Location header. Perhaps you could have removed signatures (and then JOSE) completely and rely on client IDs and secrets since we're already running over TLS, but I'm not familiar enough with the protocol to know what would be the impact. If you really didn't need any PKI for the protocol itself here, then this is a magnificent edifice of overengineering indeed.
[1] https://datatracker.ietf.org/doc/html/rfc5652 [2] https://mailarchive.ietf.org/arch/msg/cfrg/4YQH6Yj3c92VUxqo-...
Most of it is unused though, only CN, SANs and public key are used.
The spec (well, the RFC anyway) is indeed classically RFC-ish, but the same applies to HTTP or TCP/IP, and I haven't seen the same sort of complaints about those. Maybe it's just resistance to change? Most of the specs (JOSE, ACME etc) aren't really complex for the sake of complexity, but solve problems that aren't simple problems to solve simply in a simple fashion. I don't think that's bad at all, it's mostly indicative of the complexity of the problem we're solving.
Some examples of gratuitous complexity:
1. Supporting too many goddamn algorithms. Keeping RSA and HMAC-SHA256 for leagcy-compatible stuff, and Ed25519 for XChaChaPoly1305 for regular use would have been better. Instead we support both RSA with PKCS#1 v1.5 signatures and RSA-PSS with MGF1, as well as ECDH with every possible curve in theory (in practice only 3 NIST Prime curves).
2. Plethora of ways to combine JWE and JWS. You can encrypt-then-sign or sign-then-encrypt. You can even create multiple layers of nesting.
3. Different "typ"s in the header.
4. RSA JWKs can specify the d, p, q, dq, dp and qi values of the RSA private key, even though everything can be derived from "p" and "q" (and the public modulus and exponent "n" and "e").
5. JWE supports almost every combination of key encryption algorithm, content encryption algorithm and compression algorithm. To make things interesting, almost all of the options are insecure to a certain degree, but if you're not an expert you wouldn't know that.
6. Oh, and JWE supports password-based key derivation for encryption.
7. On the other, JWS is smarter. It doesn't need this fancy shmancy password-based key derivation thingamajig! Instead, you can just use HMAC-SHA256 with any key length you want. So if you fancy encrypting your tokens with a cool password like "secret007" and feel like you're a cool guy with sunglasses in a 1990s movie, just go ahead!
This is just some of the things of the top of my head. JOSE is bonkers. It's a monument to misguided overengineering. But the saddest thing about JOSE is that it's still much simpler than the standards which predated it: PKCS#7/CMS, S/MIME and the worst of all - XMLDSig.
We haven't even scratched the surface of creating an order, dealing with authorizations and challenges, the whole "key thumbprint" thing, what actually goes into those TXT records, and all of that other fun stuff."
Yikes. It's almost unbelievable. What a colossal tangle of complexity. Thank you for sharing the fruits of your labors. Great writing style and content.
The steps described in the article sound familiar to the process done in the early 2000's, but I'm not sure why you'd want to make it hard for yourself now.
I use certbot with "--preferred-challenges dns-01" and "--manual-auth-hook" / "--manual-cleanup-hook" to dynamically create DNS records, rather than needing to modify the webserver config (and the security/access risks that comes with). It just needs putting the cert/key in the right place and reloading the webserver/loadbalancer.
What registrar do people recommend in 2025?
I have built a registrar in the past and have a lot of arcane knowledge about how they work. Just need to figure out a way to monetize!
Don’t look to large, well-known registrars. I would suggest that you look for local registrars in your area. The TLD registry for your country/area usually has a list of the authorized registrars, so you can simply search that for entities with a local address.
Disclaimer: I work at such a small registrar, but you are probably not in our target market.
I forget how they handled key expiration/revocation...
What you want from a registrar is to keep existing for many years and resilience to social engineering, and AWS seems like the next best thing to Google which you famously can't even talk to for a social engineering attempt. I expect AWS account management to be almost as good as Gaia, but don't really know how hard social engineering is.
Must be other good ones? Somewhat prefer something in the UK (but have been using Gandi so its not essential).
It looks silly for a small value like 65537, but the protocol also needs to handle numbers that are thousands of bits long. It makes sense to consistently use the same format for all numbers instead of special-casing small values.
Subject Alternative Name (SAN) is not an alternative in the sense that it's an alias, SANs exist because the X.509 certificate standard is, as its name might suggest, intended for the X.500 directory system, a system from the 20th century which was never actually deployed. Mozilla (back then the Netscape Corporation) didn't like re-inventing wheels and this standard for certificates already existed so they used it in their new "Secure Sockets" technology but it has no Internet names so at first they just put names in plain text. However, X.500 was intended to be infinitely extensible, so we can just invent an alternative naming scheme, and that's what the SANs are, which is why they're mandatory for certificates in the Web PKI today - these are the Internet's names for things, so they're mandatory when talking about the Internet, they're described in detail in PKIX, the IETF document standardising the use of X.500 for the Internet.
There are several types of name we can express as SANs but in a certificate the two you'll commonly see are dnsName - the same ASCII names you'd see in URLs like "news.ycombinator.com" or "www.google.com" and ipAddress - a 32-bit integer typically spelled as four dotted decimals 10.20.30.40 [yes or an IPv6 128-bit integer will work here, don't worry]
Because the SANs aren't just free text a machine can reliably parse them which would doubtless meet Rachel's approval. The browser can mindlessly compare the bytes in the certificate "news.ycombinator.com" with the bytes in the actual DNS name it looked up "news.ycombinator.com" and those match so this cert is for this site.
With free text in a CN field like a 1990s SSL certificate (or, sadly, many certificates well into the 2010s because it was difficult to get issuers to comply properly with the rules and stop spewing nonsense into CN) it's entirely possible to see a certificate for " 10.200.300.400" which well, what's that for? Is that leading space significant? Is that an IP address? But those numbers don't even fit in one byte each I hope our parser copes!
You can’t mindlessly compare the bytes of the host name: you have to know that it’s the presentation format of the name, not the DNS wire format; you have to deal with ASCII case insensitivity; you have to guess what to do about trailing dots (because that isn’t specified); you have to deal with wildcards (being careful to note that PKIX wildcard matching is different from DNS wildcard matching).
It’s not as easy as it should be!
The names PKIX writes into dnsName are exactly the same as the hostnames in DNS. They are defined to always be Fully Qualified, and yet not to have a trailing dot, you don't have to like that but it's specified and it's exactly how the web browsers worked already 25+ years ago.
You're correct that they're not the on-wire label-by-label DNS structure, but they are the canonical human readable DNS name, specifically the Punycode encoded name, so [the website] https://xn--j1ay.xn--p1ai/ the Russian registry which most browsers will display with Cyrllic, has its names stored in certificates the same way as it is handled in DNS, as Punycode "xn--j1ay.xn--p1ai". In software I've seen the label-by-label encoding stuff tends to live deep inside DNS-specific code, but the DNS name needed for comparing with a certificate does not do this.
You don't need to "deal with" case except in the sense that you ignore it, DNS doesn't handle case, the dnsName in SANs explicitly doesn't carry this, so just ignore the case bits. Your DNS client will do the case bit wiggling entropy hack, but that's not in code the certificate checking will care about.
You do need to care about wildcards, but we eliminated the last very weird certificate wildcards because they were minted only by a single CA (which argued by their reading they were obeying PKIX) and that CA is no longer in business 'cos it turns out some of the stupid things they were doing even a creative lawyerly reading of specifications couldn't justify. So the only use actually enabled today is replacing one DNS label at the front of the name. Nothing else is used, no suffixes, no mid-label stuff, no multi-label wildcards, no labels other than the first.
Edited to better explain the IDN situation hopefully
Yes, all the popular browsers require this.
> they certainly didn't even as of ~10 years ago?
That's true, ten years ago it was likely that if a browser required this they would see unacceptably high failure rates because CAs were non-compliant and enforcement wasn't good enough. Issuing certs which would fail PKIX was prohibited, but so is speeding and yet people do that every day. CT improved our ability to inspect what was being issued and monitor fixes.
> Yes, it is "required", but CN only has worked for quite some time.
No trusted CA will issue "CN only" for many years now, if you could obtain such a certificate you'd find it won't work in any popular browser either. You can read the Chromium or Mozilla source and there just isn't any code to look in CN, the browser just parses the SANs.
> I find this tricks up some IT admins who are still used to only supplying a CN and don't know what a SAN is.
In most cases this is a sign you're using something crap like openssl's command line to make CSRs, and so you're probably expending a lot of effort filling out values which will be ignored by the CA and yet not offered parameters you did need.
X.500 really was deployed – never at the scale the designers originally intended, as a single global directory system – but, as an enterprise directory system, yes it was – and it still survives in that role today, albeit as a legacy niche.
LDAP is a direct descendant of X.500 – it was basically taking the X.500 Directory Access Protocol (DAP, X.511), simplifying it somewhat, and porting it to run on top of TCP instead of OSI TP. Many early LDAP servers were just X.500 DAP servers with LDAP support added as an additional feature–and if you read the LDAP RFCs, large parts of them were written with that assumption, and don't make much sense unless you understanding the X.500 underpinnings
Nowadays, the most popular LDAP servers never implemented X.500, and few bother to implement the full set of X.500 semantics which LDAP supports – although one of the infuriating things about LDAP is that every implementation is a slightly different subset of the X.500 feature set.
X.400 survives in some applications–it is the basis of the NATO standard Military Message Handling System (MMHS) and also the Aeronautical Message Handling System (AMHS) used by commercial aircraft – and X.400 and X.500 were designed to be used together. I know vendors like Thales Group still sell X.500 directory servers for use with AMHS (and probably MMHS too)
Isode still sells M-Vault which supports both LDAP and X.500 DAP (X.511) – primarily for military applications
I was thinking about the global directory rather than individual local X.500 instances but I didn't say that explicitly and I should have.
It's on my long list of potential side projects, but I don't think I'll ever gey around to it
You just need bash, no weird dependency, not that much complicated. Easy to manipulate.
Why I still have an old-school cert on my HTTPS site - https://news.ycombinator.com/item?id=34242028 - Jan 2023 (63 comments)
I've been thinking of using a Blockchain to register domain name-to-IP-address mappings in a cryptographically secure way and then writing a Chrome extension which connects to the Blockchain (to any known peer IP; it would start from a few hardcoded seed node IPs and discover more peers as is standard for most Blockchains and P2P protocols; or you could host your own blockchain node locally and use it as your personal DNS service) and then the extension can do DNS lookups on-chain. The Chrome extension could act as an alternative address bar; type the address in there and it would read the Blockchain to figure out the IP address, completely bypassing the whole mess of a DNS system and the current centralized mess of an internet... We could then store all the certs on-chain in a similar way, just make it support the bare minimum necessary to get the browser to shut up and accept the cert whilst maintaining the essential cryptographic security guarantees.
It's kind of ridiculous how easy it would be, technically, to create an alternative internet and DNS system. I think there are already similar solutions like Brave browser supporting a parallel internet with .eth domains but they don't seem to get much attention. There needs to be search engines for these alternative internets to get things going.
Surely once the current internet gets spammed into oblivion and becomes devoid of opportunities, there should be an incentive use create an alternative system from scratch. Surely there is a point when a network of scarce data is better than one of abundant spam data.
Edit: to be clear, I'd not be too surprised if their homegrown client survives an audit unscathed, I'm sure they're a great coder, but the odds just don't seem better than to the alternative of using an existing client that was already audited by professionals as well as other people
I've recently heard similar takes about Gandi but I am out of the loop, can someone explain the controversy? Currently using it for one of my domains and I'd like to know more.
EDIT: tried googling but results are about people whose name is either Gandi or Ghandi, could not find much about Gandi the company.
https://techrights.org/n/2023/12/14/_Video_Lessons_to_be_Lea...
https://blog.cogitactive.com/website/gandi-outrageous-price/
Does anyone know why they're there?
I don't really buy this explanation. It's a very large unsigned number. Everyone knows this. Is there some arbitrary precision library in use that forces large integers to be signed? Even if it were signed, or had the MSB set, it wouldn't change any of the bits, so the key would still be the same. So why would we care about the sign?
> They haven't earned the right to run with privileges for my private keys and/or ability to frob the web server (as root!)
None of that is needed. You can setup the update system in isolation, redirect the required paths and copy the keys manually. None of that needs to run as root either if you can set the permissions correctly or delegate actions to other processes.
Why not just use JsonCpp then?
https://github.com/open-source-parsers/jsoncpp
It's a native C++ parser which is mature, actively maintained, and likely safer than a low-level C parser implementing its own string buffers.
https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
>2. Query for TXT *records* for the validation domain name
>3. Verify that the contents of *one of the TXT records* match the digest value
(Emphasis mine.)
Part of not wanting to let go is the sunk cost fallacy. Part of it is being suspicious of being (more) dependent on someone else (than you are already dependent on a different someone else).
(As an aside, the n-gate guy who ranted against HTTPS in general and thought static content should just be HTTP also thought like that. Unfortunately, as I'm at a sketchy cafe using their wifi, his page currently says I should click here to enter my bank details, and I should download new cursors, and oddly doesn't include any of his own content at all. Bit weird, but of course I can trust he didn't modify his page, and it's just a silly unnecessary imposition on him that I would like him to use HTTPS)
Unfortunately for those rugged individuals, you're in a worldwide community of people who want themselves, and you, to be dependent on someone else. We're still going with "trust the CAs" as our security model. But with certificate transparency and mandatory stapling from multiple verifiers, we're going with "trust but verify the CAs".
Maximum acceptable durations for certificates are coming down, down, down. You have to get new ones sooner, sooner, sooner. This is to limit the harm a rogue CA or a naive mis-issuing CA can do, as CRLs just don't work.
The only way that can happen is with automation, and being required to prove you still own a domain and/or a web-server on that domain, to a CA, on a regular basis. No "deal with this once a year" anymore. That's gone and it's not coming back.
It's good to know the whole protocol, and yes certbot can be overbearing, but Debian's python3-certbot + python3-certbot-apache integrates perfectly with how Debian has set up apache2. It shouldn't be a hardship.
And if you don't like certbot, there are lots of other ACME clients.
And if you don't like Let's Encrypt, there are other entities offering certificates via the ACME protocol (YMMV, do you trust them enough to vouch for you?)
Yep, I've seen that argument so many times and it should never make sense to anyone that understands MITM.
The only way it could possibly work is if the static content were signed somehow, but then you need another protocol the browser and you need a way to exchange keys securely, for example like signed RPMs. It would be less expensive as the encryption happens once, but is it worth having yet another implementation?
Professionally it's been cert-manager.
I haven't paid for a TLS certificate in almost a decade I guess.
I don't know much about Caddy scalability but it's worked great for my personal sites.
Is base64-encoded number really something to be mad about?
- Is female [TIL the term "wogrammer"]
- Works for Facebook [formerly Rackspace and Google] so an undeniably Big MAMAA
- Has been blogging prolifically for at least 14 years [let's call it 40 years: she admin'd a BBS at age 12]
- Website is custom self-hosted; very old school and accessible; no ads or popup bullshit
- Probably has more CSE/SWE experience+talent in her little pinky finger than 80% of HN commenters
https://medium.com/wogrammer/rachel-kroll-7944eeb8c692
So I'd say that her position and experience command enough respect that we cannot judge her merely by peeking at a few trifling journal entries.
https://letsencrypt.org/docs/integration-guide/#supported-ke...
This makes me wonder what world of development she is in. Does she prefer SOAP?
If the author says they dislike JSON, especially given the tone of this article with respect to nonsensical protocols, I highly doubt they approve of SOAP.