initial commit

This commit is contained in:
Hendrik Hofstadt 2018-09-02 14:46:37 +02:00
commit 372febe620
13 changed files with 1086 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.idea
*.iml

53
commands/command.go Normal file
View File

@ -0,0 +1,53 @@
package commands
import (
"bytes"
"encoding/binary"
)
type (
CommandMessage struct {
UUID uint8
CommandType CommandType
SessionID *uint8
Data []byte
MAC []byte
}
)
func (c *CommandMessage) BodyLength() uint16 {
length := len(c.Data)
if c.MAC != nil {
length += len(c.MAC)
}
if c.SessionID != nil {
length += 1
}
return uint16(length)
}
func (c *CommandMessage) Serialize() ([]byte, error) {
buffer := new(bytes.Buffer)
// Write command type
binary.Write(buffer, binary.BigEndian, c.CommandType)
// Write length
binary.Write(buffer, binary.BigEndian, uint16(c.BodyLength()))
// Write sessionID
if c.SessionID != nil {
binary.Write(buffer, binary.BigEndian, *c.SessionID)
}
// Write data
buffer.Write(c.Data)
// Write MAC
buffer.Write(c.MAC)
return buffer.Bytes(), nil
}

105
commands/constructors.go Normal file
View File

@ -0,0 +1,105 @@
package commands
import (
"bytes"
"encoding/binary"
"errors"
)
func CreateCreateSessionCommand(keySetID uint16, hostChallenge []byte) (*CommandMessage, error) {
command := &CommandMessage{
CommandType: CommandTypeCreateSession,
}
payload := bytes.NewBuffer([]byte{})
binary.Write(payload, binary.BigEndian, keySetID)
payload.Write(hostChallenge)
command.Data = payload.Bytes()
return command, nil
}
func CreateAuthenticateSessionCommand(hostCryptogram []byte) (*CommandMessage, error) {
command := &CommandMessage{
CommandType: CommandTypeAuthenticateSession,
Data: hostCryptogram,
}
return command, nil
}
// Authenticated
func CreateResetCommand() (*CommandMessage, error) {
command := &CommandMessage{
CommandType: CommandTypeReset,
}
return command, nil
}
func CreateGenerateAsymmetricKeyCommand(keyID uint16, label []byte, domains uint16, capabilities uint64, algorithm Algorithm) (*CommandMessage, error) {
if len(label) > LabelLength {
return nil, errors.New("label is too long")
}
if len(label) < LabelLength {
label = append(label, bytes.Repeat([]byte{0x00}, LabelLength-len(label))...)
}
command := &CommandMessage{
CommandType: CommandTypeGenerateAsymmetricKey,
}
payload := bytes.NewBuffer([]byte{})
binary.Write(payload, binary.BigEndian, keyID)
payload.Write(label)
binary.Write(payload, binary.BigEndian, domains)
binary.Write(payload, binary.BigEndian, capabilities)
binary.Write(payload, binary.BigEndian, algorithm)
command.Data = payload.Bytes()
return command, nil
}
func CreateSignDataEddsaCommand(keyID uint16, data []byte) (*CommandMessage, error) {
command := &CommandMessage{
CommandType: CommandTypeSignDataEddsa,
}
payload := bytes.NewBuffer([]byte{})
binary.Write(payload, binary.BigEndian, keyID)
payload.Write(data)
command.Data = payload.Bytes()
return command, nil
}
func CreatePutAsymmetricKeyCommand(keyID uint16, label []byte, domains uint16, capabilities uint64, algorithm Algorithm, keyPart1 []byte, keyPart2 []byte) (*CommandMessage, error) {
if len(label) > LabelLength {
return nil, errors.New("label is too long")
}
if len(label) < LabelLength {
label = append(label, bytes.Repeat([]byte{0x00}, LabelLength-len(label))...)
}
command := &CommandMessage{
CommandType: CommandTypePutAsymmetric,
}
payload := bytes.NewBuffer([]byte{})
binary.Write(payload, binary.BigEndian, keyID)
payload.Write(label)
binary.Write(payload, binary.BigEndian, domains)
binary.Write(payload, binary.BigEndian, capabilities)
binary.Write(payload, binary.BigEndian, algorithm)
payload.Write(keyPart1)
if keyPart2 != nil {
payload.Write(keyPart2)
}
command.Data = payload.Bytes()
return command, nil
}

