i wrote the lion's share of this a while ago but wasn't sure i wanted to publish yet another post about GOTO here since this isn't a programming blog. my mind was made up yesterday when i read this post by Steven J Vaughan-Nichols where he quotes a number of technology personalities essentially giving bullshit excuses for why GOTO is OK to use. it's no wonder 2 separate crypto libraries (both making prodigious use of GOTO) suffered embarrassing and dangerous defects recently when programming thought leaders perpetuate myths about structured programming.
i'm providing this as an object lesson in how to avoid the use of GOTO, especially in security-related code where a higher standard of quality is sorely needed. i'll be using Apple's Goto Fail bug as the example. here is the complete function where the fail was found, with the bug intact:
static OSStatus SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams, uint8_t *signature, UInt16 signatureLen)
{
OSStatus err;
SSLBuffer hashOut, hashCtx, clientRandom, serverRandom;
uint8_t hashes[SSL_SHA1_DIGEST_LEN + SSL_MD5_DIGEST_LEN];
SSLBuffer signedHashes;
uint8_t *dataToSign;
size_t dataToSignLen;
signedHashes.data = 0;
hashCtx.data = 0;
clientRandom.data = ctx->clientRandom;
clientRandom.length = SSL_CLIENT_SRVR_RAND_SIZE;
serverRandom.data = ctx->serverRandom;
serverRandom.length = SSL_CLIENT_SRVR_RAND_SIZE;
if(isRsa) {
/* skip this if signing with DSA */
dataToSign = hashes;
dataToSignLen = SSL_SHA1_DIGEST_LEN + SSL_MD5_DIGEST_LEN;
hashOut.data = hashes;
hashOut.length = SSL_MD5_DIGEST_LEN;
if ((err = ReadyHash(&SSLHashMD5, &hashCtx)) != 0)
goto fail;
if ((err = SSLHashMD5.update(&hashCtx, &clientRandom)) != 0)
goto fail;
if ((err = SSLHashMD5.update(&hashCtx, &serverRandom)) != 0)
goto fail;
if ((err = SSLHashMD5.update(&hashCtx, &signedParams)) != 0)
goto fail;
if ((err = SSLHashMD5.final(&hashCtx, &hashOut)) != 0)
goto fail;
}
else {
/* DSA, ECDSA - just use the SHA1 hash */
dataToSign = &hashes[SSL_MD5_DIGEST_LEN];
dataToSignLen = SSL_SHA1_DIGEST_LEN;
}
hashOut.data = hashes + SSL_MD5_DIGEST_LEN;
hashOut.length = SSL_SHA1_DIGEST_LEN;
if ((err = SSLFreeBuffer(&hashCtx)) != 0)
goto fail;
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &clientRandom)) != 0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
err = sslRawVerify(ctx,
ctx->peerPubKey,
dataToSign, /* plaintext */
dataToSignLen, /* plaintext length */
signature,
signatureLen);
if(err) {
sslErrorLog("SSLDecodeSignedServerKeyExchange: sslRawVerify "
"returned %d\n", (int)err);
goto fail;
}
fail:
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return err;
}
one of the things you might notice is that all roads lead to "fail:", meaning "fail:" isn't really just for failures, it's for clean-up.
another thing you might notice is that the final "goto fail;" doesn't actually bypass any code - it's completely redundant and if it weren't there the next thing to execute would still be the code after the "fail:" label.
the first thing we're going to try is the most obvious approach to refactoring this function, to get rid of GOTO by making proper use of IF.
static OSStatus SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams, uint8_t *signature, UInt16 signatureLen)
{
OSStatus err;
SSLBuffer hashOut, hashCtx, clientRandom, serverRandom;
uint8_t hashes[SSL_SHA1_DIGEST_LEN + SSL_MD5_DIGEST_LEN];
SSLBuffer signedHashes;
uint8_t *dataToSign;
size_t dataToSignLen;
signedHashes.data = 0;
hashCtx.data = 0;
clientRandom.data = ctx->clientRandom;
clientRandom.length = SSL_CLIENT_SRVR_RAND_SIZE;
serverRandom.data = ctx->serverRandom;
serverRandom.length = SSL_CLIENT_SRVR_RAND_SIZE;
if(isRsa) {
/* skip this if signing with DSA */
dataToSign = hashes;
dataToSignLen = SSL_SHA1_DIGEST_LEN + SSL_MD5_DIGEST_LEN;
hashOut.data = hashes;
hashOut.length = SSL_MD5_DIGEST_LEN;
if ((err = ReadyHash(&SSLHashMD5, &hashCtx)) == 0) {
if ((err = SSLHashMD5.update(&hashCtx, &clientRandom)) == 0) {
if ((err = SSLHashMD5.update(&hashCtx, &serverRandom)) == 0) {
if ((err = SSLHashMD5.update(&hashCtx, &signedParams)) == 0) {
if ((err = SSLHashMD5.final(&hashCtx, &hashOut)) == 0) {
hashOut.data = hashes + SSL_MD5_DIGEST_LEN;
hashOut.length = SSL_SHA1_DIGEST_LEN;
if ((err = SSLFreeBuffer(&hashCtx)) == 0) {
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) {
if ((err = SSLHashSHA1.update(&hashCtx, &clientRandom)) == 0) {
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) == 0) {
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) == 0) {
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) == 0) {
err = sslRawVerify(ctx,
ctx->peerPubKey,
dataToSign, /* plaintext */
dataToSignLen, /* plaintext length */
signature,
signatureLen);
if(err) {
sslErrorLog("SSLDecodeSignedServerKeyExchange: sslRawVerify "
"returned %d\n", (int)err);
}
}
}
}
}
}
}
}
}
}
}
}
}
else {
/* DSA, ECDSA - just use the SHA1 hash */
dataToSign = &hashes[SSL_MD5_DIGEST_LEN];
dataToSignLen = SSL_SHA1_DIGEST_LEN;
hashOut.data = hashes + SSL_MD5_DIGEST_LEN;
hashOut.length = SSL_SHA1_DIGEST_LEN;
if ((err = SSLFreeBuffer(&hashCtx)) == 0) {
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) {
if ((err = SSLHashSHA1.update(&hashCtx, &clientRandom)) == 0) {
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) == 0) {
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) == 0) {
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) == 0) {
err = sslRawVerify(ctx,
ctx->peerPubKey,
dataToSign, /* plaintext */
dataToSignLen, /* plaintext length */
signature,
signatureLen);
if(err) {
sslErrorLog("SSLDecodeSignedServerKeyExchange: sslRawVerify "
"returned %d\n", (int)err);
}
}
}
}
}
}
}
}
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return err;
}
as you can see this version of the function is quite a bit longer as well as being deeply nested. this is the kind of code that actually makes programmers think the use of GOTO isn't as bad as their teachers told them it was, because that deep nesting makes the function seem more complex and more difficult to read. on top of which there is a considerable amount of duplicated code. neither of these things are appealing to programmers because they make reading and maintaining the code more work.
however, this is the most simple-minded and unimaginative way to refactor the original function. if we were to also tackle that complex pattern used in virtually all of the IF statements at the same time as getting rid of the GOTOs, we would instead get something like this:
static OSStatus SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams, uint8_t *signature, UInt16 signatureLen)
{
OSStatus err;
SSLBuffer hashOut, hashCtx, clientRandom, serverRandom;
uint8_t hashes[SSL_SHA1_DIGEST_LEN + SSL_MD5_DIGEST_LEN];
SSLBuffer signedHashes;
uint8_t *dataToSign;
size_t dataToSignLen;
signedHashes.data = 0;
hashCtx.data = 0;
err = 0;
clientRandom.data = ctx->clientRandom;
clientRandom.length = SSL_CLIENT_SRVR_RAND_SIZE;
serverRandom.data = ctx->serverRandom;
serverRandom.length = SSL_CLIENT_SRVR_RAND_SIZE;
if(isRsa) {
/* skip this if signing with DSA */
dataToSign = hashes;
dataToSignLen = SSL_SHA1_DIGEST_LEN + SSL_MD5_DIGEST_LEN;
hashOut.data = hashes;
hashOut.length = SSL_MD5_DIGEST_LEN;
err = ReadyHash(&SSLHashMD5, &hashCtx);
if (err == 0)
err = SSLHashMD5.update(&hashCtx, &clientRandom);
if (err == 0)
err = SSLHashMD5.update(&hashCtx, &serverRandom);
if (err == 0)
err = SSLHashMD5.update(&hashCtx, &signedParams);
if (err == 0)
err = SSLHashMD5.final(&hashCtx, &hashOut);
}
else {
/* DSA, ECDSA - just use the SHA1 hash */
dataToSign = &hashes[SSL_MD5_DIGEST_LEN];
dataToSignLen = SSL_SHA1_DIGEST_LEN;
}
if(err == 0) {
hashOut.data = hashes + SSL_MD5_DIGEST_LEN;
hashOut.length = SSL_SHA1_DIGEST_LEN;
err = SSLFreeBuffer(&hashCtx);
}
if (err == 0)
err = ReadyHash(&SSLHashSHA1, &hashCtx);
if (err == 0)
err = SSLHashSHA1.update(&hashCtx, &clientRandom);
if (err == 0)
err = SSLHashSHA1.update(&hashCtx, &serverRandom);
if (err == 0)
err = SSLHashSHA1.update(&hashCtx, &signedParams);
if (err == 0)
err = SSLHashSHA1.final(&hashCtx, &hashOut);
if (err == 0) {
err = sslRawVerify(ctx,
ctx->peerPubKey,
dataToSign, /* plaintext */
dataToSignLen, /* plaintext length */
signature,
signatureLen);
if(err) {
sslErrorLog("SSLDecodeSignedServerKeyExchange: sslRawVerify "
"returned %d\n", (int)err);
}
}
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return err;
}
not only does this follow almost exactly the same format as the original function (thereby retaining it's readability), it makes the condition checking simpler and easier to read, and it has virtually the same number of lines of code as the original.
combining assignment and equivalence tests into a single line IF statement was clearly intended to reduce the overall size of the source code but it failed, and in the process it made the code more complex and difficult to read. the combined assignment/condition checking and GOTO statements were complementary to each other. they supported each other and jointly contributed to the complexity of the original function.
this third version of the function, by contrast, has neither complex expressions nor the potential for complex control flow. the only real complaint one might make is that after an error occurs in one of the many steps in the method, the computer still needs to perform the "if(err == 0)" check numerous times. however that is only true if the compiler can't optimize that code, and checking the same variable against the same constant value over and over again seems like the kind of pattern a compiler's optimization routines might be able to detect and do something about.
complexity is the worse enemy of security, sloppiness begets complexity, and GOTO is a crutch for sloppy, undisciplined programmers - it is part of that sloppiness and contributes to that complexity, even when it's supposedly used the right way. what i did above isn't rocket science or magic. the same basic technique can be used in any case where GOTO is used to jump forward in the code (if you're using it to jump backward then god help you). the excuses people trot out for the continued use of GOTO not only make them sound like dumb-asses, it leads to lesser programmers trying to follow their lead and doing much worse at it. it is never used as sparingly as the gurus think it should be, and even their own examples occasionally contain redundant invocations of it, thoughtlessly applied.
if you actually work at adhering to structured programming rather than abandoning it the moment the going gets tough, you will eventually learn ways to make it just as easy as unstructured programming, you'll be a better programmer for having done so, and your programs will be less complex, easier to validate, and ultimately more secure.
devising a framework for thinking about malware and related issues such as viruses, spyware, worms, rootkits, drm, trojans, botnets, keyloggers, droppers, downloaders, rats, adware, spam, stealth, fud, snake oil, and hype...
Showing posts with label structured programming. Show all posts
Showing posts with label structured programming. Show all posts
Friday, April 04, 2014
Tuesday, February 25, 2014
goto fail, do not pass go, do not collect your next paycheck
by now you've probably heard about the rather widely reported SSL bug that Apple quietly dropped on friday afternoon, teasing security researchers into finding out what was up. if not, the gist of it is that the C code Apple used for verifying signatures used in SSL had what appears to have been a copy&paste error that broke the security and allowed people to read your supposedly secure traffic. literally there were 2 lines that said "goto fail;" when there should only have been one. now i'm not about to make a big deal about copy&paste errors because that can legitimately happen to anyone, but i am going to make a big deal about the content of that copy&paste error.
the overall lack of acknowledgement (and in some cases denial) that the use of goto represents a deeper problem in Apple's application security is itself suggestive of a failure to recognize a fundamental principle: in software, quality is the foundation upon which security must stand.
goto is representative of the kind of spaghetti code we had before the introduction of structured programming approximately 50 years ago. no, that's not a typo. goto has been falling out of favour for about half a century and when i saw how much it was used in Apple's code it raised a red flag for me. every programmer i broached the subject with similarly found it concerning - including my boss who admitted he hasn't coded in C in over 30 years. he wondered, as do i, if the programmer responsible for the code in question still has a job at Apple.
you may think me rehashing a decades old debate, and perhaps i am - i wouldn't know, i never read any of that stuff - Edsger Dijkstra's letter "Go To Statement Considered Harmful" was published about 7 years before i was born. what i'm not doing, however, is mindlessly repeating dogma. we're interested today in application security and, as we have already covered, that requires software quality. structured programming produces software that is higher quality, easier to read, easier to model, easier to audit, easier to prove, etc. than unstructured programming.
why is this so? to answer that we need to think about what "structured programming" means. it is the nature of structure (all structure) to serve as a kind of constraint. your bones, for example, provide structure for your body and in so doing limit the way your body can move and bend. the support pillars for a bridge provide structure for that bridge and limit the extent to which the bridge can move and bend (yes, they bend and flex, but if the structure is doing it's job they only flex a little). likewise, code that follows the structured programming paradigm is constrained such that program control flows in a more limited number of ways. reducing the number of possibilities for program control flow makes it easier to predict (almost by definition) how a block of code's control will flow with just a quick glance. fewer possibilities mean it's easier to know what to expect. i'm sure you've seen the same effect with the written word. just like it's easier to read and understand sentences made with familiar words and phrases and following a few familiar construction patterns, the same is true for reading and understanding code as well. it's just another language, after all.
that reduction of possibilities also reduces the complexity of the code, which makes building a mental model of an arbitrary block of code easier. the constructs that make structured programming what it is lend themselves more naturally to abstraction since random lines of code within them are unlikely to cause program control to jump to random other places in the code. making it easier to build a mental model of the code makes it easier to formally prove the code's correctness because it's easier to describe the algorithm you're trying to prove. less formally, greater ease in building accurate mental models of the code means that it's easier to anticipate outcomes, especially unwanted ones that you want to eliminate, before they happen because it becomes easier to run through those possibilities in your head.
finally, both the greater ease of reading/understanding and the greater ease of modeling benefit efforts of others to review or audit the code. they're really only doing the same thing the programmer him/herself would have done by reading and understanding the code, creating a mental model of it, and trying to anticipate unwanted outcomes.
i work as a programmer professionally in a small company with not a lot of resources. we fly by the seat of our pants in some ways, but if someone asked me to review code that relied as much on goto as this source file from Apple, i wouldn't accept it. i'm surprised that code like that is able to survive for so long in a company with as many resources as Apple has. it makes me wonder about the programming culture within the company and it reminds me of a talk Jacob Appelbaum gave not too long ago where he accused them of writing shitty software. sure code reviews and more rigorous testing might have found the copy&paste error that sparked this off, but those processes don't add quality, they subtract problems. it's still a garbage-in/garbage-out sort of scenario so there's only so much they can do to affect the quality of Apple's software. quality has to go into those filters before you can get quality out.
i've often heard it said that regular programmers typically don't understand the nuances involved in writing secure code, especially when it comes to crypto, and having seen programmers more senior than myself flub crypto code i can certainly agree with that sentiment. that being said, i think it's probably also true that regular security people typically don't understand the nuances involved in writing quality code. since quality is a prerequisite for security, it's just as important for a programmer responsible for security-related code to have mastered the coding practices and techniques that lead to quality software as it is for them to understand secure software development.
i'll concede that it may well be possible for a master programmer to produce high quality, highly readable code that relies as heavily on goto as Apple's programmers appear to; but, as "The Tao Of Programming" satirically points out, you are not a master programmer, almost none of you are, so stop pretending and learn the lessons they've been trying to teach for the past 50 years.
(and now i'll go read Dijkstra's letter. maybe this is a rehash, but that wouldn't make it wrong even if it were)
(updated to fix the spelling of Jacob Appelbaum's name. thanks to Martijn Grooten)
the overall lack of acknowledgement (and in some cases denial) that the use of goto represents a deeper problem in Apple's application security is itself suggestive of a failure to recognize a fundamental principle: in software, quality is the foundation upon which security must stand.
goto is representative of the kind of spaghetti code we had before the introduction of structured programming approximately 50 years ago. no, that's not a typo. goto has been falling out of favour for about half a century and when i saw how much it was used in Apple's code it raised a red flag for me. every programmer i broached the subject with similarly found it concerning - including my boss who admitted he hasn't coded in C in over 30 years. he wondered, as do i, if the programmer responsible for the code in question still has a job at Apple.
you may think me rehashing a decades old debate, and perhaps i am - i wouldn't know, i never read any of that stuff - Edsger Dijkstra's letter "Go To Statement Considered Harmful" was published about 7 years before i was born. what i'm not doing, however, is mindlessly repeating dogma. we're interested today in application security and, as we have already covered, that requires software quality. structured programming produces software that is higher quality, easier to read, easier to model, easier to audit, easier to prove, etc. than unstructured programming.
why is this so? to answer that we need to think about what "structured programming" means. it is the nature of structure (all structure) to serve as a kind of constraint. your bones, for example, provide structure for your body and in so doing limit the way your body can move and bend. the support pillars for a bridge provide structure for that bridge and limit the extent to which the bridge can move and bend (yes, they bend and flex, but if the structure is doing it's job they only flex a little). likewise, code that follows the structured programming paradigm is constrained such that program control flows in a more limited number of ways. reducing the number of possibilities for program control flow makes it easier to predict (almost by definition) how a block of code's control will flow with just a quick glance. fewer possibilities mean it's easier to know what to expect. i'm sure you've seen the same effect with the written word. just like it's easier to read and understand sentences made with familiar words and phrases and following a few familiar construction patterns, the same is true for reading and understanding code as well. it's just another language, after all.
that reduction of possibilities also reduces the complexity of the code, which makes building a mental model of an arbitrary block of code easier. the constructs that make structured programming what it is lend themselves more naturally to abstraction since random lines of code within them are unlikely to cause program control to jump to random other places in the code. making it easier to build a mental model of the code makes it easier to formally prove the code's correctness because it's easier to describe the algorithm you're trying to prove. less formally, greater ease in building accurate mental models of the code means that it's easier to anticipate outcomes, especially unwanted ones that you want to eliminate, before they happen because it becomes easier to run through those possibilities in your head.
finally, both the greater ease of reading/understanding and the greater ease of modeling benefit efforts of others to review or audit the code. they're really only doing the same thing the programmer him/herself would have done by reading and understanding the code, creating a mental model of it, and trying to anticipate unwanted outcomes.
i work as a programmer professionally in a small company with not a lot of resources. we fly by the seat of our pants in some ways, but if someone asked me to review code that relied as much on goto as this source file from Apple, i wouldn't accept it. i'm surprised that code like that is able to survive for so long in a company with as many resources as Apple has. it makes me wonder about the programming culture within the company and it reminds me of a talk Jacob Appelbaum gave not too long ago where he accused them of writing shitty software. sure code reviews and more rigorous testing might have found the copy&paste error that sparked this off, but those processes don't add quality, they subtract problems. it's still a garbage-in/garbage-out sort of scenario so there's only so much they can do to affect the quality of Apple's software. quality has to go into those filters before you can get quality out.
i've often heard it said that regular programmers typically don't understand the nuances involved in writing secure code, especially when it comes to crypto, and having seen programmers more senior than myself flub crypto code i can certainly agree with that sentiment. that being said, i think it's probably also true that regular security people typically don't understand the nuances involved in writing quality code. since quality is a prerequisite for security, it's just as important for a programmer responsible for security-related code to have mastered the coding practices and techniques that lead to quality software as it is for them to understand secure software development.
i'll concede that it may well be possible for a master programmer to produce high quality, highly readable code that relies as heavily on goto as Apple's programmers appear to; but, as "The Tao Of Programming" satirically points out, you are not a master programmer, almost none of you are, so stop pretending and learn the lessons they've been trying to teach for the past 50 years.
(and now i'll go read Dijkstra's letter. maybe this is a rehash, but that wouldn't make it wrong even if it were)
(updated to fix the spelling of Jacob Appelbaum's name. thanks to Martijn Grooten)
Subscribe to:
Posts (Atom)