Also in the series

The importance of data security

I’m going to tell you something that you already know: security is important. No matter how much we might rather be looking at shiny new JavaScript frameworks, data integrity is one of those few issues that can destroy the reputation of our products and companies overnight. What UI framework BigBank’s web app uses will never make the front page news, but a visible security breach definitely will. There’s a lot at stake in getting it right.

Considering its importance, it’s clear that everybody involved in software development needs to try and find a way to enhance their security know-how, even if the subject area might not be their favorite thing. I am in no way a security expert and I’ve never worked in a dedicated security role. I definitely get more of a kick out of building stuff than securing stuff and you might be the same way inclined. If you are, we’re exactly the sort of engineers that need to up our game. In the grand scheme of things, a lot of what we would rather be thinking about is completely worthless if we get the basics of security wrong.

This post is the first in a series that I’m calling: Security IQ. The series will go through the basics of software security via my preferred learning method:

  1. Read about it.
  2. Explain it so that your Grandma could understand it. Or at least attempt to.
  3. Hack together some code that does it.
  4. Make tons of mistakes.
  5. Keep the experience, but shelve the code.

We need to be writing code, it’s not enough to just read about something. You haven’t really learned anything until you have applied it. Thankfully, we’re not going to get precious about the code, we’re treating it as disposable. It’s not going into production and it doesn’t have to be general purpose. It’s just a safe vehicle for us to make those early mistakes that we all make when we pick up something new so that we’re better prepared when we need to do it properly.

We’re starting at the only sensible place to start in security: public key cryptography. I’m going to explain the very basics to you, then guide you through a simple program that I hacked together that does it.

Let’s get started.

What is it for?

Public key cryptography serves two purposes:

  1. Privacy: only the intended receiver of a message should be able to read that message. Nobody else can read it.
  2. Tamper-proofing: the message received should be identical to that which was sent. We can be sure that it hasn’t been tampered with.

Grandma’s Explanation

Mathematically linked key pairs

In public key cryptography, each sender or receiver of information has a pair of mathematically linked keys, one is the public key and the other is the private key. Each key can encode data into a secret code so that it becomes indecipherable to others and this process can only be reversed by the other key in the pair.

encryption_and_decryption

Unencrypted data is typically referred to as plaintext, with its encrypted form known as ciphertext. Key pairs can be generated with different strengths, this strength is a product of the number of bits used to produce the keys. Ciphertext looks like a series of random bytes and its size is proportional to the strength/size of the key that encrypted it, not the original plaintext. For example, ciphertext that was produced with a 2048-bit RSA key will be exactly 256 bytes (2048 bits) long no matter how small the plaintext representation is.

Public key exchange

If the encryption produced by one key can only be undone by its partner, it stands that a person who wishes to exchange encrypted messages with another will need access to one of the other person’s two keys. The difference between the public key and the private key is that the former can be made available for anybody to use, but the private key must be kept secret and only accessible by the owner.

Two entities wishing to communicate securely normally exchange public keys before they communicate.

public_key_swap

If Luke and Vader swap public keys, keeping their corresponding private keys safe and Vader encrypts a message with Luke’s public key before transfer, he can be sure that only Luke can decrypt and understand it on receiving the message. If Luke wants to respond, he can encrypt that response using Vader’s public key, safe in the knowledge that only Vader’s private key would be able to decrypt it.

Signing for authenticity

If a public key is freely available, then anybody could send a message that only the private key owner could understand, but how does the recipient know that the message is the same as the one sent? How do we know it hasn’t been corrupted?

In addition to encrypting a message with the receiver’s public key, the sender also produces a digital signature from the plaintext message and their own private key. The recipient decrypts the ciphertext with their own private key, then validates the plaintext with the signature and the sender’s public key.

encrypting_and_signing
Icons credit

These signatures are made by hashing the plaintext message and encrypting the hash with the private key. If the decrypted message produces the same hash on receipt, we can be sure that the plaintext is identical to that which was originally signed. We can also be certain that the message was signed by the owner of a specific private key.

Show me the code

The throwaway program that I have written to demonstrate public key cryptography is a CLI application written in Go (Golang). Go is a great language for this exercise because it’s low-level enough that it doesn’t hide anything important to your understanding, but it does have a well thought-out standard library that cleanly represents the basic building blocks that you need to use.

