├── .gitignore ├── LICENSE ├── README.md ├── authkey └── authkey.go ├── commands ├── command.go ├── constructors.go ├── response.go └── types.go ├── connector ├── connector.go └── http.go ├── go.mod ├── go.sum ├── manager.go └── securechannel ├── channel.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.swp 4 | *.swo 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ 2 | 3 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 4 | 5 | 1. Definitions. 6 | 7 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. 8 | 9 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. 10 | 11 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 12 | 13 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. 14 | 15 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. 16 | 17 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. 18 | 19 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). 20 | 21 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. 22 | 23 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." 24 | 25 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 26 | 27 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 28 | 29 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 30 | 31 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: 32 | 33 | (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and 34 | 35 | (b) You must cause any modified files to carry prominent notices stating that You changed the files; and 36 | 37 | (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and 38 | 39 | (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. 40 | 41 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 42 | 43 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 44 | 45 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 46 | 47 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 48 | 49 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 50 | 51 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. 52 | 53 | END OF TERMS AND CONDITIONS 54 | 55 | APPENDIX: How to apply the Apache License to your work. 56 | 57 | To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. 58 | 59 | Copyright [yyyy] [name of copyright owner] 60 | 61 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 62 | 63 | http://www.apache.org/licenses/LICENSE-2.0 64 | 65 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yubihsm-go 2 | Yubihsm-go is a minimal implementation of the securechannel and connector protocol of the YubiHSM2. 3 | 4 | It also implements a simple SessionManager which keeps connections alive and swaps them if the maximum number of 5 | messages is depleted. 6 | 7 | Currently the following commands are implemented: 8 | 9 | * DeviceInfo 10 | * Reset 11 | * GenerateAsymmetricKey 12 | * SignDataEddsa 13 | * SignDataPkcs1 14 | * PutAsymmetricKey 15 | * GetPubKey 16 | * DeriveEcdh 17 | * Echo 18 | * ChangeAuthenticationKey 19 | * PutAuthenticationKey 20 | * GetOpaque 21 | * PutOpaque 22 | * SignAttestationCertificate 23 | * Authentication & Session related commands 24 | * GetPseudoRandom 25 | 26 | Implementing new commands is really easy. Please consult `commands/constructors.go` and `commands/response.go` for reference. 27 | 28 | Please submit a PR if you have implemented new commands or extended existing constructors. 29 | 30 | ## Example of usage 31 | 32 | ```go 33 | c := connector.NewHTTPConnector("localhost:1234") 34 | sm, err := yubihsm.NewSessionManager(c, 1, "password", 2) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | echoMessage := []byte("test") 40 | 41 | command, err := commands.CreateEchoCommand(echoMessage) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | resp, err := sm.SendEncryptedCommand(command) 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | parsedResp, matched := resp.(*commands.EchoResponse) 52 | if !matched { 53 | panic("invalid response type") 54 | } 55 | 56 | if bytes.Equal(parsedResp.Data, echoMessage) { 57 | println("successfully echoed data") 58 | } else { 59 | panic(errors.New("echoed message did not equal requested message")) 60 | } 61 | 62 | ``` 63 | -------------------------------------------------------------------------------- /authkey/authkey.go: -------------------------------------------------------------------------------- 1 | package authkey 2 | 3 | import ( 4 | "crypto/sha256" 5 | 6 | "golang.org/x/crypto/pbkdf2" 7 | ) 8 | 9 | type ( 10 | // AuthKey is a key to authenticate with the HSM 11 | AuthKey []byte 12 | ) 13 | 14 | const ( 15 | authKeyLength = 32 16 | authKeyIterations = 10000 17 | yubicoSeed = "Yubico" 18 | ) 19 | 20 | // NewFromPassword derives an AuthKey using pkdf2 as specified in the HSM documentation 21 | func NewFromPassword(password string) AuthKey { 22 | return pbkdf2.Key([]byte(password), []byte(yubicoSeed), authKeyIterations, authKeyLength, sha256.New) 23 | } 24 | 25 | // GetEncKey returns the EncryptionKey part of the AuthKey 26 | func (k AuthKey) GetEncKey() []byte { 27 | return k[:authKeyLength/2] 28 | } 29 | 30 | // GetMacKey returns the MACKey part of the AuthKey 31 | func (k AuthKey) GetMacKey() []byte { 32 | return k[authKeyLength/2:] 33 | } 34 | -------------------------------------------------------------------------------- /commands/command.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | ) 7 | 8 | type ( 9 | CommandMessage struct { 10 | UUID uint8 11 | CommandType CommandType 12 | SessionID *uint8 13 | Data []byte 14 | MAC []byte 15 | } 16 | ) 17 | 18 | func (c *CommandMessage) BodyLength() uint16 { 19 | length := len(c.Data) 20 | 21 | if c.MAC != nil { 22 | length += len(c.MAC) 23 | } 24 | 25 | if c.SessionID != nil { 26 | length += 1 27 | } 28 | 29 | return uint16(length) 30 | } 31 | 32 | func (c *CommandMessage) Serialize() ([]byte, error) { 33 | buffer := new(bytes.Buffer) 34 | 35 | // Write command type 36 | binary.Write(buffer, binary.BigEndian, c.CommandType) 37 | 38 | // Write length 39 | binary.Write(buffer, binary.BigEndian, uint16(c.BodyLength())) 40 | 41 | // Write sessionID 42 | if c.SessionID != nil { 43 | binary.Write(buffer, binary.BigEndian, *c.SessionID) 44 | } 45 | 46 | // Write data 47 | buffer.Write(c.Data) 48 | 49 | // Write MAC 50 | buffer.Write(c.MAC) 51 | 52 | return buffer.Bytes(), nil 53 | } 54 | -------------------------------------------------------------------------------- /commands/constructors.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "io" 8 | 9 | "github.com/certusone/yubihsm-go/authkey" 10 | ) 11 | 12 | 13 | func CreateDeviceInfoCommand() (*CommandMessage, error) { 14 | command := &CommandMessage{ 15 | CommandType: CommandTypeDeviceInfo, 16 | } 17 | 18 | return command, nil 19 | } 20 | 21 | func CreateCreateSessionCommand(keySetID uint16, hostChallenge []byte) (*CommandMessage, error) { 22 | command := &CommandMessage{ 23 | CommandType: CommandTypeCreateSession, 24 | } 25 | 26 | payload := bytes.NewBuffer([]byte{}) 27 | binary.Write(payload, binary.BigEndian, keySetID) 28 | payload.Write(hostChallenge) 29 | 30 | command.Data = payload.Bytes() 31 | 32 | return command, nil 33 | } 34 | 35 | func CreateAuthenticateSessionCommand(hostCryptogram []byte) (*CommandMessage, error) { 36 | command := &CommandMessage{ 37 | CommandType: CommandTypeAuthenticateSession, 38 | Data: hostCryptogram, 39 | } 40 | 41 | return command, nil 42 | } 43 | 44 | // Authenticated 45 | 46 | func CreateResetCommand() (*CommandMessage, error) { 47 | command := &CommandMessage{ 48 | CommandType: CommandTypeReset, 49 | } 50 | 51 | return command, nil 52 | } 53 | 54 | func CreateGenerateAsymmetricKeyCommand(keyID uint16, label []byte, domains uint16, capabilities uint64, algorithm Algorithm) (*CommandMessage, error) { 55 | if len(label) > LabelLength { 56 | return nil, errors.New("label is too long") 57 | } 58 | if len(label) < LabelLength { 59 | label = append(label, bytes.Repeat([]byte{0x00}, LabelLength-len(label))...) 60 | } 61 | 62 | command := &CommandMessage{ 63 | CommandType: CommandTypeGenerateAsymmetricKey, 64 | } 65 | 66 | payload := bytes.NewBuffer([]byte{}) 67 | binary.Write(payload, binary.BigEndian, keyID) 68 | payload.Write(label) 69 | binary.Write(payload, binary.BigEndian, domains) 70 | binary.Write(payload, binary.BigEndian, capabilities) 71 | binary.Write(payload, binary.BigEndian, algorithm) 72 | 73 | command.Data = payload.Bytes() 74 | 75 | return command, nil 76 | } 77 | 78 | func CreateSignDataEddsaCommand(keyID uint16, data []byte) (*CommandMessage, error) { 79 | command := &CommandMessage{ 80 | CommandType: CommandTypeSignDataEddsa, 81 | } 82 | 83 | payload := bytes.NewBuffer([]byte{}) 84 | binary.Write(payload, binary.BigEndian, keyID) 85 | payload.Write(data) 86 | 87 | command.Data = payload.Bytes() 88 | 89 | return command, nil 90 | } 91 | 92 | func CreateSignDataEcdsaCommand(keyID uint16, data []byte) (*CommandMessage, error) { 93 | command := &CommandMessage{ 94 | CommandType: CommandTypeSignDataEcdsa, 95 | } 96 | 97 | payload := bytes.NewBuffer([]byte{}) 98 | binary.Write(payload, binary.BigEndian, keyID) 99 | payload.Write(data) 100 | 101 | command.Data = payload.Bytes() 102 | 103 | return command, nil 104 | } 105 | 106 | func CreateSignDataPkcs1Command(keyID uint16, data []byte) (*CommandMessage, error) { 107 | command := &CommandMessage{ 108 | CommandType: CommandTypeSignDataPkcs1, 109 | } 110 | 111 | payload := bytes.NewBuffer([]byte{}) 112 | binary.Write(payload, binary.BigEndian, keyID) 113 | payload.Write(data) 114 | 115 | command.Data = payload.Bytes() 116 | 117 | return command, nil 118 | } 119 | 120 | func CreatePutAsymmetricKeyCommand(keyID uint16, label []byte, domains uint16, capabilities uint64, algorithm Algorithm, keyPart1 []byte, keyPart2 []byte) (*CommandMessage, error) { 121 | if len(label) > LabelLength { 122 | return nil, errors.New("label is too long") 123 | } 124 | if len(label) < LabelLength { 125 | label = append(label, bytes.Repeat([]byte{0x00}, LabelLength-len(label))...) 126 | } 127 | command := &CommandMessage{ 128 | CommandType: CommandTypePutAsymmetric, 129 | } 130 | 131 | payload := bytes.NewBuffer([]byte{}) 132 | binary.Write(payload, binary.BigEndian, keyID) 133 | payload.Write(label) 134 | binary.Write(payload, binary.BigEndian, domains) 135 | binary.Write(payload, binary.BigEndian, capabilities) 136 | binary.Write(payload, binary.BigEndian, algorithm) 137 | payload.Write(keyPart1) 138 | if keyPart2 != nil { 139 | payload.Write(keyPart2) 140 | } 141 | 142 | command.Data = payload.Bytes() 143 | 144 | return command, nil 145 | } 146 | 147 | type ListCommandOption func(w io.Writer) 148 | 149 | func NewObjectTypeOption(objectType uint8) ListCommandOption { 150 | return func(w io.Writer) { 151 | binary.Write(w, binary.BigEndian, ListObjectParamType) 152 | binary.Write(w, binary.BigEndian, objectType) 153 | } 154 | } 155 | 156 | func NewIDOption(id uint16) ListCommandOption { 157 | return func(w io.Writer) { 158 | binary.Write(w, binary.BigEndian, ListObjectParamID) 159 | binary.Write(w, binary.BigEndian, id) 160 | } 161 | } 162 | 163 | func NewDomainOption(domain uint16) ListCommandOption { 164 | return func(w io.Writer) { 165 | binary.Write(w, binary.BigEndian, ListObjectParamDomains) 166 | binary.Write(w, binary.BigEndian, domain) 167 | } 168 | } 169 | 170 | func NewLabelOption(label []byte) (ListCommandOption, error) { 171 | if len(label) > LabelLength { 172 | return nil, errors.New("label is too long") 173 | } 174 | if len(label) < LabelLength { 175 | label = append(label, bytes.Repeat([]byte{0x00}, LabelLength-len(label))...) 176 | } 177 | return func(w io.Writer) { 178 | binary.Write(w, binary.BigEndian, ListObjectParamLabel) 179 | binary.Write(w, binary.BigEndian, label) 180 | }, nil 181 | } 182 | 183 | func CreateListObjectsCommand(options ...ListCommandOption) (*CommandMessage, error) { 184 | command := &CommandMessage{ 185 | CommandType: CommandTypeListObjects, 186 | } 187 | 188 | payload := bytes.NewBuffer([]byte{}) 189 | for _, opt := range options { 190 | opt(payload) 191 | } 192 | 193 | command.Data = payload.Bytes() 194 | 195 | return command, nil 196 | } 197 | 198 | func CreateGetObjectInfoCommand(keyID uint16, objectType uint8) (*CommandMessage, error) { 199 | command := &CommandMessage{ 200 | CommandType: CommandTypeGetObjectInfo, 201 | } 202 | 203 | payload := bytes.NewBuffer([]byte{}) 204 | binary.Write(payload, binary.BigEndian, keyID) 205 | binary.Write(payload, binary.BigEndian, objectType) 206 | 207 | command.Data = payload.Bytes() 208 | 209 | return command, nil 210 | } 211 | 212 | func CreateCloseSessionCommand() (*CommandMessage, error) { 213 | command := &CommandMessage{ 214 | CommandType: CommandTypeCloseSession, 215 | } 216 | 217 | return command, nil 218 | } 219 | 220 | func CreateGetPubKeyCommand(keyID uint16) (*CommandMessage, error) { 221 | command := &CommandMessage{ 222 | CommandType: CommandTypeGetPubKey, 223 | } 224 | 225 | payload := bytes.NewBuffer([]byte{}) 226 | binary.Write(payload, binary.BigEndian, keyID) 227 | command.Data = payload.Bytes() 228 | 229 | return command, nil 230 | } 231 | 232 | func CreateDeleteObjectCommand(objID uint16, objType uint8) (*CommandMessage, error) { 233 | command := &CommandMessage{ 234 | CommandType: CommandTypeDeleteObject, 235 | } 236 | 237 | payload := bytes.NewBuffer([]byte{}) 238 | binary.Write(payload, binary.BigEndian, objID) 239 | binary.Write(payload, binary.BigEndian, objType) 240 | command.Data = payload.Bytes() 241 | 242 | return command, nil 243 | } 244 | 245 | func CreateEchoCommand(data []byte) (*CommandMessage, error) { 246 | command := &CommandMessage{ 247 | CommandType: CommandTypeEcho, 248 | Data: data, 249 | } 250 | 251 | return command, nil 252 | } 253 | 254 | func CreateDeriveEcdhCommand(objID uint16, pubkey []byte) (*CommandMessage, error) { 255 | command := &CommandMessage{ 256 | CommandType: CommandTypeDeriveEcdh, 257 | } 258 | 259 | payload := bytes.NewBuffer([]byte{}) 260 | binary.Write(payload, binary.BigEndian, objID) 261 | payload.Write(pubkey) 262 | command.Data = payload.Bytes() 263 | 264 | return command, nil 265 | } 266 | 267 | func CreateChangeAuthenticationKeyCommand(objID uint16, newPassword string) (*CommandMessage, error) { 268 | command := &CommandMessage{ 269 | CommandType: CommandTypeChangeAuthenticationKey, 270 | } 271 | 272 | authKey := authkey.NewFromPassword(newPassword) 273 | payload := bytes.NewBuffer([]byte{}) 274 | binary.Write(payload, binary.BigEndian, objID) 275 | binary.Write(payload, binary.BigEndian, AlgorithmYubicoAESAuthentication) 276 | payload.Write(authKey.GetEncKey()) 277 | payload.Write(authKey.GetMacKey()) 278 | command.Data = payload.Bytes() 279 | 280 | return command, nil 281 | } 282 | 283 | func CreatePutOpaqueCommand(objID uint16, label []byte, domains uint16, capabilities uint64, algorithm Algorithm, data []byte) (*CommandMessage, error) { 284 | if len(label) > LabelLength { 285 | return nil, errors.New("label is too long") 286 | } 287 | if len(label) < LabelLength { 288 | label = append(label, bytes.Repeat([]byte{0x00}, LabelLength-len(label))...) 289 | } 290 | 291 | command := &CommandMessage{ 292 | CommandType: CommandTypePutOpaque, 293 | } 294 | 295 | payload := bytes.NewBuffer(nil) 296 | binary.Write(payload, binary.BigEndian, objID) 297 | payload.Write(label) 298 | binary.Write(payload, binary.BigEndian, domains) 299 | binary.Write(payload, binary.BigEndian, capabilities) 300 | binary.Write(payload, binary.BigEndian, algorithm) 301 | payload.Write(data) 302 | 303 | command.Data = payload.Bytes() 304 | 305 | return command, nil 306 | } 307 | 308 | func CreateGetOpaqueCommand(objID uint16) (*CommandMessage, error) { 309 | command := &CommandMessage{ 310 | CommandType: CommandTypeGetOpaque, 311 | } 312 | 313 | payload := bytes.NewBuffer(nil) 314 | binary.Write(payload, binary.BigEndian, objID) 315 | command.Data = payload.Bytes() 316 | 317 | return command, nil 318 | } 319 | 320 | func CreateGetPseudoRandomCommand(numBytes uint16) *CommandMessage { 321 | command := &CommandMessage{ 322 | CommandType: CommandTypeGetPseudoRandom, 323 | } 324 | 325 | payload := bytes.NewBuffer([]byte{}) 326 | binary.Write(payload, binary.BigEndian, numBytes) 327 | command.Data = payload.Bytes() 328 | 329 | return command 330 | } 331 | 332 | func CreatePutWrapkeyCommand(objID uint16, label []byte, domains uint16, capabilities uint64, algorithm Algorithm, delegated uint64, wrapkey []byte) (*CommandMessage, error) { 333 | if len(label) > LabelLength { 334 | return nil, errors.New("label is too long") 335 | } 336 | if len(label) < LabelLength { 337 | label = append(label, bytes.Repeat([]byte{0x00}, LabelLength-len(label))...) 338 | } 339 | switch algorithm { 340 | case AlgorithmAES128CCMWrap: 341 | if keyLen := len(wrapkey); keyLen != 16 { 342 | return nil, errors.New("wrapkey is wrong length") 343 | } 344 | case AlgorithmAES192CCMWrap: 345 | if keyLen := len(wrapkey); keyLen != 24 { 346 | return nil, errors.New("wrapkey is wrong length") 347 | } 348 | case AlgorithmAES256CCMWrap: 349 | if keyLen := len(wrapkey); keyLen != 32 { 350 | return nil, errors.New("wrapkey is wrong length") 351 | } 352 | default: 353 | return nil, errors.New("invalid algorithm") 354 | } 355 | 356 | command := &CommandMessage{ 357 | CommandType: CommandTypePutWrapKey, 358 | } 359 | 360 | payload := bytes.NewBuffer([]byte{}) 361 | binary.Write(payload, binary.BigEndian, objID) 362 | payload.Write(label) 363 | binary.Write(payload, binary.BigEndian, domains) 364 | binary.Write(payload, binary.BigEndian, capabilities) 365 | binary.Write(payload, binary.BigEndian, algorithm) 366 | binary.Write(payload, binary.BigEndian, delegated) 367 | payload.Write(wrapkey) 368 | 369 | command.Data = payload.Bytes() 370 | 371 | return command, nil 372 | } 373 | 374 | func CreatePutAuthkeyCommand(objID uint16, label []byte, domains uint16, capabilities, delegated uint64, encKey, macKey []byte) (*CommandMessage, error) { 375 | if len(label) > LabelLength { 376 | return nil, errors.New("label is too long") 377 | } 378 | if len(label) < LabelLength { 379 | label = append(label, bytes.Repeat([]byte{0x00}, LabelLength-len(label))...) 380 | } 381 | algorithm := AlgorithmYubicoAESAuthentication 382 | // TODO: support P256 Authentication when it is released 383 | // https://github.com/Yubico/yubihsm-shell/blob/1c8e254603e72f3f39cf1c3910996dbfcdba2b12/lib/yubihsm.c#L3110 384 | if len(encKey) != 16 { 385 | return nil, errors.New("invalid encryption key length") 386 | } 387 | if len(macKey) != 16 { 388 | return nil, errors.New("invalid mac key length") 389 | } 390 | 391 | command := &CommandMessage{ 392 | CommandType: CommandTypePutAuthKey, 393 | } 394 | 395 | payload := bytes.NewBuffer([]byte{}) 396 | binary.Write(payload, binary.BigEndian, objID) 397 | payload.Write(label) 398 | binary.Write(payload, binary.BigEndian, domains) 399 | binary.Write(payload, binary.BigEndian, capabilities) 400 | binary.Write(payload, binary.BigEndian, algorithm) 401 | binary.Write(payload, binary.BigEndian, delegated) 402 | payload.Write(encKey) 403 | payload.Write(macKey) 404 | 405 | command.Data = payload.Bytes() 406 | 407 | return command, nil 408 | } 409 | 410 | func CreatePutDerivedAuthenticationKeyCommand(objID uint16, label []byte, domains uint16, capabilities uint64, delegated uint64, password string) (*CommandMessage, error) { 411 | authKey := authkey.NewFromPassword(password) 412 | return CreatePutAuthkeyCommand(objID, label, domains, capabilities, delegated, authKey.GetEncKey(), authKey.GetMacKey()) 413 | } 414 | 415 | func CreateSignAttestationCertCommand(keyObjID, attestationObjID uint16) (*CommandMessage, error) { 416 | command := &CommandMessage{ 417 | CommandType: CommandTypeAttestAsymmetric, 418 | } 419 | 420 | payload := bytes.NewBuffer([]byte{}) 421 | binary.Write(payload, binary.BigEndian, keyObjID) 422 | binary.Write(payload, binary.BigEndian, attestationObjID) 423 | command.Data = payload.Bytes() 424 | 425 | return command, nil 426 | } 427 | 428 | func CreateExportWrappedCommand(wrapObjID uint16, objType uint8, objID uint16) (*CommandMessage, error) { 429 | command := &CommandMessage{ 430 | CommandType: CommandTypeExportWrapped, 431 | } 432 | 433 | payload := bytes.NewBuffer([]byte{}) 434 | binary.Write(payload, binary.BigEndian, wrapObjID) 435 | binary.Write(payload, binary.BigEndian, objType) 436 | binary.Write(payload, binary.BigEndian, objID) 437 | command.Data = payload.Bytes() 438 | 439 | return command, nil 440 | } 441 | 442 | // CreateImportWrappedCommand will import a wrapped/encrypted Object that was 443 | // previously exported by an YubiHSM2 device. The imported object will retain 444 | // its metadata (Object ID, Domains, Capabilities …etc), however, the object’s 445 | // origin will be marked as imported instead of generated. 446 | func CreateImportWrappedCommand(wrapObjID uint16, nonce, data []byte) (*CommandMessage, error) { 447 | command := &CommandMessage{ 448 | CommandType: CommandTypeImportWrapped, 449 | } 450 | if len(nonce) != 13 { 451 | return nil, errors.New("invalid nonce length") 452 | } 453 | 454 | payload := bytes.NewBuffer([]byte{}) 455 | binary.Write(payload, binary.BigEndian, wrapObjID) 456 | payload.Write(nonce) 457 | payload.Write(data) 458 | command.Data = payload.Bytes() 459 | 460 | return command, nil 461 | } 462 | -------------------------------------------------------------------------------- /commands/response.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | type ( 11 | Response interface { 12 | } 13 | 14 | Error struct { 15 | Code ErrorCode 16 | } 17 | 18 | DeviceInfoResponse struct { 19 | MajorVersion uint8 20 | MinorVersion uint8 21 | BuildVersion uint8 22 | SerialNumber uint32 23 | LogTotal uint8 24 | LogUsed uint8 25 | SupportedAlgorithms []Algorithm 26 | } 27 | 28 | CreateSessionResponse struct { 29 | SessionID uint8 30 | CardChallenge []byte 31 | CardCryptogram []byte 32 | } 33 | 34 | SessionMessageResponse struct { 35 | SessionID uint8 36 | EncryptedData []byte 37 | MAC []byte 38 | } 39 | 40 | CreateAsymmetricKeyResponse struct { 41 | KeyID uint16 42 | } 43 | 44 | PutAsymmetricKeyResponse struct { 45 | KeyID uint16 46 | } 47 | 48 | ObjectInfoResponse struct { 49 | Capabilities uint64 50 | ObjectID uint16 51 | Length uint16 52 | Domains uint16 53 | Type uint8 54 | Algorithm Algorithm 55 | Sequence uint8 56 | Origin uint8 57 | Label [40]byte 58 | DelegatedCapabilites uint64 59 | } 60 | 61 | Object struct { 62 | ObjectID uint16 63 | ObjectType uint8 64 | Sequence uint8 65 | } 66 | 67 | ListObjectsResponse struct { 68 | Objects []Object 69 | } 70 | 71 | SignDataEddsaResponse struct { 72 | Signature []byte 73 | } 74 | 75 | SignDataPkcs1Response struct { 76 | Signature []byte 77 | } 78 | 79 | SignDataEcdsaResponse struct { 80 | Signature []byte 81 | } 82 | 83 | GetPubKeyResponse struct { 84 | Algorithm Algorithm 85 | // KeyData can contain different formats depending on the algorithm according to the YubiHSM2 documentation. 86 | KeyData []byte 87 | } 88 | 89 | EchoResponse struct { 90 | Data []byte 91 | } 92 | 93 | DeriveEcdhResponse struct { 94 | XCoordinate []byte 95 | } 96 | 97 | ChangeAuthenticationKeyResponse struct { 98 | ObjectID uint16 99 | } 100 | 101 | PutWrapkeyResponse struct { 102 | ObjectID uint16 103 | } 104 | 105 | PutAuthkeyResponse struct { 106 | ObjectID uint16 107 | } 108 | 109 | PutOpaqueResponse struct { 110 | ObjectID uint16 111 | } 112 | 113 | GetOpaqueResponse struct { 114 | Data []byte 115 | } 116 | 117 | SignAttestationCertResponse struct { 118 | Cert []byte 119 | } 120 | 121 | ExportWrappedResponse struct { 122 | Nonce []byte 123 | Data []byte 124 | } 125 | 126 | ImportWrappedResponse struct { 127 | ObjectType uint8 128 | ObjectID uint16 129 | } 130 | ) 131 | 132 | // ParseResponse parses the binary response from the card to the relevant Response type. 133 | // If the response is an error zu parses the Error type response and returns an error of the 134 | // type commands.Error with the parsed error message. 135 | func ParseResponse(data []byte) (Response, error) { 136 | if len(data) < 3 { 137 | return nil, errors.New("invalid response") 138 | } 139 | 140 | transactionType := CommandType(data[0] + ResponseCommandOffset) 141 | 142 | var payloadLength uint16 143 | err := binary.Read(bytes.NewReader(data[1:3]), binary.BigEndian, &payloadLength) 144 | if err != nil { 145 | return nil, err 146 | } 147 | 148 | payload := data[3:] 149 | if len(payload) != int(payloadLength) { 150 | return nil, errors.New("response payload length does not equal the given length") 151 | } 152 | 153 | switch transactionType { 154 | case CommandTypeDeviceInfo: 155 | return parseDeviceInfoResponse(payload) 156 | case CommandTypeCreateSession: 157 | return parseCreateSessionResponse(payload) 158 | case CommandTypeAuthenticateSession: 159 | return nil, nil 160 | case CommandTypeSessionMessage: 161 | return parseSessionMessage(payload) 162 | case CommandTypeGenerateAsymmetricKey: 163 | return parseCreateAsymmetricKeyResponse(payload) 164 | case CommandTypeSignDataEddsa: 165 | return parseSignDataEddsaResponse(payload) 166 | case CommandTypeSignDataEcdsa: 167 | return parseSignDataEcdsaResponse(payload) 168 | case CommandTypeSignDataPkcs1: 169 | return parseSignDataPkcs1Response(payload) 170 | case CommandTypePutAsymmetric: 171 | return parsePutAsymmetricKeyResponse(payload) 172 | case CommandTypeListObjects: 173 | return parseListObjectsResponse(payload) 174 | case CommandTypeGetObjectInfo: 175 | return parseGetObjectInfoResponse(payload) 176 | case CommandTypeCloseSession: 177 | return nil, nil 178 | case CommandTypeGetPubKey: 179 | return parseGetPubKeyResponse(payload) 180 | case CommandTypeDeleteObject: 181 | return nil, nil 182 | case CommandTypeEcho: 183 | return parseEchoResponse(payload) 184 | case CommandTypeDeriveEcdh: 185 | return parseDeriveEcdhResponse(payload) 186 | case CommandTypeChangeAuthenticationKey: 187 | return parseChangeAuthenticationKeyResponse(payload) 188 | case CommandTypeGetPseudoRandom: 189 | return parseGetPseudoRandomResponse(payload), nil 190 | case CommandTypePutWrapKey: 191 | return parsePutWrapkeyResponse(payload) 192 | case CommandTypePutAuthKey: 193 | return parsePutAuthkeyResponse(payload) 194 | case CommandTypePutOpaque: 195 | return parsePutOpaqueResponse(payload) 196 | case CommandTypeGetOpaque: 197 | return parseGetOpaqueResponse(payload) 198 | case CommandTypeAttestAsymmetric: 199 | return parseAttestationCertResponse(payload) 200 | case CommandTypeExportWrapped: 201 | return parseExportWrappedResponse(payload) 202 | case CommandTypeImportWrapped: 203 | return parseImportWrappedResponse(payload) 204 | case ErrorResponseCode: 205 | return nil, parseErrorResponse(payload) 206 | default: 207 | return nil, errors.New("response type unknown / not implemented") 208 | } 209 | } 210 | 211 | func parseErrorResponse(payload []byte) error { 212 | if len(payload) != 1 { 213 | return errors.New("invalid response payload length") 214 | } 215 | 216 | return &Error{ 217 | Code: ErrorCode(payload[0]), 218 | } 219 | } 220 | 221 | func parseSessionMessage(payload []byte) (Response, error) { 222 | return &SessionMessageResponse{ 223 | SessionID: payload[0], 224 | EncryptedData: payload[1 : len(payload)-8], 225 | MAC: payload[len(payload)-8:], 226 | }, nil 227 | } 228 | 229 | func parseDeviceInfoResponse(payload []byte) (Response, error) { 230 | var serialNumber uint32 231 | err := binary.Read(bytes.NewReader(payload[3:7]), binary.BigEndian, &serialNumber) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | var supportedAlgorithms []Algorithm 237 | for _, alg := range payload[9:] { 238 | supportedAlgorithms = append(supportedAlgorithms, Algorithm(alg)) 239 | } 240 | 241 | return &DeviceInfoResponse{ 242 | MajorVersion: payload[0], 243 | MinorVersion: payload[1], 244 | BuildVersion: payload[2], 245 | SerialNumber: serialNumber, 246 | LogTotal: payload[7], 247 | LogUsed: payload[8], 248 | SupportedAlgorithms: supportedAlgorithms, 249 | }, nil 250 | } 251 | func parseCreateSessionResponse(payload []byte) (Response, error) { 252 | if len(payload) != 17 { 253 | return nil, errors.New("invalid response payload length") 254 | } 255 | 256 | return &CreateSessionResponse{ 257 | SessionID: uint8(payload[0]), 258 | CardChallenge: payload[1:9], 259 | CardCryptogram: payload[9:], 260 | }, nil 261 | } 262 | 263 | func parseCreateAsymmetricKeyResponse(payload []byte) (Response, error) { 264 | if len(payload) != 2 { 265 | return nil, errors.New("invalid response payload length") 266 | } 267 | 268 | var keyID uint16 269 | err := binary.Read(bytes.NewReader(payload[1:3]), binary.BigEndian, &keyID) 270 | if err != nil { 271 | return nil, err 272 | } 273 | 274 | return &CreateAsymmetricKeyResponse{ 275 | KeyID: keyID, 276 | }, nil 277 | } 278 | 279 | func parseSignDataEddsaResponse(payload []byte) (Response, error) { 280 | return &SignDataEddsaResponse{ 281 | Signature: payload, 282 | }, nil 283 | } 284 | 285 | func parseSignDataPkcs1Response(payload []byte) (Response, error) { 286 | if len(payload) < 1 { 287 | return nil, errors.New("invalid response payload length") 288 | } 289 | 290 | return &SignDataPkcs1Response{ 291 | Signature: payload, 292 | }, nil 293 | } 294 | 295 | func parseSignDataEcdsaResponse(payload []byte) (Response, error) { 296 | return &SignDataEcdsaResponse{ 297 | Signature: payload, 298 | }, nil 299 | } 300 | 301 | func parsePutAsymmetricKeyResponse(payload []byte) (Response, error) { 302 | if len(payload) != 2 { 303 | return nil, errors.New("invalid response payload length") 304 | } 305 | 306 | var keyID uint16 307 | err := binary.Read(bytes.NewReader(payload), binary.BigEndian, &keyID) 308 | if err != nil { 309 | return nil, err 310 | } 311 | 312 | return &PutAsymmetricKeyResponse{ 313 | KeyID: keyID, 314 | }, nil 315 | } 316 | 317 | func parseListObjectsResponse(payload []byte) (Response, error) { 318 | if len(payload)%4 != 0 { 319 | return nil, errors.New("invalid response payload length") 320 | } 321 | 322 | response := ListObjectsResponse{ 323 | Objects: make([]Object, len(payload)/4), 324 | } 325 | 326 | err := binary.Read(bytes.NewReader(payload), binary.BigEndian, &response.Objects) 327 | if err != nil { 328 | return nil, err 329 | } 330 | 331 | return &response, nil 332 | } 333 | 334 | func parseGetObjectInfoResponse(payload []byte) (Response, error) { 335 | response := ObjectInfoResponse{} 336 | 337 | err := binary.Read(bytes.NewReader(payload), binary.BigEndian, &response) 338 | if err != nil { 339 | return nil, err 340 | } 341 | 342 | return &response, nil 343 | } 344 | 345 | func parseGetPubKeyResponse(payload []byte) (Response, error) { 346 | if len(payload) < 1 { 347 | return nil, errors.New("invalid response payload length") 348 | } 349 | return &GetPubKeyResponse{ 350 | Algorithm: Algorithm(payload[0]), 351 | KeyData: payload[1:], 352 | }, nil 353 | } 354 | 355 | func parseEchoResponse(payload []byte) (Response, error) { 356 | return &EchoResponse{ 357 | Data: payload, 358 | }, nil 359 | } 360 | 361 | func parseDeriveEcdhResponse(payload []byte) (Response, error) { 362 | return &DeriveEcdhResponse{ 363 | XCoordinate: payload, 364 | }, nil 365 | } 366 | 367 | func parseChangeAuthenticationKeyResponse(payload []byte) (Response, error) { 368 | if len(payload) != 2 { 369 | return nil, errors.New("invalid response payload length") 370 | } 371 | 372 | var objectID uint16 373 | err := binary.Read(bytes.NewReader(payload), binary.BigEndian, &objectID) 374 | if err != nil { 375 | return nil, err 376 | } 377 | 378 | return &ChangeAuthenticationKeyResponse{ObjectID: objectID}, nil 379 | } 380 | 381 | func parseGetPseudoRandomResponse(payload []byte) Response { 382 | return payload 383 | } 384 | 385 | func parsePutWrapkeyResponse(payload []byte) (Response, error) { 386 | if len(payload) != 2 { 387 | return nil, errors.New("invalid response payload length") 388 | } 389 | 390 | var objectID uint16 391 | err := binary.Read(bytes.NewReader(payload), binary.BigEndian, &objectID) 392 | if err != nil { 393 | return nil, err 394 | } 395 | return &PutWrapkeyResponse{ObjectID: objectID}, nil 396 | } 397 | 398 | func parsePutAuthkeyResponse(payload []byte) (Response, error) { 399 | if len(payload) != 2 { 400 | return nil, errors.New("invalid response payload length") 401 | } 402 | 403 | var objectID uint16 404 | err := binary.Read(bytes.NewReader(payload), binary.BigEndian, &objectID) 405 | if err != nil { 406 | return nil, err 407 | } 408 | 409 | return &PutAuthkeyResponse{ObjectID: objectID}, nil 410 | } 411 | 412 | func parsePutOpaqueResponse(payload []byte) (Response, error) { 413 | if len(payload) != 2 { 414 | return nil, errors.New("invalid response payload length") 415 | } 416 | 417 | var objectID uint16 418 | err := binary.Read(bytes.NewReader(payload), binary.BigEndian, &objectID) 419 | if err != nil { 420 | return nil, err 421 | } 422 | 423 | return &PutOpaqueResponse{ 424 | ObjectID: objectID, 425 | }, nil 426 | } 427 | 428 | func parseGetOpaqueResponse(payload []byte) (Response, error) { 429 | if len(payload) < 1 { 430 | return nil, errors.New("invalid response payload length") 431 | } 432 | 433 | return &GetOpaqueResponse{ 434 | Data: payload, 435 | }, nil 436 | } 437 | 438 | func parseAttestationCertResponse(payload []byte) (Response, error) { 439 | if len(payload) < 1 { 440 | return nil, errors.New("invalid response payload length") 441 | } 442 | 443 | return &SignAttestationCertResponse{ 444 | Cert: payload, 445 | }, nil 446 | } 447 | 448 | func parseExportWrappedResponse(payload []byte) (Response, error) { 449 | if len(payload) < 13 { 450 | return nil, errors.New("invalid response payload length") 451 | } 452 | 453 | return &ExportWrappedResponse{ 454 | Nonce: payload[:13], 455 | Data: payload[13:], 456 | }, nil 457 | } 458 | 459 | func parseImportWrappedResponse(payload []byte) (Response, error) { 460 | if len(payload) != 3 { 461 | return nil, errors.New("invalid response payload length") 462 | } 463 | 464 | var objID uint16 465 | err := binary.Read(bytes.NewReader(payload[1:3]), binary.BigEndian, &objID) 466 | if err != nil { 467 | return nil, err 468 | } 469 | 470 | return &ImportWrappedResponse{ 471 | ObjectType: uint8(payload[0]), 472 | ObjectID: objID, 473 | }, nil 474 | } 475 | 476 | // Error formats a card error message into a human readable format 477 | func (e *Error) Error() string { 478 | message := "" 479 | switch e.Code { 480 | case ErrorCodeOK: 481 | message = "OK" 482 | case ErrorCodeInvalidCommand: 483 | message = "Invalid command" 484 | case ErrorCodeInvalidData: 485 | message = "Invalid data" 486 | case ErrorCodeInvalidSession: 487 | message = "Invalid session" 488 | case ErrorCodeAuthFail: 489 | message = "Auth fail" 490 | case ErrorCodeSessionFull: 491 | message = "Session full" 492 | case ErrorCodeSessionFailed: 493 | message = "Session failed" 494 | case ErrorCodeStorageFailed: 495 | message = "Storage failed" 496 | case ErrorCodeWrongLength: 497 | message = "Wrong length" 498 | case ErrorCodeInvalidPermission: 499 | message = "Invalid permission" 500 | case ErrorCodeLogFull: 501 | message = "Log full" 502 | case ErrorCodeObjectNotFound: 503 | message = "Object not found" 504 | case ErrorCodeInvalidID: 505 | message = "Invalid ID" 506 | case ErrorCodeCommandUnexecuted: 507 | message = "Command unexecuted" 508 | case ErrorCodeSSHCAConstraintViolation: 509 | message = "SSH CA constraint violation" 510 | case ErrorCodeInvalidOTP: 511 | message = "Invalid OTP" 512 | case ErrorCodeDemoMode: 513 | message = "Demo mode" 514 | case ErrorCodeObjectExists: 515 | message = "Object exists" 516 | default: 517 | message = "Unknown" 518 | } 519 | 520 | return fmt.Sprintf("card responded with error: %s", message) 521 | } 522 | -------------------------------------------------------------------------------- /commands/types.go: -------------------------------------------------------------------------------- 1 | package commands 2 | 3 | type ( 4 | CommandType uint8 5 | ErrorCode uint8 6 | Algorithm uint8 7 | ) 8 | 9 | const ( 10 | ResponseCommandOffset = 0x80 11 | ErrorResponseCode = 0xff 12 | 13 | // LabelLength is the max length of a label 14 | LabelLength = 40 15 | 16 | CommandTypeEcho CommandType = 0x01 17 | CommandTypeCreateSession CommandType = 0x03 18 | CommandTypeAuthenticateSession CommandType = 0x04 19 | CommandTypeSessionMessage CommandType = 0x05 20 | CommandTypeDeviceInfo CommandType = 0x06 21 | CommandTypeReset CommandType = 0x08 22 | CommandTypeCloseSession CommandType = 0x40 23 | CommandTypeStorageStatus CommandType = 0x41 24 | CommandTypePutOpaque CommandType = 0x42 25 | CommandTypeGetOpaque CommandType = 0x43 26 | CommandTypePutAuthKey CommandType = 0x44 27 | CommandTypePutAsymmetric CommandType = 0x45 28 | CommandTypeGenerateAsymmetricKey CommandType = 0x46 29 | CommandTypeSignDataPkcs1 CommandType = 0x47 30 | CommandTypeListObjects CommandType = 0x48 31 | CommandTypeDecryptPkcs1 CommandType = 0x49 32 | CommandTypeExportWrapped CommandType = 0x4a 33 | CommandTypeImportWrapped CommandType = 0x4b 34 | CommandTypePutWrapKey CommandType = 0x4c 35 | CommandTypeGetLogs CommandType = 0x4d 36 | CommandTypeGetObjectInfo CommandType = 0x4e 37 | CommandTypePutOption CommandType = 0x4f 38 | CommandTypeGetOption CommandType = 0x50 39 | CommandTypeGetPseudoRandom CommandType = 0x51 40 | CommandTypePutHMACKey CommandType = 0x52 41 | CommandTypeHMACData CommandType = 0x53 42 | CommandTypeGetPubKey CommandType = 0x54 43 | CommandTypeSignDataPss CommandType = 0x55 44 | CommandTypeSignDataEcdsa CommandType = 0x56 45 | CommandTypeDecryptEcdh CommandType = 0x57 // here for backwards compatibility 46 | CommandTypeDeriveEcdh CommandType = 0x57 47 | CommandTypeDeleteObject CommandType = 0x58 48 | CommandTypeDecryptOaep CommandType = 0x59 49 | CommandTypeGenerateHMACKey CommandType = 0x5a 50 | CommandTypeGenerateWrapKey CommandType = 0x5b 51 | CommandTypeVerifyHMAC CommandType = 0x5c 52 | CommandTypeOTPDecrypt CommandType = 0x60 53 | CommandTypeOTPAeadCreate CommandType = 0x61 54 | CommandTypeOTPAeadRandom CommandType = 0x62 55 | CommandTypeOTPAeadRewrap CommandType = 0x63 56 | CommandTypeAttestAsymmetric CommandType = 0x64 57 | CommandTypePutOTPAeadKey CommandType = 0x65 58 | CommandTypeGenerateOTPAeadKey CommandType = 0x66 59 | CommandTypeSetLogIndex CommandType = 0x67 60 | CommandTypeWrapData CommandType = 0x68 61 | CommandTypeUnwrapData CommandType = 0x69 62 | CommandTypeSignDataEddsa CommandType = 0x6a 63 | CommandTypeSetBlink CommandType = 0x6b 64 | CommandTypeChangeAuthenticationKey CommandType = 0x6c 65 | 66 | // Errors 67 | ErrorCodeOK ErrorCode = 0x00 68 | ErrorCodeInvalidCommand ErrorCode = 0x01 69 | ErrorCodeInvalidData ErrorCode = 0x02 70 | ErrorCodeInvalidSession ErrorCode = 0x03 71 | ErrorCodeAuthFail ErrorCode = 0x04 72 | ErrorCodeSessionFull ErrorCode = 0x05 73 | ErrorCodeSessionFailed ErrorCode = 0x06 74 | ErrorCodeStorageFailed ErrorCode = 0x07 75 | ErrorCodeWrongLength ErrorCode = 0x08 76 | ErrorCodeInvalidPermission ErrorCode = 0x09 77 | ErrorCodeLogFull ErrorCode = 0x0a 78 | ErrorCodeObjectNotFound ErrorCode = 0x0b 79 | ErrorCodeInvalidID ErrorCode = 0x0c 80 | ErrorCodeSSHCAConstraintViolation ErrorCode = 0x0e 81 | ErrorCodeInvalidOTP ErrorCode = 0x0f 82 | ErrorCodeDemoMode ErrorCode = 0x10 83 | ErrorCodeObjectExists ErrorCode = 0x11 84 | ErrorCodeCommandUnexecuted ErrorCode = 0xff 85 | 86 | // Algorithms 87 | AlgorithmRSAPKCS1SHA1 Algorithm = 1 88 | AlgorithmRSAPKCS1SHA256 Algorithm = 2 89 | AlgorithmRSAPKCS1SHA384 Algorithm = 3 90 | AlgorithmRSAPKCS1SHA512 Algorithm = 4 91 | AlgorithmRSAPSSSHA1 Algorithm = 5 92 | AlgorithmRSAPSSSHA256 Algorithm = 6 93 | AlgorithmRSAPSSSHA384 Algorithm = 7 94 | AlgorithmRSAPSSSHA512 Algorithm = 8 95 | AlgorithmRSA2048 Algorithm = 9 96 | AlgorithmRSA3072 Algorithm = 10 97 | AlgorithmRSA4096 Algorithm = 11 98 | AlgorithmP256 Algorithm = 12 99 | AlgorithmP384 Algorithm = 13 100 | AlgorithmP521 Algorithm = 14 101 | AlgorithmSecp256k1 Algorithm = 15 102 | AlgorithmECBP256 Algorithm = 16 103 | AlgorithmECBP384 Algorithm = 17 104 | AlgorithmECBP512 Algorithm = 18 105 | AlgorithmHMACSHA1 Algorithm = 19 106 | AlgorithmHMACSHA256 Algorithm = 20 107 | AlgorithmHMACSHA384 Algorithm = 21 108 | AlgorithmHMACSHA512 Algorithm = 22 109 | AlgorithmECECDSASHA1 Algorithm = 23 110 | AlgorithmECECDH Algorithm = 24 111 | AlgorithmRSAOAEPSHA1 Algorithm = 25 112 | AlgorithmRSAOAEPSHA256 Algorithm = 26 113 | AlgorithmRSAOAEPSHA384 Algorithm = 27 114 | AlgorithmRSAOAEPSHA512 Algorithm = 28 115 | AlgorithmAES128CCMWrap Algorithm = 29 116 | AlgorithmOpaqueData Algorithm = 30 117 | AlgorithmOpaqueX509Certificate Algorithm = 31 118 | AlgorithmRSAMGF1SHA1 Algorithm = 32 119 | AlgorithmRSAMGF1SHA256 Algorithm = 33 120 | AlgorithmRSAMGF1SHA384 Algorithm = 34 121 | AlgorithmRSAMGF1SHA512 Algorithm = 35 122 | AlgorithmTEMPLATESSH Algorithm = 36 123 | AlgorithmAES128YUBICOOTP Algorithm = 37 124 | AlgorithmYubicoAESAuthentication Algorithm = 38 125 | AlgorithmAES192YUBICOOTP Algorithm = 39 126 | AlgorithmAES256YUBICOOTP Algorithm = 40 127 | AlgorithmAES192CCMWrap Algorithm = 41 128 | AlgorithmAES256CCMWrap Algorithm = 42 129 | AlgorithmECECDSASHA256 Algorithm = 43 130 | AlgorithmECECDSASHA384 Algorithm = 44 131 | AlgorithmECECDSASHA512 Algorithm = 45 132 | AlgorithmED25519 Algorithm = 46 133 | AlgorithmECP224 Algorithm = 47 134 | 135 | // Capabilities 136 | CapabilityNone uint64 = 0x0000000000000000 137 | CapabilityGetOpaque uint64 = 0x0000000000000001 138 | CapabilityPutOpaque uint64 = 0x0000000000000002 139 | CapabilityPutAuthenticationKey uint64 = 0x0000000000000004 140 | CapabilityPutAsymmetric uint64 = 0x0000000000000008 141 | CapabilityAsymmetricGen uint64 = 0x0000000000000010 142 | CapabilityAsymmetricSignPkcs uint64 = 0x0000000000000020 143 | CapabilityAsymmetricSignPss uint64 = 0x0000000000000040 144 | CapabilityAsymmetricSignEcdsa uint64 = 0x0000000000000080 145 | CapabilityAsymmetricSignEddsa uint64 = 0x0000000000000100 146 | CapabilityAsymmetricDecryptPkcs uint64 = 0x0000000000000200 147 | CapabilityAsymmetricDecryptOaep uint64 = 0x0000000000000400 148 | CapabilityAsymmetricDecryptEcdh uint64 = 0x0000000000000800 // here for backwards compatibility 149 | CapabilityAsymmetricDeriveEcdh uint64 = 0x0000000000000800 150 | CapabilityExportWrapped uint64 = 0x0000000000001000 151 | CapabilityImportWrapped uint64 = 0x0000000000002000 152 | CapabilityPutWrapKey uint64 = 0x0000000000004000 153 | CapabilityGenerateWrapKey uint64 = 0x0000000000008000 154 | CapabilityExportableUnderWrap uint64 = 0x0000000000010000 155 | CapabilityPutOption uint64 = 0x0000000000020000 156 | CapabilityGetOption uint64 = 0x0000000000040000 157 | CapabilityGetRandomness uint64 = 0x0000000000080000 158 | CapabilityPutHmacKey uint64 = 0x0000000000100000 159 | CapabilityHmacKeyGenerate uint64 = 0x0000000000200000 160 | CapabilityHmacData uint64 = 0x0000000000400000 161 | CapabilityHmacVerify uint64 = 0x0000000000800000 162 | CapabilityAudit uint64 = 0x0000000001000000 163 | CapabilitySshCertify uint64 = 0x0000000002000000 164 | CapabilityGetTemplate uint64 = 0x0000000004000000 165 | CapabilityPutTemplate uint64 = 0x0000000008000000 166 | CapabilityReset uint64 = 0x0000000010000000 167 | CapabilityOtpDecrypt uint64 = 0x0000000020000000 168 | CapabilityOtpAeadCreate uint64 = 0x0000000040000000 169 | CapabilityOtpAeadRandom uint64 = 0x0000000080000000 170 | CapabilityOtpAeadRewrapFrom uint64 = 0x0000000100000000 171 | CapabilityOtpAeadRewrapTo uint64 = 0x0000000200000000 172 | CapabilityAttest uint64 = 0x0000000400000000 173 | CapabilityPutOtpAeadKey uint64 = 0x0000000800000000 174 | CapabilityGenerateOtpAeadKey uint64 = 0x0000001000000000 175 | CapabilityWrapData uint64 = 0x0000002000000000 176 | CapabilityUnwrapData uint64 = 0x0000004000000000 177 | CapabilityDeleteOpaque uint64 = 0x0000008000000000 178 | CapabilityDeleteAuthKey uint64 = 0x0000010000000000 179 | CapabilityDeleteAsymmetric uint64 = 0x0000020000000000 180 | CapabilityDeleteWrapKey uint64 = 0x0000040000000000 181 | CapabilityDeleteHmacKey uint64 = 0x0000080000000000 182 | CapabilityDeleteTemplate uint64 = 0x0000100000000000 183 | CapabilityDeleteOtpAeadKey uint64 = 0x0000200000000000 184 | CapabilityChangeAuthenticationKey uint64 = 0x0000400000000000 185 | 186 | // Domains 187 | Domain1 uint16 = 0x0001 188 | Domain2 uint16 = 0x0002 189 | Domain3 uint16 = 0x0004 190 | Domain4 uint16 = 0x0008 191 | Domain5 uint16 = 0x0010 192 | Domain6 uint16 = 0x0020 193 | Domain7 uint16 = 0x0040 194 | Domain8 uint16 = 0x0080 195 | Domain9 uint16 = 0x0100 196 | Domain10 uint16 = 0x0200 197 | Domain11 uint16 = 0x0400 198 | Domain12 uint16 = 0x0800 199 | Domain13 uint16 = 0x1000 200 | Domain14 uint16 = 0x2000 201 | Domain15 uint16 = 0x4000 202 | Domain16 uint16 = 0x8000 203 | 204 | // object types 205 | ObjectTypeOpaque uint8 = 0x01 206 | ObjectTypeAuthenticationKey uint8 = 0x02 207 | ObjectTypeAsymmetricKey uint8 = 0x03 208 | ObjectTypeWrapKey uint8 = 0x04 209 | ObjectTypeHmacKey uint8 = 0x05 210 | ObjectTypeTemplate uint8 = 0x06 211 | ObjectTypeOtpAeadKey uint8 = 0x07 212 | 213 | // list objects params 214 | ListObjectParamID uint8 = 0x01 215 | ListObjectParamType uint8 = 0x02 216 | ListObjectParamDomains uint8 = 0x03 217 | ListObjectParamCapabilities uint8 = 0x04 218 | ListObjectParamAlgorithm uint8 = 0x05 219 | ListObjectParamLabel uint8 = 0x06 220 | ) 221 | 222 | // CapabilityPrimitiveFromSlice OR's all the capabilitites together. 223 | func CapabilityPrimitiveFromSlice(capabilitites []uint64) uint64 { 224 | var primitive uint64 225 | for _, c := range capabilitites { 226 | primitive |= c 227 | } 228 | return primitive 229 | } 230 | -------------------------------------------------------------------------------- /connector/connector.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import "github.com/certusone/yubihsm-go/commands" 4 | 5 | type ( 6 | // Connector implements a simple request interface with a YubiHSM2 7 | Connector interface { 8 | // Request executes a command on the HSM and returns the binary response 9 | Request(command *commands.CommandMessage) ([]byte, error) 10 | // GetStatus requests the status of the HSM connector (not working for direct USB) 11 | GetStatus() (*StatusResponse, error) 12 | } 13 | 14 | // Status represents a status state of the HSM 15 | Status string 16 | 17 | // StatusResponse is the response to the GetStatus command containing information about the connector and HSM 18 | StatusResponse struct { 19 | Status Status 20 | Serial string 21 | Version string 22 | Pid string 23 | Address string 24 | Port string 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /connector/http.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/certusone/yubihsm-go/commands" 12 | ) 13 | 14 | var ErrInvalidResponseValueLength = errors.New("invalid response value length") 15 | 16 | type ( 17 | // HTTPConnector implements the HTTP based connection with the YubiHSM2 connector 18 | HTTPConnector struct { 19 | URL string 20 | } 21 | ) 22 | 23 | // NewHTTPConnector creates a new instance of HTTPConnector 24 | func NewHTTPConnector(url string) *HTTPConnector { 25 | return &HTTPConnector{ 26 | URL: url, 27 | } 28 | } 29 | 30 | // Request encodes and executes a command on the HSM and returns the binary response 31 | func (c *HTTPConnector) Request(command *commands.CommandMessage) (data []byte, err error) { 32 | var requestData []byte 33 | requestData, err = command.Serialize() 34 | if err != nil { 35 | return 36 | } 37 | 38 | var res *http.Response 39 | res, err = http.DefaultClient.Post("http://"+c.URL+"/connector/api", "application/octet-stream", bytes.NewReader(requestData)) 40 | if err != nil { 41 | return 42 | } 43 | 44 | defer func() { 45 | closeErr := res.Body.Close() 46 | if err == nil { 47 | err = closeErr 48 | } 49 | }() 50 | 51 | if res.StatusCode != http.StatusOK { 52 | err = fmt.Errorf("server returned non OK status code %d", res.StatusCode) 53 | return 54 | } 55 | 56 | data, err = ioutil.ReadAll(res.Body) 57 | 58 | return 59 | } 60 | 61 | // GetStatus requests the status of the HSM connector route /connector/status 62 | func (c *HTTPConnector) GetStatus() (status *StatusResponse, err error) { 63 | var res *http.Response 64 | res, err = http.DefaultClient.Get("http://" + c.URL + "/connector/status") 65 | if err != nil { 66 | return 67 | } 68 | 69 | var data []byte 70 | data, err = ioutil.ReadAll(res.Body) 71 | if err != nil { 72 | return 73 | } 74 | 75 | bodyString := string(data) 76 | pairs := strings.Split(bodyString, "\n") 77 | 78 | var values []string 79 | for _, pair := range pairs { 80 | values = append(values, strings.Split(pair, "=")...) 81 | } 82 | 83 | if values == nil || len(values) < 12 { 84 | return nil, ErrInvalidResponseValueLength 85 | } 86 | 87 | status = &StatusResponse{} 88 | status.Status = Status(values[1]) 89 | status.Serial = values[3] 90 | status.Version = values[5] 91 | status.Pid = values[7] 92 | status.Address = values[9] 93 | status.Port = values[11] 94 | 95 | err = res.Body.Close() 96 | 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/certusone/yubihsm-go 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815 7 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815 h1:D22EM5TeYZJp43hGDx6dUng8mvtyYbB9BnE3+BmJR1Q= 2 | github.com/enceve/crypto v0.0.0-20160707101852-34d48bb93815/go.mod h1:wYFFK4LYXbX7j+76mOq7aiC/EAw2S22CrzPHqgsisPw= 3 | golang.org/x/crypto v0.0.0-20180830192347-182538f80094 h1:rVTAlhYa4+lCfNxmAIEOGQRoD23UqP72M3+rSWVGDTg= 4 | golang.org/x/crypto v0.0.0-20180830192347-182538f80094/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 5 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= 6 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 7 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 8 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 10 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 11 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 12 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | package yubihsm 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "sync" 8 | "time" 9 | 10 | "github.com/certusone/yubihsm-go/commands" 11 | "github.com/certusone/yubihsm-go/connector" 12 | "github.com/certusone/yubihsm-go/securechannel" 13 | ) 14 | 15 | type ( 16 | // SessionManager manages a pool of authenticated secure sessions with a YubiHSM2 17 | SessionManager struct { 18 | session *securechannel.SecureChannel 19 | lock sync.Mutex 20 | connector connector.Connector 21 | authKeyID uint16 22 | password string 23 | 24 | creationWait sync.WaitGroup 25 | destroyed bool 26 | keepAlive *time.Timer 27 | swapping bool 28 | } 29 | ) 30 | 31 | var ( 32 | echoPayload = []byte("keepalive") 33 | ) 34 | 35 | const ( 36 | pingInterval = 15 * time.Second 37 | ) 38 | 39 | // NewSessionManager creates a new instance of the SessionManager with poolSize connections. 40 | // Wait on channel Connected with a timeout to wait for active connections to be ready. 41 | func NewSessionManager(connector connector.Connector, authKeyID uint16, password string) (*SessionManager, error) { 42 | manager := &SessionManager{ 43 | connector: connector, 44 | authKeyID: authKeyID, 45 | password: password, 46 | destroyed: false, 47 | } 48 | 49 | err := manager.swapSession() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | manager.keepAlive = time.NewTimer(pingInterval) 55 | go manager.pingRoutine() 56 | 57 | return manager, err 58 | } 59 | 60 | func (s *SessionManager) pingRoutine() { 61 | for range s.keepAlive.C { 62 | command, _ := commands.CreateEchoCommand(echoPayload) 63 | 64 | resp, err := s.SendEncryptedCommand(command) 65 | if err == nil { 66 | parsedResp, matched := resp.(*commands.EchoResponse) 67 | if !matched { 68 | err = errors.New("invalid response type") 69 | } 70 | if !bytes.Equal(parsedResp.Data, echoPayload) { 71 | err = errors.New("echoed data is invalid") 72 | } 73 | } else { 74 | // Session seems to be dead - reconnect and swap 75 | err = s.swapSession() 76 | if err != nil { 77 | log.Printf("swapping dead session failed; err=%v", err) 78 | } 79 | } 80 | 81 | s.keepAlive.Reset(pingInterval) 82 | } 83 | } 84 | 85 | func (s *SessionManager) swapSession() error { 86 | // Lock swapping process 87 | s.swapping = true 88 | defer func() { s.swapping = false }() 89 | 90 | newSession, err := securechannel.NewSecureChannel(s.connector, s.authKeyID, s.password) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | err = newSession.Authenticate() 96 | if err != nil { 97 | return err 98 | } 99 | 100 | s.lock.Lock() 101 | defer s.lock.Unlock() 102 | // Close old session 103 | if s.session != nil { 104 | go s.session.Close() 105 | } 106 | 107 | // Replace primary session 108 | s.session = newSession 109 | 110 | return nil 111 | } 112 | 113 | func (s *SessionManager) checkSessionHealth() { 114 | if s.session.Counter >= securechannel.MaxMessagesPerSession*0.9 && !s.swapping { 115 | go s.swapSession() 116 | } 117 | } 118 | 119 | // SendEncryptedCommand sends an encrypted & authenticated command to the HSM 120 | // and returns the decrypted and parsed response. 121 | func (s *SessionManager) SendEncryptedCommand(c *commands.CommandMessage) (commands.Response, error) { 122 | s.lock.Lock() 123 | defer s.lock.Unlock() 124 | 125 | // Check session health after executing the command 126 | defer s.checkSessionHealth() 127 | 128 | if s.destroyed { 129 | return nil, errors.New("sessionmanager has already been destroyed") 130 | } 131 | if s.session == nil { 132 | return nil, errors.New("no session available") 133 | } 134 | 135 | return s.session.SendEncryptedCommand(c) 136 | } 137 | 138 | // SendCommand sends an unauthenticated command to the HSM and returns the parsed response 139 | func (s *SessionManager) SendCommand(c *commands.CommandMessage) (commands.Response, error) { 140 | s.lock.Lock() 141 | defer s.lock.Unlock() 142 | 143 | if s.destroyed { 144 | return nil, errors.New("sessionmanager has already been destroyed") 145 | } 146 | if s.session == nil { 147 | return nil, errors.New("no session available") 148 | } 149 | 150 | return s.session.SendCommand(c) 151 | } 152 | 153 | // Destroy closes all connections in the pool. 154 | // SessionManager instances can't be reused. 155 | func (s *SessionManager) Destroy() { 156 | s.lock.Lock() 157 | defer s.lock.Unlock() 158 | 159 | s.keepAlive.Stop() 160 | s.session.Close() 161 | s.destroyed = true 162 | } 163 | -------------------------------------------------------------------------------- /securechannel/channel.go: -------------------------------------------------------------------------------- 1 | package securechannel 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "encoding/binary" 9 | "errors" 10 | "sync" 11 | 12 | "github.com/enceve/crypto/cmac" 13 | "github.com/certusone/yubihsm-go/authkey" 14 | "github.com/certusone/yubihsm-go/commands" 15 | "github.com/certusone/yubihsm-go/connector" 16 | ) 17 | 18 | type ( 19 | // SecureChannel implements a communication channel with a YubiHSM2 as specified in the SCP03 standard 20 | SecureChannel struct { 21 | // connector is used to communicate with the card 22 | connector connector.Connector 23 | // authKeySlot is the slot of the used authKey on the HSM 24 | authKeySlot uint16 25 | // keyChain holds the keys generated in the authentication ceremony 26 | keyChain *KeyChain 27 | // channelLock is used to lock encrypted communications to prevent race conditions 28 | channelLock sync.Mutex 29 | 30 | // ID is the ID of the session with the HSM 31 | ID uint8 32 | // Counter of commands performed on the session 33 | Counter uint32 34 | // SecurityLevel is the authentication state of the session 35 | SecurityLevel SecurityLevel 36 | 37 | // HostChallenge is the auth challenge of the host 38 | HostChallenge []byte 39 | // DeviceChallenge is the auth challenge of the device 40 | DeviceChallenge []byte 41 | 42 | // AuthKey to authenticate against the HSM; must match authKeySlot 43 | AuthKey authkey.AuthKey 44 | 45 | // MACChainValue is the last MAC to allow MAC chaining 46 | MACChainValue []byte 47 | } 48 | 49 | // KeyDerivationConstant used to derive keys using KDF 50 | KeyDerivationConstant byte 51 | 52 | // SecurityLevel indicates an auth state of a session/channel 53 | SecurityLevel byte 54 | 55 | // KeyChain holds session keys 56 | KeyChain struct { 57 | EncKey []byte 58 | MACKey []byte 59 | RMACKey []byte 60 | } 61 | 62 | // MessageType indicates whether a message is a command or response 63 | MessageType byte 64 | ) 65 | 66 | const ( 67 | MACLength = 8 68 | ChallengeLength = 8 69 | CryptogramLength = 8 70 | KeyLength = 16 71 | 72 | DerivationConstantEncKey KeyDerivationConstant = 0x04 73 | DerivationConstantMACKey KeyDerivationConstant = 0x06 74 | DerivationConstantRMACKey KeyDerivationConstant = 0x07 75 | 76 | DerivationConstantDeviceCryptogram KeyDerivationConstant = 0x00 77 | DerivationConstantHostCryptogram KeyDerivationConstant = 0x01 78 | 79 | SecurityLevelUnauthenticated SecurityLevel = 0 80 | SecurityLevelAuthenticated SecurityLevel = 1 81 | 82 | MessageTypeCommand MessageType = 0 83 | MessageTypeResponse MessageType = 1 84 | 85 | MaxMessagesPerSession = 10000 86 | ) 87 | 88 | var ErrAuthCryptogram = errors.New("authentication failed: device sent wrong cryptogram") 89 | 90 | // NewSecureChannel initiates a new secure channel to communicate with an HSM using the given authKey 91 | // Call Authenticate next to establish a session. 92 | func NewSecureChannel(connector connector.Connector, authKeySlot uint16, password string) (*SecureChannel, error) { 93 | channel := &SecureChannel{ 94 | ID: 0, 95 | AuthKey: authkey.NewFromPassword(password), 96 | MACChainValue: make([]byte, 16), 97 | SecurityLevel: SecurityLevelUnauthenticated, 98 | authKeySlot: authKeySlot, 99 | connector: connector, 100 | } 101 | 102 | hostChallenge := make([]byte, 8) 103 | _, err := rand.Read(hostChallenge) 104 | if err != nil { 105 | return nil, err 106 | } 107 | channel.HostChallenge = hostChallenge 108 | 109 | return channel, nil 110 | } 111 | 112 | // Authenticate establishes an authenticated session with the HSM 113 | func (s *SecureChannel) Authenticate() error { 114 | if s.SecurityLevel != SecurityLevelUnauthenticated { 115 | return errors.New("the session is already authenticated") 116 | } 117 | 118 | s.channelLock.Lock() 119 | defer s.channelLock.Unlock() 120 | 121 | command, _ := commands.CreateCreateSessionCommand(s.authKeySlot, s.HostChallenge) 122 | response, err := s.SendCommand(command) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | createSessionResp, match := response.(*commands.CreateSessionResponse) 128 | if !match { 129 | return errors.New("invalid response type") 130 | } 131 | 132 | s.ID = createSessionResp.SessionID 133 | s.DeviceChallenge = createSessionResp.CardChallenge 134 | 135 | // Update keychain 136 | err = s.updateKeychain() 137 | if err != nil { 138 | return err 139 | } 140 | 141 | // Validate device cryptogram 142 | deviceCryptogram, err := s.deriveKDF(s.keyChain.MACKey, DerivationConstantDeviceCryptogram, CryptogramLength) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | if !bytes.Equal(deviceCryptogram, createSessionResp.CardCryptogram) { 148 | return ErrAuthCryptogram 149 | } 150 | 151 | // Create host cryptogram 152 | hostCryptogram, err := s.deriveKDF(s.keyChain.MACKey, DerivationConstantHostCryptogram, CryptogramLength) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | // Authenticate session 158 | authenticateCommand, err := commands.CreateAuthenticateSessionCommand(hostCryptogram) 159 | if err != nil { 160 | return err 161 | } 162 | _, err = s.sendMACCommand(authenticateCommand) 163 | if err != nil { 164 | return err 165 | } 166 | 167 | // Set counter to 1 as specified by the protocol 168 | s.Counter = 1 169 | 170 | s.SecurityLevel = SecurityLevelAuthenticated 171 | 172 | return nil 173 | } 174 | 175 | // SendCommand sends an unauthenticated command to the HSM and returns the parsed response 176 | func (s *SecureChannel) SendCommand(c *commands.CommandMessage) (commands.Response, error) { 177 | resp, err := s.connector.Request(c) 178 | if err != nil { 179 | return nil, err 180 | } 181 | 182 | return commands.ParseResponse(resp) 183 | } 184 | 185 | // SendEncryptedCommand sends an encrypted & authenticated command to the HSM 186 | // and returns the decrypted and parsed response. 187 | func (s *SecureChannel) SendEncryptedCommand(c *commands.CommandMessage) (commands.Response, error) { 188 | if s.SecurityLevel != SecurityLevelAuthenticated { 189 | return nil, errors.New("the session is not authenticated") 190 | } 191 | 192 | if s.Counter >= MaxMessagesPerSession { 193 | return nil, errors.New("channel has reached its message limit; please recreate") 194 | } 195 | 196 | // Lock the encrypted channel 197 | s.channelLock.Lock() 198 | defer s.channelLock.Unlock() 199 | 200 | // Create the cipher using the session encryption key 201 | block, err := aes.NewCipher(s.keyChain.EncKey) 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | // Pad the counter by 12 bytes 207 | icv := new(bytes.Buffer) 208 | icv.Write(bytes.Repeat([]byte{0}, 12)) 209 | binary.Write(icv, binary.BigEndian, s.Counter) 210 | 211 | // Encrypt the padded counter to generate the IV 212 | iv := make([]byte, KeyLength) 213 | block.Encrypt(iv, icv.Bytes()) 214 | 215 | // Setup the CBC encrypter 216 | encrypter := cipher.NewCBCEncrypter(block, iv) 217 | 218 | // Serialize and encrypt the wrapped command 219 | commandData, _ := c.Serialize() 220 | encryptedCommand := make([]byte, len(pad(commandData))) 221 | encrypter.CryptBlocks(encryptedCommand, pad(commandData)) 222 | 223 | // Send the wrapped command in a SessionMessage 224 | resp, err := s.sendMACCommand(&commands.CommandMessage{ 225 | CommandType: commands.CommandTypeSessionMessage, 226 | Data: encryptedCommand, 227 | }) 228 | if err != nil { 229 | return nil, err 230 | } 231 | 232 | // Cast and check the response 233 | sessionMessage, match := resp.(*commands.SessionMessageResponse) 234 | if !match { 235 | return nil, errors.New("invalid response type") 236 | } 237 | 238 | // Verify MAC 239 | expectedMac, err := s.calculateMAC(&commands.CommandMessage{ 240 | CommandType: commands.CommandTypeSessionMessage + commands.ResponseCommandOffset, 241 | SessionID: &sessionMessage.SessionID, 242 | Data: sessionMessage.EncryptedData, 243 | }, MessageTypeResponse) 244 | 245 | if !bytes.Equal(expectedMac[:MACLength], sessionMessage.MAC) { 246 | return nil, errors.New("invalid response MAC") 247 | } 248 | 249 | // Update session state 250 | s.Counter++ 251 | 252 | // Init the CBC decrypter 253 | decrypter := cipher.NewCBCDecrypter(block, iv) 254 | 255 | // Decrypt the wrapped response 256 | decryptedResponse := make([]byte, len(sessionMessage.EncryptedData)) 257 | decrypter.CryptBlocks(decryptedResponse, sessionMessage.EncryptedData) 258 | 259 | // Parse and return the wrapped response 260 | return commands.ParseResponse(unpad(decryptedResponse)) 261 | } 262 | 263 | func (s *SecureChannel) Close() error { 264 | command, err := commands.CreateCloseSessionCommand() 265 | if err != nil { 266 | return err 267 | } 268 | 269 | _, err = s.SendEncryptedCommand(command) 270 | if err != nil { 271 | return err 272 | } 273 | 274 | return nil 275 | } 276 | 277 | // sendMACCommand sends a MAC authenticated command to the HSM and returns a parsed response 278 | func (s *SecureChannel) sendMACCommand(c *commands.CommandMessage) (commands.Response, error) { 279 | 280 | // Set command sessionID to this session 281 | c.SessionID = &s.ID 282 | 283 | // Calculate MAC for the command 284 | sum, err := s.calculateMAC(c, MessageTypeCommand) 285 | if err != nil { 286 | return nil, err 287 | } 288 | 289 | // Update chain value 290 | s.MACChainValue = sum 291 | 292 | // Set command MAC to calculated mac 293 | c.MAC = sum[:MACLength] 294 | 295 | return s.SendCommand(c) 296 | } 297 | 298 | // calculateMAC calculates the authenticated MAC for a command or response. 299 | // This is stateful since it uses the MACChainValue. 300 | func (s *SecureChannel) calculateMAC(c *commands.CommandMessage, messageType MessageType) ([]byte, error) { 301 | 302 | // Select the right key 303 | var key []byte 304 | switch messageType { 305 | case MessageTypeCommand: 306 | key = s.keyChain.MACKey 307 | case MessageTypeResponse: 308 | key = s.keyChain.RMACKey 309 | default: 310 | return nil, errors.New("invalid messageType") 311 | } 312 | 313 | // Setup CMAC using aes 314 | block, err := aes.NewCipher(key) 315 | if err != nil { 316 | return nil, err 317 | } 318 | mac, err := cmac.New(block) 319 | if err != nil { 320 | return nil, err 321 | } 322 | 323 | // Setup a buffer for the cmac data 324 | buffer := new(bytes.Buffer) 325 | 326 | // Write the MacChainValue 327 | buffer.Write(s.MACChainValue) 328 | 329 | // Write command type 330 | binary.Write(buffer, binary.BigEndian, c.CommandType) 331 | 332 | // Write length 333 | binary.Write(buffer, binary.BigEndian, uint16(1+len(c.Data)+MACLength)) 334 | 335 | // Write sessionID 336 | binary.Write(buffer, binary.BigEndian, c.SessionID) 337 | 338 | // Write data 339 | buffer.Write(c.Data) 340 | 341 | // Write buffer to MAC 342 | mac.Write(buffer.Bytes()) 343 | 344 | return mac.Sum([]byte{}), nil 345 | } 346 | 347 | // updateKeychain derives and stores the session keys. 348 | func (s *SecureChannel) updateKeychain() error { 349 | keyChain := &KeyChain{} 350 | 351 | encKey, err := s.deriveKDF(s.AuthKey.GetEncKey(), DerivationConstantEncKey, KeyLength) 352 | if err != nil { 353 | return err 354 | } 355 | keyChain.EncKey = encKey 356 | 357 | macKey, err := s.deriveKDF(s.AuthKey.GetMacKey(), DerivationConstantMACKey, KeyLength) 358 | if err != nil { 359 | return err 360 | } 361 | keyChain.MACKey = macKey 362 | 363 | rmacKey, err := s.deriveKDF(s.AuthKey.GetMacKey(), DerivationConstantRMACKey, KeyLength) 364 | if err != nil { 365 | return err 366 | } 367 | keyChain.RMACKey = rmacKey 368 | 369 | s.keyChain = keyChain 370 | return nil 371 | } 372 | 373 | // deriveKDF derives a key using SCP03's KDF. 374 | // derivationConstant and keyLen define which key to derive. 375 | func (s *SecureChannel) deriveKDF(key []byte, derivationConstant KeyDerivationConstant, keyLen uint8) ([]byte, error) { 376 | if len(key) != KeyLength { 377 | return nil, errors.New("invalid macKey length; should be 16") 378 | } 379 | 380 | if len(s.HostChallenge) != ChallengeLength { 381 | return nil, errors.New("invalid HostChallenge length; should be 8") 382 | } 383 | 384 | if len(s.DeviceChallenge) != ChallengeLength { 385 | return nil, errors.New("invalid DeviceChallenge length; should be 8") 386 | } 387 | 388 | derivationData := new(bytes.Buffer) 389 | derivationData.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, byte(derivationConstant)}) 390 | 391 | derivationData.WriteByte(0x00) 392 | 393 | binary.Write(derivationData, binary.BigEndian, uint16(keyLen*8)) 394 | 395 | derivationData.WriteByte(0x01) 396 | derivationData.Write(s.HostChallenge) 397 | derivationData.Write(s.DeviceChallenge) 398 | 399 | block, err := aes.NewCipher(key) 400 | if err != nil { 401 | return nil, err 402 | } 403 | mac, err := cmac.New(block) 404 | if err != nil { 405 | return nil, err 406 | } 407 | 408 | mac.Write(derivationData.Bytes()) 409 | kdf := mac.Sum([]byte{}) 410 | 411 | return kdf[:keyLen], nil 412 | } 413 | -------------------------------------------------------------------------------- /securechannel/util.go: -------------------------------------------------------------------------------- 1 | package securechannel 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | ) 7 | 8 | // pad adds a padding to src until using the mechanism specified in SCP03 until it has a len that is a multiple of 9 | // aes.BlockSize and returns the result 10 | func pad(src []byte) []byte { 11 | if aes.BlockSize-len(src)%aes.BlockSize == 0 { 12 | return src 13 | } 14 | 15 | padding := aes.BlockSize - len(src)%aes.BlockSize - 1 16 | padtext := bytes.Repeat([]byte{0}, padding) 17 | padtext = append([]byte{0x80}, padtext...) 18 | return append(src, padtext...) 19 | } 20 | 21 | // unpad removes the padding from src using the mechanism specified in SCP03 and returns the result 22 | func unpad(src []byte) []byte { 23 | if src[len(src)-1] != 0x00 && src[len(src)-1] != 0x80 { 24 | return src 25 | } 26 | 27 | padLen := 0 28 | for i := len(src) - 1; i >= 0; i-- { 29 | if src[i] == 0x00 { 30 | padLen++ 31 | continue 32 | } 33 | if src[i] == 0x80 { 34 | padLen++ 35 | break 36 | } 37 | } 38 | 39 | return src[:len(src)-padLen] 40 | } 41 | --------------------------------------------------------------------------------