187
commands/response.go Normal file
View File

@ -0,0 +1,187 @@
package commands
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
)
type (
Response interface {
}
Error struct {
Code ErrorCode
}
CreateSessionResponse struct {
SessionID uint8
CardChallenge []byte
CardCryptogram []byte
}
SessionMessageResponse struct {
SessionID uint8
EncryptedData []byte
MAC []byte
}
CreateAsymmetricKeyResponse struct {
KeyID uint16
}
PutAsymmetricKeyResponse struct {
KeyID uint16
}
SignDataEddsaResponse struct {
Signature []byte
}
)
// ParseResponse parses the binary response from the card to the relevant Response type.
// If the response is an error zu parses the Error type response and returns an error of the
// type commands.Error with the parsed error message.
func ParseResponse(data []byte) (Response, error) {
if len(data) < 3 {
return nil, errors.New("invalid response")
}
transactionType := CommandType(data[0] + ResponseCommandOffset)
var payloadLength uint16
err := binary.Read(bytes.NewReader(data[1:3]), binary.BigEndian, &payloadLength)
if err != nil {
return nil, err
}
payload := data[3:]
if len(payload) != int(payloadLength) {
return nil, errors.New("response payload length does not equal the given length")
}
switch transactionType {
case CommandTypeCreateSession:
return parseCreateSessionResponse(payload)
case CommandTypeAuthenticateSession:
return nil, nil
case CommandTypeSessionMessage:
return parseSessionMessage(payload)
case CommandTypeGenerateAsymmetricKey:
return parseCreateAsymmetricKeyResponse(payload)
case CommandTypeSignDataEddsa:
return parseSignDataEddsaResponse(payload)
case CommandTypePutAsymmetric:
return parsePutAsymmetricKeyResponse(payload)
case ErrorResponseCode:
return nil, parseErrorResponse(payload)
default:
return nil, errors.New("response type unknown / not implemented")
}
}
func parseErrorResponse(payload []byte) error {
if len(payload) != 1 {
return errors.New("invalid response payload length")
}
return &Error{
Code: ErrorCode(payload[0]),
}
}
func parseSessionMessage(payload []byte) (Response, error) {
return &SessionMessageResponse{
SessionID: payload[0],
EncryptedData: payload[1 : len(payload)-8],
MAC: payload[len(payload)-8:],
}, nil
}
func parseCreateSessionResponse(payload []byte) (Response, error) {
if len(payload) != 17 {
return nil, errors.New("invalid response payload length")
}
return &CreateSessionResponse{
SessionID: uint8(payload[0]),
CardChallenge: payload[1:9],
CardCryptogram: payload[9:],
}, nil
}
func parseCreateAsymmetricKeyResponse(payload []byte) (Response, error) {
if len(payload) != 2 {
return nil, errors.New("invalid response payload length")
}
var keyID uint16
err := binary.Read(bytes.NewReader(payload[1:3]), binary.BigEndian, &keyID)
if err != nil {
return nil, err
}
return &CreateAsymmetricKeyResponse{
KeyID: keyID,
}, nil
}
func parseSignDataEddsaResponse(payload []byte) (Response, error) {
return &SignDataEddsaResponse{
Signature: payload,
}, nil
}
func parsePutAsymmetricKeyResponse(payload []byte) (Response, error) {
if len(payload) != 2 {
return nil, errors.New("invalid response payload length")
}
var keyID uint16
err := binary.Read(bytes.NewReader(payload[1:3]), binary.BigEndian, &keyID)
if err != nil {
return nil, err
}
return &PutAsymmetricKeyResponse{
KeyID: keyID,
}, nil
}
// Error formats a card error message into a human readable format
func (e *Error) Error() string {
message := ""
switch e.Code {
case ErrorCodeOK:
message = "OK"
case ErrorCodeInvalidCommand:
message = "Invalid command"
case ErrorCodeInvalidData:
message = "Invalid data"
case ErrorCodeInvalidSession:
message = "Invalid session"
case ErrorCodeAuthFail:
message = "Auth fail"
case ErrorCodeSessionFull:
message = "Session full"
case ErrorCodeSessionFailed:
message = "Session failed"
case ErrorCodeStorageFailed:
message = "Storage failed"
case ErrorCodeWrongLength:
message = "Wrong length"
case ErrorCodeInvalidPermission:
message = "Invalid permission"
case ErrorCodeLogFull:
message = "Log full"
case ErrorCodeObjectNotFound:
message = "Object not found"
case ErrorCodeIDIllegal:
message = "ID illegal"
case ErrorCodeCommandUnexecuted:
message = "Command unexecuted"
}
return fmt.Sprintf("card responded with error: %s", message)
}