Introducing pkcrypto

The full code listing for pkcrypto is available here: GitHub.

The application is called pkcrypto. Before we dive into the code, let’s start by looking at the help.

pkcrypto help
NAME:
   pkcrypto - Create RSA key pairs and transmit encrypted messages.

USAGE:
   pkcrypto [global options] command [command options] [arguments...]

VERSION:
   0.0.0

COMMANDS:
     keygen, k    Generate a public/private key pair.
     converse, c  Send and receive encrypted messages to/from a target.
     listen, l    Listen for a message, then send and receive encrypted messages.
     help, h      Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version

The program can do three things. Generate a key pair (keygen), initiate an encrypted conversation (converse) and respond to a request for an encrypted conversation (listen). The idea is that two instances of this program can talk to each other.

pkcrypto keygen help
NAME:
   pkcrypto keygen - Generate a public/private key pair.

USAGE:
   pkcrypto keygen [command options] [arguments...]

OPTIONS:
   --output value, -o value  Directory to output generated keys to. (default: ".")
   --bits value, -b value    Size of the generated keys. (default: 0)

keygen takes an output directory to store the keys in and the strength that they should have in bits.

pkcrypto help converse
NAME:
   pkcrypto converse - Send and receive encrypted messages to/from a target.

USAGE:
   pkcrypto converse [command options] [arguments...]

OPTIONS:
   --target value, -t value    The target to converse with. (default: "localhost:35196")
   --keys value, -k value      Directory containing keys to converse with. (default: ".")
   --messages value, -m value  File containing messages to encrypt and send.

converse takes a target to initiate a conversation with, the location of the keys to use and a file containing messages to encrypt and transmit.

pkcrypto help listen
NAME:
   pkcrypto listen - Listen for a message, then send and receive encrypted messages.

USAGE:
   pkcrypto listen [command options] [arguments...]

OPTIONS:
   --port value, -p value      Port to listen on. (default: "35196")
   --keys value, -k value      Directory containing keys to converse with. (default: ".")
   --messages value, -m value  File containing messages to encrypt and send.

listen is similar to converse, except that it listens for the conversation rather than initiates it. Instead of a target it takes a port to listen on. The other arguments have the same meaning as they do for converse.

Hopefully, you agree that this is a straightforward, simple demo program. Nothing too ambitious while our primary goal is learning. Each instance of the application participates in one conversation with one other instance and then exits. It’s not intended to do anything more than that.

Alright, let’s look at some code.

Generating Keys

Below is the entire keygen function:

// Generate an RSA key pair in the given output directory. The strength of the keys are
// determined by the number of bits used. The private key is output to a file called 'private'
// and the public key is output to a file called 'public'.
func keygen(output string, bits int) error {
    // Generate key pair.
    privateKey, err := rsa.GenerateKey(rand.Reader, bits)
    if err != nil {
        return cli.NewExitError(err, -1)
    }

    // Encode and output private key to file in output dir.
    pemPrivateKey := &pem.Block {
        Type: "PRIVATE KEY",
        Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
    }

    privateKeyFile, err := os.Create(output + "/private")
    if err != nil {
        return cli.NewExitError(err, -1)
    }
    defer privateKeyFile.Close()

    err = pem.Encode(privateKeyFile, pemPrivateKey)
    if err != nil {
        return cli.NewExitError(err, -1)
    }

    // Encode and output public key to file in output dir.
    publicKey := privateKey.PublicKey
    publicKeyBytes, err := x509.MarshalPKIXPublicKey(&publicKey)
    if err != nil {
        return cli.NewExitError(err, -1)
    }
    pemPublicKey := &pem.Block {
        Type: "PUBLIC KEY",
        Bytes: publicKeyBytes,
    }

    publicKeyFile, err := os.Create(output + "/public")
    if err != nil {
        return cli.NewExitError(err, -1)
    }
    defer publicKeyFile.Close()

    err = pem.Encode(publicKeyFile, pemPublicKey)
    if err != nil {
        return cli.NewExitError(err, -1)
    }

    return nil
}

The keys we generate are RSA public and private key pairs. RSA is one of the most popular public key cryptosystems and you probably use it already for SSH-ing to servers. Functions for working with RSA keys are available in the crypto/rsa package of the Go standard library.

