18F/e-QIP-prototype

View on GitHub
api/eqip/security.go

Summary

Maintainability
A
0 mins
Test Coverage
package eqip

import (
    "bytes"
    "crypto"
    "crypto/rand"
    "crypto/rsa"
    "crypto/sha256"
    "crypto/x509"
    "encoding/hex"
    "encoding/pem"
    "fmt"
    "io/ioutil"
    "time"
    "unicode"

    "github.com/satori/go.uuid"
)

const (
    defaultAuthNamespace = "urn:gov.opm.eqip.ws/Authentication"
)

// generateSecurityToken creates a security token that is sent across with the SOAP request.
// The security token consists of three components: the header, a time stamp, and a long unique identifier string.
//
// The three fields are generated as follows:
//          Header
//                Always the two letters "WS" and is case sensitive.
//          Time Stamp
//                The current time in milliseconds as generated by the Java System.currentTimeMillis() method
//          Unique Identifier
//                This portion of the token consists of a long unique string. The e-QIP client creates this string by
//                generating two UUIDs using different algorithms, and then concatenating them together with a "-".
// The three token components are concatenated together with a "_" to create a token like the following:
// WS_1393440378647_520ffdca-a179-4771-9fea-0a959ecb1a3e-e29376a3-3291-30b1-a96a-f1508a5dc628
// This uses the go.uuid package which generates UUID's that are RFC 4122 compliant
func generateSecurityToken() string {
    header := "WS"
    // The client uses the Java System.currentTimeMillis() so we must convert our Nano time to milliseconds
    timestamp := time.Now().UnixNano() / int64(time.Millisecond)
    u1 := uuid.NewV1()
    u2 := uuid.NewV4()
    return fmt.Sprintf("%s_%v_%s-%s", header, timestamp, u1.String(), u2.String())
}

// signSecurityToken takes a token and signs the contents using the
// private key file encoded as PKCS#8 DER format.
func signSecurityToken(token, privateKeyPath string) ([]byte, error) {
    // Read the private key file
    b, err := ioutil.ReadFile(privateKeyPath)
    if err != nil {
        return nil, err
    }
    // Parse the private key file
    cert, err := x509.ParsePKCS8PrivateKey(b)
    if err != nil {
        return nil, err
    }

    // ParsePKCS8PrivateKey returns an interface{} so we must cast to rsa.PrivateKey
    rsaKey, ok := cert.(*rsa.PrivateKey)
    if !ok {
        return nil, fmt.Errorf("Unable to cast to PrivateKey")
    }

    // Apply the hex hash
    unsignedBytes, err := hexHash(token)
    if err != nil {
        return nil, err
    }

    // Execute sha256 hash on the unsignedBytes
    hashed := sha256.Sum256(unsignedBytes)

    // Sign the content
    rng := rand.Reader
    signature, err := rsaKey.Sign(rng, hashed[:], crypto.SHA256)
    if err != nil {
        return nil, err
    }

    signatureHex := make([]byte, hex.EncodedLen(len(signature)))
    hex.Encode(signatureHex, signature)
    return signatureHex, nil
}

// verifySecurityToken validates that the security token information
// was generated properly and that it was signed by the corresponding
// certificate. This is meant for local testing and will not be used
// in production because this is what the webservice server will be validating.
func verifySecurityToken(unsignedToken, signedToken []byte, publicCertPath string) error {
    // Read in public certificate
    b, err := ioutil.ReadFile(publicCertPath)
    if err != nil {
        return err
    }

    // Parse certificate
    block, _ := pem.Decode(b)
    cert, err := x509.ParseCertificate(block.Bytes)
    if err != nil {
        return err
    }

    // The signed signature is hex encoded so we must decode
    decoded := make([]byte, hex.DecodedLen(len(signedToken)))
    _, err = hex.Decode(decoded, signedToken)
    if err != nil {
        return err
    }

    pk := cert.PublicKey.(*rsa.PublicKey)

    // We need to process our unsigned token
    originalHexed, err := hexHash(string(unsignedToken))
    if err != nil {
        return err
    }

    // Hash the unsigned token using sha256
    hashed := sha256.Sum256(originalHexed)
    err = rsa.VerifyPKCS1v15(pk, crypto.SHA256, hashed[:], decoded)
    return err
}

// hexHash takes a string and decodes each character into a hex value. This
// function was translated from code that was provided in the eqip Web Services
// Documentation. The original method name was HexDecode which was slightly misleading
// because it allows invalid hex values to be processed.
func hexHash(hash string) ([]byte, error) {
    var buf bytes.Buffer
    for i := 0; i < len(hash)-1; i += 2 {
        hByte := rune(hash[i])
        lByte := rune(hash[i+1])
        byteInt := int8(characterDigitRadix(hByte, 16))
        byteInt = (byteInt << 4)
        byteInt += int8(characterDigitRadix(lByte, 16))
        if err := buf.WriteByte(byte(byteInt)); err != nil {
            return nil, err
        }
    }
    return buf.Bytes(), nil
}

// characterDigitRadix returns the numeric value of the rune in the specified radix. This
// logic was translated from code that was provided in the eqip Web Services Documentation.
// This logic stems from the Java Character.Digit(char, int) located at
// https://docs.oracle.com/javase/7/docs/api/java/lang/Character.html#digit(char,%20int)
func characterDigitRadix(r rune, radix int) int {
    val := -1
    switch {
    case unicode.IsDigit(r):
        val = int(r - '0')
    case unicode.IsLower(r):
        val = int(r-'a') + 10
    case unicode.IsUpper(r):
        val = int(r-'A') + 10
    }
    if val >= radix {
        return -1
    }
    return val
}