Enter the 36 chambers of infrastructure wu-tang

Saturday, August 23, 2008

Tuesday, July 01, 2008

ratproxy unleashed

Google just released their internal tool for passive web security assessment. While it has the unfortunate name ratproxy, it looks, frankly, badass. If you care about the security of your site (and even if you don't, your users probably do), you should consider making ratproxy a regular part of your secure development process.

Sunday, June 15, 2008

What not to do, part 2

As expected, there are many TLS sites using keys generated using the flawed, Ubuntu version of OpenSSL. Netcraft has the latest.

Thursday, June 12, 2008

Selecting cryptographic key sizes

Selecting cryptographic key sizes is a valuable reference for estimating the security margin for algorithms and key sizes and is deliciously applicable to TLS configuration choices.

A few tasty tidbits:

Does anyone seriously believe that published attacks represent the state of the art? It may safely be assumed that unpublished work is many years ahead of what the public at large gets to see: a public announcement that a system is broken provides at best a rather trivial upper bound – and a very simple-minded one, in our opinion – for the date that the system became vulnerable.

According to Table 1, 512-bit RSA keys should not have been used beyond 1986.

According to Table 1 usage of 768-bit RSA keys can no longer be recommended. Even in the cost-equivalent model 768-bit RSA keys will soon no longer offer security comparable to the security of the DES in 1982.

SNI is goodness

SSL-enabled Name-based Apache Virtual Hosts with mod_gnutls.

I encourage you to try it out. I have no experience with mod_gnutls, but gnutls is top notch and 80% less code than mod_ssl is a good thing.

Tuesday, June 10, 2008

tls report goes geek hipster big time

O'Reilly Radar post about us.

And this guy, too.

Vertebra == genius

Vertebra is absolute genius. If the implementation is even half as spectacular as the concept, it will take over the planet and we will all worship it like a big golden calf written in Erlang and Ruby.

SSLv2 still considered harmful

Some argue that, although it is deeply flawed, SSLv2 is better than nothing and some very old clients don't support anything better. This is true in the same way that a beautiful lie is better than an ugly truth. I'm an ugly truth kind of guy. What about you?

From 2005: SSL V2 SNAFU

No doubt, everyone is patched up by now and no other such vulnerabilities are present, waiting to be discovered. No doubt.

Monday, June 09, 2008

The excuses

I got some feedback from someone responsible for security at a major site currently tracked in the TLS Report. They took issue with my methodology and gave what I can only describe as excuses for how they were choosing to configure their servers. His argument went something like this:

We support (several) export ciphers because we want to make sure everyone on every browser ever released can access our service. If users want to negotiate a strong cipher, they can! Also, we use a mediocre default cipher because stronger ciphers would be too slow.

Let's break this down:

1) Support for export ciphers

It's 2008. There is no good reason anymore to support export ciphers. If you insist on supporting export ciphers, at least alert users when such a weak cipher is negotiated. Heck, give them suggestions for upgrading to a better browser. Users see the little lock and they assume everything is locked down. They don't know the difference between DHE-RSA-AES256-SHA and EXP-RC2-CBC-MD5. You do.

2) Stronger ciphers are there if people want them

Browser vendors recognize they must support a huge variety of ciphers to deal with the huge variety of ciphers supported by servers. By default, they are almost all turned on. The result is obvious: regardless of the stronger ciphers that could be negotiated, the server default is what will be used. In order to get those stronger ciphers, users would either have to disable all the weaker ones, locking them out of numerous sites, or switch to a browser like Opera, that plays tricks to figure out the strongest available cipher on a server.

The power for this is almost entirely in the hands of the server operators. You know this. Use your power for niceness.

3) Stronger ciphers would be too slow

This is mostly true, although not always relevant. For pages that are not huge, the total processing is often dominated by the session setup. Public key crypto is extremely expensive. If you are spending most of your time on it, then moving to a stronger cipher may have no interesting impact on total performance. See this and this for some really interesting analysis.