148
commands/types.go Normal file
View File

@ -0,0 +1,148 @@
package commands
type (
CommandType uint8
ErrorCode uint8
Algorithm uint8
)
const (
ResponseCommandOffset = 0x80
ErrorResponseCode = 0xff
// LabelLength is the max length of a label
LabelLength = 40
CommandTypeEcho CommandType = 0x01
CommandTypeCreateSession CommandType = 0x03
CommandTypeAuthenticateSession CommandType = 0x04
CommandTypeSessionMessage CommandType = 0x05
CommandTypeDeviceInfo CommandType = 0x06
CommandTypeReset CommandType = 0x08
CommandTypeCloseSession CommandType = 0x40
CommandTypeStorageStatus CommandType = 0x41
CommandTypePutOpaque CommandType = 0x42
CommandTypeGetOpaque CommandType = 0x43
CommandTypePutAuthKey CommandType = 0x44
CommandTypePutAsymmetric CommandType = 0x45
CommandTypeGenerateAsymmetricKey CommandType = 0x46
CommandTypeSignDataPkcs1 CommandType = 0x47
CommandTypeListObjects CommandType = 0x48
CommandTypeDecryptPkcs1 CommandType = 0x49
CommandTypeExportWrapped CommandType = 0x4a
CommandTypeImportWrapped CommandType = 0x4b
CommandTypePutWrapKey CommandType = 0x4c
CommandTypeGetLogs CommandType = 0x4d
CommandTypeGetObjectInfo CommandType = 0x4e
CommandTypePutOption CommandType = 0x4f
CommandTypeGetOption CommandType = 0x50
CommandTypeGetPseudoRandom CommandType = 0x51
CommandTypePutHMACKey CommandType = 0x52
CommandTypeHMACData CommandType = 0x53
CommandTypeGetPubKey CommandType = 0x54
CommandTypeSignDataPss CommandType = 0x55
CommandTypeSignDataEcdsa CommandType = 0x56
CommandTypeDecryptEcdh CommandType = 0x57
CommandTypeDeleteObject CommandType = 0x58
CommandTypeDecryptOaep CommandType = 0x59
CommandTypeGenerateHMACKey CommandType = 0x5a
CommandTypeGenerateWrapKey CommandType = 0x5b
CommandTypeVerifyHMAC CommandType = 0x5c
CommandTypeOTPDecrypt CommandType = 0x60
CommandTypeOTPAeadCreate CommandType = 0x61
CommandTypeOTPAeadRandom CommandType = 0x62
CommandTypeOTPAeadRewrap CommandType = 0x63
CommandTypeAttestAsymmetric CommandType = 0x64
CommandTypePutOTPAeadKey CommandType = 0x65
CommandTypeGenerateOTPAeadKey CommandType = 0x66
CommandTypeSetLogIndex CommandType = 0x67
CommandTypeWrapData CommandType = 0x68
CommandTypeUnwrapData CommandType = 0x69
CommandTypeSignDataEddsa CommandType = 0x6a
CommandTypeSetBlink CommandType = 0x6b
// Errors
ErrorCodeOK ErrorCode = 0x00
ErrorCodeInvalidCommand ErrorCode = 0x01
ErrorCodeInvalidData ErrorCode = 0x02
ErrorCodeInvalidSession ErrorCode = 0x03
ErrorCodeAuthFail ErrorCode = 0x04
ErrorCodeSessionFull ErrorCode = 0x05
ErrorCodeSessionFailed ErrorCode = 0x06
ErrorCodeStorageFailed ErrorCode = 0x07
ErrorCodeWrongLength ErrorCode = 0x08
ErrorCodeInvalidPermission ErrorCode = 0x09
ErrorCodeLogFull ErrorCode = 0x0a
ErrorCodeObjectNotFound ErrorCode = 0x0b
ErrorCodeIDIllegal ErrorCode = 0x0c
ErrorCodeCommandUnexecuted ErrorCode = 0xff
// Algorithms
AlgorighmED25519 Algorithm = 46
// Capabilities
CapabilityGetOpaque uint64 = 0x0000000000000001
CapabilityPutOpaque uint64 = 0x0000000000000002
CapabilityPutAuthKey uint64 = 0x0000000000000004
CapabilityPutAsymmetric uint64 = 0x0000000000000008
CapabilityAsymmetricGen uint64 = 0x0000000000000010
CapabilityAsymmetricSignPkcs uint64 = 0x0000000000000020
CapabilityAsymmetricSignPss uint64 = 0x0000000000000040
CapabilityAsymmetricSignEcdsa uint64 = 0x0000000000000080
CapabilityAsymmetricSignEddsa uint64 = 0x0000000000000100
CapabilityAsymmetricDecryptPkcs uint64 = 0x0000000000000200
CapabilityAsymmetricDecryptOaep uint64 = 0x0000000000000400
CapabilityAsymmetricDecryptEcdh uint64 = 0x0000000000000800
CapabilityExportWrapped uint64 = 0x0000000000001000
CapabilityImportWrapped uint64 = 0x0000000000002000
CapabilityPutWrapKey uint64 = 0x0000000000004000
CapabilityGenerateWrapKey uint64 = 0x0000000000008000
CapabilityExportUnderWrap uint64 = 0x0000000000010000
CapabilityPutOption uint64 = 0x0000000000020000
CapabilityGetOption uint64 = 0x0000000000040000
CapabilityGetRandomness uint64 = 0x0000000000080000
CapabilityPutHmacKey uint64 = 0x0000000000100000
CapabilityHmacKeyGenerate uint64 = 0x0000000000200000
CapabilityHmacData uint64 = 0x0000000000400000
CapabilityHmacVerify uint64 = 0x0000000000800000
CapabilityAudit uint64 = 0x0000000001000000
CapabilitySshCertify uint64 = 0x0000000002000000
CapabilityGetTemplate uint64 = 0x0000000004000000
CapabilityPutTemplate uint64 = 0x0000000008000000
CapabilityReset uint64 = 0x0000000010000000
CapabilityOtpDecrypt uint64 = 0x0000000020000000
CapabilityOtpAeadCreate uint64 = 0x0000000040000000
CapabilityOtpAeadRandom uint64 = 0x0000000080000000
CapabilityOtpAeadRewrapFrom uint64 = 0x0000000100000000
CapabilityOtpAeadRewrapTo uint64 = 0x0000000200000000
CapabilityAttest uint64 = 0x0000000400000000
CapabilityPutOtpAeadKey uint64 = 0x0000000800000000
CapabilityGenerateOtpAeadKey uint64 = 0x0000001000000000
CapabilityWrapData uint64 = 0x0000002000000000
CapabilityUnwrapData uint64 = 0x0000004000000000
CapabilityDeleteOpaque uint64 = 0x0000008000000000
CapabilityDeleteAuthKey uint64 = 0x0000010000000000
CapabilityDeleteAsymmetric uint64 = 0x0000020000000000
CapabilityDeleteWrapKey uint64 = 0x0000040000000000
CapabilityDeleteHmacKey uint64 = 0x0000080000000000
CapabilityDeleteTemplate uint64 = 0x0000100000000000
CapabilityDeleteOtpAeadKey uint64 = 0x0000200000000000
// Domains
Domain1 uint16 = 0x0001
Domain2 uint16 = 0x0002
Domain3 uint16 = 0x0004
Domain4 uint16 = 0x0008
Domain5 uint16 = 0x0010
Domain6 uint16 = 0x0020
Domain7 uint16 = 0x0040
Domain8 uint16 = 0x0080
Domain9 uint16 = 0x0100
Domain10 uint16 = 0x0200
Domain11 uint16 = 0x0400
Domain12 uint16 = 0x0800
Domain13 uint16 = 0x1000
Domain14 uint16 = 0x2000
Domain15 uint16 = 0x4000
Domain16 uint16 = 0x8000
)

