Understanding rainbow tables
In yesterday's entry, I casually misused the term 'rainbow table' because I thought I knew what it meant when I actually didn't; what I was actually talking about was a (reverse) lookup table, which is not at all the same thing. I'm sure that I've read a bit about rainbow tables before but evidently it didn't stick and I didn't really get them. As a result of dozzie's comment pointing out my error, I wound up reading the Wikipedia writeup of rainbow tables, and I think I may now understand them. In an attempt to make this understanding stick, I'm going to write down my version of how they work and some remaining questions I have.
The core ingredient of a rainbow table is a set of reduction functions, R1 through Rn. Reduction functions take a hash value and generate a password from it in some magic way. To create one entry in a rainbow table, you start with a password p1, hash the password to generate hash h1, apply the first reduction function R1 to generate a new password p2, hash that password to generate h2, apply R2 to generate password p3, and so on along this hash chain. Eventually you hit hash hn, use your last reduction function Rn, and generate p(n+1). You then store p1, the first password (the starting point), and p(n+1), the endpoint (I think you could store the hash of p(n+1) instead, but that would take up more space).
To see if a given hash of interest is in the rainbow table, you successively pretend that it is every hash h1 through hn and compute the output of each of the hash chains from that point onward. When you're pretending the hash is hn, this is just applying Rn; when it's h(n-1) this is applying R(n-1) to generate pn, hashing it to get hn and then applying Rn, and so on. You then check to see if any of the computed endpoint passwords are in your rainbow table. If any of them are, you recreate that chain starting with your stored p1 starting point up to the point where in theory you should find the hash of interest, call it hx. If the chain's computed hx actually is the hash of interest, password px from just before it is the corresponding password.
Matching an endpoint password doesn't guarantee that you've found a password for the hash; instead it could be that you have two hashes that a given reduction function maps to the same next password. If your passwords of interest are shorter than the output of the password hash function this is guaranteed to happen some of the time (and shorter passwords is the usual case, especially with modern hash functions like SHA256 that have large outputs).
The length of the hash chains in your rainbow table is a tradeoff between storage space and compute time. The longer the hash chains are the less you have to store to cover roughly the same number of passwords, but the longer it will take to check each hash of interest because you will have to compute more versions of chains (and check more endpoint passwords). Also, the longer the hash chain is, the more reduction function variations you have to come up with.
(See the Wikipedia page for an explanation of why you have multiple reduction functions.)
As far as I can see, a rainbow table doesn't normally give you exhaustive coverage of a password space (eg, 'all eight character lower case passwords'). Most of the passwords covered by the rainbow table come from applying your reduction functions to cryptographic hashes; the hashes should have randomly distributed values so normally this will mean that your reduction functions produce passwords that are randomly distributed through your password space. There's no guarantee that these randomly distributed passwords completely exhaust that space. To get a good probability of this, I think you'd need to 'oversample' your rainbow table so that the total number of passwords it theoretically covers is greater than your password space.
(Although I haven't attempted to do any math on this, I suspect that oversampled rainbow tables still take up (much) less space than a full lookup table, especially if you're willing to lengthen your hash chains as part of it. Longer hash chains cover more passwords in the same space, at the cost of more computation.)
The total number of passwords a rainbow table theoretically covers is simply the number of entries times the length of the hash chains. If you have hash chains with 10 passwords (excluding the endpoint password) and you have 10,000 entries in your rainbow table, your table covers at most 100,000 passwords. The number of unique passwords that a rainbow table actually contains is not something that can be determined without recording all of the passwords generated by all of the reduction functions during table generation.
Sidebar: Storing the endpoint password versus the endpoint's hash
Storing the hash of the endpoint password instead of the endpoint password seems superficially attractive and it feels like it should be better, but I've wound up believing that it's usually or always going to be a bad tradeoff. In most situations, your passwords are a lot shorter than your hash values, and often you already have relatively long hash chains. If you have long hash chains, adding one more entry is a marginal gain and you pay a real space penalty for it. Even if you have relatively short chains and a relatively small table, you get basically the same result in less space by adding another reduction function and officially lengthening your chain by one.
(Reduction functions are easily added as far as I can tell;
apparently they're often basically '(common calculation) + i',
i is the index of the reduction function.)
Hashed Ethernet addresses are not anonymous identifiers
Somewhat recently, I read What we've learned from .NET Core SDK Telemtry, in which Microsoft mentioned that in .NET Core 2.0 they will be collecting, well, let's quote them:
- Hashed MAC address — Determine a cryptographically (SHA256) anonymous and unique ID for a machine. Useful to determine the aggregate number of machines that use .NET Core. This data will not be shared in the public data releases.
So, here's the question: is a hashed Ethernet address really anonymous, or at least sufficiently anonymous for most purposes? I will spoil the answer: hashing Ethernet addresses with SHA256 does not appear to make them anonymous in practice.
Hashing by itself does not make things anonymous. For instance, suppose you want to keep anonymous traffic records for IPv4 traffic and you propose to (separately) hash the source and destination IPs with MD5. Unfortunately this is at best weakly anonymous. There are few enough IPv4 addresses that an attacker can simply pre-compute the hashes of all of them, probably keep them in memory, and then immediately de-anonymize your 'anonymous' source and destination data.
Ethernet MAC addresses are 6 bytes long, meaning that there are
2^48 of them that are theoretically possible. However the first
three bytes (24 bits) are the vendor OUI, and there are only a limited number of them that have
been assigned (you can see one list of these here), so the practical number
of MACs is significantly smaller. Even at full size, six bytes is
not that many these days and is vulnerable to brute force attacks.
Modern GPUs can apparently compute SHA256 hashes at a rate of roughly
2.9 billion hashes a second (from here),
or perhaps 4 billion hashes a second (from here).
Assuming I'm doing the math right, it would take roughly a day or
so to compute the SHA256 hash of all possible Ethernet addresses,
which is not very long. The sort of good news is that using SHA256
probably makes it infeasible to pre-compute a
reverse lookup table for
this, due to the massive amount of space required.
However, we shouldn't brute force search the entire theoretical
Ethernet address space, because we can do far better (with far worse
results for the anonymity of the results). If we confine ourselves
to known OUIs, the search space shrinks significantly. There appear
to be only around 23,800 assigned OUIs at the moment; even at only
2.9 billion SHA256 hashes a second, it takes less than three minutes
to exhaustively hash and search all their MACs (and that's with
only a single GPU). The memory requirements for a
reverse lookup table
remain excessive, but it doesn't really matter; three minutes is
fast enough for non-realtime deanonymization for analysis and other
things. In practice those Ethernet addresses that Microsoft are
collecting are not anonymous in the least; they're simply obscured,
so it would take Microsoft a modest amount of work to see what they
I don't know whether Microsoft is up to evil here or simply didn't run the numbers before they decided that using SHA256 on Ethernet addresses produced anonymous results. It doesn't really matter, because not running the numbers when planning data collection such as this is incompetence. If you proposed to collect anonymous identifiers, it is your responsibility to make sure that they actually are anonymous. Microsoft has failed to do so.
On the Internet, merely blocking eavesdropping is a big practical win
One of the things said against many basic encryption measures, such as SMTP's generally weak TLS when one mail server is delivering email to another one, is that that they're unauthenticated and thus completely vulnerable to man in the middle attacks (and sometimes to downgrade attacks). This is (obviously) true, but it is focused on the mathematical side of security. On the practical side, the reality is simple:
Forcing attackers to move from passive listening to active interception is almost always a big win.
There are a lot of attackers that can (and will) engage in passive eavesdropping. It is relatively easy, relatively covert, and quite useful, and as a result can be used pervasively and often is. Far fewer attackers can and will engage in active attacks like MITM interception or forced protocol downgrades; such attacks are not always possible for an attacker (they may have only limited network access) and when the attacks are possible they're more expensive and riskier.
Forcing attackers to move from passive eavesdropping to some form of active interception is thus almost always a big practical win. Most of the time you'll wind up with fewer attackers doing fewer things against less traffic. Sometimes attackers will mostly give up; I don't think there are very many people attempting to MITM SSH connections, for example, although in theory you might be able to get away with it some of the time.
(There certainly were people snooping on Telnet and
connections back in the days.)
If you can prevent eavesdropping, the theoretical security of the environment may not have gotten any better (you have to assume that an attacker can run a MITM attack if they really want to badly enough), but the practical security certainly has. This makes it a worthwhile thing to do by itself if you can. Of course full protection against even active attacks is better, but don't let the perfect be the enemy of the good. SMTP's basic server to server TLS encryption may be easily defeated by an active attacker and frequently derided by security mavens, but it has probably kept a great deal of email out of the hands of passive listeners (see eg Google's report on this).
(I mentioned this yesterday in the context of the web, but I think it's worth covering in its own entry.)
Understanding a bit about the SSH connection protocol
The SSH connection protocol is the final SSH protocol; depending on your perspective, it sits either on top of or after the SSH transport protocol and SSH user authentication protocol. It's the level of SSH where all of the useful things happen, like remote logins, command execution, and port forwarding.
An important thing to know about the connection protocol is that it's a multiplexed protocol. There is not one logical connection that everything happens over but instead multiple channels, all operating more or less independently. SSH has its own internal flow control mechanism for channels to make sure that data from a single channel won't saturate the overall stream and prevent other channels from being responsive. There are different types of channels (and subtypes of some of them as well); one type of channel is used for 'sessions', another for X11 forwarding, another for port forwarding, and so on. However, the connection protocol and SSH as a whole doesn't really interpret the actual data flowing over a particular channel; once a channel is set up, data is just data and is shuttled back and forth blindly. Like many things in the SSH protocol, channel types and subtypes are specified as strings.
(SSH opted to make a lot of things be strings to make them easily extensible, especially for experiments and implementation specific extensions. With strings, you just need a naming convention to avoid collisions instead of any sort of central authority to register your new number. This is why you will see some SSH ciphers with names like 'email@example.com'.)
The major channel type is a 'session', which are basically containers
that are used to ask for login shells, command execution, X11
forwarding, and 'subsystems', which is a general concept for other
sorts of sessions that can be used to extend SSH (with the right
magic on both the server and the client). Subsystems probably aren't
used much, although they are used to implement SFTP. A single session
can ask for and contain multiple things; if you
ssh in to a server
interactively with X11 forwarding enabled, your session will ask for
both a shell and X11 forwarding.
(However, the RFC requires a session to only have one of a shell, a command execution, or a subsystem. This is probably partly because of data flow issues; if you asked for more than one, the connection protocol provides no way to sort out which input and output is attached to which thing within a single channel. X11 forwarding is different, because a new channel gets opened for each client.)
Channels can be opened by either end. Normally the client opens
most channels, but the server can wind up opening channels for X11
clients and for ports being forwarded from the server to the client
(with eg OpenSSH
-R set of options).
OpenSSH's connection sharing works through channel multiplexing, since a client can open multiple 'session' channels over a single connection. The client side is going to be a little complicated, but from the server side everything is generic and general.
When the SSH protocol does and doesn't do user authentication
I haven’t taken a deep look at this, but doesn’t OpenSSH need frequent access to the [user] secret key material? [...]
My gut reaction was that it didn't, but gut reactions are dangerous in crytographic protocols so I decided to find out for sure. This turned out to be more involved than I expected, partly because the various RFCs describing the SSH protocol use somewhat confusing terminology.
SSH is composed of three protocols, which are nominally stacked on top of each other: the SSH transport protocol, the SSH user authentication protocol, and the SSH connection protocol. It is the connection protocol that actually gets things like interactive logins and port forwarding done; everything else is just there to support it. However these protocols are not nested in a direct sense; instead they are sort of sequenced, one after the other, and their actual operation all happens at the same level.
What flows over an SSH connection is a sequence of messages (in a binary packet format); each message has a type (a byte) and different protocols have different range of message types that they use for their various messages. When the SSH connection starts these packets are unencrypted, so the first job is to use the SSH transport protocol to work out session keys and switch on encryption (and in the process the client obtains the server's host key). Once the connection is encrypted the next thing the client must do is request a service, and in basically all situations the client is going to request (and the server is going to require) that this be the user authentication service.
The user authentication service (and protocol) has its own set of messages that are exchanged over the now-encrypted connection between client and server. User authentication is obviously where you initially need the user's secret key material (once challenged by the server). As part of requesting user authentication the client specifies a service name, which is the service to 'start' for the user after user authentication is complete. What starting the service really means is that the server and client will stop sending user authentication messages to each other and start sending connection protocol messages back and forth.
The SSH transport protocol has explicit provisions for re-keying the connection; since all of the SSH (sub)protocols are just sending messages in the overall message stream, this re-keying can happen at any time during another protocol's message flow. Server and client implementations will probably arrange to make it transparent to higher-level code that handles things like the connection protocol. By contrast, the SSH user authentication protocol has no provisions for re-authenticating you; in fact the protocol explicitly states that after user authentication is successful, all further authentication related messages received should be silently ignored. Once user authentication is complete, control is transferred to the SSH connection protocol and that's it.
So that brings us around to our answer: SSH only needs user secret key material once, at the start of your session. Once authenticated you can never be challenged to re-authenticate, and re-keying the overall encryption is a separate thing entirely (and doesn't use your secret keys, although the initial session keys can figure into the user authentication process).
Sidebar: The terminology confusion
I say that the SSH RFCs use confusing terminology because they often say things like that the SSH connection protocol 'run[s] on top of the SSH transport layer and user authentication protocols'. This is sort of true in a logical sense, but it is not true in a wire sense. SSH runs on top of TCP which runs on top of IP in a literal and wire sense, in that SSH data is inside TCP data which is inside IP data. But the various SSH protocols are all messages at the same level in the TCP stream and are not 'on top of' each other in that way. This is especially true of user authentication and the connection protocol, because what really happens is that first user authentication is done and then we shift over to the connection protocol.
Re-applying CPU thermal paste fixed my CPU throttling issues
Back at the start of May, my office workstation started reporting thermal throttling problems when I had all four cores fully busy:
kernel: CPU1: Core temperature above threshold, cpu clock throttled (total events = 1) kernel: CPU3: Package temperature above threshold, cpu clock throttled (total events = 1) kernel: CPU2: Package temperature above threshold, cpu clock throttled (total events = 1) kernel: CPU0: Package temperature above threshold, cpu clock throttled (total events = 1) kernel: CPU1: Package temperature above threshold, cpu clock throttled (total events = 1)
As usual, I stuck these messages into some Internet searches, and the general advice I found was to re-apply thermal paste to the CPU, because apparently inexpensive thermal paste can dry out and get less effective over time (this assumes that you've exhausted obvious measures like blowing out all of the dust and making sure your case fans are working). I put this off for various reasons, including that I was going on vacation and the procedure seemed kind of scary, but eventually things pushed me into doing it.
The short version: it worked. I didn't destroy my office machine's CPU, it was not too annoying to get the standard Intel CPU fan off and then on again, and after my re-application my office machine's CPU doesn't thermally throttle any more and runs reasonably cool. As measured by the CPU itself, when I build Firefox using all four cores the temperature now maxes out around 71 C, and this was what previously ran headlong into those thermal throttling issues (which I believe happen when the CPU reaches 90 C).
(Note that this is with an i5-2500 CPU, which has a 95 W TDP, and the stock Intel cooler. I could probably have gotten the temperature lower by also getting a better aftermarket cooler, but I didn't feel like trying to talk work into spending the extra money for that. Especially when I want to replace the machine anyway.)
In fact my office machine's CPU is now clearly cooler than my identical home machine's CPU while doing the same Firefox build. The situation is not completely comparable (my home machine has probably been exposed to more dust than my work machine, although I try to keep it cleaned out), but this suggests that maybe my home machine would also benefit from me redoing its CPU thermal paste. Alternately I could get around to replacing it with a new home machine, which would hopefully render the issue mostly moot (although if I wind up assembling said new home machine myself, I'll get to apply CPU thermal paste to it).
(It wouldn't be entirely moot, because I'd like to have my current home machine be a functioning backup for any new machine since I don't have a laptop or other additional spare machine lying around.)
PS: I used ArctiClean to clean off the CPU's old thermal paste and Arctic Silver 5 as the new thermal paste. I didn't do any particular research on this, I just picked products that I'd heard of and people seem to talk about favorably.
(This sort of follows up my earlier mention of this.)
Why I am not installing your app on my phone
For reasons beyond the scope of this entry, I spent a decent chunk of time today using my phone to amuse myself. Part of that time was reading Twitter, and part of that reading involved following links to interesting articles on various places. Quite a number of those places wanted me to install their iPhone app instead of reading things on their website, and some of them were quite obnoxious about it. For example, Medium sticks a prominent non-dismissable button in the middle of the bottom of the screen, effectively shrinking an already-too-small screen that much further.
Did I install any of the apps that these websites wanted me to? Of course not. This is not because my phone has only limited space, and it's not really because I prefer keeping my phone uncluttered. There's a much more fundamental reason: I don't trust your app. In fact, I assume that almost all apps that websites want me to use instead of reading the site are actually trojan horses.
By this, I don't mean that I expect any of these apps to quietly attack the security of my phone and attempt to compromise it (although I wouldn't take that bet on Android). I don't even necessarily expect all of these apps to demand intrusive device permissions, like constant access to location services (although I suspect that a lot of them will at least ask for lots of permissions, because maybe I'll be foolish enough to agree). I do definitely expect that all of these apps will put their app nature to 'good' use in order to spy on, track, and monetize my in-app activity to a much larger extent than their websites can. Any potential improvement in my user experience over just reading the website is incidental to their actual reason for existing, which is why they're trojan horses.
There is nothing particularly surprising here, of course. This is simply the inevitable result of the business model of these websites. I'm not their customer, although they may pretend otherwise; instead, I am part of the product, to be packed up and sold off to advertisers. Trying to get me to accept the app is part of fattening me up for their actual customers.
(This is a bit of a grumpy rant, because I got sick and tired of all the 'install our app, really' badgering from various places, especially when it makes their websites less usable. Some of the time these nags encouraged me to close the page as not sufficiently fascinating, which may or may not have been a win for the websites in question.)
The IPv6 address lookup problem (and brute force solution)
In Julia Evans' article Async IO on Linux: select, poll, and epoll,
she mentioned in passing that she
straceed a Go program making a
HTTP request and noticed something odd:
Then [the Go program] makes 2 DNS queries for example.com (why 2? I don’t know!), and uses
epoll_waitto wait for replies [...]
It turns out that this is all due to IPv6 (and the DNS standards),
and it (probably) happens in more than Go programs (although I
straced anything else to be sure). So let's start with
Suppose that you have a 'dual-stack' machine, one with both IPv4 and IPv6 connectivity. You need to talk to a wide variety of other hosts; some of them are available over IPv4 only, some of them are available over IPv6 only, and some of them are available over both (in which case you traditionally want to use IPv6 instead of IPv4). How do you look up their addresses using DNS?
DNS currently has
no way for a client to say 'give me whatever IPv4 and IPv6 addresses
a host may have'. Instead you have to ask specifically for either
IPv4 addresses (with a DNS
A record query) or IPv6 addresses (with
AAAA record query). The straightforward way for a dual-stack
machine to find the IP addresses of a remote host would be to issue
AAAA query to get any IPv6 addresses, wait for it to complete
(or error out or time out), and then issue an
A query for IPv4
addresses if necessary. However, there are a lot of machines that
have no IPv6 addresses, so a lot of the time you'd be adding the
latency of an extra DNS query to your IP address lookups. Extra
latency (and slower connections) doesn't make people happy, and DNS
queries are not necessarily the fastest thing in the world in the
first place for various reasons.
(Plus, there are probably some DNS servers and overall DNS systems
that will simply time out for IPv6
AAAA queries instead of promptly
giving you a 'no' answer. Waiting for a timeout adds substantial
amounts of time. Properly operated DNS systems shouldn't do this,
but there are plenty of DNS systems that don't operate properly.)
To deal with this, modern clients increasingly opt to send out their
AAAA DNS queries in parallel. This is what Go is doing
here and in general (in its all-Go resolver, which is what the Go
runtime tries to use), although it's hard to see it in the
package's source code until you dig quite far down. Go waits for
both queries to complete, but there are probably some languages,
libraries, and environments that immediately start a connection
attempt when they get an answer back, without waiting for the other
protocol's query to finish too.
(There is a related algorithm called Happy Eyeballs which is about
trying to make IPv6 and IPv4 connections in parallel and using
whichever completes first. And there is a complicated RFC on how you should select a
destination address out of the collection that you may get from your
A DNS queries.)
Sidebar: DNS's lack of an 'all types of IP address' query type
I don't know for sure why DNS doesn't have a standard query type
for 'give me all IP addresses, either IPv4 or IPv6'. Per Wikipedia, DNS
itself was created in the mid 1980s, well before IPv6 was designed.
However, IPv6 itself is decades old at this point, which is lots
of time to add such a query type to DNS and have people adopt it
(although it might still not be universally supported, which would
leave you falling back to explicit
A queries at least). My best
guess for why such a query type was never added is a combination
of backwards compatibility worries (since initially not many DNS
servers would support it, so clients would mostly be making an extra
DNS query for nothing) and a general belief on the part of IPv6 people
that IPv4 was going to just go away entirely any day soon, really.
(We know how that one turned out. It's 2017, and IPv4 only hosts and networks remain very significant.)
My views on the JSON Feed syndication feed format
When I first read the JSON Feed version 1 specification, I came away feeling frustrated (and expressed it on Twitter) because my initial impression was that the JSON Feed people had not bothered to look at prior art and (painful) prior experiences. Then I read more, including things like Mapping RSS and Atom to JSON Feed, which made it clear that several things that I thought might be accidental omissions were in fact deliberate decisions. Now my current dominant feeling about JSON Feed is quiet sadness.
On a straightforward level I think that the current JSON Feed
specification makes some bad suggestions about
id elements (and also). I also think that the
specification is at least loosely written overall, with imprecise
language and important general qualifications that are mentioned
only in one spot. I think that this is a bad idea given how I
expect JSON Feed's specification to be read.
Since people implementing JSON Feed seem to currently be coordinating
with each other, JSON Feed may still avoid potential misunderstandings
and being redefined by implementations.
Stepping beyond issues of how the specification is written, I'm sad that JSON Feed has chosen to drop various things that Atom allows. The thing that specifically matters to me is HTML in feed entry titles, because I use that quite frequently, usually for fonts. Resources like Mapping RSS and Atom to JSON Feed make it plain that this was a deliberate choice in creating the specification. I think that Atom encapsulates a lot of wisdom about what's important and useful in a syndication feed format and it would clearly be useful to have a JSON mapping of that, but that's not what JSON Feed is; it has deliberately chosen to be less than Atom, eliminating some features and some requirements outright.
(The whole thing leaves me with the feeling that JSON Feed is mostly crafted to be the minimum thing that more or less works, both in the actual content of the specification and how it's written. Some people will undoubtedly consider this praise for JSON Feed.)
As you might suspect from this, I have no plans to add JSON Feed generation to DWiki, the wacky Python-based wiki engine behind Wandering Thoughts. Among other issues, DWiki is simply not written in a way that would make generating JSON natively at all an easy process. Adding a JSON Feed is probably reasonably easy in most environments where you assemble your syndication feed as a complete data structure in memory and then serialize it in various formats, because JSON is just another format there (and these days, probably an easy one to serialize to). But for better or worse, DWiki uses a fundamentally different way of building feeds.
Should you provide a JSON Feed version of your syndication feed? I have no opinion either way. Do it if you want to, especially if it's easy. I do hope very much that we don't start seeing things that are JSON-Feed-only, because of course there are a lot of syndication feed consumers out there that certainly don't understand JSON Feed now and may never be updated to understand it.
(But then, maybe syndication feeds are on the way out in general. Certainly there has been rumbles about that in the past, although you couldn't prove it from my Atom feed fetch rates.)
How a lot of specifications are often read
In the minds of specification authors, I suspect that they have an 'ideal reader' of their specification. This ideal reader is a careful person; they read the specification all the way through, cross-referencing what they read with other sections and perhaps keeping notes. When there is ambiguity in one part, the ideal reader keeps it in mind as an unsettled issue and looks for things said in other parts that will resolve it, and when something of global importance is mentioned in one section, the reader remembers and applies it to the entire specification.
I'm sure that some specifications are read by some people in this way. If you're working on something of significant importance (especially commercial importance) and there's a core standard, probably you approach it with this degree of care and time, because there is a lot on the line. However, I don't think that this is common. In practice I believe that most people read most specifications rather differently; they read them as if they were references.
People very rarely read references front to back, much less taking notes and reconciling confusions. Instead, they perhaps skim your overview and then when they have a question they open up the reference (the specification), go to the specific section for their issue, and try to read as little as possible in order to get an answer. Perhaps they'll skim some amount of things around the section just in case. People do this for a straightforward reason; they don't want to spend the time to read the entire thing carefully, especially when they have a specific question.
(If it's not too long and is written decently well, people may read your entire specification once, casually and with some skimming, just to get a broad understanding of it. But they're unlikely to read it closely with lots of care, because that's too much work, and then when they wind up with further questions they're going to flip over to treating the specification as a reference and trying to read as little as possible.)
The corollary to this is that in a specification that you want to be implemented unambiguously, it's important that each part or section is either complete in itself or clearly incomplete in a way that actively forces people to go follow cross-references. If you write a section so that it looks complete but is actually modified in an important way by another section, you can probably expect a fair number of the specification's readers to not realize this; they will just assume that it's complete and then they won't remember, notice, or find your qualifications elsewhere.
(This includes sections that are quietly ambiguous as written but have that ambiguity resolved by another section. When this happens, readers are basically invited to assume that they know what you mean and to make up their own answers. This is a great way to wind up with implementations that don't do what you intended.)