The most important aspect of this response, however, is that it is free of any factual basis. Do the analysis, and make decisions based on the facts. If you collect statistics on the set of ciphers your customers' browsers are offering, and you see lots of folks who require export ciphers, figure out how to support them. If you don't see that sort of traffic, or not enough of it to keep supporting it, turn the export ciphers off. If you do performance analysis based on request traces to see the impact of, say, switching from RC4-SHA to AES128-SHA as your default cipher, and discover it would be hideously expensive to change, then don't. What you should not do is assert, without analysis, that these things are true (or false).

No excuses, y'all. As operators of TLS servers, you are the experts on which your users depend. Don't let them down.

Thursday, May 15, 2008

What not to do

I was discussing the following with a good friend of mine earlier and we are both confused about why it is not major news, getting flogged everywhere such geekery is common. This is one of the more egregious failures ever perpetrated.

Debian (and Ubuntu) OpenSSL stupidity.

This is why it is so important that you can't simply hack crypto together and cross your fingers. Some bright spark decided to comment out the source of entropy for the random number generator, so everything works fine, but the keys are no longer random and no longer from an enormous pool. Commented out not for a carefully considered engineering reason, but simply because it was generating errors from a code analysis tool. This is badness.

I won't get into a rant about how a proper development process could let this through and instead will offer these two tidbits. The first is a thread from 2003 regarding errors from Valgrind on this very line of code and exactly why they should be ignored. The second is the entry from the OpenSSL FAQ (added in September 2007) reiterating the point. This was a known issue with a known solution (ignore it).