10
connector/connector.go Normal file
View File

@ -0,0 +1,10 @@
package connector
import "aiakos/commands"
type (
Connector interface {
Request(command *commands.CommandMessage) ([]byte, error)
GetStatus() (*StatusResponse, error)
}
)

84
connector/http.go Normal file
View File

@ -0,0 +1,84 @@
package connector
import (
"aiakos/commands"
"bytes"
"fmt"
"io/ioutil"
"net/http"
"strings"
)
type (
HTTPConnector struct {
URL string
}
Status string
StatusResponse struct {
Status Status
Serial string
Version string
Pid string
Address string
Port string
}
)
func NewHTTPConnector(url string) *HTTPConnector {
return &HTTPConnector{
URL: url,
}
}
func (c *HTTPConnector) Request(command *commands.CommandMessage) ([]byte, error) {
requestData, err := command.Serialize()
if err != nil {
return nil, err
}
res, err := http.DefaultClient.Post("http://"+c.URL+"/connector/api", "application/octet-stream", bytes.NewReader(requestData))
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("server returned non OK status code %d", res.StatusCode)
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
return data, nil
}
func (c *HTTPConnector) GetStatus() (*StatusResponse, error) {
res, err := http.DefaultClient.Get("http://" + c.URL + "/connector/status")
if err != nil {
return nil, err
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
bodyString := string(data)
pairs := strings.Split(bodyString, "\n")
var values []string
for _, pair := range pairs {
values = append(values, strings.Split(pair, "=")...)
}
status := &StatusResponse{}
status.Status = Status(values[1])
status.Serial = values[3]
status.Version = values[5]
status.Pid = values[7]
status.Address = values[9]
status.Port = values[11]
return status, nil
}

6
go.mod Normal file
View File

@ -0,0 +1,6 @@
module aiakos
require (
github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815
golang.org/x/crypto v0.0.0-20180830192347-182538f80094
)

4
go.sum Normal file
View File

@ -0,0 +1,4 @@
github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815 h1:D22EM5TeYZJp43hGDx6dUng8mvtyYbB9BnE3+BmJR1Q=
github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815/go.mod h1:wYFFK4LYXbX7j+76mOq7aiC/EAw2S22CrzPHqgsisPw=
golang.org/x/crypto v0.0.0-20180830192347-182538f80094 h1:rVTAlhYa4+lCfNxmAIEOGQRoD23UqP72M3+rSWVGDTg=
golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=

43
main.go Normal file
View File

@ -0,0 +1,43 @@
package main
import (
"aiakos/commands"
"aiakos/connector"
"aiakos/securechannel"
"fmt"
)
func main() {
channel, err := securechannel.NewSecureChannel(connector.NewHTTPConnector("127.0.0.1:12345"), 1, "password")
if err != nil {
panic(err)
}
err = channel.Authenticate()
if err != nil {
panic(err)
}
cmd, _ := commands.CreateGenerateAsymmetricKeyCommand(2, []byte("myKey"), commands.Domain1, commands.CapabilityAsymmetricSignEddsa, commands.AlgorighmED25519)
res, err := channel.SendEncryptedCommand(cmd)
if err != nil {
fmt.Printf("%v\n", err)
}
fmt.Printf("%v\n", res)
cmd, _ = commands.CreateSignDataEddsaCommand(2, []byte("my test message"))
res, err = channel.SendEncryptedCommand(cmd)
if err != nil {
fmt.Printf("%v\n", err)
}
fmt.Printf("signature: %v\n", res)
cmd, _ = commands.CreateResetCommand()
_, err = channel.SendEncryptedCommand(cmd)
if err != nil {
panic(err)
}
}

32
securechannel/authkey.go Normal file
View File

@ -0,0 +1,32 @@
package securechannel
import (
"crypto/sha256"
"golang.org/x/crypto/pbkdf2"
)
type (
// AuthKey is a key to authenticate with the HSM
AuthKey []byte
)
const (
authKeyLength = 32
authKeyIterations = 10000
yubicoSeed = "Yubico"
)
// deriveAuthKeyFromPwd derives an AuthKey using pkdf2 as specified in the HSM documentation
func deriveAuthKeyFromPwd(password string) AuthKey {
return pbkdf2.Key([]byte(password), []byte(yubicoSeed), authKeyIterations, authKeyLength, sha256.New)
}
// GetEncKey returns the EncryptionKey part of the AuthKey
func (k AuthKey) GetEncKey() []byte {
return k[:KeyLength]
}
// GetEncKey returns the MACKey part of the AuthKey
func (k AuthKey) GetMacKey() []byte {
return k[KeyLength:]
}

372
securechannel/channel.go Normal file
View File

@ -0,0 +1,372 @@
package securechannel
import (
"aiakos/commands"
"aiakos/connector"
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"errors"
"github.com/enceve/crypto/cmac"
)
type (
// SecureChannel implements a communication channel with a YubiHSM2 as specified in the SCP03 standard
SecureChannel struct {
// connector is used to communicate with the card
connector connector.Connector
// authKeySlot is the slot of the used authKey on the HSM
authKeySlot uint16
// keyChain holds the keys generated in the authentication ceremony
keyChain *KeyChain
// ID is the ID of the session with the HSM
ID uint8
// Counter of commands performed on the session
Counter uint32
// SecurityLevel is the authentication state of the session
SecurityLevel SecurityLevel
// HostChallenge is the auth challenge of the host
HostChallenge []byte
// DeviceChallenge is the auth challenge of the device
DeviceChallenge []byte
// AuthKey to authenticate against the HSM; must match authKeySlot
AuthKey AuthKey
// MACChainValue is the last MAC to allow MAC chaining
MACChainValue []byte
}
// KeyDerivationConstant used to derive keys using KDF
KeyDerivationConstant byte
// SecurityLevel indicates an auth state of a session/channel
SecurityLevel byte
// KeyChain holds session keys
KeyChain struct {
EncKey []byte
MACKey []byte
RMACKey []byte
}
// MessageType indicates whether a message is a command or response
MessageType byte
)
const (
MACLength = 8
ChallengeLength = 8
CryptogramLength = 8
KeyLength = 16
DerivationConstantEncKey KeyDerivationConstant = 0x04
DerivationConstantMACKey KeyDerivationConstant = 0x06
DerivationConstantRMACKey KeyDerivationConstant = 0x07
DerivationConstantDeviceCryptogram KeyDerivationConstant = 0x00
DerivationConstantHostCryptogram KeyDerivationConstant = 0x01
SecurityLevelUnauthenticated SecurityLevel = 0
SecurityLevelAuthenticated SecurityLevel = 1
MessageTypeCommand MessageType = 0
MessageTypeResponse MessageType = 1
)
// NewSecureChannel initiates a new secure channel to communicate with an HSM using the given authKey
// Call Authenticate next to establish a session.
func NewSecureChannel(connector connector.Connector, authKeySlot uint16, password string) (*SecureChannel, error) {
channel := &SecureChannel{
ID: 0,
AuthKey: deriveAuthKeyFromPwd(password),
MACChainValue: make([]byte, 16),
SecurityLevel: SecurityLevelUnauthenticated,
authKeySlot: authKeySlot,
connector: connector,
}
hostChallenge := make([]byte, 8)
_, err := rand.Read(hostChallenge)
if err != nil {
return nil, err
}
channel.HostChallenge = hostChallenge
return channel, nil
}
// Authenticate establishes an authenticated session with the HSM
func (s *SecureChannel) Authenticate() error {
command, _ := commands.CreateCreateSessionCommand(s.authKeySlot, s.HostChallenge)
response, err := s.SendCommand(command)
if err != nil {
return err
}
createSessionResp, match := response.(*commands.CreateSessionResponse)
if !match {
return errors.New("invalid response type")
}
s.ID = createSessionResp.SessionID
s.DeviceChallenge = createSessionResp.CardChallenge
// Update keychain
err = s.updateKeychain()
if err != nil {
return err
}
// Validate device cryptogram
deviceCryptogram, err := s.deriveKDF(s.keyChain.MACKey, DerivationConstantDeviceCryptogram, CryptogramLength)
if err != nil {
return err
}
if !bytes.Equal(deviceCryptogram, createSessionResp.CardCryptogram) {
return errors.New("authentication failed: device sent wrong cryptogram")
}
// Create host cryptogram
hostCryptogram, err := s.deriveKDF(s.keyChain.MACKey, DerivationConstantHostCryptogram, CryptogramLength)
if err != nil {
return err
}
// Authenticate session
authenticateCommand, err := commands.CreateAuthenticateSessionCommand(hostCryptogram)
if err != nil {
return err
}
_, err = s.SendMACCommand(authenticateCommand)
if err != nil {
return err
}
// Set counter to 1 as specified by the protocol
s.Counter = 1
s.SecurityLevel = SecurityLevelAuthenticated
return nil
}
// SendMACCommand sends a MAC authenticated command to the HSM and returns a parsed response
func (s *SecureChannel) SendMACCommand(c *commands.CommandMessage) (commands.Response, error) {
// Set command sessionID to this session
c.SessionID = &s.ID
// Calculate MAC for the command
sum, err := s.calculateMAC(c, MessageTypeCommand)
if err != nil {
return nil, err
}
// Update chain value
s.MACChainValue = sum
// Set command MAC to calculated mac
c.MAC = sum[:MACLength]
return s.SendCommand(c)
}
// SendCommand sends an unauthenticated command to the HSM and returns the parsed response
func (s *SecureChannel) SendCommand(c *commands.CommandMessage) (commands.Response, error) {
resp, err := s.connector.Request(c)
if err != nil {
return nil, err
}
return commands.ParseResponse(resp)
}
// SendEncryptedCommand sends an encrypted & authenticated command to the HSM
// and returns the decrypted and parsed response.
func (s *SecureChannel) SendEncryptedCommand(c *commands.CommandMessage) (commands.Response, error) {
// Create the cipher using the session encryption key
block, err := aes.NewCipher(s.keyChain.EncKey)
if err != nil {
return nil, err
}
// Pad the counter by 12 bytes
icv := new(bytes.Buffer)
icv.Write(bytes.Repeat([]byte{0}, 12))
binary.Write(icv, binary.BigEndian, s.Counter)
// Encrypt the padded counter to generate the IV
iv := make([]byte, KeyLength)
block.Encrypt(iv, icv.Bytes())
// Setup the CBC encrypter
encrypter := cipher.NewCBCEncrypter(block, iv)
// Serialize and encrypt the wrapped command
commandData, _ := c.Serialize()
encryptedCommand := make([]byte, len(pad(commandData)))
encrypter.CryptBlocks(encryptedCommand, pad(commandData))
// Send the wrapped command in a SessionMessage
resp, err := s.SendMACCommand(&commands.CommandMessage{
CommandType: commands.CommandTypeSessionMessage,
Data: encryptedCommand,
})
if err != nil {
return nil, err
}
// Cast and check the response
sessionMessage, match := resp.(*commands.SessionMessageResponse)
if !match {
return nil, errors.New("invalid response type")
}
// Verify MAC
expectedMac, err := s.calculateMAC(&commands.CommandMessage{
CommandType: commands.CommandTypeSessionMessage + commands.ResponseCommandOffset,
SessionID: &sessionMessage.SessionID,
Data: sessionMessage.EncryptedData,
}, MessageTypeResponse)
if !bytes.Equal(expectedMac[:MACLength], sessionMessage.MAC) {
return nil, errors.New("invalid response MAC")
}
// Update session state
s.Counter++
// Init the CBC decrypter
decrypter := cipher.NewCBCDecrypter(block, iv)
// Decrypt the wrapped response
decryptedResponse := make([]byte, len(sessionMessage.EncryptedData))
decrypter.CryptBlocks(decryptedResponse, sessionMessage.EncryptedData)
// Parse and return the wrapped response
return commands.ParseResponse(unpad(decryptedResponse))
}
// calculateMAC calculates the authenticated MAC for a command or response.
// This is stateful since it uses the MACChainValue.
func (s *SecureChannel) calculateMAC(c *commands.CommandMessage, messageType MessageType) ([]byte, error) {
// Select the right key
var key []byte
switch messageType {
case MessageTypeCommand:
key = s.keyChain.MACKey
case MessageTypeResponse:
key = s.keyChain.RMACKey
default:
return nil, errors.New("invalid messageType")
}
// Setup CMAC using aes
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
mac, err := cmac.New(block)
if err != nil {
return nil, err
}
// Setup a buffer for the cmac data
buffer := new(bytes.Buffer)
// Write the MacChainValue
buffer.Write(s.MACChainValue)
// Write command type
binary.Write(buffer, binary.BigEndian, c.CommandType)
// Write length
binary.Write(buffer, binary.BigEndian, uint16(1+len(c.Data)+MACLength))
// Write sessionID
binary.Write(buffer, binary.BigEndian, c.SessionID)
// Write data
buffer.Write(c.Data)
// Write buffer to MAC
mac.Write(buffer.Bytes())
return mac.Sum([]byte{}), nil
}
// updateKeychain derives and stores the session keys.
func (s *SecureChannel) updateKeychain() error {
keyChain := &KeyChain{}
encKey, err := s.deriveKDF(s.AuthKey.GetEncKey(), DerivationConstantEncKey, KeyLength)
if err != nil {
return err
}
keyChain.EncKey = encKey
macKey, err := s.deriveKDF(s.AuthKey.GetMacKey(), DerivationConstantMACKey, KeyLength)
if err != nil {
return err
}
keyChain.MACKey = macKey
rmacKey, err := s.deriveKDF(s.AuthKey.GetMacKey(), DerivationConstantRMACKey, KeyLength)
if err != nil {
return err
}
keyChain.RMACKey = rmacKey
s.keyChain = keyChain
return nil
}
// deriveKDF derives a key using SCP03's KDF.
// derivationConstant and keyLen define which key to derive.
func (s *SecureChannel) deriveKDF(key []byte, derivationConstant KeyDerivationConstant, keyLen uint8) ([]byte, error) {
if len(key) != KeyLength {
return nil, errors.New("invalid macKey length; should be 16")
}
if len(s.HostChallenge) != ChallengeLength {
return nil, errors.New("invalid HostChallenge length; should be 8")
}
if len(s.DeviceChallenge) != ChallengeLength {
return nil, errors.New("invalid DeviceChallenge length; should be 8")
}
derivationData := new(bytes.Buffer)
derivationData.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, byte(derivationConstant)})
derivationData.WriteByte(0x00)
binary.Write(derivationData, binary.BigEndian, uint16(keyLen*8))
derivationData.WriteByte(0x01)
derivationData.Write(s.HostChallenge)
derivationData.Write(s.DeviceChallenge)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
mac, err := cmac.New(block)
if err != nil {
return nil, err
}
mac.Write(derivationData.Bytes())
kdf := mac.Sum([]byte{})
return kdf[:keyLen], nil
}

40
securechannel/util.go Normal file
View File

@ -0,0 +1,40 @@
package securechannel
import (
"bytes"
"crypto/aes"
)
// pad adds a padding to src until using the mechanism specified in SCP03 until it has a len that is a multiple of
// aes.BlockSize and returns the result
func pad(src []byte) []byte {
if aes.BlockSize-len(src)%aes.BlockSize == 0 {
return src
}
padding := aes.BlockSize - len(src)%aes.BlockSize - 1
padtext := bytes.Repeat([]byte{0}, padding)
padtext = append([]byte{0x80}, padtext...)
return append(src, padtext...)
}
// unpad removes the padding from src using the mechanism specified in SCP03 and returns the result
func unpad(src []byte) []byte {
if src[len(src)-1] != 0x00 && src[len(src)-1] != 0x80 {
return src
}
padLen := 0
for i := len(src) - 1; i >= 0; i-- {
if src[i] == 0x00 {
padLen++
continue
}
if src[i] == 0x80 {
padLen++
break
}
}
return src[:len(src)-padLen]
}