commit 372febe620271393b229570dad076939b71ac236 Author: Hendrik Hofstadt Date: Sun Sep 2 14:46:37 2018 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29b636a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +*.iml \ No newline at end of file diff --git a/commands/command.go b/commands/command.go new file mode 100644 index 0000000..674be92 --- /dev/null +++ b/commands/command.go @@ -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 +} diff --git a/commands/constructors.go b/commands/constructors.go new file mode 100644 index 0000000..c86745b --- /dev/null +++ b/commands/constructors.go @@ -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 +} diff --git a/commands/response.go b/commands/response.go new file mode 100644 index 0000000..968731e --- /dev/null +++ b/commands/response.go @@ -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) +} diff --git a/commands/types.go b/commands/types.go new file mode 100644 index 0000000..683a353 --- /dev/null +++ b/commands/types.go @@ -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 +) diff --git a/connector/connector.go b/connector/connector.go new file mode 100644 index 0000000..0eef273 --- /dev/null +++ b/connector/connector.go @@ -0,0 +1,10 @@ +package connector + +import "aiakos/commands" + +type ( + Connector interface { + Request(command *commands.CommandMessage) ([]byte, error) + GetStatus() (*StatusResponse, error) + } +) diff --git a/connector/http.go b/connector/http.go new file mode 100644 index 0000000..eaea7bb --- /dev/null +++ b/connector/http.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4a32275 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d019521 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a9ea1d6 --- /dev/null +++ b/main.go @@ -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) + } +} diff --git a/securechannel/authkey.go b/securechannel/authkey.go new file mode 100644 index 0000000..1a03906 --- /dev/null +++ b/securechannel/authkey.go @@ -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:] +} diff --git a/securechannel/channel.go b/securechannel/channel.go new file mode 100644 index 0000000..9f5ebd4 --- /dev/null +++ b/securechannel/channel.go @@ -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 +} diff --git a/securechannel/util.go b/securechannel/util.go new file mode 100644 index 0000000..2d641f3 --- /dev/null +++ b/securechannel/util.go @@ -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] +}