I cannot overstate this: leave crypto to the pros or become a pro yourself (I'm merely an interested amateur).

The thread.
The FAQ.

TLS Report beta

The TLS Report is stable (give or take). I'll be migrating it to AWS soon, even though their report is not great. But, hey, could be worse!

Wednesday, April 30, 2008

TLS Report alpha

The TLS Report is in alpha here. No promises on stability.

Tuesday, April 15, 2008

The TLS report is on the way!

UPDATE: The production site can be found here.

My reporting tools are taking over! Well, taking over my free time. The TLS Report service will be online in a few weeks. In the mean time, here are some static pages showing the draft format. I switched to a very recent, non-OSX version of OpenSSL, as well, so there are some new ciphers shown (the coolest additions being the EC ciphers at Microsoft). The other, pleasant surprise of the past few days is the sudden, significant improvement of the configuration for www.paypal.com.

Amazon
Facebook
Microsoft
Thawte

Friday, April 04, 2008

Hard data!

Just found this paper: Cryptographic Strength of SSL/TLS Servers: Current and Recent Practices. Looks like I'm not the first hound down this trail! Tons of great data in there.

Devilish details (more on my TLS config obsession)

I updated the TLS tool to check the complete order of preference for all ciphers supported by a given server. While the good ones stayed darned good, the bad ones got even worse. Here are a couple of examples. Notice that, in both cases, the weakest, non-export ciphers are at the top, and there doesn't seem to be any sense to the ordering of the rest of the ciphers. In the case of Facebook, they even prefer several export-grade ciphers over those using ephemeral keying!

Facebook

test run at Sat Apr 05 11:32:09 -0700 2008

grade for www.facebook.com:443 is low

supported protocols for www.facebook.com:
-> SSLv3, TLSv1

default cipher for www.facebook.com:
-> RC4-MD5 TLSv1/SSLv3

server certificate strength is low
-> excessive certificate lifetime (Fri Sep 28 23:53:12 UTC 2007 to Tue Sep 28 23:53:12 UTC 2010)
-> MD5, RSAEncryption, 1024 bits
-> expires Tue Sep 28 23:53:12 UTC 2010

valid ciphers for www.facebook.com, in order of preference:
-> RC4-MD5 TLSv1/SSLv3
-> RC4-SHA TLSv1/SSLv3
-> AES128-SHA TLSv1/SSLv3
-> AES256-SHA TLSv1/SSLv3
-> DES-CBC3-SHA TLSv1/SSLv3
-> DES-CBC-SHA TLSv1/SSLv3
-> EXP-RC4-MD5 TLSv1/SSLv3
-> EXP-DES-CBC-SHA TLSv1/SSLv3
-> DHE-RSA-AES256-SHA TLSv1/SSLv3
-> EDH-RSA-DES-CBC3-SHA TLSv1/SSLv3
-> DHE-RSA-AES128-SHA TLSv1/SSLv3
-> EDH-RSA-DES-CBC-SHA TLSv1/SSLv3
-> EXP-EDH-RSA-DES-CBC-SHA TLSv1/SSLv3
-> EXP-RC2-CBC-MD5 TLSv1/SSLv3


Amazon

test run at Sat Apr 05 11:30:07 -0700 2008

grade for www.amazon.com:443 is low

supported protocols for www.amazon.com:
-> SSLv2, SSLv3, TLSv1

default cipher for www.amazon.com:
-> RC4-MD5 TLSv1/SSLv3

server certificate strength is low
-> SHA1, RSAEncryption, 1024 bits
-> expires Wed Sep 17 23:59:59 UTC 2008

valid ciphers for www.amazon.com, in order of preference:
-> RC4-MD5 TLSv1/SSLv3
-> RC4-MD5 SSLv2
-> RC4-SHA TLSv1/SSLv3
-> DES-CBC3-SHA TLSv1/SSLv3
-> AES256-SHA TLSv1/SSLv3
-> AES128-SHA TLSv1/SSLv3
-> DES-CBC-SHA TLSv1/SSLv3
-> EXP-RC4-MD5 TLSv1/SSLv3
-> EXP-RC4-MD5 SSLv2
-> EXP-DES-CBC-SHA TLSv1/SSLv3
-> EXP-RC2-CBC-MD5 TLSv1/SSLv3
-> EXP-RC2-CBC-MD5 SSLv2

Saturday, March 29, 2008

Recommended SSLCipherSuite configurations for Apache

UPDATE 2008-06-25: Here's my current recommendation (the rest is left for historical context).
SSLCipherSuite DHE-RSA-AES256-SHA:EDH-RSA-DES-CBC3-SHA:DHE-RSA-AES128-SHA: AES256-SHA:DES-CBC3-SHA:AES128-SHA:RC4-SHA

--

The completist (Thawte-style)

SSLCipherSuite DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:EDH-RSA-DES-CBC3-SHA: AES256-SHA:DES-CBC3-SHA:AES128-SHA:RC4-SHA:RC4-MD5

The minimalist (Microsoft-style)

SSLCipherSuite AES128-SHA:AES256-SHA:RC4-SHA:DES-CBC3-SHA:RC4-MD5

For the Comodo version of the minimalist, swap the order of the 2 AES ciphers.

UPDATE 2008-04-04: Slight change to the minimalist config based on more detailed results from the new tool. This also means that the Comodo config is not what is stated above, but is instead:

The descending minimalist (Comodo-style)

SSLCipherSuite AES256-SHA:AES128-SHA:DES-CBC3-SHA:RC4-SHA:RC4-MD5

SSL/TLS cipher selection, with examples and discussion

The last post has examples of some TLS web server crypto configs, but I didn't explain them, particularly why some are better than others. Let's remedy that.

We'll start with some good ones (always start by examining properly packed parachutes!).

Thawte
The completist

grade for www.thawte.com is HIGH

negotiated cipher for www.thawte.com:
DHE-RSA-AES256-SHA
ephemeral keying -> 1024 bits [expires Sat Jan 17 23:59:59 UTC 2009]

valid ciphers for www.thawte.com:
DHE-RSA-AES256-SHA
AES256-SHA
EDH-RSA-DES-CBC3-SHA
DES-CBC3-SHA
DHE-RSA-AES128-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)