rsa.GenerateKeys take a random number generator (rand.Reader is a convenient, secure and globally available one) and the number of bits that should be used to generate the keys. The more bits used the stronger the encryption the keys can produce. Typical RSA keys are between 1024-4096 bits.

x509.MarshalPKCS1PrivateKey and x509.MarshalPKIXPublicKey encodes each key into DER ASN.1 form (a standard for serializing this type of data structure). The encoded bytes are then put into a pem.Block structure (a format for structuring certificates and keys) and output to the files we want to store them in with the pem.Encode method.

Any program that uses RSA keys can now read these in and use them. These aren’t really any different to RSA keys you would produce with OpenSSH or similar. Next, we’re going to use them to send encrypted messages.

Public Key Exchange

Two instances of pkcrypto exchange messages over a single TCP connection. Once a connection, initiated by the instance in converse-mode, is accepted by the instance in listen-mode, public keys are exchanged over the connection. Converse-mode sends first then receives second and vice-versa for listen-mode.

The functions that perform the public key exchange are as follows:

// Writes the given RSA public key to the ostream wrapped by 'writer'. Used to send our instance's
// public key in a key exchange. First we send 2 bytes that represent the size of the key, which
// differs based on how many bits it was generated with. This tells the receiver how many bytes to
// read off the wire to receive the key, as there isn't a sensible delimiting byte we could use.
// Then we send our x509-encoded public key.
func sendPublicKey(writer *bufio.Writer, publicKey *rsa.PublicKey, logger *log.Logger) error {
    bytes, err := x509.MarshalPKIXPublicKey(publicKey)
    if err != nil {
        logger.Println("Failed to marshal public key. ", err)
        return err
    }

    // First 2 bytes will tell the receiver how many bytes long the public key is, so that it
    // knows how many to read.
    header := make([]byte, 2)
    binary.LittleEndian.PutUint16(header, uint16(len(bytes)))
    _, err = writer.Write(header)
    if err != nil {
        logger.Println("Failed to write public key header.", err)
        return err
    }

    // Now send the public key
    _, err = writer.Write(bytes)
    if err != nil {
        logger.Println("Failed to write public key.", err)
        return err
    }
    err = writer.Flush()
    if err != nil {
        logger.Println("Failed to transmit public key. ", err)
    }
    logger.Printf("Sent public key: %x.\n", bytes)

    return nil
}
// Receives and parses an RSA public key from the istream wrapped by 'reader'. The first two
// bytes read tell us how large the key is, then we read that many bytes out and parse the result
// into an rsa.PublicKey.
func receivePublicKey(reader *bufio.Reader, logger *log.Logger) (*rsa.PublicKey, error) {
    // Read the first 2 bytes off the wire first, this tells us the number of remaining bytes
    // we need to read.
    header := make([]byte, 2)
    _, err := reader.Read(header)
    if err != nil {
        logger.Println("Failed to read public key header.", err)
        return nil, err
    }
    size := binary.LittleEndian.Uint16(header)
    logger.Printf("Received header. Incoming public key is %d bytes long.", size)

    // Now read the public key.
    bytes := make([]byte, size)
    _, err = reader.Read(bytes)
    if err != nil {
        logger.Println("Failed to read public key. ", err)
        return nil, err
    }
    logger.Printf("Received public key: %x.\n", bytes)

    publicKey, err := x509.ParsePKIXPublicKey(bytes)
    if err != nil {
        logger.Println("Failed to parse public key. ", err)
        return nil, err
    }

    return publicKey.(*rsa.PublicKey), nil
}

The ASN.1 bytes are sent over the wire to the receiver, which can be parsed at the other end into a public key. One problem is that the receiver does not know how long the stream of encoded bytes actually is, it varies depending on the number of bits used to produce the key when it is created. A program that doesn’t use one connection for everything could just terminate the connection at the sender once all the bytes had been transmitted, the receiver could then stop reading when it encounters EOF, but using one connection simplified this program in other ways. It’s only a demo hack, after all.

Instead, the first two bytes sent over the wire contain the number of bytes the receiver should read to parse the entire public key. The receiver function reads the first two bytes, creates a byte slice of that size to read the rest of the encoded bytes into and instantiates the rsa.PublicKey from that.

Using these functions, each instance of pkcrypto gets the public key of the other.

Encrypting and Signing

