api/eqip/security.go
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
}