It is clear that a lot of thought went into the selection of these ciphers and their order of preference (this test only shows us the top choice of the server). Starting at the bottom we have the sole SSLv2 cipher for use by the crustiest old browsers. RC4-MD5 is the strongest, widely deployed, unencumbered SSLv2 cipher, so this is the best choice for backwards compatibility. Next there are 2 more RC4 ciphers (both MD5 versions likely come from the same config statement): the same as the bottom, but in TLSv1/SSLv3 guise, and a slightly improved version with SHA1. Again, these are great choices for balancing the minimum security supported against the need to support as many clients as possible.

The top 6 ciphers can be divided into 3 pairs, with each pair using a strong symmetric cipher with and without ephemeral keying. AES128 and Triple DES (the DES ciphers shown with 168 bit keys) are the most common, high strength, symmetric ciphers in modern browsers. The top cipher (DHE-RSA-AES256-SHA) is not only the strongest cipher you can reasonably expect browsers to support, it's also the most preferred cipher from this server. Although I'd like to see a 2048 bit server key to give a bit more strength to sessions not using the DH ciphers, this is a really clean config.

Great job, Thawte!

Microsoft
The minimalist

grade for www.microsoft.com is MEDIUM

negotiated cipher for www.microsoft.com:
AES128-SHA
server certificate strength is LOW -> 1024 bits [expires Wed Feb 11 18:25:18 UTC 2009]

valid ciphers for www.microsoft.com:
AES256-SHA
DES-CBC3-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)

This config is very similar to the one from Thawte (great minds think alike?), with the elimination of the DH ciphers. One point of note is that they have chosen not to make the default cipher the strongest one available. This, again, is likely a conscious choice to balance compatibility, security, and performance. AES128-SHA is a very high security cipher, but it also has good performance on a variety of devices. A 2048 bit server key would make this a perfect, all-purpose config.

I haven't checked whether these ciphers are the defaults for IIS. If they are, even more credit to Microsoft for delivering a default security posture superior to many, competing products. Either way, good job!

As a side note, Comodo has a very good, very similar configuration, but the difference makes it less useful as an example.

UPDATE 2008-03-30: This is a really useful little post about IE ciphers. Good reading!

And now, some not so good ones.

Facebook
The kitchen sink

grade for www.facebook.com is LOW

negotiated cipher for www.facebook.com:
RC4-MD5
server certificate strength is LOW -> 1024 bits [expires Tue Sep 28 23:53:12 UTC 2010]

valid ciphers for www.facebook.com:
DHE-RSA-AES256-SHA
AES256-SHA
EDH-RSA-DES-CBC3-SHA
DES-CBC3-SHA
DHE-RSA-AES128-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)
EDH-RSA-DES-CBC-SHA
DES-CBC-SHA
EXP-EDH-RSA-DES-CBC-SHA
EXP-DES-CBC-SHA
EXP-RC2-CBC-MD5
EXP-RC2-CBC-MD5 (SSLv2)
EXP-RC4-MD5
EXP-RC4-MD5 (SSLv2)

Yikes! I have to think they didn't put much thought into this configuration. It looks like they either went with the Apache default config (which is a bloated mess), or just turned on almost every option and (mostly) called it a day. There is no good reason to include any export ciphers, much less lots of export ciphers (if anyone has solid data supporting or contradicting that assertion, I'd really love to see it). Those 56 bit DES ciphers are similarly antiquated and have no business in there.

The worst part is that, having thrown in every cipher they could find, they set the default to RC4-MD5, a medium security cipher. Facebook has chosen to take the same cipher our good examples above use as a last-ditch fallback for TLSv1/SSLv3 negotiation and made it their preferred cipher. Adding insult to injury is that 1024 bit server key.