Now that we have performed the public key exchange we can send and receive encrypted messages using those keys. The instance of pkcrypto in converse-mode goes first again, then each instance takes turns receiving a message, decrypting it and validating the signature, before sending back an encrypted message and signature of its own. Both instances terminate once they have sent all of their messages, which they read out of a file before the public key exchange.

The function that encrypts, signs and sends the messages is as follows:

// Encrypts the given message with the given RSA public key, then writes it to the ostream wrapped
// by 'writer'. The SHA256 hash of the message is signed with our private key and sent also.
func encryptSignAndSend(message string, writer *bufio.Writer, publicKey *rsa.PublicKey,
        privateKey *rsa.PrivateKey, logger *log.Logger) error {

    logger.Println("Sending (plaintext): " + message)

    // Encrypt message
    label := []byte("pkcrypto")
    messageBytes := []byte(message)
    ciphertext, err :=  rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, messageBytes,
        label)
    if err != nil {
        logger.Println("Failed to encrypt message.", err)
        return err
    }

    // Sign the hash of message
    hash := sha256.New()
    hash.Write(messageBytes)
    hashed := hash.Sum(nil)
    signature, err := rsa.SignPSS(rand.Reader, privateKey, crypto.SHA256, hashed, nil)
    if err != nil {
        logger.Println("Failed to sign message.", err)
        return err
    }

    // First two bytes over the wire tell the receiver how large any ciphertext is, so that it
    // knows how many bytes to read.
    header := make([]byte, 2)
    binary.LittleEndian.PutUint16(header, uint16(len(ciphertext)))

    logger.Printf("Sending (ciphertext): %x\n", ciphertext)
    _, err = writer.Write(header)
    if err != nil {
        logger.Println("Failed to write ciphertext header to target.", err)
    }
    // Next write the ciphertext
    _, err = writer.Write(ciphertext)
    if err != nil {
        logger.Println("Failed to write ciphertext to target.", err)
        return err
    }
    // Then we send the length of the signature. As this is from our private key, not the
    // target's public key, it isn't necessarily the same size as the ciphertext.
    sigHeader := make([]byte, 2)
    binary.LittleEndian.PutUint16(sigHeader, uint16(len(signature)))
    _, err = writer.Write(sigHeader)
    if err != nil {
        logger.Println("Failed to write signature header.", err)
        return err
    }
    // Finally we write our signature
    logger.Printf("Signature: %x\n", signature)
    _, err = writer.Write(signature)
    if err != nil {
        logger.Println("Failed to write signature.", err)
        return err
    }
    // Flush all the bytes
    err = writer.Flush()
    if err != nil  {
        logger.Println("Failed to transmit encrypted message to target.", err)
        return err
    }

    return nil
}

rsa.EncryptOAEP is the function that produces the ciphertext. There are multiple choices of encryption algorithm in the rsa package, but OAEP is the one recommended as the most secure choice. This method takes a hashing function (here we use SHA256), a random number generator (rand.Reader again) the public key of the receiver and the bytes of the plaintext message.

Next, we produce the signature that the receiver will use to verify the message after decryption. Here we use PSS signatures, again recommended in the rsa package. The signature is the hash of the message bytes, signed by the sender’s private key.

We transmit the ciphertext and the signature using a similar approach used in the public key exchange. A two-byte header is sent before each containing their size.

Decrypting and Verifying