A little thought and planning would go a long way.

Bank of America
The heist

grade for www.bankofamerica.com is LOW

negotiated cipher for www.bankofamerica.com:
RC4-MD5
server certificate strength is LOW -> 1024 bits [expires Sat Jan 17 23:59:59 UTC 2009]

valid ciphers for www.bankofamerica.com:
DES-CBC3-SHA
RC4-MD5
RC4-MD5 (SSLv2)
DES-CBC-SHA

It looks like someone spent a little time thinking about what to do here, and then decided on the wrong things. The 56 bit DES cipher serves no purpose, there are no AES-based ciphers, and the server default cipher is, as with Facebook, a medium security cipher best used as a fallback. Finally, they have the traditional, 1024 bit server key to round things out.

I'd like to say I expect better from a bank. Instead, all I can say is that I hope for better from a bank. I won't hold my breath.

Yes, gentle reader, the state of affairs in the rarefied world of SSL/TLS web server configuration is pretty rotten. I know we can do better than this. The problems are not technical, but social. Vendors must ship products with sane defaults. Companies who run secure web servers must take due care in configuring them. Customers of those companies should pressure them to fix things that are broken and then take their business elsewhere if problems persist.

setec astronomy

The sorry state of SSL/TLS operational best practice

A recent discussion on the TLS Working Group list got me curious about how well folks are doing at configuring SSL/TLS on their "secure" web servers.  I put together a simple tool to help answer the question, and the the results are pretty grim.  Little adoption of the ciphers that use ephemeral keys, widespread use of known-bad export ciphers, almost universal use of 1024 bit RSA keys for server certificates, etc.

Without discussion, here are some results:

Amazon.com

grade for www.amazon.com is LOW

negotiated cipher for www.amazon.com:
RC4-MD5
server certificate strength is LOW -> 1024 bits [expires Wed Sep 17 23:59:59 UTC 2008]

valid ciphers for www.amazon.com:
AES256-SHA
DES-CBC3-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)
DES-CBC-SHA
EXP-DES-CBC-SHA
EXP-RC2-CBC-MD5
EXP-RC2-CBC-MD5 (SSLv2)
EXP-RC4-MD5
EXP-RC4-MD5 (SSLv2)

Microsoft

grade for www.microsoft.com is MEDIUM

negotiated cipher for www.microsoft.com:
AES128-SHA
server certificate strength is LOW -> 1024 bits [expires Wed Feb 11 18:25:18 UTC 2009]

valid ciphers for www.microsoft.com:
AES256-SHA
DES-CBC3-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)

Comodo

grade for www.comodo.com is MEDIUM

negotiated cipher for www.comodo.com:
AES256-SHA
server certificate strength is LOW -> 1024 bits [expires Mon Jun 28 23:59:59 UTC 2010]

valid ciphers for www.comodo.com:
AES256-SHA
DES-CBC3-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)

PayPal

grade for www.paypal.com is LOW

negotiated cipher for www.paypal.com:
RC4-MD5
server certificate strength is LOW -> 1024 bits [expires Thu Jan 29 23:59:59 UTC 2009]

valid ciphers for www.paypal.com:
AES256-SHA
DES-CBC3-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)
DES-CBC-SHA
EXP-DES-CBC-SHA
EXP-RC4-MD5
EXP-RC4-MD5 (SSLv2)

Facebook

grade for www.facebook.com is LOW

negotiated cipher for www.facebook.com:
RC4-MD5
server certificate strength is LOW -> 1024 bits [expires Tue Sep 28 23:53:12 UTC 2010]

valid ciphers for www.facebook.com:
DHE-RSA-AES256-SHA
AES256-SHA
EDH-RSA-DES-CBC3-SHA
DES-CBC3-SHA
DHE-RSA-AES128-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)
EDH-RSA-DES-CBC-SHA
DES-CBC-SHA
EXP-EDH-RSA-DES-CBC-SHA
EXP-DES-CBC-SHA
EXP-RC2-CBC-MD5
EXP-RC2-CBC-MD5 (SSLv2)
EXP-RC4-MD5
EXP-RC4-MD5 (SSLv2)

Thawte

grade for www.thawte.com is HIGH

negotiated cipher for www.thawte.com:
DHE-RSA-AES256-SHA
ephemeral keying -> 1024 bits [expires Sat Jan 17 23:59:59 UTC 2009]

valid ciphers for www.thawte.com:
DHE-RSA-AES256-SHA
AES256-SHA
EDH-RSA-DES-CBC3-SHA
DES-CBC3-SHA
DHE-RSA-AES128-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)

Verisign
Note: Verisign will not negotiate TLSv1

grade for www.verisign.com is LOW

negotiated cipher for www.verisign.com:
RC4-MD5
server certificate strength is LOW -> 1024 bits [expires Fri May 08 23:59:59 UTC 2009]

valid ciphers for www.verisign.com:
DES-CBC3-SHA
RC4-MD5
RC4-MD5 (SSLv2)
DES-CBC-SHA
EXP-RC2-CBC-MD5
EXP-RC2-CBC-MD5 (SSLv2)
EXP-RC4-MD5
EXP-RC4-MD5 (SSLv2)

Bank of America

grade for www.bankofamerica.com is LOW

negotiated cipher for www.bankofamerica.com:
RC4-MD5
server certificate strength is LOW -> 1024 bits [expires Sat Jan 17 23:59:59 UTC 2009]

valid ciphers for www.bankofamerica.com:
DES-CBC3-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)
DES-CBC-SHA

GMail

grade for www.gmail.com is MEDIUM

negotiated cipher for www.gmail.com:
AES256-SHA
server certificate strength is LOW -> 1024 bits [expires Thu May 15 17:24:01 UTC 2008]

valid ciphers for www.gmail.com:
AES256-SHA
DES-CBC3-SHA
AES128-SHA
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)
DES-CBC-SHA
EXP-DES-CBC-SHA
EXP-RC2-CBC-MD5
EXP-RC2-CBC-MD5 (SSLv2)
EXP-RC4-MD5
EXP-RC4-MD5 (SSLv2)

CIA

grade for www.cia.gov is LOW

negotiated cipher for www.cia.gov:
RC4-SHA
server certificate strength is LOW -> 1024 bits [expires Sat Feb 12 23:59:59 UTC 2011]

valid ciphers for www.cia.gov:
RC4-SHA
RC4-MD5
RC4-MD5 (SSLv2)
EXP-RC4-MD5
EXP-RC4-MD5 (SSLv2)

Wednesday, March 26, 2008

BigDecimal: mostly acceptable alternative to Float

I forgot to mention in yesterday's extended whine about Ruby floating point that there is a package in the standard library called BigDecimal that is a mostly good alternative to the normal Float class.  Here is the simplest way to use it:

require "bigdecimal"
require "bigdecimal/util"

The second line adds a new method, to_d, to String, Float, and Rational.  Back to my annoying example of broken floating point math:

>> require "bigdecimal"
=> true
>> require "bigdecimal/util"
=> true
>> (9.54 / 0.001)
=> 9540.0
>> (9.54 / 0.001).to_i
=> 9539
>> (9.54 / 0.001).to_d.to_i
=> 9540

Well, almost.  BigDecimal slows things down a bit compared to Float and the extra work (the remembering part, not the typing part) of adding to_d is effort that shouldn't be required.  I should be clear here that I have no problem with BigDecimal itself.  BigDecimal is extremely useful, as are the extended precision libraries for other languages.  No, the problem remains that, even though the Ruby Core team could implement a reasonable compromise in Float so it behaves consistently, they refuse to.

rb_spread patch for Ruby 1.8.recent/1.9 and Spread 4.0