// Receives an encrypted message and a signature from the istream wrapped by 'reader', decrypts the
// message with our RSA private key, calculates the SHA256 hash of the plaintext and validates the
// signature. Returns the plaintext message.
func receiveDecryptAndVerify(reader *bufio.Reader, privateKey *rsa.PrivateKey,
        publicKey *rsa.PublicKey, logger *log.Logger) (string, error) {

    // Read the two byte header, which tells us the length (bytes) of the ciphertext.
    header := make([]byte, 2)
    _, err := reader.Read(header)
    if err != nil {
        logger.Println("Failed to read ciphertext header.", err)
        return "", err
    }
    size := binary.LittleEndian.Uint16(header)
    logger.Printf("Received header. Incoming ciphertext is %d bytes long.", size)
    // Read ciphertext
    ciphertext := make([]byte, size)
    _, err = reader.Read(ciphertext)
    if err != nil {
        logger.Println("Failed to read ciphertext from target.", err)
        return "", err
    }
    logger.Printf("Received (ciphertext): %x\n", ciphertext)
    // Read another two byte header, which tells us the length (bytes) of the signature.
    sigHeader := make([]byte, 2)
    _, err = reader.Read(sigHeader)
    if err != nil {
        logger.Println("Failed to read signature header.", err)
        return "", err
    }
    sigSize := binary.LittleEndian.Uint16(sigHeader)
    logger.Printf("Received signature header. Incoming signature is %d bytes long.", sigSize)
    // Read signature
    signature := make([]byte, sigSize)
    _, err = reader.Read(signature)
    if err != nil {
        logger.Println("Failed to read signature from target.", err)
        return "", err
    }
    logger.Printf("Received signature: %x\n", signature)

    // Decrypt message
    label := []byte("pkcrypto")
    plaintext, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, ciphertext, label)
    if err != nil {
        logger.Println("Failed to decrypt cipher.", err)
        return "", err
    }
    logger.Printf("Received (plaintext): %s\n", plaintext)

    // Hash plaintext and verify signature
    hash:= sha256.New()
    hash.Write(plaintext)
    hashed := hash.Sum(nil)
    err = rsa.VerifyPSS(publicKey, crypto.SHA256, hashed, signature, nil)
    if err != nil {
        logger.Println("Failed signature verification.", err)
        return "", err
    }
    logger.Println("Signature verified.")

    return string(plaintext), nil
}

The receiving function is basically the inverse of the sending function. It receives the ciphertext and the signature and decrypts the message using the rsa.DecryptOAEP method. To verify the message, we calculate the SHA256 hash of the plaintext and validate it with the PSS signature and public key of the sender. If the hash checks out, we know the message has not been modified in transit.

A demo

You can see the rest of the code on GitHub, most of the rest of it isn’t too interesting. Just to prove the code actually does something, here is a walkthrough of it working.

First, we generate two pairs of keys into two separate directories, a pair for each instance of the application we will use. I’ve gone with two different key strengths here.

mkdir luke_keys
pkcrypto keygen --output luke_keys --bits 2048
mkdir vader_keys
pkcrypto keygen --output vader_keys --bits 1024

Next, we start an instance of the application with the listen command. Here we start listening on port 35196. luke.txt contains a list of messages to send, that you will see shortly (one per line).

./pkcrypto listen --port 35196 --keys ./luke_keys --messages ./luke.txt

Finally, we start another instance with the converse command, with the target being our listener instance.

pkcrypto converse --keys vader_keys --target localhost:35196 --messages vader.txt

We then see the following output to the console:

Sending: Obi-Wan never told you what happened to your father.
Received: He told me enough! He told me *you* killed him! 

Sending: No, *I* am your father.
Received: No. No. That's not true. That's impossible! 

Sending: Search your feelings, you know it to be true!
Received: No! No!

The listener instance outputs the reverse:

Received: Obi-Wan never told you what happened to your father.
Sending: He told me enough! He told me *you* killed him!

Received: No, *I* am your father.
Sending: No. No. That's not true. That's impossible!

Received: Search your feelings, you know it to be true!
Sending: No! No!

We only see the plaintext messages in the console, but each instance of the program produces a log file that shows what we have encrypted, decrypted, signed and verified. Here is converse.log:

2017/08/28 17:01:48 pkcrypto.go:162: Loading keys from: vader_keys
2017/08/28 17:01:48 pkcrypto.go:169: Successfully loaded private and public keys
2017/08/28 17:01:48 pkcrypto.go:178: Successfully loaded 3 lines from messages file: vader.txt
2017/08/28 17:01:48 pkcrypto.go:186: Successfully dialed localhost:35196
2017/08/28 17:01:48 pkcrypto.go:396: Sent public key: 30819f300d06092a864886f70d010101050003818d00308189028181009d1ae3d1d8de480959fbea89830472899b34c0ddbf231103fdeb00003cf2646abf57a878b5fd8553e90854b7ed9c58b331446826b12d0bcdd9faba679000615901375d1e34c4743d75806d6e8f500b5fa40f93294bf9cdbbd963d6f66afc246b7ff44dc8adac91c154c5182b5b86f3852d92c4558ab79a097caa1850027a09ff0203010001.
2017/08/28 17:01:48 pkcrypto.go:414: Received header. Incoming public key is 294 bytes long.
2017/08/28 17:01:48 pkcrypto.go:423: Received public key: 30820122300d06092a864886f70d01010105000382010f003082010a0282010100cd1c837d634e020808d323f26854e7904660e845a594104baa8a356cb215acd630d867e09eeae138a251f7139dd97be44bd0acf0a051b098ec6554de34d1cb5e6f825271f8171be0ec1669acc00c985700a16316a5f24a5071a40dfdcb0c0bf9d4beadcecb733334629b86de3a8d3cba71f555106327397fb76e6ac846220cba55b87750b7f744396e243d5ff8101cf3e9dbfee7a82ecb348f9c2fbfefe4cf96a5910c83848ab29826740cae5b929d24f9adca930cffe2b943655ad01c4fa93451aebb06b64d59cf083e3e9f3cad17bf645ea71cacb2384d766cc8c89ed6b5e11babaf7b3620315850c6d9df6cad8481265bb2c7e72dcd7ac053cd4614f8149d0203010001.
2017/08/28 17:01:48 pkcrypto.go:439: Sending (plaintext): Obi-Wan never told you what happened to your father.
2017/08/28 17:01:48 pkcrypto.go:466: Sending (ciphertext): 61f9013e21572710d663e7a842e0ec0b800dd20fbc2cb34cd023ff9001bb0ca7d2ea0cb999c2bfbf371ef3676a69a352ff77569779e52184c9b1c05e8facf0da6d2ddb8596890125cf129a7422ea54a429df3e4303ba2277ea8c2827f937752d495191f1701bd5c33449ee3099f4102a41f854be35099230431712c5720a2679e0ae2b5d78a9ef2e0fae47bef2abe36ee610591104a09525e67bba68bb52fbd025164f486e1f1171fbbdd9fc077e624c78809567593c4c21c48b4e473314661d6d81d86560cd80b742dace2ab6ba5345185ef9aed90a97bb698e44eedad1329521b3b8d838dc9d59e6740859be68ed54e6b689140be3e78cbb1d1ddc036e66d6
2017/08/28 17:01:48 pkcrypto.go:487: Signature: 25bfaab68d59fc1c498c7e9ba6c3fb7ebf9bd244028dddd524c6b19e1ea66e302ae891255e653c369a5f380eb9077d961599f7a39cff51046d3c5e9330b3a50d36fe2f78efd6a3b2b41374275f3b10ddb62354ef7876449fe2ca3e41bc0175041158e22e8f6601944ed6ba37ab6283f9954a5cd91e7f189fe92ae6120e865ca0
2017/08/28 17:01:48 pkcrypto.go:517: Received header. Incoming ciphertext is 128 bytes long.
2017/08/28 17:01:48 pkcrypto.go:525: Received (ciphertext): 7cce70eb5ad80abff37383df3d988cc39030bcdde0ab86d4e73b47166559450fd5a47c285829202cca5d8101a5caa201821ed065af481d5fe06a92b7bfb4e7223624589f95938a1683bc850685400df6ea999cbc4fc395e0a81d8358484cad0e95fcd588694c8cbe306058d4c5650d3d70f9368eb369173e3dd832d263b67200
2017/08/28 17:01:48 pkcrypto.go:534: Received signature header. Incoming signature is 256 bytes long.
2017/08/28 17:01:48 pkcrypto.go:542: Received signature: 4dd1d173821a4d4e0576d2b2456ad05d6eeb8233dd833bdd5a9150142fcacde78d4a580933ed697bec3575cf9f554b721b7bc4215b5f2b103e3f4fa815e04bac05ff98bb7569574acf378b570989fbd9ce2b0f979a7d2e4b6ee3535b8deef3bd2e0ac5d407c5d04ab8d756c04a2be254dbfae168b992a155e417e19ac7746d43d82fe70776b004c077d7f11455396f697569c3ac96eef9550e93e32a820627e7fe16b75394dfdcc6ddea1808958aec9165ca7d36d91e3005802767f35fea1cd2aee67a27e6c81f084d8f29520c32f0ff4912e1e5910d230abfb898d7c7cb9e488021a380a9b01469a464f3c7bd11d951112e9b333709a6bb0abb2fd78846977e
2017/08/28 17:01:48 pkcrypto.go:551: Received (plaintext): He told me enough! He told me *you* killed him!
2017/08/28 17:01:48 pkcrypto.go:562: Signature verified.
2017/08/28 17:01:48 pkcrypto.go:439: Sending (plaintext): No, *I* am your father.
2017/08/28 17:01:48 pkcrypto.go:466: Sending (ciphertext): 832853acb451c1989c9c614366121dd34a3c0e8f285756d28babcd00fc6f1077ca0da1e4a1b2cd9e2a1dac371d7c8bcc2e621e9ff599dda8edfd74b67113cc7f59460530d9091103f56e291655fb582bc6eae62035936a969803080110e233ad8e4dae4f24b3ee816219c606a185565f1b126ba0f44f44eeeaa83e7d6d0da51bdb9be49d7f12340b9b1721de5c7f660115afbc000c04ac6a8cfe60c69351a74e6ceece1c4df5fe2a37ec8cca5663b4a94ee4496544126c04157b9afb0068b542d822e980be979d3fa45475ea1fd7f79aa67ba1d6e06ade7dc139cbaac730c2d67f898fa3af0aae45f1b825a1593a34206b6d85cca675337b624de397367fc075
2017/08/28 17:01:48 pkcrypto.go:487: Signature: 92ce5ff1c60a35525f2413b828b2993ee7030112c4395eb761d5fef0f60e7bc144e1005711dcc1946b6faff8542c4dd6e195215e6ef5d5e4cd7dadf1bad4823f91e14c34a5baf3d8290d2b7effdc64dda1e8242b249fc41c956225888844d1dd182f5f6bb30f02806256abe6238701dc40161ccd07ef78dc06c7c1a9704b7d7a
2017/08/28 17:01:48 pkcrypto.go:517: Received header. Incoming ciphertext is 128 bytes long.
2017/08/28 17:01:48 pkcrypto.go:525: Received (ciphertext): 566730aa4400a004fd7f1255a0f65b1f8f22bb113539e2dd224a7b4573730451528fa56bd5abcd54bd705b50c1458a8211365e0891fa3251c5afce5cfe960a4ac55518450661d47c4d269ae04aaa78d5d6d744cb096dc730f9b9b86c215d7a8547de45da110b6a3925bfba1d0c07be0860b7c8de9486ed5aa0d3a4b6519750c2
2017/08/28 17:01:48 pkcrypto.go:534: Received signature header. Incoming signature is 256 bytes long.
2017/08/28 17:01:48 pkcrypto.go:542: Received signature: aee867b097f331db6ed8b142c222dac9f2dbac12595403b38d6df8f7a546936664906d9353761bdbd4dbe801d01914e3cd989fc86a1fadb9f6e479ee951314b522d1c8a666aa7c4384e20a0cacc3ee4f9b1f5f8fccabfb0d2d03ca3e3d2400e79ea29857042667b5154e1043f64a586838d79fed236b1b4c392b9f9162dc219f2c21d1bff6853db34abedf9f7167461dbcb33764b91bc9a41fbf3bf2e12a9342579ef9c771206c83c6e65c1d69b97fa1f987b9bc9de19d92ec91d33fe79b1069cf254748b51202438edcccaaa0d1cec2d68c06b185488977438ab6445d3581e7cb0ce9f4c64ed23e6ee4975f9956ecaf90f9b4f66bff71346a3034c93076744b
2017/08/28 17:01:48 pkcrypto.go:551: Received (plaintext): No. No. That's not true. That's impossible!
2017/08/28 17:01:48 pkcrypto.go:562: Signature verified.
2017/08/28 17:01:48 pkcrypto.go:439: Sending (plaintext): Search your feelings, you know it to be true!
2017/08/28 17:01:48 pkcrypto.go:466: Sending (ciphertext): 8f85712ed3fc4b343001cba2d40072aa68cb8db41c0d7bccd3df18bb53c0c65aa949e0f3628ae378ee6a63c72d91cc729379df8d1601e80887f4c30f86f0b8a1a76ee9da49ced31aa88a1aa3dfae68a50cd20865204fec2f7374cdf1e3e53af37a0b936dcda01d6fc28e82c48a95962bab782ee3375953c78923fada9cbbcc5fb587e906d262c471508bd582719b7f3e7c46ad577088f9534d7d1b9277486126f59594194d6b30c8ecad92bfe92ef7c785955cb164d92f8d6137a374904be3e9378b7419e77132e95c9b8df522ac22feafb57b6f79fc6c7402f3a0267c575036c5a5bebb17a3a4299747e69a9ed365845691aa867a96f0e485997c38f18670c5
2017/08/28 17:01:48 pkcrypto.go:487: Signature: 2c00dd46fd057ed78e120b9eb2584e1513431d1f7b5398acfc3b2190410f1b41b5546031a817d1d6e476bc12fba8861951bb35a1a1819499498071d033be32d87d9da0825cea651f4399a73818d9feea2c94418e82e0b378197ed226f0dbeafbba9c399108d9423be864ea768224357cc3bf0ed8ca19d8787add74772195e499
2017/08/28 17:01:48 pkcrypto.go:517: Received header. Incoming ciphertext is 128 bytes long.
2017/08/28 17:01:48 pkcrypto.go:525: Received (ciphertext): 8cf2a51485533c48213cb52cc57de85cf6a9a704c9edd63a6f67b70ece384f0bda62c8292a02f6e04461502135f05e4b5e031574dc7a2935074b763862cf4943ba43e82c04eabfc2ad8a668c9b0396edb24fa9bc2a69d16cd393c09c8a384379bcf0e0db5aaf38e5736f43ae354fd817207f415ba28ef2822788e0eccff2cefa
2017/08/28 17:01:48 pkcrypto.go:534: Received signature header. Incoming signature is 256 bytes long.
2017/08/28 17:01:48 pkcrypto.go:542: Received signature: 53b9a84c9193d5f9c7e37596aa7dcc8be775341e0cd62f23199da8304f868cd9c82150376815623522a6084996d8fc0606be9326cb6d66f5ed60340d4463f0ee7c05ea25bb98f754fb7a4ad0605c1c0b577eb5e4d2cf00069536e9c34474c1a575431a1122b426739a85cc1073af14bd830ef317aefd2f4ce8f6be4fbc7e2b234969505d7666f51be9695e9143a69af76c7376f4ade9915d0164b592172a1ba2888c1e5e1cffd2397c850b8ecfd7df17676f175e974e5b1009d6e469a2b3ab8d53edd99236c78d58afc07370a6cc012d789b5a66d92f97b23b10b234861666cf9691a376058bb7a3313208f4626676225fb8efe52f8502886f8d5002868ebbe1
2017/08/28 17:01:48 pkcrypto.go:551: Received (plaintext): No! No!
2017/08/28 17:01:48 pkcrypto.go:562: Signature verified.