Spread is a group communication system, developed at Johns Hopkins, based on Virtual Synchrony and Extended Virtual Synchrony. Lots of neat stuff therein. The Ruby API hasn't been updated since 2005, though, and will not compile against recent versions of Ruby 1.8 or 1.9, nor is it compatible with the 4.0 release of Spread.

I put together this patch to update the API to the current goodness and make the test and sample programs actually work. My apologies for not doing lots of #ifdefs to make it work with both Spread 3.x and 4.x. I have only tested this on OS X 10.5 on i386. Success or failure, please let me know your experiences on other platforms.

Tuesday, March 25, 2008

Floating point arithmetic, bug reports, and monkey patching

It turns out that Barbie was right and math actually is hard. At least, math is hard if you adhere to IEEE floating point spec. C adheres to that spec. Languages built on C inherit that adherence and, even though they could fix some of the problems that come with it, they leave things broken.

In the case of Ruby, they also tell you there is no problem. And it is fixed in the next version. With this monkey patch. But I'm getting ahead of myself.

Here's Ruby (1.8.6 on OS X 10.5) in action:

>> (9.54 / 0.001)
=> 9540.0
>>

Well, that seems straightforward enough. 9540 is certainly the right answer. I need the integer representation, not the floating point version, though, so we'll just call the handy Float#to_i function:

>> (9.54 / 0.001).to_i
=> 9539
>>

Don't feel badly, I blinked several times and had a few more sips of beer before I realized what I was seeing. A second ago, the value was 9540, but now it is 9539. I'll just bang on it a bit to see what I've done wrong:

>> (9.54 / 0.001).to_s.to_i
=> 9540
>>

Yup. By converting the value to a String and then converting that String to an Integer we get the right value back. Sometimes Ruby admits to the underlying IEEE spec edge case, other times it hides it. Sadly, Python behaves similarly (though we are alerted to a problem immediately, rather than having it sneak up on us later):

>>> (9.54 / 0.001)
9539.9999999999982
>>>

Yikes! And, frowny face, the other trouble is here, too:

>>> str(9.54 / 0.001)
'9540.0'
>>> int(9.54 / 0.001)
9539
>>>

I wrote a simple C program to play directly with the underlying types. Here are two variants that illustrate the problem clearly:

#include

int
main()
{
  double f = 9.54 / 0.001;

  printf("%f %i\n", f, (int)f);

  return 0;
}

$ ./test
9540.000000 9539
$

#include

int
main()
{
  double f = 9.54 / 0.001;

  printf("%f %i\n", f, (int)(float)f);

  return 0;
}

$ ./test
9540.000000 9540
$

So, I filed a bug against Ruby. Turns out someone else filed a similar bug a few weeks ago. Both of our bugs were rejected on the grounds that things were working as they were supposed to. I continued to argue that, regardless of what the underlying data types are doing, Ruby could and should do better. We went back and forth a bit, and the Ruby gent finally noticed I filed the bug against 1.9.

Why does that matter? Well, it seems that, even though it totally, definitely, under no circumstances is a bug, they've added functionality to Float#round that, among other things, means it is possible to mostly work around the problem. You can now pass it an argument giving the rounding precision desired. So now we can do this (Ruby 1.9 on OS X 10.5):

irb(main):002:0> (9.54 / 0.001).round(2).to_i
=> 9540
irb(main):003:0>

Our grumpy Ruby Core gent was even kind enough to provide a monkey patch to Float to provide a truncate method that actually worked reliably:

class Float
  def truncate_rounding(figs)
    if figs > 0
      n = 10 ** figs
      (self * n).round / n
    else
      n = 10 ** -figs
      (self / n).round / 10 * (n * 10)
    end
  end
end

Since this is absolutely not a bug, the workaround (I can't call it a fix since it introduces problems with overflow) will no doubt remain a hack. A hack to make Float behave correctly. That's a darned shame, Ruby folks.