We can see from the log how key strength has an effect on the size of the encrypted messages. In this log, the received ciphertext is encrypted with this instance’s public key (which is a 1024-bit key) and the ciphertext is always 128 bytes long. The signature we receive is signed by the sender’s private key, which is stronger (2048-bit), these are always 256 bytes long. The latter will be much harder to crack.

Are we secure yet?

What we have above is the very basics of public key cryptography. There is more to it than that, but we’ve done quite well for first steps. The key takeaways are that encryption gives you privacy and signing tells you that a message is unaltered since it was sent. However, what the above doesn’t give you is the validation of identity.

We could easily add a third command to pkcrypto, called intercept, that would trade its public key with both the converse-mode instance and the listen-instance and impersonate both of them, passing messages between them like a man-in-the-middle. The program doesn’t actually know who it is talking to, it just knows that any messages it sends are only readable by the owner of a certain key and that they can’t really be changed. We need to be certain of much more than this to be secure.

Next time: Public Key Infrastructure

The problem of identity and knowing who you are talking to is addressed by public key infrastructure, which builds on top of the concepts introduced above. We’ll go there for the next Security IQ post in due course.

I hope you learned something diving into that code, I sure learned plenty writing it. Even constructing fairly trivial and basic programs like pkcrypto holds lessons for me, even though I’ve been a developer for years. If something is new to you or you’re lacking a bit of coverage in some technical skill (we’re all a bit weak somewhere), don’t be afraid to jump into the sandbox and have a play with the building blocks. Everyone has to start somewhere.

Until next time.

About the Author Kirk MacPhee

An experienced software developer and technical lead, specializing in automation technologies and their application.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s