├── .gitignore ├── LICENSE.md ├── Makefile ├── README.md ├── _assets └── ci │ └── Jenkinsfile ├── apdu ├── command.go ├── command_test.go ├── response.go ├── response_test.go ├── utils.go └── utils_test.go ├── cash_command_set.go ├── command_set.go ├── commands.go ├── crypto ├── crypto.go └── crypto_test.go ├── derivationpath ├── decoder.go ├── decoder_test.go ├── encoder.go └── encoder_test.go ├── globalplatform ├── command_set.go ├── commands.go ├── commands_test.go ├── crypto │ ├── crypto.go │ └── crypto_test.go ├── globalplatform.go ├── load.go ├── scp02_keys.go ├── scp02_wrapper.go ├── scp02_wrapper_test.go ├── secure_channel.go ├── session.go └── session_test.go ├── go.mod ├── go.sum ├── hexutils └── hexutils.go ├── identifiers └── identifiers.go ├── io └── normal_channel.go ├── keycard.go ├── secrets.go ├── secure_channel.go ├── secure_channel_test.go └── types ├── application_info.go ├── application_status.go ├── card_status.go ├── cash_application_info.go ├── certificate.go ├── certificate_test.go ├── exported_key.go ├── metadata.go ├── metadata_test.go ├── signature.go └── types.go /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | *.cap 3 | /applets 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | ### 1. Definitions 5 | 6 | **1.1. “Contributor”** 7 | means each individual or legal entity that creates, contributes to 8 | the creation of, or owns Covered Software. 9 | 10 | **1.2. “Contributor Version”** 11 | means the combination of the Contributions of others (if any) used 12 | by a Contributor and that particular Contributor's Contribution. 13 | 14 | **1.3. “Contribution”** 15 | means Covered Software of a particular Contributor. 16 | 17 | **1.4. “Covered Software”** 18 | means Source Code Form to which the initial Contributor has attached 19 | the notice in Exhibit A, the Executable Form of such Source Code 20 | Form, and Modifications of such Source Code Form, in each case 21 | including portions thereof. 22 | 23 | **1.5. “Incompatible With Secondary Licenses”** 24 | means 25 | 26 | * **(a)** that the initial Contributor has attached the notice described 27 | in Exhibit B to the Covered Software; or 28 | * **(b)** that the Covered Software was made available under the terms of 29 | version 1.1 or earlier of the License, but not also under the 30 | terms of a Secondary License. 31 | 32 | **1.6. “Executable Form”** 33 | means any form of the work other than Source Code Form. 34 | 35 | **1.7. “Larger Work”** 36 | means a work that combines Covered Software with other material, in 37 | a separate file or files, that is not Covered Software. 38 | 39 | **1.8. “License”** 40 | means this document. 41 | 42 | **1.9. “Licensable”** 43 | means having the right to grant, to the maximum extent possible, 44 | whether at the time of the initial grant or subsequently, any and 45 | all of the rights conveyed by this License. 46 | 47 | **1.10. “Modifications”** 48 | means any of the following: 49 | 50 | * **(a)** any file in Source Code Form that results from an addition to, 51 | deletion from, or modification of the contents of Covered 52 | Software; or 53 | * **(b)** any new file in Source Code Form that contains any Covered 54 | Software. 55 | 56 | **1.11. “Patent Claims” of a Contributor** 57 | means any patent claim(s), including without limitation, method, 58 | process, and apparatus claims, in any patent Licensable by such 59 | Contributor that would be infringed, but for the grant of the 60 | License, by the making, using, selling, offering for sale, having 61 | made, import, or transfer of either its Contributions or its 62 | Contributor Version. 63 | 64 | **1.12. “Secondary License”** 65 | means either the GNU General Public License, Version 2.0, the GNU 66 | Lesser General Public License, Version 2.1, the GNU Affero General 67 | Public License, Version 3.0, or any later versions of those 68 | licenses. 69 | 70 | **1.13. “Source Code Form”** 71 | means the form of the work preferred for making modifications. 72 | 73 | **1.14. “You” (or “Your”)** 74 | means an individual or a legal entity exercising rights under this 75 | License. For legal entities, “You” includes any entity that 76 | controls, is controlled by, or is under common control with You. For 77 | purposes of this definition, “control” means **(a)** the power, direct 78 | or indirect, to cause the direction or management of such entity, 79 | whether by contract or otherwise, or **(b)** ownership of more than 80 | fifty percent (50%) of the outstanding shares or beneficial 81 | ownership of such entity. 82 | 83 | 84 | ### 2. License Grants and Conditions 85 | 86 | #### 2.1. Grants 87 | 88 | Each Contributor hereby grants You a world-wide, royalty-free, 89 | non-exclusive license: 90 | 91 | * **(a)** under intellectual property rights (other than patent or trademark) 92 | Licensable by such Contributor to use, reproduce, make available, 93 | modify, display, perform, distribute, and otherwise exploit its 94 | Contributions, either on an unmodified basis, with Modifications, or 95 | as part of a Larger Work; and 96 | * **(b)** under Patent Claims of such Contributor to make, use, sell, offer 97 | for sale, have made, import, and otherwise transfer either its 98 | Contributions or its Contributor Version. 99 | 100 | #### 2.2. Effective Date 101 | 102 | The licenses granted in Section 2.1 with respect to any Contribution 103 | become effective for each Contribution on the date the Contributor first 104 | distributes such Contribution. 105 | 106 | #### 2.3. Limitations on Grant Scope 107 | 108 | The licenses granted in this Section 2 are the only rights granted under 109 | this License. No additional rights or licenses will be implied from the 110 | distribution or licensing of Covered Software under this License. 111 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 112 | Contributor: 113 | 114 | * **(a)** for any code that a Contributor has removed from Covered Software; 115 | or 116 | * **(b)** for infringements caused by: **(i)** Your and any other third party's 117 | modifications of Covered Software, or **(ii)** the combination of its 118 | Contributions with other software (except as part of its Contributor 119 | Version); or 120 | * **(c)** under Patent Claims infringed by Covered Software in the absence of 121 | its Contributions. 122 | 123 | This License does not grant any rights in the trademarks, service marks, 124 | or logos of any Contributor (except as may be necessary to comply with 125 | the notice requirements in Section 3.4). 126 | 127 | #### 2.4. Subsequent Licenses 128 | 129 | No Contributor makes additional grants as a result of Your choice to 130 | distribute the Covered Software under a subsequent version of this 131 | License (see Section 10.2) or under the terms of a Secondary License (if 132 | permitted under the terms of Section 3.3). 133 | 134 | #### 2.5. Representation 135 | 136 | Each Contributor represents that the Contributor believes its 137 | Contributions are its original creation(s) or it has sufficient rights 138 | to grant the rights to its Contributions conveyed by this License. 139 | 140 | #### 2.6. Fair Use 141 | 142 | This License is not intended to limit any rights You have under 143 | applicable copyright doctrines of fair use, fair dealing, or other 144 | equivalents. 145 | 146 | #### 2.7. Conditions 147 | 148 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 149 | in Section 2.1. 150 | 151 | 152 | ### 3. Responsibilities 153 | 154 | #### 3.1. Distribution of Source Form 155 | 156 | All distribution of Covered Software in Source Code Form, including any 157 | Modifications that You create or to which You contribute, must be under 158 | the terms of this License. You must inform recipients that the Source 159 | Code Form of the Covered Software is governed by the terms of this 160 | License, and how they can obtain a copy of this License. You may not 161 | attempt to alter or restrict the recipients' rights in the Source Code 162 | Form. 163 | 164 | #### 3.2. Distribution of Executable Form 165 | 166 | If You distribute Covered Software in Executable Form then: 167 | 168 | * **(a)** such Covered Software must also be made available in Source Code 169 | Form, as described in Section 3.1, and You must inform recipients of 170 | the Executable Form how they can obtain a copy of such Source Code 171 | Form by reasonable means in a timely manner, at a charge no more 172 | than the cost of distribution to the recipient; and 173 | 174 | * **(b)** You may distribute such Executable Form under the terms of this 175 | License, or sublicense it under different terms, provided that the 176 | license for the Executable Form does not attempt to limit or alter 177 | the recipients' rights in the Source Code Form under this License. 178 | 179 | #### 3.3. Distribution of a Larger Work 180 | 181 | You may create and distribute a Larger Work under terms of Your choice, 182 | provided that You also comply with the requirements of this License for 183 | the Covered Software. If the Larger Work is a combination of Covered 184 | Software with a work governed by one or more Secondary Licenses, and the 185 | Covered Software is not Incompatible With Secondary Licenses, this 186 | License permits You to additionally distribute such Covered Software 187 | under the terms of such Secondary License(s), so that the recipient of 188 | the Larger Work may, at their option, further distribute the Covered 189 | Software under the terms of either this License or such Secondary 190 | License(s). 191 | 192 | #### 3.4. Notices 193 | 194 | You may not remove or alter the substance of any license notices 195 | (including copyright notices, patent notices, disclaimers of warranty, 196 | or limitations of liability) contained within the Source Code Form of 197 | the Covered Software, except that You may alter any license notices to 198 | the extent required to remedy known factual inaccuracies. 199 | 200 | #### 3.5. Application of Additional Terms 201 | 202 | You may choose to offer, and to charge a fee for, warranty, support, 203 | indemnity or liability obligations to one or more recipients of Covered 204 | Software. However, You may do so only on Your own behalf, and not on 205 | behalf of any Contributor. You must make it absolutely clear that any 206 | such warranty, support, indemnity, or liability obligation is offered by 207 | You alone, and You hereby agree to indemnify every Contributor for any 208 | liability incurred by such Contributor as a result of warranty, support, 209 | indemnity or liability terms You offer. You may include additional 210 | disclaimers of warranty and limitations of liability specific to any 211 | jurisdiction. 212 | 213 | 214 | ### 4. Inability to Comply Due to Statute or Regulation 215 | 216 | If it is impossible for You to comply with any of the terms of this 217 | License with respect to some or all of the Covered Software due to 218 | statute, judicial order, or regulation then You must: **(a)** comply with 219 | the terms of this License to the maximum extent possible; and **(b)** 220 | describe the limitations and the code they affect. Such description must 221 | be placed in a text file included with all distributions of the Covered 222 | Software under this License. Except to the extent prohibited by statute 223 | or regulation, such description must be sufficiently detailed for a 224 | recipient of ordinary skill to be able to understand it. 225 | 226 | 227 | ### 5. Termination 228 | 229 | **5.1.** The rights granted under this License will terminate automatically 230 | if You fail to comply with any of its terms. However, if You become 231 | compliant, then the rights granted under this License from a particular 232 | Contributor are reinstated **(a)** provisionally, unless and until such 233 | Contributor explicitly and finally terminates Your grants, and **(b)** on an 234 | ongoing basis, if such Contributor fails to notify You of the 235 | non-compliance by some reasonable means prior to 60 days after You have 236 | come back into compliance. Moreover, Your grants from a particular 237 | Contributor are reinstated on an ongoing basis if such Contributor 238 | notifies You of the non-compliance by some reasonable means, this is the 239 | first time You have received notice of non-compliance with this License 240 | from such Contributor, and You become compliant prior to 30 days after 241 | Your receipt of the notice. 242 | 243 | **5.2.** If You initiate litigation against any entity by asserting a patent 244 | infringement claim (excluding declaratory judgment actions, 245 | counter-claims, and cross-claims) alleging that a Contributor Version 246 | directly or indirectly infringes any patent, then the rights granted to 247 | You by any and all Contributors for the Covered Software under Section 248 | 2.1 of this License shall terminate. 249 | 250 | **5.3.** In the event of termination under Sections 5.1 or 5.2 above, all 251 | end user license agreements (excluding distributors and resellers) which 252 | have been validly granted by You or Your distributors under this License 253 | prior to termination shall survive termination. 254 | 255 | 256 | ### 6. Disclaimer of Warranty 257 | 258 | > Covered Software is provided under this License on an “as is” 259 | > basis, without warranty of any kind, either expressed, implied, or 260 | > statutory, including, without limitation, warranties that the 261 | > Covered Software is free of defects, merchantable, fit for a 262 | > particular purpose or non-infringing. The entire risk as to the 263 | > quality and performance of the Covered Software is with You. 264 | > Should any Covered Software prove defective in any respect, You 265 | > (not any Contributor) assume the cost of any necessary servicing, 266 | > repair, or correction. This disclaimer of warranty constitutes an 267 | > essential part of this License. No use of any Covered Software is 268 | > authorized under this License except under this disclaimer. 269 | 270 | ### 7. Limitation of Liability 271 | 272 | > Under no circumstances and under no legal theory, whether tort 273 | > (including negligence), contract, or otherwise, shall any 274 | > Contributor, or anyone who distributes Covered Software as 275 | > permitted above, be liable to You for any direct, indirect, 276 | > special, incidental, or consequential damages of any character 277 | > including, without limitation, damages for lost profits, loss of 278 | > goodwill, work stoppage, computer failure or malfunction, or any 279 | > and all other commercial damages or losses, even if such party 280 | > shall have been informed of the possibility of such damages. This 281 | > limitation of liability shall not apply to liability for death or 282 | > personal injury resulting from such party's negligence to the 283 | > extent applicable law prohibits such limitation. Some 284 | > jurisdictions do not allow the exclusion or limitation of 285 | > incidental or consequential damages, so this exclusion and 286 | > limitation may not apply to You. 287 | 288 | 289 | ### 8. Litigation 290 | 291 | Any litigation relating to this License may be brought only in the 292 | courts of a jurisdiction where the defendant maintains its principal 293 | place of business and such litigation shall be governed by laws of that 294 | jurisdiction, without reference to its conflict-of-law provisions. 295 | Nothing in this Section shall prevent a party's ability to bring 296 | cross-claims or counter-claims. 297 | 298 | 299 | ### 9. Miscellaneous 300 | 301 | This License represents the complete agreement concerning the subject 302 | matter hereof. If any provision of this License is held to be 303 | unenforceable, such provision shall be reformed only to the extent 304 | necessary to make it enforceable. Any law or regulation which provides 305 | that the language of a contract shall be construed against the drafter 306 | shall not be used to construe this License against a Contributor. 307 | 308 | 309 | ### 10. Versions of the License 310 | 311 | #### 10.1. New Versions 312 | 313 | Mozilla Foundation is the license steward. Except as provided in Section 314 | 10.3, no one other than the license steward has the right to modify or 315 | publish new versions of this License. Each version will be given a 316 | distinguishing version number. 317 | 318 | #### 10.2. Effect of New Versions 319 | 320 | You may distribute the Covered Software under the terms of the version 321 | of the License under which You originally received the Covered Software, 322 | or under the terms of any subsequent version published by the license 323 | steward. 324 | 325 | #### 10.3. Modified Versions 326 | 327 | If you create software not governed by this License, and you want to 328 | create a new license for such software, you may create and use a 329 | modified version of this License if you rename the license and remove 330 | any references to the name of the license steward (except to note that 331 | such modified license differs from this License). 332 | 333 | #### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses 334 | 335 | If You choose to distribute Source Code Form that is Incompatible With 336 | Secondary Licenses under the terms of this version of the License, the 337 | notice described in Exhibit B of this License must be attached. 338 | 339 | ## Exhibit A - Source Code Form License Notice 340 | 341 | This Source Code Form is subject to the terms of the Mozilla Public 342 | License, v. 2.0. If a copy of the MPL was not distributed with this 343 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 344 | 345 | If it is not possible or desirable to put the notice in a particular 346 | file, then You may include the notice in a location (such as a LICENSE 347 | file in a relevant directory) where a recipient would be likely to look 348 | for such a notice. 349 | 350 | You may add additional accurate notices of copyright ownership. 351 | 352 | ## Exhibit B - “Incompatible With Secondary Licenses” Notice 353 | 354 | This Source Code Form is "Incompatible With Secondary Licenses", as 355 | defined by the Mozilla Public License, v. 2.0. 356 | 357 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | GOBIN=./build 4 | 5 | deps: 6 | go get -t ./... 7 | 8 | test: 9 | go test -v ./... 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keycard-go 2 | 3 | `keycard-go` is a set of Go packages built to interact with the [Status Keycard](https://github.com/status-im/status-keycard). 4 | 5 | If you only need a tool to initialize your card, check out [keycard-cli](https://github.com/status-im/keycard-cli). 6 | 7 | ## Keycard commands 8 | 9 | - [x] SELECT 10 | - [x] INIT 11 | - [x] OPEN SECURE CHANNEL 12 | - [x] MUTUALLY AUTHENTICATE 13 | - [x] PAIR 14 | - [x] UNPAIR 15 | - [x] GET STATUS 16 | - [x] STORE DATA 17 | - [x] GET DATA 18 | - [x] VERIFY PIN 19 | - [x] CHANGE PIN 20 | - [x] UNBLOCK PIN 21 | - [x] LOAD KEY 22 | - [x] DERIVE KEY 23 | - [x] GENERATE MNEMONIC 24 | - [x] REMOVE KEY 25 | - [x] GENERATE KEY 26 | - [x] INIT 27 | - [x] SIGN 28 | - [ ] SET PINLESS PATH 29 | - [x] EXPORT KEY 30 | -------------------------------------------------------------------------------- /_assets/ci/Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent { 3 | docker { 4 | label 'linux' 5 | image 'golang:1.12' 6 | } 7 | } 8 | 9 | options { 10 | /* manage how many builds we keep */ 11 | buildDiscarder(logRotator( 12 | numToKeepStr: '20', 13 | daysToKeepStr: '30', 14 | )) 15 | disableConcurrentBuilds() 16 | /* Go requires a certain directory structure */ 17 | checkoutToSubdirectory('src/github.com/status-im/keycard-go') 18 | } 19 | 20 | environment { 21 | PROJECT = 'src/github.com/status-im/keycard-go' 22 | GOPATH = "${env.WORKSPACE}" 23 | PATH = "${env.PATH}:${env.GOPATH}/bin" 24 | GOCACHE = '/tmp/gocache' 25 | } 26 | 27 | stages { 28 | stage('Prep') { 29 | steps { dir(env.PROJECT) { 30 | sh 'make deps' 31 | } } 32 | } 33 | 34 | stage('Test') { 35 | steps { dir(env.PROJECT) { 36 | sh 'make test' 37 | } } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apdu/command.go: -------------------------------------------------------------------------------- 1 | package apdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | ) 8 | 9 | // ErrBadRawCommand is an error returned by ParseCommand in case the command data is not long enough. 10 | var ErrBadRawCommand = errors.New("command must be at least 4 bytes") 11 | 12 | // Command struct represent the data sent as an APDU command with CLA, Ins, P1, P2, Lc, Data, and Le. 13 | type Command struct { 14 | Cla uint8 15 | Ins uint8 16 | P1 uint8 17 | P2 uint8 18 | Data []byte 19 | le uint8 20 | requiresLe bool 21 | } 22 | 23 | // NewCommand returns a new apdu Command. 24 | func NewCommand(cla, ins, p1, p2 uint8, data []byte) *Command { 25 | return &Command{ 26 | Cla: cla, 27 | Ins: ins, 28 | P1: p1, 29 | P2: p2, 30 | Data: data, 31 | requiresLe: false, 32 | } 33 | } 34 | 35 | // SetLe sets the expected Le value and makes sure the Le value is sent in the apdu Command. 36 | func (c *Command) SetLe(le uint8) { 37 | c.requiresLe = true 38 | c.le = le 39 | } 40 | 41 | // Le returns if Le is set and its value. 42 | func (c *Command) Le() (bool, uint8) { 43 | return c.requiresLe, c.le 44 | } 45 | 46 | // Serialize serielizes the command into a raw bytes sequence. 47 | func (c *Command) Serialize() ([]byte, error) { 48 | buf := new(bytes.Buffer) 49 | 50 | if err := binary.Write(buf, binary.BigEndian, c.Cla); err != nil { 51 | return nil, err 52 | } 53 | 54 | if err := binary.Write(buf, binary.BigEndian, c.Ins); err != nil { 55 | return nil, err 56 | } 57 | 58 | if err := binary.Write(buf, binary.BigEndian, c.P1); err != nil { 59 | return nil, err 60 | } 61 | 62 | if err := binary.Write(buf, binary.BigEndian, c.P2); err != nil { 63 | return nil, err 64 | } 65 | 66 | if len(c.Data) > 0 { 67 | if err := binary.Write(buf, binary.BigEndian, uint8(len(c.Data))); err != nil { 68 | return nil, err 69 | } 70 | if err := binary.Write(buf, binary.BigEndian, c.Data); err != nil { 71 | return nil, err 72 | } 73 | } 74 | 75 | if c.requiresLe { 76 | if err := binary.Write(buf, binary.BigEndian, c.le); err != nil { 77 | return nil, err 78 | } 79 | } 80 | 81 | return buf.Bytes(), nil 82 | } 83 | 84 | func (c *Command) deserialize(data []byte) error { 85 | if len(data) < 4 { 86 | return ErrBadRawCommand 87 | } 88 | 89 | buf := bytes.NewReader(data) 90 | 91 | if err := binary.Read(buf, binary.BigEndian, &c.Cla); err != nil { 92 | return err 93 | } 94 | 95 | if err := binary.Read(buf, binary.BigEndian, &c.Ins); err != nil { 96 | return err 97 | } 98 | 99 | if err := binary.Read(buf, binary.BigEndian, &c.P1); err != nil { 100 | return err 101 | } 102 | 103 | if err := binary.Read(buf, binary.BigEndian, &c.P2); err != nil { 104 | return err 105 | } 106 | 107 | var lc uint8 108 | if err := binary.Read(buf, binary.BigEndian, &lc); err != nil { 109 | return nil 110 | } 111 | 112 | cmdData := make([]byte, lc) 113 | if err := binary.Read(buf, binary.BigEndian, &cmdData); err != nil { 114 | return nil 115 | } 116 | c.Data = cmdData 117 | 118 | var le uint8 119 | if err := binary.Read(buf, binary.BigEndian, &le); err != nil { 120 | return nil 121 | } 122 | c.SetLe(le) 123 | 124 | return nil 125 | } 126 | 127 | // ParseCommand parses a raw command and returns a Command 128 | func ParseCommand(raw []byte) (*Command, error) { 129 | cmd := &Command{} 130 | return cmd, cmd.deserialize(raw) 131 | } 132 | -------------------------------------------------------------------------------- /apdu/command_test.go: -------------------------------------------------------------------------------- 1 | package apdu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/status-im/keycard-go/hexutils" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNewCommand(t *testing.T) { 12 | var cla uint8 = 0x80 13 | var ins uint8 = 0x50 14 | var p1 uint8 = 1 15 | var p2 uint8 = 2 16 | data := hexutils.HexToBytes("84762336c5187fe8") 17 | 18 | cmd := NewCommand(cla, ins, p1, p2, data) 19 | 20 | expected := "80 50 01 02 08 84 76 23 36 C5 18 7F E8" 21 | result, err := cmd.Serialize() 22 | assert.NoError(t, err) 23 | assert.Equal(t, expected, hexutils.BytesToHexWithSpaces(result)) 24 | 25 | cmd.SetLe(uint8(0x77)) 26 | expected = "80 50 01 02 08 84 76 23 36 C5 18 7F E8 77" 27 | result, err = cmd.Serialize() 28 | assert.NoError(t, err) 29 | assert.Equal(t, expected, hexutils.BytesToHexWithSpaces(result)) 30 | } 31 | 32 | func TestParseCommand(t *testing.T) { 33 | raw := hexutils.HexToBytes("0102030402050607") 34 | cmd, err := ParseCommand(raw) 35 | require.Nil(t, err) 36 | assert.Equal(t, uint8(0x01), cmd.Cla) 37 | assert.Equal(t, uint8(0x02), cmd.Ins) 38 | assert.Equal(t, uint8(0x03), cmd.P1) 39 | assert.Equal(t, uint8(0x04), cmd.P2) 40 | assert.Equal(t, []byte{0x05, 0x06}, cmd.Data) 41 | assert.True(t, cmd.requiresLe) 42 | assert.Equal(t, uint8(0x07), cmd.le) 43 | } 44 | -------------------------------------------------------------------------------- /apdu/response.go: -------------------------------------------------------------------------------- 1 | package apdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | ) 9 | 10 | const ( 11 | // SwOK is returned from smartcards as a positive response code. 12 | SwOK = 0x9000 13 | ) 14 | 15 | // ErrBadResponse defines an error conaining the returned Sw code and a description message. 16 | type ErrBadResponse struct { 17 | Sw uint16 18 | message string 19 | } 20 | 21 | // NewErrBadResponse returns a ErrBadResponse with the specified sw and message values. 22 | func NewErrBadResponse(sw uint16, message string) *ErrBadResponse { 23 | return &ErrBadResponse{ 24 | Sw: sw, 25 | message: message, 26 | } 27 | } 28 | 29 | // Error implements the error interface. 30 | func (e *ErrBadResponse) Error() string { 31 | return fmt.Sprintf("bad response %x: %s", e.Sw, e.message) 32 | } 33 | 34 | // Response represents a struct containing the smartcard response fields. 35 | type Response struct { 36 | Data []byte 37 | Sw1 uint8 38 | Sw2 uint8 39 | Sw uint16 40 | } 41 | 42 | // ErrBadRawResponse is an error returned by ParseResponse in case the response data is not long enough. 43 | var ErrBadRawResponse = errors.New("response data must be at least 2 bytes") 44 | 45 | // ParseResponse parses a raw response and return a Response. 46 | func ParseResponse(data []byte) (*Response, error) { 47 | r := &Response{} 48 | return r, r.deserialize(data) 49 | } 50 | 51 | func (r *Response) deserialize(data []byte) error { 52 | if len(data) < 2 { 53 | return ErrBadRawResponse 54 | } 55 | 56 | r.Data = make([]byte, len(data)-2) 57 | buf := bytes.NewReader(data) 58 | 59 | if err := binary.Read(buf, binary.BigEndian, &r.Data); err != nil { 60 | return err 61 | } 62 | 63 | if err := binary.Read(buf, binary.BigEndian, &r.Sw1); err != nil { 64 | return err 65 | } 66 | 67 | if err := binary.Read(buf, binary.BigEndian, &r.Sw2); err != nil { 68 | return err 69 | } 70 | 71 | r.Sw = (uint16(r.Sw1) << 8) | uint16(r.Sw2) 72 | 73 | return nil 74 | } 75 | 76 | // IsOK returns true if the response Sw code is 0x9000. 77 | func (r *Response) IsOK() bool { 78 | return r.Sw == SwOK 79 | } 80 | -------------------------------------------------------------------------------- /apdu/response_test.go: -------------------------------------------------------------------------------- 1 | package apdu 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/status-im/keycard-go/hexutils" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseResponse(t *testing.T) { 11 | raw := hexutils.HexToBytes("000002650183039536622002003b5e508f751c0af3016e3fbc23d3a69000") 12 | resp, err := ParseResponse(raw) 13 | 14 | assert.NoError(t, err) 15 | assert.Equal(t, uint8(0x90), resp.Sw1) 16 | assert.Equal(t, uint8(0x00), resp.Sw2) 17 | assert.Equal(t, uint16(0x9000), resp.Sw) 18 | 19 | expected := "000002650183039536622002003B5E508F751C0AF3016E3FBC23D3A6" 20 | assert.Equal(t, expected, hexutils.BytesToHex(resp.Data)) 21 | } 22 | 23 | func TestParseResponse_BadData(t *testing.T) { 24 | raw := hexutils.HexToBytes("") 25 | _, err := ParseResponse(raw) 26 | assert.Equal(t, ErrBadRawResponse, err) 27 | } 28 | 29 | func TestResp_IsOK(t *testing.T) { 30 | raw := hexutils.HexToBytes("01029000") 31 | resp, err := ParseResponse(raw) 32 | assert.NoError(t, err) 33 | assert.True(t, resp.IsOK()) 34 | } 35 | -------------------------------------------------------------------------------- /apdu/utils.go: -------------------------------------------------------------------------------- 1 | package apdu 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | ) 10 | 11 | type Tag []byte 12 | 13 | var ( 14 | ErrUnsupportedLenth80 = errors.New("length cannot be 0x80") 15 | ErrLengthTooBig = errors.New("length cannot be more than 3 bytes") 16 | ) 17 | 18 | // ErrTagNotFound is an error returned if a tag is not found in a TLV sequence. 19 | type ErrTagNotFound struct { 20 | tag Tag 21 | } 22 | 23 | // Error implements the error interface 24 | func (e *ErrTagNotFound) Error() string { 25 | return fmt.Sprintf("tag %x not found", e.tag) 26 | } 27 | 28 | // FindTag searches for a tag value within a TLV sequence. 29 | func FindTag(raw []byte, tags ...Tag) ([]byte, error) { 30 | return findTag(raw, 0, tags...) 31 | } 32 | 33 | // FindTagN searches for a tag value within a TLV sequence and returns the n occurrence 34 | func FindTagN(raw []byte, n int, tags ...Tag) ([]byte, error) { 35 | return findTag(raw, n, tags...) 36 | } 37 | 38 | func findTag(raw []byte, occurrence int, tags ...Tag) ([]byte, error) { 39 | if len(tags) == 0 { 40 | return raw, nil 41 | } 42 | 43 | target := tags[0] 44 | buf := bytes.NewBuffer(raw) 45 | 46 | var ( 47 | tag Tag 48 | length uint32 49 | err error 50 | ) 51 | 52 | for { 53 | tag, err = parseTag(buf) 54 | switch { 55 | case err == io.EOF: 56 | return []byte{}, &ErrTagNotFound{target} 57 | case err != nil: 58 | return nil, err 59 | } 60 | 61 | length, err = ParseLength(buf) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | data := make([]byte, length) 67 | if length != 0 { 68 | _, err = buf.Read(data) 69 | if err != nil { 70 | return nil, err 71 | } 72 | } 73 | 74 | if bytes.Equal(tag, target) { 75 | // if it's the last tag in the search path, we start counting the occurrences 76 | if len(tags) == 1 && occurrence > 0 { 77 | occurrence-- 78 | continue 79 | } 80 | 81 | if len(tags) == 1 { 82 | return data, nil 83 | } 84 | 85 | return findTag(data, occurrence, tags[1:]...) 86 | } 87 | } 88 | } 89 | 90 | func ParseLength(buf *bytes.Buffer) (uint32, error) { 91 | length, err := buf.ReadByte() 92 | if err != nil { 93 | return 0, err 94 | } 95 | 96 | if length == 0x80 { 97 | return 0, ErrUnsupportedLenth80 98 | } 99 | 100 | if length > 0x80 { 101 | lengthSize := length - 0x80 102 | if lengthSize > 3 { 103 | return 0, ErrLengthTooBig 104 | } 105 | 106 | data := make([]byte, lengthSize) 107 | _, err = buf.Read(data) 108 | if err != nil { 109 | return 0, err 110 | } 111 | 112 | num := make([]byte, 4) 113 | copy(num[4-lengthSize:], data) 114 | 115 | return binary.BigEndian.Uint32(num), nil 116 | } 117 | 118 | return uint32(length), nil 119 | } 120 | 121 | func WriteLength(buf *bytes.Buffer, length uint32) { 122 | if length < 0x80 { 123 | buf.WriteByte(byte(length)) 124 | } else if length < 0x100 { 125 | buf.WriteByte(0x81) 126 | buf.WriteByte(byte(length)) 127 | } else if length < 0x10000 { 128 | buf.WriteByte(0x82) 129 | buf.WriteByte(byte(length >> 8)) 130 | buf.WriteByte(byte(length)) 131 | } else if length < 0x1000000 { 132 | buf.WriteByte(0x83) 133 | buf.WriteByte(byte(length >> 16)) 134 | buf.WriteByte(byte(length >> 8)) 135 | buf.WriteByte(byte(length)) 136 | } else { 137 | buf.WriteByte(0x84) 138 | buf.WriteByte(byte(length >> 24)) 139 | buf.WriteByte(byte(length >> 16)) 140 | buf.WriteByte(byte(length >> 8)) 141 | buf.WriteByte(byte(length)) 142 | } 143 | } 144 | 145 | func parseTag(buf *bytes.Buffer) (Tag, error) { 146 | tag := make(Tag, 0) 147 | b, err := buf.ReadByte() 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | tag = append(tag, b) 153 | if b&0x1F != 0x1F { 154 | return tag, nil 155 | } 156 | 157 | for { 158 | b, err = buf.ReadByte() 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | tag = append(tag, b) 164 | 165 | if b&0x80 != 0x80 { 166 | return tag, nil 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /apdu/utils_test.go: -------------------------------------------------------------------------------- 1 | package apdu 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/status-im/keycard-go/hexutils" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestFindTag(t *testing.T) { 13 | var ( 14 | tagData []byte 15 | err error 16 | ) 17 | 18 | data := hexutils.HexToBytes("C1 02 BB CC C2 04 C3 02 11 22 C3 02 88 99") 19 | 20 | tagData, err = FindTag(data, Tag{0xC1}) 21 | assert.NoError(t, err) 22 | assert.Equal(t, "BB CC", hexutils.BytesToHexWithSpaces(tagData)) 23 | 24 | tagData, err = FindTag(data, Tag{0xC2}) 25 | assert.NoError(t, err) 26 | assert.Equal(t, "C3 02 11 22", hexutils.BytesToHexWithSpaces(tagData)) 27 | 28 | tagData, err = FindTag(data, Tag{0xC3}) 29 | assert.NoError(t, err) 30 | assert.Equal(t, "88 99", hexutils.BytesToHexWithSpaces(tagData)) 31 | 32 | tagData, err = FindTag(data, Tag{0xC2}, Tag{0xC3}) 33 | assert.NoError(t, err) 34 | assert.Equal(t, "11 22", hexutils.BytesToHexWithSpaces(tagData)) 35 | 36 | // tag not found 37 | data = hexutils.HexToBytes("C1 00") 38 | _, err = FindTag(data, Tag{0xC2}) 39 | assert.Equal(t, &ErrTagNotFound{Tag{0xC2}}, err) 40 | 41 | // sub-tag not found 42 | data = hexutils.HexToBytes("C1 02 C2 00") 43 | _, err = FindTag(data, Tag{0xC1}, Tag{0xC3}) 44 | assert.Equal(t, &ErrTagNotFound{Tag{0xC3}}, err) 45 | } 46 | 47 | func TestParseLength(t *testing.T) { 48 | scenarios := []struct { 49 | data []byte 50 | expectedLength uint32 51 | err error 52 | }{ 53 | { 54 | data: []byte{0x01, 0xAA}, 55 | expectedLength: 1, 56 | err: nil, 57 | }, 58 | { 59 | data: []byte{0x7F, 0xAA}, 60 | expectedLength: 127, 61 | err: nil, 62 | }, 63 | { 64 | data: []byte{0x81, 0x80, 0xAA}, 65 | expectedLength: 128, 66 | err: nil, 67 | }, 68 | { 69 | data: []byte{0x82, 0x80, 0x80, 0xAA}, 70 | expectedLength: 32896, 71 | err: nil, 72 | }, 73 | { 74 | data: []byte{0x83, 0x80, 0x80, 0x80, 0xAA}, 75 | expectedLength: 8421504, 76 | err: nil, 77 | }, 78 | { 79 | data: []byte{0x80, 0xAA}, 80 | expectedLength: 0, 81 | err: ErrUnsupportedLenth80, 82 | }, 83 | { 84 | data: []byte{0x84, 0xAA}, 85 | expectedLength: 0, 86 | err: ErrLengthTooBig, 87 | }, 88 | } 89 | 90 | for _, s := range scenarios { 91 | buf := bytes.NewBuffer(s.data) 92 | length, err := ParseLength(buf) 93 | if s.err == nil { 94 | assert.NoError(t, err) 95 | assert.Equal(t, s.expectedLength, length) 96 | } else { 97 | assert.Equal(t, s.err, err) 98 | } 99 | } 100 | } 101 | 102 | func TestFindTagN(t *testing.T) { 103 | data := hexutils.HexToBytes("0A 01 A1 0A 01 A2") 104 | 105 | tagData, err := FindTagN(data, 0, Tag{0x0A}) 106 | assert.NoError(t, err) 107 | assert.Equal(t, "A1", hexutils.BytesToHexWithSpaces(tagData)) 108 | 109 | tagData, err = FindTagN(data, 1, Tag{0x0A}) 110 | assert.NoError(t, err) 111 | assert.Equal(t, "A2", hexutils.BytesToHexWithSpaces(tagData)) 112 | } 113 | 114 | func TestParseTag(t *testing.T) { 115 | scenarios := []struct { 116 | rawTag []byte 117 | expectedTag Tag 118 | }{ 119 | { 120 | rawTag: []byte{0x01, 0x02}, 121 | expectedTag: Tag{0x01}, 122 | }, 123 | { 124 | rawTag: []byte{0x9F, 0x70, 0x01}, 125 | expectedTag: Tag{0x9f, 0x70}, 126 | }, 127 | } 128 | 129 | for _, s := range scenarios { 130 | buf := bytes.NewBuffer(s.rawTag) 131 | tag, err := parseTag(buf) 132 | require.Nil(t, err) 133 | assert.Equal(t, s.expectedTag, tag) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /cash_command_set.go: -------------------------------------------------------------------------------- 1 | package keycard 2 | 3 | import ( 4 | "github.com/status-im/keycard-go/apdu" 5 | "github.com/status-im/keycard-go/globalplatform" 6 | "github.com/status-im/keycard-go/identifiers" 7 | "github.com/status-im/keycard-go/types" 8 | ) 9 | 10 | type CashCommandSet struct { 11 | c types.Channel 12 | CashApplicationInfo *types.CashApplicationInfo 13 | } 14 | 15 | func NewCashCommandSet(c types.Channel) *CashCommandSet { 16 | return &CashCommandSet{ 17 | c: c, 18 | CashApplicationInfo: &types.CashApplicationInfo{}, 19 | } 20 | } 21 | 22 | func (cs *CashCommandSet) Select() error { 23 | cmd := globalplatform.NewCommandSelect(identifiers.CashInstanceAID) 24 | cmd.SetLe(0) 25 | resp, err := cs.c.Send(cmd) 26 | if err = cs.checkOK(resp, err); err != nil { 27 | return err 28 | } 29 | 30 | appInfo, err := types.ParseCashApplicationInfo(resp.Data) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | cs.CashApplicationInfo = appInfo 36 | 37 | return nil 38 | } 39 | 40 | func (cs *CashCommandSet) Sign(data []byte) (*types.Signature, error) { 41 | cmd, err := NewCommandSign(data, 0x00, "") 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | resp, err := cs.c.Send(cmd) 47 | if err = cs.checkOK(resp, err); err != nil { 48 | return nil, err 49 | } 50 | 51 | return types.ParseSignature(data, resp.Data) 52 | } 53 | 54 | func (cs *CashCommandSet) checkOK(resp *apdu.Response, err error, allowedResponses ...uint16) error { 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if len(allowedResponses) == 0 { 60 | allowedResponses = []uint16{apdu.SwOK} 61 | } 62 | 63 | for _, code := range allowedResponses { 64 | if code == resp.Sw { 65 | return nil 66 | } 67 | } 68 | 69 | return apdu.NewErrBadResponse(resp.Sw, "unexpected response") 70 | } 71 | -------------------------------------------------------------------------------- /command_set.go: -------------------------------------------------------------------------------- 1 | package keycard 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | 11 | "github.com/status-im/keycard-go/apdu" 12 | "github.com/status-im/keycard-go/crypto" 13 | "github.com/status-im/keycard-go/globalplatform" 14 | "github.com/status-im/keycard-go/identifiers" 15 | "github.com/status-im/keycard-go/types" 16 | ) 17 | 18 | var ErrNoAvailablePairingSlots = errors.New("no available pairing slots") 19 | var ErrBadChecksumSize = errors.New("bad checksum size") 20 | 21 | type WrongPINError struct { 22 | RemainingAttempts int 23 | } 24 | 25 | func (e *WrongPINError) Error() string { 26 | return fmt.Sprintf("wrong pin. remaining attempts: %d", e.RemainingAttempts) 27 | } 28 | 29 | type WrongPUKError struct { 30 | RemainingAttempts int 31 | } 32 | 33 | func (e *WrongPUKError) Error() string { 34 | return fmt.Sprintf("wrong puk. remaining attempts: %d", e.RemainingAttempts) 35 | } 36 | 37 | type CommandSet struct { 38 | c types.Channel 39 | sc *SecureChannel 40 | ApplicationInfo *types.ApplicationInfo 41 | PairingInfo *types.PairingInfo 42 | } 43 | 44 | func NewCommandSet(c types.Channel) *CommandSet { 45 | return &CommandSet{ 46 | c: c, 47 | sc: NewSecureChannel(c), 48 | ApplicationInfo: &types.ApplicationInfo{}, 49 | } 50 | } 51 | 52 | func (cs *CommandSet) SetPairingInfo(key []byte, index int) { 53 | cs.PairingInfo = &types.PairingInfo{ 54 | Key: key, 55 | Index: index, 56 | } 57 | } 58 | 59 | func (cs *CommandSet) Select() error { 60 | instanceAID, err := identifiers.KeycardInstanceAID(identifiers.KeycardDefaultInstanceIndex) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | cmd := globalplatform.NewCommandSelect(instanceAID) 66 | cmd.SetLe(0) 67 | resp, err := cs.c.Send(cmd) 68 | if err = cs.checkOK(resp, err); err != nil { 69 | return err 70 | } 71 | 72 | appInfo, err := types.ParseApplicationInfo(resp.Data) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | cs.ApplicationInfo = appInfo 78 | 79 | if cs.ApplicationInfo.HasSecureChannelCapability() { 80 | err = cs.sc.GenerateSecret(cs.ApplicationInfo.SecureChannelPublicKey) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | cs.sc.Reset() 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (cs *CommandSet) Init(secrets *Secrets) error { 92 | data, err := cs.sc.OneShotEncrypt(secrets) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | init := NewCommandInit(data) 98 | resp, err := cs.c.Send(init) 99 | 100 | return cs.checkOK(resp, err) 101 | } 102 | 103 | func (cs *CommandSet) Pair(pairingPass string) error { 104 | challenge := make([]byte, 32) 105 | if _, err := rand.Read(challenge); err != nil { 106 | return err 107 | } 108 | 109 | cmd := NewCommandPairFirstStep(challenge) 110 | resp, err := cs.c.Send(cmd) 111 | if resp != nil && resp.Sw == SwNoAvailablePairingSlots { 112 | return ErrNoAvailablePairingSlots 113 | } 114 | 115 | if err = cs.checkOK(resp, err); err != nil { 116 | return err 117 | } 118 | 119 | cardCryptogram := resp.Data[:32] 120 | cardChallenge := resp.Data[32:] 121 | 122 | secretHash, err := crypto.VerifyCryptogram(challenge, pairingPass, cardCryptogram) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | h := sha256.New() 128 | h.Write(secretHash[:]) 129 | h.Write(cardChallenge) 130 | cmd = NewCommandPairFinalStep(h.Sum(nil)) 131 | resp, err = cs.c.Send(cmd) 132 | if err = cs.checkOK(resp, err); err != nil { 133 | return err 134 | } 135 | 136 | h.Reset() 137 | h.Write(secretHash[:]) 138 | h.Write(resp.Data[1:]) 139 | 140 | pairingKey := h.Sum(nil) 141 | pairingIndex := resp.Data[0] 142 | 143 | cs.PairingInfo = &types.PairingInfo{ 144 | Key: pairingKey, 145 | Index: int(pairingIndex), 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func (cs *CommandSet) Unpair(index uint8) error { 152 | cmd := NewCommandUnpair(index) 153 | resp, err := cs.sc.Send(cmd) 154 | return cs.checkOK(resp, err) 155 | } 156 | 157 | func (cs *CommandSet) Identify() ([]byte, error) { 158 | challenge := make([]byte, 32) 159 | if _, err := rand.Read(challenge); err != nil { 160 | return nil, err 161 | } 162 | 163 | cmd := NewCommandIdentify(challenge) 164 | resp, err := cs.sc.Send(cmd) 165 | 166 | if err = cs.checkOK(resp, err); err != nil { 167 | return nil, err 168 | } 169 | 170 | return types.VerifyIdentity(challenge, resp.Data) 171 | } 172 | 173 | func (cs *CommandSet) OpenSecureChannel() error { 174 | if cs.ApplicationInfo == nil { 175 | return errors.New("cannot open secure channel without setting PairingInfo") 176 | } 177 | 178 | cmd := NewCommandOpenSecureChannel(uint8(cs.PairingInfo.Index), cs.sc.RawPublicKey()) 179 | resp, err := cs.c.Send(cmd) 180 | if err = cs.checkOK(resp, err); err != nil { 181 | return err 182 | } 183 | 184 | encKey, macKey, iv := crypto.DeriveSessionKeys(cs.sc.Secret(), cs.PairingInfo.Key, resp.Data) 185 | cs.sc.Init(iv, encKey, macKey) 186 | 187 | err = cs.mutualAuthenticate() 188 | if err != nil { 189 | return err 190 | } 191 | 192 | return nil 193 | } 194 | 195 | func (cs *CommandSet) GetStatus(info uint8) (*types.ApplicationStatus, error) { 196 | cmd := NewCommandGetStatus(info) 197 | resp, err := cs.sc.Send(cmd) 198 | if err = cs.checkOK(resp, err); err != nil { 199 | return nil, err 200 | } 201 | 202 | return types.ParseApplicationStatus(resp.Data) 203 | } 204 | 205 | func (cs *CommandSet) GetStatusApplication() (*types.ApplicationStatus, error) { 206 | return cs.GetStatus(P1GetStatusApplication) 207 | } 208 | 209 | func (cs *CommandSet) GetStatusKeyPath() (*types.ApplicationStatus, error) { 210 | return cs.GetStatus(P1GetStatusKeyPath) 211 | } 212 | 213 | func (cs *CommandSet) VerifyPIN(pin string) error { 214 | cmd := NewCommandVerifyPIN(pin) 215 | resp, err := cs.sc.Send(cmd) 216 | if err = cs.checkOK(resp, err); err != nil { 217 | if resp != nil && ((resp.Sw & 0x63C0) == 0x63C0) { 218 | remainingAttempts := resp.Sw & 0x000F 219 | return &WrongPINError{ 220 | RemainingAttempts: int(remainingAttempts), 221 | } 222 | } 223 | return err 224 | } 225 | 226 | return nil 227 | } 228 | 229 | func (cs *CommandSet) ChangePIN(pin string) error { 230 | cmd := NewCommandChangePIN(pin) 231 | resp, err := cs.sc.Send(cmd) 232 | return cs.checkOK(resp, err) 233 | } 234 | 235 | func (cs *CommandSet) UnblockPIN(puk string, newPIN string) error { 236 | cmd := NewCommandUnblockPIN(puk, newPIN) 237 | resp, err := cs.sc.Send(cmd) 238 | if err = cs.checkOK(resp, err); err != nil { 239 | if resp != nil && ((resp.Sw & 0x63C0) == 0x63C0) { 240 | remainingAttempts := resp.Sw & 0x000F 241 | return &WrongPUKError{ 242 | RemainingAttempts: int(remainingAttempts), 243 | } 244 | } 245 | return err 246 | } 247 | 248 | return nil 249 | } 250 | 251 | func (cs *CommandSet) ChangePUK(puk string) error { 252 | cmd := NewCommandChangePUK(puk) 253 | resp, err := cs.sc.Send(cmd) 254 | 255 | return cs.checkOK(resp, err) 256 | } 257 | 258 | func (cs *CommandSet) ChangePairingSecret(password string) error { 259 | secret := generatePairingToken(password) 260 | cmd := NewCommandChangePairingSecret(secret) 261 | resp, err := cs.sc.Send(cmd) 262 | 263 | return cs.checkOK(resp, err) 264 | } 265 | 266 | func (cs *CommandSet) GenerateKey() ([]byte, error) { 267 | cmd := NewCommandGenerateKey() 268 | resp, err := cs.sc.Send(cmd) 269 | if err = cs.checkOK(resp, err); err != nil { 270 | return nil, err 271 | } 272 | 273 | return resp.Data, nil 274 | } 275 | 276 | func (cs *CommandSet) GenerateMnemonic(checksumSize int) ([]int, error) { 277 | if checksumSize < 4 || checksumSize > 8 { 278 | return nil, ErrBadChecksumSize 279 | } 280 | 281 | cmd := NewCommandGenerateMnemonic(byte(checksumSize)) 282 | resp, err := cs.sc.Send(cmd) 283 | if err = cs.checkOK(resp, err); err != nil { 284 | return nil, err 285 | } 286 | 287 | buf := bytes.NewBuffer(resp.Data) 288 | indexes := make([]int, 0) 289 | for { 290 | var index int16 291 | err := binary.Read(buf, binary.BigEndian, &index) 292 | if err != nil { 293 | break 294 | } 295 | 296 | indexes = append(indexes, int(index)) 297 | } 298 | 299 | return indexes, nil 300 | } 301 | 302 | func (cs *CommandSet) RemoveKey() error { 303 | cmd := NewCommandRemoveKey() 304 | resp, err := cs.sc.Send(cmd) 305 | return cs.checkOK(resp, err) 306 | } 307 | 308 | func (cs *CommandSet) DeriveKey(path string) error { 309 | cmd, err := NewCommandDeriveKey(path) 310 | if err != nil { 311 | return err 312 | } 313 | 314 | resp, err := cs.sc.Send(cmd) 315 | return cs.checkOK(resp, err) 316 | } 317 | 318 | func (cs *CommandSet) ExportKey(derive bool, makeCurrent bool, onlyPublic bool, path string) ([]byte, []byte, error) { 319 | var p2 uint8 320 | if onlyPublic { 321 | p2 = P2ExportKeyPublicOnly 322 | } else { 323 | p2 = P2ExportKeyPrivateAndPublic 324 | } 325 | 326 | key, err := cs.ExportKeyExtended(derive, makeCurrent, p2, path) 327 | 328 | if err != nil { 329 | return nil, nil, err 330 | } 331 | 332 | return key.PrivKey(), key.PubKey(), err 333 | } 334 | 335 | func (cs *CommandSet) ExportKeyExtended(derive bool, makeCurrent bool, p2 uint8, path string) (*types.ExportedKey, error) { 336 | var p1 uint8 337 | if !derive { 338 | p1 = P1ExportKeyCurrent 339 | } else if !makeCurrent { 340 | p1 = P1ExportKeyDerive 341 | } else { 342 | p1 = P1ExportKeyDeriveAndMakeCurrent 343 | } 344 | 345 | cmd, err := NewCommandExportKey(p1, p2, path) 346 | if err != nil { 347 | return nil, err 348 | } 349 | 350 | resp, err := cs.sc.Send(cmd) 351 | err = cs.checkOK(resp, err) 352 | if err != nil { 353 | return nil, err 354 | } 355 | 356 | return types.ParseExportKeyResponse(resp.Data) 357 | } 358 | 359 | func (cs *CommandSet) SetPinlessPath(path string) error { 360 | cmd, err := NewCommandSetPinlessPath(path) 361 | if err != nil { 362 | return err 363 | } 364 | 365 | resp, err := cs.sc.Send(cmd) 366 | return cs.checkOK(resp, err) 367 | } 368 | 369 | func (cs *CommandSet) Sign(data []byte) (*types.Signature, error) { 370 | cmd, err := NewCommandSign(data, P1SignCurrentKey, "") 371 | if err != nil { 372 | return nil, err 373 | } 374 | 375 | resp, err := cs.sc.Send(cmd) 376 | if err = cs.checkOK(resp, err); err != nil { 377 | return nil, err 378 | } 379 | 380 | return types.ParseSignature(data, resp.Data) 381 | } 382 | 383 | func (cs *CommandSet) SignWithPath(data []byte, path string) (*types.Signature, error) { 384 | cmd, err := NewCommandSign(data, P1SignDerive, path) 385 | if err != nil { 386 | return nil, err 387 | } 388 | 389 | resp, err := cs.sc.Send(cmd) 390 | if err = cs.checkOK(resp, err); err != nil { 391 | return nil, err 392 | } 393 | 394 | return types.ParseSignature(data, resp.Data) 395 | } 396 | 397 | func (cs *CommandSet) SignPinless(data []byte) (*types.Signature, error) { 398 | cmd, err := NewCommandSign(data, P1SignPinless, "") 399 | if err != nil { 400 | return nil, err 401 | } 402 | 403 | resp, err := cs.c.Send(cmd) 404 | if err = cs.checkOK(resp, err); err != nil { 405 | return nil, err 406 | } 407 | 408 | return types.ParseSignature(data, resp.Data) 409 | } 410 | 411 | func (cs *CommandSet) LoadSeed(seed []byte) ([]byte, error) { 412 | cmd := NewCommandLoadSeed(seed) 413 | resp, err := cs.sc.Send(cmd) 414 | if err = cs.checkOK(resp, err); err != nil { 415 | return nil, err 416 | } 417 | 418 | return resp.Data, nil 419 | } 420 | 421 | func (cs *CommandSet) GetData(typ uint8) ([]byte, error) { 422 | cmd := NewCommandGetData(typ) 423 | resp, err := cs.sc.Send(cmd) 424 | if err = cs.checkOK(resp, err); err != nil { 425 | return nil, err 426 | } 427 | 428 | return resp.Data, nil 429 | } 430 | 431 | func (cs *CommandSet) StoreData(typ uint8, data []byte) error { 432 | cmd := NewCommandStoreData(typ, data) 433 | resp, err := cs.sc.Send(cmd) 434 | return cs.checkOK(resp, err) 435 | } 436 | 437 | func (cs *CommandSet) FactoryReset() error { 438 | cmd := NewCommandFactoryReset() 439 | resp, err := cs.c.Send(cmd) 440 | return cs.checkOK(resp, err) 441 | } 442 | 443 | func (cs *CommandSet) mutualAuthenticate() error { 444 | data := make([]byte, 32) 445 | if _, err := rand.Read(data); err != nil { 446 | return err 447 | } 448 | 449 | cmd := NewCommandMutuallyAuthenticate(data) 450 | resp, err := cs.sc.Send(cmd) 451 | 452 | return cs.checkOK(resp, err) 453 | } 454 | 455 | func (cs *CommandSet) checkOK(resp *apdu.Response, err error, allowedResponses ...uint16) error { 456 | if err != nil { 457 | return err 458 | } 459 | 460 | if len(allowedResponses) == 0 { 461 | allowedResponses = []uint16{apdu.SwOK} 462 | } 463 | 464 | for _, code := range allowedResponses { 465 | if code == resp.Sw { 466 | return nil 467 | } 468 | } 469 | 470 | return apdu.NewErrBadResponse(resp.Sw, "unexpected response") 471 | } 472 | -------------------------------------------------------------------------------- /commands.go: -------------------------------------------------------------------------------- 1 | package keycard 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | 8 | "github.com/status-im/keycard-go/apdu" 9 | "github.com/status-im/keycard-go/derivationpath" 10 | "github.com/status-im/keycard-go/globalplatform" 11 | ) 12 | 13 | const ( 14 | InsInit = 0xFE 15 | InsFactoryReset = 0xFD 16 | InsOpenSecureChannel = 0x10 17 | InsMutuallyAuthenticate = 0x11 18 | InsPair = 0x12 19 | InsUnpair = 0x13 20 | InsIdentify = 0x14 21 | InsGetStatus = 0xF2 22 | InsGenerateKey = 0xD4 23 | InsRemoveKey = 0xD3 24 | InsVerifyPIN = 0x20 25 | InsChangePIN = 0x21 26 | InsUnblockPIN = 0x22 27 | InsDeriveKey = 0xD1 28 | InsExportKey = 0xC2 29 | InsSign = 0xC0 30 | InsSetPinlessPath = 0xC1 31 | InsGetData = 0xCA 32 | InsLoadKey = 0xD0 33 | InsGenerateMnemonic = 0xD2 34 | InsStoreData = 0xE2 35 | 36 | P1PairingFirstStep = 0x00 37 | P1PairingFinalStep = 0x01 38 | P1GetStatusApplication = 0x00 39 | P1GetStatusKeyPath = 0x01 40 | P1DeriveKeyFromMaster = 0x00 41 | P1DeriveKeyFromParent = 0x40 42 | P1DeriveKeyFromCurrent = 0x80 43 | P1ChangePinPIN = 0x00 44 | P1ChangePinPUK = 0x01 45 | P1ChangePinPairingSecret = 0x02 46 | P1SignCurrentKey = 0x00 47 | P1SignDerive = 0x01 48 | P1SignDeriveAndMakeCurrent = 0x02 49 | P1SignPinless = 0x03 50 | P1StoreDataPublic = 0x00 51 | P1StoreDataNDEF = 0x01 52 | P1StoreDataCash = 0x02 53 | P1ExportKeyCurrent = 0x00 54 | P1ExportKeyDerive = 0x01 55 | P1ExportKeyDeriveAndMakeCurrent = 0x02 56 | P2ExportKeyPrivateAndPublic = 0x00 57 | P2ExportKeyPublicOnly = 0x01 58 | P2ExportKeyExtendedPublic = 0x02 59 | P1LoadKeySeed = 0x03 60 | P1FactoryResetMagic = 0xAA 61 | P2FactoryResetMagic = 0x55 62 | 63 | SwNoAvailablePairingSlots = 0x6A84 64 | ) 65 | 66 | func NewCommandInit(data []byte) *apdu.Command { 67 | return apdu.NewCommand( 68 | globalplatform.ClaGp, 69 | InsInit, 70 | 0, 71 | 0, 72 | data, 73 | ) 74 | } 75 | 76 | func NewCommandPairFirstStep(challenge []byte) *apdu.Command { 77 | return apdu.NewCommand( 78 | globalplatform.ClaGp, 79 | InsPair, 80 | P1PairingFirstStep, 81 | 0, 82 | challenge, 83 | ) 84 | } 85 | 86 | func NewCommandPairFinalStep(cryptogramHash []byte) *apdu.Command { 87 | return apdu.NewCommand( 88 | globalplatform.ClaGp, 89 | InsPair, 90 | P1PairingFinalStep, 91 | 0, 92 | cryptogramHash, 93 | ) 94 | } 95 | 96 | func NewCommandUnpair(index uint8) *apdu.Command { 97 | return apdu.NewCommand( 98 | globalplatform.ClaGp, 99 | InsUnpair, 100 | index, 101 | 0, 102 | []byte{}, 103 | ) 104 | } 105 | 106 | func NewCommandIdentify(challenge []byte) *apdu.Command { 107 | return apdu.NewCommand( 108 | globalplatform.ClaGp, 109 | InsIdentify, 110 | 0, 111 | 0, 112 | challenge, 113 | ) 114 | } 115 | 116 | func NewCommandOpenSecureChannel(pairingIndex uint8, pubKey []byte) *apdu.Command { 117 | return apdu.NewCommand( 118 | globalplatform.ClaGp, 119 | InsOpenSecureChannel, 120 | pairingIndex, 121 | 0, 122 | pubKey, 123 | ) 124 | } 125 | 126 | func NewCommandMutuallyAuthenticate(data []byte) *apdu.Command { 127 | return apdu.NewCommand( 128 | globalplatform.ClaGp, 129 | InsMutuallyAuthenticate, 130 | 0, 131 | 0, 132 | data, 133 | ) 134 | } 135 | 136 | func NewCommandGetStatus(p1 uint8) *apdu.Command { 137 | return apdu.NewCommand( 138 | globalplatform.ClaGp, 139 | InsGetStatus, 140 | p1, 141 | 0, 142 | []byte{}, 143 | ) 144 | } 145 | 146 | func NewCommandGenerateKey() *apdu.Command { 147 | return apdu.NewCommand( 148 | globalplatform.ClaGp, 149 | InsGenerateKey, 150 | 0, 151 | 0, 152 | []byte{}, 153 | ) 154 | } 155 | 156 | func NewCommandGenerateMnemonic(checksumSize byte) *apdu.Command { 157 | return apdu.NewCommand( 158 | globalplatform.ClaGp, 159 | InsGenerateMnemonic, 160 | checksumSize, 161 | 0, 162 | []byte{}, 163 | ) 164 | } 165 | 166 | func NewCommandRemoveKey() *apdu.Command { 167 | return apdu.NewCommand( 168 | globalplatform.ClaGp, 169 | InsRemoveKey, 170 | 0, 171 | 0, 172 | []byte{}, 173 | ) 174 | } 175 | 176 | func NewCommandVerifyPIN(pin string) *apdu.Command { 177 | return apdu.NewCommand( 178 | globalplatform.ClaGp, 179 | InsVerifyPIN, 180 | 0, 181 | 0, 182 | []byte(pin), 183 | ) 184 | } 185 | 186 | func NewCommandChangePIN(pin string) *apdu.Command { 187 | return apdu.NewCommand( 188 | globalplatform.ClaGp, 189 | InsChangePIN, 190 | P1ChangePinPIN, 191 | 0, 192 | []byte(pin), 193 | ) 194 | } 195 | 196 | func NewCommandUnblockPIN(puk string, newPIN string) *apdu.Command { 197 | return apdu.NewCommand( 198 | globalplatform.ClaGp, 199 | InsUnblockPIN, 200 | 0, 201 | 0, 202 | []byte(puk+newPIN), 203 | ) 204 | } 205 | 206 | func NewCommandChangePUK(puk string) *apdu.Command { 207 | return apdu.NewCommand( 208 | globalplatform.ClaGp, 209 | InsChangePIN, 210 | P1ChangePinPUK, 211 | 0, 212 | []byte(puk), 213 | ) 214 | } 215 | 216 | func NewCommandChangePairingSecret(secret []byte) *apdu.Command { 217 | return apdu.NewCommand( 218 | globalplatform.ClaGp, 219 | InsChangePIN, 220 | P1ChangePinPairingSecret, 221 | 0, 222 | secret, 223 | ) 224 | } 225 | 226 | func NewCommandLoadSeed(seed []byte) *apdu.Command { 227 | return apdu.NewCommand( 228 | globalplatform.ClaGp, 229 | InsLoadKey, 230 | P1LoadKeySeed, 231 | 0, 232 | seed, 233 | ) 234 | } 235 | 236 | func NewCommandDeriveKey(pathStr string) (*apdu.Command, error) { 237 | startingPoint, path, err := derivationpath.Decode(pathStr) 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | p1, err := derivationP1FromStartingPoint(startingPoint) 243 | if err != nil { 244 | return nil, err 245 | } 246 | 247 | data := new(bytes.Buffer) 248 | for _, segment := range path { 249 | if err := binary.Write(data, binary.BigEndian, segment); err != nil { 250 | return nil, err 251 | } 252 | } 253 | 254 | return apdu.NewCommand( 255 | globalplatform.ClaGp, 256 | InsDeriveKey, 257 | p1, 258 | 0, 259 | data.Bytes(), 260 | ), nil 261 | } 262 | 263 | // Export a key 264 | // 265 | // @param {p1} 266 | // 0x00: current key - returns the key that is currently loaded and ready for signing. Does not use derivation path 267 | // 0x01: derive - returns derived key 268 | // 0x02: derive and make current - returns derived key and also sets it to the current key 269 | // @param {p2} 270 | // 0x00: return public and private key pair 271 | // 0x01: return only the public key 272 | // 0x02: return extended public key 273 | // @param {pathStr} 274 | // Derivation path of format "m/x/x/x/x/x", e.g. "m/44'/0'/0'/0/0" 275 | func NewCommandExportKey(p1 uint8, p2 uint8, pathStr string) (*apdu.Command, error) { 276 | startingPoint, path, err := derivationpath.Decode(pathStr) 277 | if err != nil { 278 | return nil, err 279 | } 280 | 281 | deriveP1, err := derivationP1FromStartingPoint(startingPoint) 282 | if err != nil { 283 | return nil, err 284 | } 285 | 286 | data := new(bytes.Buffer) 287 | for _, segment := range path { 288 | if err := binary.Write(data, binary.BigEndian, segment); err != nil { 289 | return nil, err 290 | } 291 | } 292 | 293 | return apdu.NewCommand( 294 | globalplatform.ClaGp, 295 | InsExportKey, 296 | p1|deriveP1, 297 | p2, 298 | data.Bytes(), 299 | ), nil 300 | } 301 | 302 | func NewCommandSetPinlessPath(pathStr string) (*apdu.Command, error) { 303 | startingPoint, path, err := derivationpath.Decode(pathStr) 304 | if err != nil { 305 | return nil, err 306 | } 307 | 308 | if len(path) > 0 && startingPoint != derivationpath.StartingPointMaster { 309 | return nil, fmt.Errorf("pinless path must be set with an absolute path") 310 | } 311 | 312 | data := new(bytes.Buffer) 313 | for _, segment := range path { 314 | if err := binary.Write(data, binary.BigEndian, segment); err != nil { 315 | return nil, err 316 | } 317 | } 318 | 319 | return apdu.NewCommand( 320 | globalplatform.ClaGp, 321 | InsSetPinlessPath, 322 | 0, 323 | 0, 324 | data.Bytes(), 325 | ), nil 326 | } 327 | 328 | func NewCommandSign(data []byte, p1 uint8, pathStr string) (*apdu.Command, error) { 329 | if len(data) != 32 { 330 | return nil, fmt.Errorf("data length must be 32, got %d", len(data)) 331 | } 332 | 333 | if p1 == P1SignDerive || p1 == P1SignDeriveAndMakeCurrent { 334 | _, path, err := derivationpath.Decode(pathStr) 335 | if err != nil { 336 | return nil, err 337 | } 338 | 339 | pathData := new(bytes.Buffer) 340 | for _, segment := range path { 341 | if err := binary.Write(pathData, binary.BigEndian, segment); err != nil { 342 | return nil, err 343 | } 344 | } 345 | 346 | data = append(data, pathData.Bytes()...) 347 | } 348 | 349 | return apdu.NewCommand( 350 | globalplatform.ClaGp, 351 | InsSign, 352 | p1, 353 | 1, 354 | data, 355 | ), nil 356 | } 357 | 358 | func NewCommandGetData(typ uint8) *apdu.Command { 359 | return apdu.NewCommand( 360 | globalplatform.ClaGp, 361 | InsGetData, 362 | typ, 363 | 0, 364 | []byte{}, 365 | ) 366 | } 367 | 368 | func NewCommandStoreData(typ uint8, data []byte) *apdu.Command { 369 | return apdu.NewCommand( 370 | globalplatform.ClaGp, 371 | InsStoreData, 372 | typ, 373 | 0, 374 | data, 375 | ) 376 | } 377 | 378 | func NewCommandFactoryReset() *apdu.Command { 379 | return apdu.NewCommand( 380 | globalplatform.ClaGp, 381 | InsFactoryReset, 382 | P1FactoryResetMagic, 383 | P2FactoryResetMagic, 384 | []byte{}, 385 | ) 386 | } 387 | 388 | // Internal function. Get the type of starting point for the derivation path. 389 | // Used for both DeriveKey and ExportKey 390 | func derivationP1FromStartingPoint(s derivationpath.StartingPoint) (uint8, error) { 391 | switch s { 392 | case derivationpath.StartingPointMaster: 393 | return P1DeriveKeyFromMaster, nil 394 | case derivationpath.StartingPointParent: 395 | return P1DeriveKeyFromParent, nil 396 | case derivationpath.StartingPointCurrent: 397 | return P1DeriveKeyFromCurrent, nil 398 | default: 399 | return uint8(0), fmt.Errorf("invalid startingPoint %d", s) 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/ecdsa" 8 | "crypto/rand" 9 | "crypto/sha256" 10 | "crypto/sha512" 11 | "errors" 12 | 13 | "github.com/ethereum/go-ethereum/crypto" 14 | "golang.org/x/crypto/pbkdf2" 15 | "golang.org/x/text/unicode/norm" 16 | ) 17 | 18 | const PairingTokenSalt = "Keycard Pairing Password Salt" 19 | 20 | var ErrInvalidCardCryptogram = errors.New("invalid card cryptogram") 21 | 22 | func GenerateECDHSharedSecret(priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey) []byte { 23 | x, _ := crypto.S256().ScalarMult(pub.X, pub.Y, priv.D.Bytes()) 24 | return x.FillBytes(make([]byte, 32)) 25 | } 26 | 27 | func VerifyCryptogram(challenge []byte, pairingPass string, cardCryptogram []byte) ([]byte, error) { 28 | secretHash := pbkdf2.Key(norm.NFKD.Bytes([]byte(pairingPass)), norm.NFKD.Bytes([]byte(PairingTokenSalt)), 50000, 32, sha256.New) 29 | 30 | h := sha256.New() 31 | h.Write(secretHash[:]) 32 | h.Write(challenge) 33 | expectedCryptogram := h.Sum(nil) 34 | 35 | if !bytes.Equal(expectedCryptogram, cardCryptogram) { 36 | return nil, ErrInvalidCardCryptogram 37 | } 38 | 39 | return secretHash, nil 40 | } 41 | 42 | func OneShotEncrypt(pubKeyData, secret, data []byte) ([]byte, error) { 43 | data = appendPadding(16, data) 44 | 45 | iv := make([]byte, 16) 46 | _, err := rand.Read(iv) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | block, err := aes.NewCipher(secret) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | ciphertext := make([]byte, len(data)) 57 | mode := cipher.NewCBCEncrypter(block, iv) 58 | mode.CryptBlocks(ciphertext, data) 59 | 60 | encrypted := append([]byte{byte(len(pubKeyData))}, pubKeyData...) 61 | encrypted = append(encrypted, iv...) 62 | encrypted = append(encrypted, ciphertext...) 63 | 64 | return encrypted, nil 65 | } 66 | 67 | func DeriveSessionKeys(secret, pairingKey, cardData []byte) ([]byte, []byte, []byte) { 68 | salt := cardData[:32] 69 | iv := cardData[32:] 70 | 71 | h := sha512.New() 72 | h.Write(secret) 73 | h.Write(pairingKey) 74 | h.Write(salt) 75 | data := h.Sum(nil) 76 | 77 | encKey := data[:32] 78 | macKey := data[32:] 79 | 80 | return encKey, macKey, iv 81 | } 82 | 83 | func EncryptData(data []byte, encKey []byte, iv []byte) ([]byte, error) { 84 | data = appendPadding(16, data) 85 | 86 | block, err := aes.NewCipher(encKey) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | ciphertext := make([]byte, len(data)) 92 | mode := cipher.NewCBCEncrypter(block, iv) 93 | mode.CryptBlocks(ciphertext, data) 94 | 95 | return ciphertext, nil 96 | } 97 | 98 | func DecryptData(data []byte, encKey []byte, iv []byte) ([]byte, error) { 99 | block, err := aes.NewCipher(encKey) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | plaintext := make([]byte, len(data)) 105 | mode := cipher.NewCBCDecrypter(block, iv) 106 | mode.CryptBlocks(plaintext, data) 107 | 108 | return removePadding(16, plaintext), nil 109 | } 110 | 111 | func CalculateMac(meta []byte, data []byte, macKey []byte) ([]byte, error) { 112 | data = appendPadding(16, data) 113 | 114 | block, err := aes.NewCipher(macKey) 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | mode := cipher.NewCBCEncrypter(block, make([]byte, 16)) 120 | mode.CryptBlocks(meta, meta) 121 | mode.CryptBlocks(data, data) 122 | 123 | mac := data[len(data)-32 : len(data)-16] 124 | 125 | return mac, nil 126 | } 127 | 128 | func appendPadding(blockSize int, data []byte) []byte { 129 | paddingSize := blockSize - (len(data) % blockSize) 130 | newData := make([]byte, len(data)+paddingSize) 131 | copy(newData, data) 132 | newData[len(data)] = 0x80 133 | 134 | return newData 135 | } 136 | 137 | func removePadding(blockSize int, data []byte) []byte { 138 | i := len(data) - 1 139 | for ; i > len(data)-blockSize; i-- { 140 | if data[i] == 0x80 { 141 | break 142 | } 143 | } 144 | 145 | return data[:i] 146 | } 147 | -------------------------------------------------------------------------------- /crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ethereum/go-ethereum/crypto" 7 | "github.com/status-im/keycard-go/hexutils" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestECDH(t *testing.T) { 12 | pk1, err := crypto.GenerateKey() 13 | assert.NoError(t, err) 14 | pk2, err := crypto.GenerateKey() 15 | assert.NoError(t, err) 16 | 17 | sharedSecret1 := GenerateECDHSharedSecret(pk1, &pk2.PublicKey) 18 | sharedSecret2 := GenerateECDHSharedSecret(pk2, &pk1.PublicKey) 19 | 20 | assert.Equal(t, sharedSecret1, sharedSecret2) 21 | } 22 | 23 | func TestDeriveSessionKeys(t *testing.T) { 24 | secret := hexutils.HexToBytes("B410E816DA313545151807E25A830201FA389913A977066AB0C6DE0E8631E400") 25 | pairingKey := hexutils.HexToBytes("544FF0B9B0737E4BFC4ECDFCE09F522B837051BBE4FFCEC494FA420D8525670E") 26 | cardData := hexutils.HexToBytes("1D7C033E75E10EC578AB538F69F1B02538571BA3831441F1649E3F24B5B3E3E71D7BC2D6A3D02FC8CB2FBB3FD8711BB5") 27 | 28 | encKey, macKey, iv := DeriveSessionKeys(secret, pairingKey, cardData) 29 | 30 | expectedIV := "1D7BC2D6A3D02FC8CB2FBB3FD8711BB5" 31 | expectedEncKey := "4FF496554C01BAE0A52323E3481B448C99D43982118D95C6918FE0354D224B90" 32 | expectedMacKey := "185811013138EA1B4FFDBBFA7343EF2DBE3E54C2C231885E867F792448AC2FE5" 33 | 34 | assert.Equal(t, expectedIV, hexutils.BytesToHex(iv)) 35 | assert.Equal(t, expectedEncKey, hexutils.BytesToHex(encKey)) 36 | assert.Equal(t, expectedMacKey, hexutils.BytesToHex(macKey)) 37 | } 38 | 39 | func TestEncryptData(t *testing.T) { 40 | data := hexutils.HexToBytes("A8A686D0E3290459BCB36088A8FD04A76BF13283BE4B1EAE2E1248EF609F94DC") 41 | encKey := hexutils.HexToBytes("44D689AB4B18206F7EEE5439FB9A71A8A617406BA5259728D1EBC2786D24896C") 42 | iv := hexutils.HexToBytes("9D3EF41EF1D221DD98A54AD5470F58F2") 43 | 44 | encryptedData, err := EncryptData(data, encKey, iv) 45 | assert.NoError(t, err) 46 | 47 | expected := "FFB41FED5F71A2B57A6AE62D5D5ECD1C12616F6464637DD0A7A930920ACBA55867A7E12CC4F06B089AF34FF4ED4BAB08" 48 | assert.Equal(t, expected, hexutils.BytesToHex(encryptedData)) 49 | } 50 | 51 | func TestDecryptData(t *testing.T) { 52 | encData := hexutils.HexToBytes("73B58B66372E3446E14A9F54BA59666DB432E9DD87D24F9B0525180EE52DA2106E0C70EED7CD42B5B313E4443D6AC90D") 53 | encKey := hexutils.HexToBytes("D93D8E6164196D5C5B5F84F10E4B90D98F8D282ED145513ED666AA55C9871E79") 54 | iv := hexutils.HexToBytes("F959B1220333046D3C47D61B1E1B891B") 55 | 56 | data, err := DecryptData(encData, encKey, iv) 57 | assert.NoError(t, err) 58 | 59 | expected := "2E21F9F2B2C2CC9038D518A5C6B490613E7955BD19D19108B77786986B7ABFE69000" 60 | assert.Equal(t, expected, hexutils.BytesToHex(data)) 61 | } 62 | 63 | func TestRemovePadding(t *testing.T) { 64 | scenarios := []struct { 65 | data string 66 | expected string 67 | }{ 68 | { 69 | "0180000000000000", 70 | "01", 71 | }, 72 | { 73 | "0102800000000000", 74 | "0102", 75 | }, 76 | { 77 | "01020304050607080102030405800000", 78 | "01020304050607080102030405", 79 | }, 80 | } 81 | 82 | for _, s := range scenarios { 83 | res := removePadding(8, hexutils.HexToBytes(s.data)) 84 | assert.Equal(t, s.expected, hexutils.BytesToHex(res)) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /derivationpath/decoder.go: -------------------------------------------------------------------------------- 1 | package derivationpath 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | type StartingPoint int 11 | 12 | const ( 13 | tokenMaster = 0x6D // char m 14 | tokenSeparator = 0x2F // char / 15 | tokenHardened = 0x27 // char ' 16 | tokenDot = 0x2E // char . 17 | 18 | hardenedStart = 0x80000000 // 2^31 19 | ) 20 | 21 | const ( 22 | StartingPointMaster StartingPoint = iota + 1 23 | StartingPointCurrent 24 | StartingPointParent 25 | ) 26 | 27 | type parseFunc = func() error 28 | 29 | type decoder struct { 30 | r *strings.Reader 31 | f parseFunc 32 | pos int 33 | path []uint32 34 | start StartingPoint 35 | currentToken string 36 | currentTokenHardened bool 37 | } 38 | 39 | func newDecoder(path string) *decoder { 40 | d := &decoder{ 41 | r: strings.NewReader(path), 42 | } 43 | 44 | d.reset() 45 | 46 | return d 47 | } 48 | 49 | func (d *decoder) reset() { 50 | d.r.Seek(0, io.SeekStart) 51 | d.pos = 0 52 | d.start = StartingPointCurrent 53 | d.f = d.parseStart 54 | d.path = make([]uint32, 0) 55 | d.resetCurrentToken() 56 | } 57 | 58 | func (d *decoder) resetCurrentToken() { 59 | d.currentToken = "" 60 | d.currentTokenHardened = false 61 | } 62 | 63 | func (d *decoder) parse() (StartingPoint, []uint32, error) { 64 | for { 65 | err := d.f() 66 | if err != nil { 67 | if err == io.EOF { 68 | err = nil 69 | } else { 70 | err = fmt.Errorf("at position %d, %s", d.pos, err.Error()) 71 | } 72 | 73 | return d.start, d.path, err 74 | } 75 | } 76 | } 77 | 78 | func (d *decoder) readByte() (byte, error) { 79 | b, err := d.r.ReadByte() 80 | if err != nil { 81 | return b, err 82 | } 83 | 84 | d.pos++ 85 | 86 | return b, nil 87 | } 88 | 89 | func (d *decoder) unreadByte() error { 90 | err := d.r.UnreadByte() 91 | if err != nil { 92 | return err 93 | } 94 | 95 | d.pos-- 96 | 97 | return nil 98 | } 99 | 100 | func (d *decoder) parseStart() error { 101 | b, err := d.readByte() 102 | if err != nil { 103 | return err 104 | } 105 | 106 | if b == tokenMaster { 107 | d.start = StartingPointMaster 108 | d.f = d.parseSeparator 109 | return nil 110 | } 111 | 112 | if b == tokenDot { 113 | b2, err := d.readByte() 114 | if err != nil { 115 | return err 116 | } 117 | 118 | if b2 == tokenDot { 119 | d.f = d.parseSeparator 120 | d.start = StartingPointParent 121 | return nil 122 | } 123 | 124 | d.f = d.parseSeparator 125 | d.start = StartingPointCurrent 126 | return d.unreadByte() 127 | } 128 | 129 | d.f = d.parseSegment 130 | 131 | return d.unreadByte() 132 | } 133 | 134 | func (d *decoder) saveSegment() error { 135 | if len(d.currentToken) > 0 { 136 | i, err := strconv.ParseUint(d.currentToken, 10, 32) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | if i >= hardenedStart { 142 | d.pos -= len(d.currentToken) - 1 143 | return fmt.Errorf("index must be lower than 2^31, got %d", i) 144 | } 145 | 146 | if d.currentTokenHardened { 147 | i += hardenedStart 148 | } 149 | 150 | d.path = append(d.path, uint32(i)) 151 | } 152 | 153 | d.f = d.parseSegment 154 | d.resetCurrentToken() 155 | 156 | return nil 157 | } 158 | 159 | func (d *decoder) parseSeparator() error { 160 | b, err := d.readByte() 161 | if err != nil { 162 | return err 163 | } 164 | 165 | if b == tokenSeparator { 166 | return d.saveSegment() 167 | } 168 | 169 | return fmt.Errorf("expected %c, got %c", tokenSeparator, b) 170 | } 171 | 172 | func (d *decoder) parseSegment() error { 173 | b, err := d.readByte() 174 | if err == io.EOF { 175 | if len(d.currentToken) == 0 { 176 | return fmt.Errorf("expected number, got EOF") 177 | } 178 | 179 | if newErr := d.saveSegment(); newErr != nil { 180 | return newErr 181 | } 182 | 183 | return err 184 | } 185 | 186 | if err != nil { 187 | return err 188 | } 189 | 190 | if len(d.currentToken) > 0 && b == tokenSeparator { 191 | return d.saveSegment() 192 | } 193 | 194 | if len(d.currentToken) > 0 && b == tokenHardened { 195 | d.currentTokenHardened = true 196 | d.f = d.parseSeparator 197 | return nil 198 | } 199 | 200 | if b < 0x30 || b > 0x39 { 201 | return fmt.Errorf("expected number, got %s", string(b)) 202 | } 203 | 204 | d.currentToken = fmt.Sprintf("%s%s", d.currentToken, string(b)) 205 | 206 | return nil 207 | } 208 | 209 | func Decode(str string) (StartingPoint, []uint32, error) { 210 | d := newDecoder(str) 211 | return d.parse() 212 | } 213 | -------------------------------------------------------------------------------- /derivationpath/decoder_test.go: -------------------------------------------------------------------------------- 1 | package derivationpath 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDecode(t *testing.T) { 11 | scenarios := []struct { 12 | path string 13 | expectedPath []uint32 14 | expectedStartingPoint StartingPoint 15 | err error 16 | }{ 17 | { 18 | path: "", 19 | expectedPath: []uint32{}, 20 | expectedStartingPoint: StartingPointCurrent, 21 | }, 22 | { 23 | path: "1", 24 | expectedPath: []uint32{1}, 25 | expectedStartingPoint: StartingPointCurrent, 26 | }, 27 | { 28 | path: "..", 29 | expectedPath: []uint32{}, 30 | expectedStartingPoint: StartingPointParent, 31 | }, 32 | { 33 | path: "m", 34 | expectedPath: []uint32{}, 35 | expectedStartingPoint: StartingPointMaster, 36 | }, 37 | { 38 | path: "m/1", 39 | expectedPath: []uint32{1}, 40 | expectedStartingPoint: StartingPointMaster, 41 | }, 42 | { 43 | path: "m/1/2", 44 | expectedPath: []uint32{1, 2}, 45 | expectedStartingPoint: StartingPointMaster, 46 | }, 47 | { 48 | path: "m/1/2'/3", 49 | expectedPath: []uint32{1, 2147483650, 3}, 50 | expectedStartingPoint: StartingPointMaster, 51 | }, 52 | { 53 | path: "m/", 54 | err: fmt.Errorf("at position 2, expected number, got EOF"), 55 | }, 56 | { 57 | path: "m/1//2", 58 | err: fmt.Errorf("at position 5, expected number, got /"), 59 | }, 60 | { 61 | path: "m/1'2", 62 | err: fmt.Errorf("at position 5, expected /, got 2"), 63 | }, 64 | { 65 | path: "m/'/2", 66 | err: fmt.Errorf("at position 3, expected number, got '"), 67 | }, 68 | { 69 | path: "m/2147483648", 70 | err: fmt.Errorf("at position 3, index must be lower than 2^31, got 2147483648"), 71 | }, 72 | } 73 | 74 | for i, s := range scenarios { 75 | t.Run(fmt.Sprintf("scenario %d", i), func(t *testing.T) { 76 | startingPoint, path, err := Decode(s.path) 77 | if s.err == nil { 78 | assert.NoError(t, err) 79 | assert.Equal(t, s.expectedStartingPoint, startingPoint) 80 | assert.Equal(t, s.expectedPath, path) 81 | } else { 82 | assert.Equal(t, s.err, err) 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /derivationpath/encoder.go: -------------------------------------------------------------------------------- 1 | package derivationpath 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "strings" 8 | ) 9 | 10 | func Encode(rawPath []uint32) string { 11 | segments := []string{string(rune(tokenMaster))} 12 | 13 | for _, i := range rawPath { 14 | suffix := "" 15 | 16 | if i >= hardenedStart { 17 | i = i - hardenedStart 18 | suffix = string(rune(tokenHardened)) 19 | } 20 | 21 | segments = append(segments, fmt.Sprintf("%d%s", i, suffix)) 22 | } 23 | 24 | return strings.Join(segments, string(rune(tokenSeparator))) 25 | } 26 | 27 | func EncodeFromBytes(data []byte) (string, error) { 28 | buf := bytes.NewBuffer(data) 29 | rawPath := make([]uint32, buf.Len()/4) 30 | err := binary.Read(buf, binary.BigEndian, &rawPath) 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | return Encode(rawPath), nil 36 | } 37 | -------------------------------------------------------------------------------- /derivationpath/encoder_test.go: -------------------------------------------------------------------------------- 1 | package derivationpath 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestEncode(t *testing.T) { 11 | scenarios := []struct { 12 | path []uint32 13 | expectedPath string 14 | }{ 15 | { 16 | path: []uint32{}, 17 | expectedPath: "m", 18 | }, 19 | { 20 | path: []uint32{0, 1, 2}, 21 | expectedPath: "m/0/1/2", 22 | }, 23 | { 24 | path: []uint32{hardenedStart + 10, 1, 2}, 25 | expectedPath: "m/10'/1/2", 26 | }, 27 | } 28 | 29 | for i, s := range scenarios { 30 | t.Run(fmt.Sprintf("scenario %d", i), func(t *testing.T) { 31 | path := Encode(s.path) 32 | assert.Equal(t, s.expectedPath, path) 33 | }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /globalplatform/command_set.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import ( 4 | "crypto/rand" 5 | "errors" 6 | "os" 7 | 8 | "github.com/status-im/keycard-go/apdu" 9 | "github.com/status-im/keycard-go/identifiers" 10 | "github.com/status-im/keycard-go/types" 11 | ) 12 | 13 | var ErrSecureChannelNotOpen = errors.New("secure channel not open") 14 | 15 | type LoadingCallback = func(loadingBlock, totalBlocks int) 16 | 17 | type CommandSet struct { 18 | c types.Channel 19 | sc *SecureChannel 20 | session *Session 21 | } 22 | 23 | func NewCommandSet(c types.Channel) *CommandSet { 24 | return &CommandSet{ 25 | c: c, 26 | } 27 | } 28 | 29 | func (cs *CommandSet) Select() error { 30 | return cs.SelectAID(nil) 31 | } 32 | 33 | func (cs *CommandSet) SelectAID(aid []byte) error { 34 | cmd := NewCommandSelect(aid) 35 | cmd.SetLe(0) 36 | resp, err := cs.c.Send(cmd) 37 | 38 | return cs.checkOK(resp, err) 39 | } 40 | 41 | func (cs *CommandSet) OpenSecureChannel() error { 42 | hostChallenge, err := generateHostChallenge() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | err = cs.initializeUpdate(hostChallenge) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return cs.externalAuthenticate() 53 | } 54 | 55 | func (cs *CommandSet) DeleteKeycardInstancesAndPackage() error { 56 | if cs.sc == nil { 57 | return ErrSecureChannelNotOpen 58 | } 59 | 60 | return cs.DeleteObjectAndRelatedObject(identifiers.PackageAID) 61 | } 62 | 63 | func (cs *CommandSet) DeleteObject(aid []byte) error { 64 | return cs.Delete(aid, P2DeleteObject) 65 | } 66 | 67 | func (cs *CommandSet) DeleteObjectAndRelatedObject(aid []byte) error { 68 | return cs.Delete(aid, P2DeleteObjectAndRelatedObject) 69 | } 70 | 71 | func (cs *CommandSet) Delete(aid []byte, p2 uint8) error { 72 | cmd := NewCommandDelete(aid, p2) 73 | resp, err := cs.sc.Send(cmd) 74 | return cs.checkOK(resp, err, SwOK, SwReferencedDataNotFound) 75 | } 76 | 77 | func (cs *CommandSet) LoadKeycardPackage(capFile *os.File, callback LoadingCallback) error { 78 | return cs.LoadPackage(capFile, identifiers.PackageAID, callback) 79 | } 80 | 81 | func (cs *CommandSet) LoadPackage(capFile *os.File, pkgAID []byte, callback LoadingCallback) error { 82 | if cs.sc == nil { 83 | return ErrSecureChannelNotOpen 84 | } 85 | 86 | preLoad := NewCommandInstallForLoad(pkgAID, []byte{}) 87 | resp, err := cs.sc.Send(preLoad) 88 | if err = cs.checkOK(resp, err); err != nil { 89 | return err 90 | } 91 | 92 | load, err := NewLoadCommandStream(capFile) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | for load.Next() { 98 | cmd := load.GetCommand() 99 | callback(int(load.Index()), load.BlocksCount()) 100 | resp, err = cs.sc.Send(cmd) 101 | if err = cs.checkOK(resp, err); err != nil { 102 | return err 103 | } 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func (cs *CommandSet) InstallNDEFApplet(ndefRecord []byte) error { 110 | return cs.InstallForInstall( 111 | identifiers.PackageAID, 112 | identifiers.NdefAID, 113 | identifiers.NdefInstanceAID, 114 | ndefRecord) 115 | } 116 | 117 | func (cs *CommandSet) InstallKeycardApplet() error { 118 | instanceAID, err := identifiers.KeycardInstanceAID(identifiers.KeycardDefaultInstanceIndex) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | return cs.InstallForInstall( 124 | identifiers.PackageAID, 125 | identifiers.KeycardAID, 126 | instanceAID, 127 | []byte{}) 128 | } 129 | 130 | func (cs *CommandSet) InstallCashApplet() error { 131 | return cs.InstallForInstall( 132 | identifiers.PackageAID, 133 | identifiers.CashAID, 134 | identifiers.CashInstanceAID, 135 | []byte{}) 136 | } 137 | 138 | func (cs *CommandSet) InstallForInstall(packageAID, appletAID, instanceAID, params []byte) error { 139 | cmd := NewCommandInstallForInstall(packageAID, appletAID, instanceAID, params) 140 | resp, err := cs.sc.Send(cmd) 141 | return cs.checkOK(resp, err) 142 | } 143 | 144 | func (cs *CommandSet) GetStatus() (*types.CardStatus, error) { 145 | cmd := NewCommandGetStatus([]byte{}, P1GetStatusIssuerSecurityDomain) 146 | resp, err := cs.sc.Send(cmd) 147 | if err = cs.checkOK(resp, err); err != nil { 148 | return nil, err 149 | } 150 | 151 | return types.ParseCardStatus(resp.Data) 152 | } 153 | 154 | func (cs *CommandSet) Channel() types.Channel { 155 | return cs.c 156 | } 157 | 158 | func (cs *CommandSet) SecureChannel() *SecureChannel { 159 | return cs.sc 160 | } 161 | 162 | func (cs *CommandSet) initializeUpdate(hostChallenge []byte) error { 163 | cmd := NewCommandInitializeUpdate(hostChallenge) 164 | resp, err := cs.c.Send(cmd) 165 | if err = cs.checkOK(resp, err); err != nil { 166 | return err 167 | } 168 | 169 | // verify cryptogram and initialize session keys 170 | session, err := cs.initializeSession(resp, hostChallenge) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | cs.sc = NewSecureChannel(session, cs.c) 176 | cs.session = session 177 | 178 | return nil 179 | } 180 | 181 | func (cs *CommandSet) initializeSession(resp *apdu.Response, hostChallenge []byte) (session *Session, err error) { 182 | keySets := []struct { 183 | name string 184 | key []byte 185 | }{ 186 | {"keycard", identifiers.KeycardDevelopmentKey}, 187 | {"globalplatform", identifiers.GlobalPlatformDefaultKey}, 188 | } 189 | 190 | for _, set := range keySets { 191 | logger.Debug("initialize session", "keys", set.name) 192 | keys := NewSCP02Keys(set.key, set.key) 193 | session, err = NewSession(keys, resp, hostChallenge) 194 | 195 | // good keys 196 | if err == nil { 197 | break 198 | } 199 | 200 | // try the next keys 201 | if err == errBadCryptogram { 202 | continue 203 | } 204 | 205 | // unexpected error 206 | return nil, err 207 | } 208 | 209 | return session, err 210 | } 211 | 212 | func (cs *CommandSet) externalAuthenticate() error { 213 | if cs.session == nil { 214 | return errors.New("session must be initialized using initializeUpdate") 215 | } 216 | 217 | encKey := cs.session.Keys().Enc() 218 | cmd, err := NewCommandExternalAuthenticate(encKey, cs.session.CardChallenge(), cs.session.HostChallenge()) 219 | if err != nil { 220 | return err 221 | } 222 | 223 | resp, err := cs.sc.Send(cmd) 224 | return cs.checkOK(resp, err) 225 | } 226 | 227 | func (cs *CommandSet) checkOK(resp *apdu.Response, err error, allowedResponses ...uint16) error { 228 | if err != nil { 229 | return err 230 | } 231 | 232 | if len(allowedResponses) == 0 { 233 | allowedResponses = []uint16{apdu.SwOK} 234 | } 235 | 236 | for _, code := range allowedResponses { 237 | if code == resp.Sw { 238 | return nil 239 | } 240 | } 241 | 242 | return apdu.NewErrBadResponse(resp.Sw, "unexpected response") 243 | } 244 | 245 | func generateHostChallenge() ([]byte, error) { 246 | c := make([]byte, 8) 247 | _, err := rand.Read(c) 248 | return c, err 249 | } 250 | -------------------------------------------------------------------------------- /globalplatform/commands.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import ( 4 | "github.com/status-im/keycard-go/apdu" 5 | "github.com/status-im/keycard-go/globalplatform/crypto" 6 | ) 7 | 8 | // Constants used in apdu commands and responses as defined by iso7816 and globalplatform. 9 | const ( 10 | ClaISO7816 = 0x00 11 | ClaGp = 0x80 12 | ClaMac = 0x84 13 | 14 | InsSelect = 0xA4 15 | InsInitializeUpdate = 0x50 16 | InsExternalAuthenticate = 0x82 17 | InsGetResponse = 0xC0 18 | InsDelete = 0xE4 19 | InsLoad = 0xE8 20 | InsInstall = 0xE6 21 | InsGetStatus = 0xF2 22 | 23 | P1ExternalAuthenticateCMAC = 0x01 24 | P1InstallForLoad = 0x02 25 | P1InstallForInstall = 0x04 26 | P1InstallForMakeSelectable = 0x08 27 | P1LoadMoreBlocks = 0x00 28 | P1LoadLastBlock = 0x80 29 | P1GetStatusIssuerSecurityDomain = 0x80 30 | P1GetStatusApplications = 0x40 31 | P1GetStatusExecLoadFiles = 0x20 32 | P1GetStatusExecLoadFilesAndModules = 0x10 33 | 34 | P2GetStatusTLVData = 0x02 35 | P2DeleteObject = 0x00 36 | P2DeleteObjectAndRelatedObject = 0x80 37 | 38 | Sw1ResponseDataIncomplete = 0x61 39 | 40 | SwOK = 0x9000 41 | SwFileNotFound = 0x6A82 42 | SwReferencedDataNotFound = 0x6A88 43 | SwSecurityConditionNotSatisfied = 0x6982 44 | SwAuthenticationMethodBlocked = 0x6983 45 | 46 | tagDeleteAID = 0x4F 47 | tagLoadFileDataBlock = 0xC4 48 | tagGetStatusAID = 0x4F 49 | ) 50 | 51 | // NewCommandSelect returns a Select command as defined in the globalplatform specifications. 52 | func NewCommandSelect(aid []byte) *apdu.Command { 53 | c := apdu.NewCommand( 54 | ClaISO7816, 55 | InsSelect, 56 | 0x04, 57 | 0, 58 | aid, 59 | ) 60 | 61 | return c 62 | } 63 | 64 | // NewCommandInitializeUpdate returns an Initialize Update command as defined in the globalplatform specifications. 65 | func NewCommandInitializeUpdate(challenge []byte) *apdu.Command { 66 | c := apdu.NewCommand( 67 | ClaGp, 68 | InsInitializeUpdate, 69 | 0, 70 | 0, 71 | challenge, 72 | ) 73 | 74 | // with T=0 we can both set or not the Le value 75 | // with T=1 it works only if Le is set 76 | c.SetLe(0x00) 77 | 78 | return c 79 | } 80 | 81 | // NewCommandExternalAuthenticate returns an External Authenticate command as defined in the globalplatform specifications. 82 | func NewCommandExternalAuthenticate(encKey, cardChallenge, hostChallenge []byte) (*apdu.Command, error) { 83 | hostCryptogram, err := calculateHostCryptogram(encKey, cardChallenge, hostChallenge) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return apdu.NewCommand( 89 | ClaMac, 90 | InsExternalAuthenticate, 91 | P1ExternalAuthenticateCMAC, 92 | 0, 93 | hostCryptogram, 94 | ), nil 95 | } 96 | 97 | // NewCommandGetResponse returns a Get Response command as defined in the globalplatform specifications. 98 | func NewCommandGetResponse(length uint8) *apdu.Command { 99 | c := apdu.NewCommand( 100 | ClaISO7816, 101 | InsGetResponse, 102 | 0, 103 | 0, 104 | nil, 105 | ) 106 | 107 | c.SetLe(length) 108 | 109 | return c 110 | } 111 | 112 | // NewCommandDelete returns a Delete command as defined in the globalplatform specifications. 113 | func NewCommandDelete(aid []byte, p2 uint8) *apdu.Command { 114 | data := []byte{tagDeleteAID, byte(len(aid))} 115 | data = append(data, aid...) 116 | 117 | return apdu.NewCommand( 118 | ClaGp, 119 | InsDelete, 120 | 0, 121 | p2, 122 | data, 123 | ) 124 | } 125 | 126 | // NewCommandInstallForLoad returns an Install command with the install-for-load parameter as defined in the globalplatform specifications. 127 | func NewCommandInstallForLoad(aid, sdaid []byte) *apdu.Command { 128 | data := []byte{byte(len(aid))} 129 | data = append(data, aid...) 130 | data = append(data, byte(len(sdaid))) 131 | data = append(data, sdaid...) 132 | // empty hash length and hash 133 | data = append(data, []byte{0x00, 0x00, 0x00}...) 134 | 135 | return apdu.NewCommand( 136 | ClaGp, 137 | InsInstall, 138 | P1InstallForLoad, 139 | 0, 140 | data, 141 | ) 142 | } 143 | 144 | // NewCommandInstallForInstall returns an Install command with the install-for-instalp parameter as defined in the globalplatform specifications. 145 | func NewCommandInstallForInstall(pkgAID, appletAID, instanceAID, params []byte) *apdu.Command { 146 | data := []byte{byte(len(pkgAID))} 147 | data = append(data, pkgAID...) 148 | data = append(data, byte(len(appletAID))) 149 | data = append(data, appletAID...) 150 | data = append(data, byte(len(instanceAID))) 151 | data = append(data, instanceAID...) 152 | 153 | // privileges 154 | priv := []byte{0x00} 155 | data = append(data, byte(len(priv))) 156 | data = append(data, priv...) 157 | 158 | // params 159 | fullParams := []byte{byte(0xC9), byte(len(params))} 160 | fullParams = append(fullParams, params...) 161 | 162 | data = append(data, byte(len(fullParams))) 163 | data = append(data, fullParams...) 164 | 165 | // empty perform token 166 | data = append(data, byte(0x00)) 167 | 168 | return apdu.NewCommand( 169 | ClaGp, 170 | InsInstall, 171 | P1InstallForInstall|P1InstallForMakeSelectable, 172 | 0, 173 | data, 174 | ) 175 | } 176 | 177 | // NewCommandGetStatus returns a Get Status command as defined in the globalplatform specifications. 178 | func NewCommandGetStatus(aid []byte, p1 uint8) *apdu.Command { 179 | data := []byte{tagGetStatusAID} 180 | data = append(data, byte(len(aid))) 181 | data = append(data, aid...) 182 | 183 | return apdu.NewCommand( 184 | ClaGp, 185 | InsGetStatus, 186 | p1, 187 | P2GetStatusTLVData, 188 | data, 189 | ) 190 | } 191 | 192 | func calculateHostCryptogram(encKey, cardChallenge, hostChallenge []byte) ([]byte, error) { 193 | var data []byte 194 | data = append(data, cardChallenge...) 195 | data = append(data, hostChallenge...) 196 | data = crypto.AppendDESPadding(data) 197 | 198 | return crypto.Mac3DES(encKey, data, crypto.NullBytes8) 199 | } 200 | -------------------------------------------------------------------------------- /globalplatform/commands_test.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/status-im/keycard-go/hexutils" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNewCommandSelect(t *testing.T) { 11 | aid := []byte{} 12 | cmd := NewCommandSelect(aid) 13 | 14 | assert.Equal(t, uint8(0x00), cmd.Cla) 15 | assert.Equal(t, uint8(0xA4), cmd.Ins) 16 | assert.Equal(t, uint8(0x04), cmd.P1) 17 | assert.Equal(t, uint8(0x00), cmd.P2) 18 | } 19 | 20 | func TestNewCommandInitializeUpdate(t *testing.T) { 21 | challenge := hexutils.HexToBytes("010203") 22 | cmd := NewCommandInitializeUpdate(challenge) 23 | 24 | assert.Equal(t, uint8(0x80), cmd.Cla) 25 | assert.Equal(t, uint8(0x50), cmd.Ins) 26 | assert.Equal(t, uint8(0x00), cmd.P1) 27 | assert.Equal(t, uint8(0x00), cmd.P2) 28 | assert.Equal(t, challenge, cmd.Data) 29 | } 30 | 31 | func TestCalculateHostCryptogram(t *testing.T) { 32 | encKey := hexutils.HexToBytes("0EF72A1065236DD6CAC718D5E3F379A4") 33 | cardChallenge := hexutils.HexToBytes("0076a6c0d55e9535") 34 | hostChallenge := hexutils.HexToBytes("266195e638da1b95") 35 | 36 | result, err := calculateHostCryptogram(encKey, cardChallenge, hostChallenge) 37 | assert.NoError(t, err) 38 | 39 | expected := "45A5F48DAE68203C" 40 | assert.Equal(t, expected, hexutils.BytesToHex(result)) 41 | } 42 | 43 | func TestNewCommandExternalAuthenticate(t *testing.T) { 44 | encKey := hexutils.HexToBytes("8D289AFE0AB9C45B1C76DEEA182966F4") 45 | cardChallenge := hexutils.HexToBytes("000f3fd65d4d6e45") 46 | hostChallenge := hexutils.HexToBytes("cf307b6719bf224d") 47 | 48 | cmd, err := NewCommandExternalAuthenticate(encKey, cardChallenge, hostChallenge) 49 | assert.NoError(t, err) 50 | 51 | expected := "84 82 01 00 08 77 02 AC 6C E4 6A 47 F0" 52 | raw, err := cmd.Serialize() 53 | assert.NoError(t, err) 54 | assert.Equal(t, expected, hexutils.BytesToHexWithSpaces(raw)) 55 | } 56 | 57 | func TestNewCommandDelete(t *testing.T) { 58 | aid := hexutils.HexToBytes("0102030405") 59 | cmd := NewCommandDelete(aid, P2DeleteObject) 60 | assert.Equal(t, uint8(0x80), cmd.Cla) 61 | assert.Equal(t, uint8(0xE4), cmd.Ins) 62 | assert.Equal(t, uint8(0x00), cmd.P1) 63 | assert.Equal(t, uint8(0x00), cmd.P2) 64 | 65 | expected := "4F050102030405" 66 | assert.Equal(t, expected, hexutils.BytesToHex(cmd.Data)) 67 | } 68 | 69 | func TestNewCommandInstallForLoad(t *testing.T) { 70 | aid := hexutils.HexToBytes("53746174757357616C6C6574") 71 | sdaid := hexutils.HexToBytes("A000000151000000") 72 | cmd := NewCommandInstallForLoad(aid, sdaid) 73 | assert.Equal(t, uint8(0x80), cmd.Cla) 74 | assert.Equal(t, uint8(0xE6), cmd.Ins) 75 | assert.Equal(t, uint8(0x02), cmd.P1) 76 | assert.Equal(t, uint8(0x00), cmd.P2) 77 | 78 | expected := "0C53746174757357616C6C657408A000000151000000000000" 79 | assert.Equal(t, expected, hexutils.BytesToHex(cmd.Data)) 80 | } 81 | 82 | func TestNewCommandInstallForInstall(t *testing.T) { 83 | pkgAID := hexutils.HexToBytes("53746174757357616C6C6574") 84 | appletAID := hexutils.HexToBytes("53746174757357616C6C6574417070") 85 | instanceAID := hexutils.HexToBytes("53746174757357616C6C6574417070") 86 | params := hexutils.HexToBytes("AABBCC") 87 | 88 | cmd := NewCommandInstallForInstall(pkgAID, appletAID, instanceAID, params) 89 | assert.Equal(t, uint8(0x80), cmd.Cla) 90 | assert.Equal(t, uint8(0xE6), cmd.Ins) 91 | assert.Equal(t, uint8(0x0C), cmd.P1) 92 | assert.Equal(t, uint8(0x00), cmd.P2) 93 | 94 | expected := "0C53746174757357616C6C65740F53746174757357616C6C65744170700F53746174757357616C6C6574417070010005C903AABBCC00" 95 | assert.Equal(t, expected, hexutils.BytesToHex(cmd.Data)) 96 | } 97 | 98 | func TestNewCommandStatus(t *testing.T) { 99 | aid := hexutils.HexToBytes("AABBCC") 100 | cmd := NewCommandGetStatus(aid, P1GetStatusApplications) 101 | assert.Equal(t, uint8(0x80), cmd.Cla) 102 | assert.Equal(t, uint8(0xF2), cmd.Ins) 103 | assert.Equal(t, uint8(0x40), cmd.P1) 104 | assert.Equal(t, uint8(0x02), cmd.P2) 105 | 106 | expected := "4F03AABBCC" 107 | assert.Equal(t, expected, hexutils.BytesToHex(cmd.Data)) 108 | } 109 | -------------------------------------------------------------------------------- /globalplatform/crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | "crypto/cipher" 6 | "crypto/des" 7 | ) 8 | 9 | var ( 10 | // DerivationPurposeEnc defines 2 bytes used when deriving a encoding key. 11 | DerivationPurposeEnc = []byte{0x01, 0x82} 12 | // DerivationPurposeMac defines 2 bytes used when deriving a mac key. 13 | DerivationPurposeMac = []byte{0x01, 0x01} 14 | // NullBytes8 defined a slice of 8 zero bytes mostrly used as IV in cryptographic functions. 15 | NullBytes8 = []byte{0, 0, 0, 0, 0, 0, 0, 0} 16 | ) 17 | 18 | // DeriveKey derives a key from the current cardKey using the sequence number receive from the card and the purpose (ENC/MAC). 19 | func DeriveKey(cardKey []byte, seq []byte, purpose []byte) ([]byte, error) { 20 | key24 := resizeKey24(cardKey) 21 | 22 | derivation := make([]byte, 16) 23 | copy(derivation, purpose[:2]) 24 | copy(derivation[2:], seq[:2]) 25 | 26 | block, err := des.NewTripleDESCipher(key24) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | ciphertext := make([]byte, 16) 32 | 33 | mode := cipher.NewCBCEncrypter(block, NullBytes8) 34 | mode.CryptBlocks(ciphertext, derivation) 35 | 36 | return ciphertext, nil 37 | } 38 | 39 | // VerifyCryptogram verifies the cryptogram sends from the card to ensure that card and client are using the same keys to communicate. 40 | func VerifyCryptogram(encKey, hostChallenge, cardChallenge, cardCryptogram []byte) (bool, error) { 41 | data := make([]byte, 0) 42 | data = append(data, hostChallenge...) 43 | data = append(data, cardChallenge...) 44 | paddedData := AppendDESPadding(data) 45 | calculated, err := Mac3DES(encKey, paddedData, NullBytes8) 46 | if err != nil { 47 | return false, err 48 | } 49 | 50 | return bytes.Equal(calculated, cardCryptogram), nil 51 | } 52 | 53 | // MacFull3DES generates a full triple DES mac. 54 | func MacFull3DES(key, data, iv []byte) ([]byte, error) { 55 | data = AppendDESPadding(data) 56 | 57 | desBlock, err := des.NewCipher(resizeKey8(key)) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | des3Block, err := des.NewTripleDESCipher(resizeKey24(key)) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | des3IV := iv 68 | 69 | if len(data) > 8 { 70 | length := len(data) - 8 71 | tmp := make([]byte, length) 72 | mode := cipher.NewCBCEncrypter(desBlock, iv) 73 | mode.CryptBlocks(tmp, data[:length]) 74 | des3IV = tmp[length-8:] 75 | } 76 | 77 | ciphertext := make([]byte, 8) 78 | 79 | mode := cipher.NewCBCEncrypter(des3Block, des3IV) 80 | mode.CryptBlocks(ciphertext, data[len(data)-8:]) 81 | 82 | return ciphertext, nil 83 | } 84 | 85 | // EncryptICV encrypts an ICV with the specified macKey. 86 | // The ICV is usually the mac of the previous command sent in the current session. 87 | func EncryptICV(macKey, icv []byte) ([]byte, error) { 88 | block, err := des.NewCipher(resizeKey8(macKey)) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | ciphertext := make([]byte, 8) 94 | mode := cipher.NewCBCEncrypter(block, NullBytes8) 95 | mode.CryptBlocks(ciphertext, icv) 96 | 97 | return ciphertext, nil 98 | } 99 | 100 | // Mac3DES generates the triple DES mac of data using the specified key and icv. 101 | func Mac3DES(key, data, iv []byte) ([]byte, error) { 102 | key24 := resizeKey24(key) 103 | 104 | block, err := des.NewTripleDESCipher(key24) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | ciphertext := make([]byte, 24) 110 | 111 | mode := cipher.NewCBCEncrypter(block, iv) 112 | mode.CryptBlocks(ciphertext, data) 113 | 114 | return ciphertext[16:], nil 115 | } 116 | 117 | // AppendDESPadding appends an 0x80 bytes to data and other zero bytes to make the result length multiple of 8. 118 | func AppendDESPadding(data []byte) []byte { 119 | blockSize := 8 120 | paddingSize := blockSize - (len(data) % blockSize) 121 | newData := make([]byte, len(data)+paddingSize) 122 | copy(newData, data) 123 | newData[len(data)] = 0x80 124 | 125 | return newData 126 | } 127 | 128 | func resizeKey24(key []byte) []byte { 129 | data := make([]byte, 24) 130 | copy(data, key[0:16]) 131 | copy(data[16:], key[0:8]) 132 | 133 | return data 134 | } 135 | 136 | func resizeKey8(key []byte) []byte { 137 | return key[:8] 138 | } 139 | -------------------------------------------------------------------------------- /globalplatform/crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/status-im/keycard-go/hexutils" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDeriveKey(t *testing.T) { 11 | cardKey := hexutils.HexToBytes("404142434445464748494a4b4c4d4e4f") 12 | seq := hexutils.HexToBytes("0065") 13 | 14 | encKey, err := DeriveKey(cardKey, seq, DerivationPurposeEnc) 15 | assert.NoError(t, err) 16 | 17 | expectedEncKey := "85E72AAF47874218A202BF5EF891DD21" 18 | assert.Equal(t, expectedEncKey, hexutils.BytesToHex(encKey)) 19 | } 20 | 21 | func TestResizeKey24(t *testing.T) { 22 | key := hexutils.HexToBytes("404142434445464748494a4b4c4d4e4f") 23 | resized := resizeKey24(key) 24 | expected := "404142434445464748494A4B4C4D4E4F4041424344454647" 25 | assert.Equal(t, expected, hexutils.BytesToHex(resized)) 26 | } 27 | 28 | func TestAppendDESPadding(t *testing.T) { 29 | data := hexutils.HexToBytes("AABB") 30 | result := AppendDESPadding(data) 31 | expected := "AABB800000000000" 32 | assert.Equal(t, expected, hexutils.BytesToHex(result)) 33 | 34 | data = hexutils.HexToBytes("01020304050607") 35 | result = AppendDESPadding(data) 36 | expected = "0102030405060780" 37 | assert.Equal(t, expected, hexutils.BytesToHex(result)) 38 | 39 | data = hexutils.HexToBytes("0102030405060708") 40 | result = AppendDESPadding(data) 41 | expected = "01020304050607088000000000000000" 42 | assert.Equal(t, expected, hexutils.BytesToHex(result)) 43 | } 44 | 45 | func TestVerifyCryptogram(t *testing.T) { 46 | encKey := hexutils.HexToBytes("16B5867FF50BE7239C2BF1245B83A362") 47 | hostChallenge := hexutils.HexToBytes("32da078d7aac1cff") 48 | cardChallenge := hexutils.HexToBytes("007284f64a7d6465") 49 | cardCryptogram := hexutils.HexToBytes("05c4bb8a86014e22") 50 | 51 | result, err := VerifyCryptogram(encKey, hostChallenge, cardChallenge, cardCryptogram) 52 | assert.NoError(t, err) 53 | assert.True(t, result) 54 | } 55 | 56 | func TestMac3des(t *testing.T) { 57 | key := hexutils.HexToBytes("16B5867FF50BE7239C2BF1245B83A362") 58 | data := hexutils.HexToBytes("32DA078D7AAC1CFF007284F64A7D64658000000000000000") 59 | result, err := Mac3DES(key, data, NullBytes8) 60 | assert.NoError(t, err) 61 | 62 | expected := "05C4BB8A86014E22" 63 | assert.Equal(t, expected, hexutils.BytesToHex(result)) 64 | } 65 | 66 | func TestMacFull3DES(t *testing.T) { 67 | key := hexutils.HexToBytes("5b02e75ad63190aece0622936f11abab") 68 | data := hexutils.HexToBytes("8482010010810b098a8fbb88da") 69 | result, err := MacFull3DES(key, data, NullBytes8) 70 | assert.NoError(t, err) 71 | expected := "5271D7174A5A166A" 72 | assert.Equal(t, expected, hexutils.BytesToHex(result)) 73 | } 74 | -------------------------------------------------------------------------------- /globalplatform/globalplatform.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import "github.com/ethereum/go-ethereum/log" 4 | 5 | var logger = log.New("package", "keycard-go/globalplatform") 6 | -------------------------------------------------------------------------------- /globalplatform/load.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "io/ioutil" 7 | "math" 8 | "os" 9 | "strings" 10 | 11 | "github.com/status-im/keycard-go/apdu" 12 | ) 13 | 14 | var internalFiles = []string{ 15 | "Header", "Directory", "Import", "Applet", "Class", 16 | "Method", "StaticField", "Export", "ConstantPool", "RefLocation", 17 | } 18 | 19 | const blockSize = 247 // 255 - 8 bytes for MAC 20 | 21 | // LoadCommandStream implement a struct that generates multiple Load commands used to load files to smartcards. 22 | type LoadCommandStream struct { 23 | data *bytes.Reader 24 | currentIndex uint8 25 | currentData []byte 26 | p1 uint8 27 | blocksCount int 28 | } 29 | 30 | // NewLoadCommandStream returns a new LoadCommandStream to load the specified file. 31 | func NewLoadCommandStream(file *os.File) (*LoadCommandStream, error) { 32 | files, err := loadFiles(file) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | data, err := encodeFilesData(files) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return &LoadCommandStream{ 43 | data: bytes.NewReader(data), 44 | p1: P1LoadMoreBlocks, 45 | blocksCount: int(math.Ceil(float64(len(data)) / float64(blockSize))), 46 | }, nil 47 | } 48 | 49 | // BlocksCount returns the total number of blocks based on data length and blockSize 50 | func (lcs *LoadCommandStream) BlocksCount() int { 51 | return lcs.blocksCount 52 | } 53 | 54 | // Next returns initialize the data for the next Load command. 55 | // TODO:@gravityblast update blockSize when using encrypted data 56 | func (lcs *LoadCommandStream) Next() bool { 57 | if lcs.data.Len() == 0 { 58 | return false 59 | } 60 | 61 | buf := make([]byte, blockSize) 62 | n, err := lcs.data.Read(buf) 63 | if err != nil { 64 | return false 65 | } 66 | 67 | lcs.currentData = buf[:n] 68 | lcs.currentIndex++ 69 | 70 | if lcs.data.Len() == 0 { 71 | lcs.p1 = P1LoadLastBlock 72 | } 73 | 74 | return true 75 | } 76 | 77 | // Index returns the command index. 78 | func (lcs *LoadCommandStream) Index() uint8 { 79 | return lcs.currentIndex - 1 80 | } 81 | 82 | // GetCommand returns the current apdu command. 83 | func (lcs *LoadCommandStream) GetCommand() *apdu.Command { 84 | return apdu.NewCommand(ClaGp, InsLoad, lcs.p1, lcs.Index(), lcs.currentData) 85 | } 86 | 87 | func loadFiles(f *os.File) (map[string][]byte, error) { 88 | fi, err := f.Stat() 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | z, err := zip.NewReader(f, fi.Size()) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | files := make(map[string][]byte) 99 | 100 | for _, item := range z.File { 101 | name := strings.Split(item.FileInfo().Name(), ".")[0] 102 | f, err := item.Open() 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | data, err := ioutil.ReadAll(f) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | files[name] = data 113 | } 114 | 115 | return files, nil 116 | } 117 | 118 | func encodeFilesData(files map[string][]byte) ([]byte, error) { 119 | var buf bytes.Buffer 120 | 121 | for _, name := range internalFiles { 122 | if data, ok := files[name]; ok { 123 | buf.Write(data) 124 | } 125 | } 126 | 127 | filesData := buf.Bytes() 128 | length := encodeLength(len(filesData)) 129 | 130 | data := make([]byte, 0) 131 | data = append(data, tagLoadFileDataBlock) 132 | data = append(data, length...) 133 | data = append(data, filesData...) 134 | 135 | return data, nil 136 | } 137 | 138 | func encodeLength(length int) []byte { 139 | if length < 0x80 { 140 | return []byte{byte(length)} 141 | } 142 | 143 | if length < 0xFF { 144 | return []byte{ 145 | byte(0x81), 146 | byte(length), 147 | } 148 | } 149 | 150 | if length < 0xFFFF { 151 | return []byte{ 152 | byte(0x82), 153 | byte((length & 0xFF00) >> 8), 154 | byte(length & 0xFF), 155 | } 156 | } 157 | 158 | return []byte{ 159 | byte(0x83), 160 | byte((length & 0xFF0000) >> 16), 161 | byte((length & 0xFF00) >> 8), 162 | byte(length & 0xFF), 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /globalplatform/scp02_keys.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | // SCP02Keys is a struct that contains encoding and MAC keys used to communicate with smartcards. 4 | type SCP02Keys struct { 5 | enc []byte 6 | mac []byte 7 | } 8 | 9 | // Enc returns the enc key data. 10 | func (k *SCP02Keys) Enc() []byte { 11 | return k.enc 12 | } 13 | 14 | // Mac returns the MAC key data. 15 | func (k *SCP02Keys) Mac() []byte { 16 | return k.mac 17 | } 18 | 19 | // NewSCP02Keys returns a new SCP02Keys with the specified ENC and MAC keys. 20 | func NewSCP02Keys(enc, mac []byte) *SCP02Keys { 21 | return &SCP02Keys{ 22 | enc: enc, 23 | mac: mac, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /globalplatform/scp02_wrapper.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | 7 | "github.com/status-im/keycard-go/apdu" 8 | "github.com/status-im/keycard-go/globalplatform/crypto" 9 | ) 10 | 11 | // SCP02Wrapper is a wrapper for apdu commands inside a global platform secure channel. 12 | type SCP02Wrapper struct { 13 | macKey []byte 14 | icv []byte 15 | } 16 | 17 | // NewSCP02Wrapper returns a new SCP02Wrapper using the specified key for MAC generation. 18 | func NewSCP02Wrapper(macKey []byte) *SCP02Wrapper { 19 | return &SCP02Wrapper{ 20 | macKey: macKey, 21 | icv: crypto.NullBytes8, 22 | } 23 | } 24 | 25 | // Wrap wraps the apdu command adding the MAC to the end of the command. 26 | // Future implementations will encrypt the message when needed. 27 | func (w *SCP02Wrapper) Wrap(cmd *apdu.Command) (*apdu.Command, error) { 28 | macData := new(bytes.Buffer) 29 | 30 | cla := cmd.Cla | 0x04 31 | if err := binary.Write(macData, binary.BigEndian, cla); err != nil { 32 | return nil, err 33 | } 34 | 35 | if err := binary.Write(macData, binary.BigEndian, cmd.Ins); err != nil { 36 | return nil, err 37 | } 38 | 39 | if err := binary.Write(macData, binary.BigEndian, cmd.P1); err != nil { 40 | return nil, err 41 | } 42 | 43 | if err := binary.Write(macData, binary.BigEndian, cmd.P2); err != nil { 44 | return nil, err 45 | } 46 | 47 | if err := binary.Write(macData, binary.BigEndian, uint8(len(cmd.Data)+8)); err != nil { 48 | return nil, err 49 | } 50 | 51 | if err := binary.Write(macData, binary.BigEndian, cmd.Data); err != nil { 52 | return nil, err 53 | } 54 | 55 | var ( 56 | icv []byte 57 | err error 58 | ) 59 | 60 | if bytes.Equal(w.icv, crypto.NullBytes8) { 61 | icv = w.icv 62 | } else { 63 | icv, err = crypto.EncryptICV(w.macKey, w.icv) 64 | if err != nil { 65 | return nil, err 66 | } 67 | } 68 | 69 | mac, err := crypto.MacFull3DES(w.macKey, macData.Bytes(), icv) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | newData := make([]byte, 0) 75 | newData = append(newData, cmd.Data...) 76 | newData = append(newData, mac...) 77 | 78 | w.icv = mac 79 | 80 | newCmd := apdu.NewCommand(cla, cmd.Ins, cmd.P1, cmd.P2, newData) 81 | if ok, le := cmd.Le(); ok { 82 | newCmd.SetLe(le) 83 | } 84 | 85 | return newCmd, nil 86 | } 87 | -------------------------------------------------------------------------------- /globalplatform/scp02_wrapper_test.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/status-im/keycard-go/apdu" 7 | "github.com/status-im/keycard-go/globalplatform/crypto" 8 | "github.com/status-im/keycard-go/hexutils" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestSCP02Wrapper_Wrap(t *testing.T) { 13 | macKey := hexutils.HexToBytes("2983BA77D709C2DAA1E6000ABCCAC951") 14 | w := NewSCP02Wrapper(macKey) 15 | 16 | data := hexutils.HexToBytes("1d4de92eaf7a2c9f") 17 | cmd := apdu.NewCommand(uint8(0x80), uint8(0x82), uint8(0x01), uint8(0x00), data) 18 | 19 | // check initial icv 20 | assert.Equal(t, crypto.NullBytes8, w.icv) 21 | 22 | wrappedCmd, err := w.Wrap(cmd) 23 | assert.NoError(t, err) 24 | raw, err := wrappedCmd.Serialize() 25 | assert.NoError(t, err) 26 | 27 | expected := "84 82 01 00 10 1D 4D E9 2E AF 7A 2C 9F 8F 9B 0D F6 81 C1 D3 EC" 28 | assert.Equal(t, expected, hexutils.BytesToHexWithSpaces(raw)) 29 | 30 | // check icv generated from previous mac 31 | assert.Equal(t, "8F9B0DF681C1D3EC", hexutils.BytesToHex(w.icv)) 32 | 33 | data = hexutils.HexToBytes("4F00") 34 | cmd = apdu.NewCommand(uint8(0x80), uint8(0xF2), uint8(0x80), uint8(0x02), data) 35 | cmd.SetLe(0x00) 36 | wrappedCmd, err = w.Wrap(cmd) 37 | assert.NoError(t, err) 38 | raw, err = wrappedCmd.Serialize() 39 | assert.NoError(t, err) 40 | 41 | expected = "84 F2 80 02 0A 4F 00 30 F1 49 20 9E 17 B3 97 00" 42 | assert.Equal(t, expected, hexutils.BytesToHexWithSpaces(raw)) 43 | } 44 | -------------------------------------------------------------------------------- /globalplatform/secure_channel.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import ( 4 | "github.com/status-im/keycard-go/apdu" 5 | "github.com/status-im/keycard-go/hexutils" 6 | "github.com/status-im/keycard-go/types" 7 | ) 8 | 9 | // SecureChannel wraps another channel and sends wrapped commands using SCP02Wrapper. 10 | type SecureChannel struct { 11 | session *Session 12 | c types.Channel 13 | w *SCP02Wrapper 14 | } 15 | 16 | // NewSecureChannel returns a new SecureChannel based on a session and wrapping a Channel c. 17 | func NewSecureChannel(session *Session, c types.Channel) *SecureChannel { 18 | return &SecureChannel{ 19 | session: session, 20 | c: c, 21 | w: NewSCP02Wrapper(session.Keys().Mac()), 22 | } 23 | } 24 | 25 | // Send sends wrapped commands to the inner channel. 26 | func (c *SecureChannel) Send(cmd *apdu.Command) (*apdu.Response, error) { 27 | rawCmd, err := cmd.Serialize() 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | logger.Debug("wrapping apdu command", "hex", hexutils.BytesToHexWithSpaces(rawCmd)) 33 | wrappedCmd, err := c.w.Wrap(cmd) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return c.c.Send(wrappedCmd) 39 | } 40 | -------------------------------------------------------------------------------- /globalplatform/session.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/status-im/keycard-go/apdu" 8 | "github.com/status-im/keycard-go/globalplatform/crypto" 9 | ) 10 | 11 | const supportedSCPVersion = 2 12 | 13 | // Session is a struct containing the keys and challenges used in the current communication with a card. 14 | type Session struct { 15 | keys *SCP02Keys 16 | cardChallenge []byte 17 | hostChallenge []byte 18 | } 19 | 20 | var errBadCryptogram = errors.New("bad card cryptogram") 21 | 22 | // NewSession returns a new session after validating the cryptogram received from the card. 23 | func NewSession(cardKeys *SCP02Keys, resp *apdu.Response, hostChallenge []byte) (*Session, error) { 24 | if resp.Sw == SwSecurityConditionNotSatisfied { 25 | return nil, apdu.NewErrBadResponse(resp.Sw, "security condition not satisfied") 26 | } 27 | 28 | if resp.Sw == SwAuthenticationMethodBlocked { 29 | return nil, apdu.NewErrBadResponse(resp.Sw, "authentication method blocked") 30 | } 31 | 32 | if len(resp.Data) != 28 { 33 | return nil, apdu.NewErrBadResponse(resp.Sw, fmt.Sprintf("bad data length, expected 28, got %d", len(resp.Data))) 34 | } 35 | 36 | scpMajorVersion := resp.Data[11] 37 | if scpMajorVersion != supportedSCPVersion { 38 | return nil, fmt.Errorf("scp version %d not supported", scpMajorVersion) 39 | } 40 | 41 | cardChallenge := resp.Data[12:20] 42 | cardCryptogram := resp.Data[20:28] 43 | seq := resp.Data[12:14] 44 | 45 | sessionEncKey, err := crypto.DeriveKey(cardKeys.Enc(), seq, crypto.DerivationPurposeEnc) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | sessionMacKey, err := crypto.DeriveKey(cardKeys.Enc(), seq, crypto.DerivationPurposeMac) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | sessionKeys := NewSCP02Keys(sessionEncKey, sessionMacKey) 56 | verified, err := crypto.VerifyCryptogram(sessionKeys.Enc(), hostChallenge, cardChallenge, cardCryptogram) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | if !verified { 62 | return nil, errBadCryptogram 63 | } 64 | 65 | s := &Session{ 66 | keys: sessionKeys, 67 | cardChallenge: cardChallenge, 68 | hostChallenge: hostChallenge, 69 | } 70 | 71 | return s, nil 72 | } 73 | 74 | // Keys return the current SCP02Keys. 75 | func (s *Session) Keys() *SCP02Keys { 76 | return s.keys 77 | } 78 | 79 | // CardChallenge returns the current card challenge. 80 | func (s *Session) CardChallenge() []byte { 81 | return s.cardChallenge 82 | } 83 | 84 | // HostChallenge returns the current host challenge. 85 | func (s *Session) HostChallenge() []byte { 86 | return s.hostChallenge 87 | } 88 | -------------------------------------------------------------------------------- /globalplatform/session_test.go: -------------------------------------------------------------------------------- 1 | package globalplatform 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/status-im/keycard-go/apdu" 7 | "github.com/status-im/keycard-go/hexutils" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewSession(t *testing.T) { 12 | key := hexutils.HexToBytes("404142434445464748494a4b4c4d4e4f") 13 | keys := NewSCP02Keys(key, key) 14 | 15 | raw := hexutils.HexToBytes("000002650183039536622002000de9c62ba1c4c8e55fcb91b6654ce49000") 16 | resp, err := apdu.ParseResponse(raw) 17 | assert.NoError(t, err) 18 | 19 | hostChallenge := hexutils.HexToBytes("f0467f908e5ca23f") 20 | _, err = NewSession(keys, resp, hostChallenge) 21 | assert.NoError(t, err) 22 | } 23 | 24 | func TestNewSession_BadResponse(t *testing.T) { 25 | raw := hexutils.HexToBytes("01026982") 26 | resp, err := apdu.ParseResponse(raw) 27 | assert.NoError(t, err) 28 | _, err = NewSession(&SCP02Keys{}, resp, []byte{}) 29 | assert.Error(t, err) 30 | 31 | raw = hexutils.HexToBytes("01026983") 32 | resp, err = apdu.ParseResponse(raw) 33 | assert.NoError(t, err) 34 | _, err = NewSession(&SCP02Keys{}, resp, []byte{}) 35 | assert.Error(t, err) 36 | 37 | // bad data length 38 | raw = hexutils.HexToBytes("01029000") 39 | resp, err = apdu.ParseResponse(raw) 40 | assert.NoError(t, err) 41 | _, err = NewSession(&SCP02Keys{}, resp, []byte{}) 42 | assert.Error(t, err) 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/status-im/keycard-go 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/ethereum/go-ethereum v1.10.26 7 | github.com/stretchr/testify v1.7.2 8 | golang.org/x/crypto v0.1.0 9 | golang.org/x/text v0.4.0 10 | ) 11 | 12 | require ( 13 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect 16 | github.com/go-stack/stack v1.8.1 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | golang.org/x/sys v0.2.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= 2 | github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= 3 | github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= 8 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 9 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= 10 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= 11 | github.com/ethereum/go-ethereum v1.10.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s= 12 | github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= 13 | github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= 14 | github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 19 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 20 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= 21 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 22 | golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= 23 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= 25 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 29 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | -------------------------------------------------------------------------------- /hexutils/hexutils.go: -------------------------------------------------------------------------------- 1 | package hexutils 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "log" 7 | "regexp" 8 | ) 9 | 10 | // HexToBytes convert a hex string to a byte sequence. 11 | // The hex string can have spaces between bytes. 12 | func HexToBytes(s string) []byte { 13 | s = regexp.MustCompile(" ").ReplaceAllString(s, "") 14 | b := make([]byte, hex.DecodedLen(len(s))) 15 | _, err := hex.Decode(b, []byte(s)) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | return b[:] 21 | } 22 | 23 | // BytesToHexWithSpaces returns an hex string of b adding spaces between bytes. 24 | func BytesToHexWithSpaces(b []byte) string { 25 | return fmt.Sprintf("% X", b) 26 | } 27 | 28 | // BytesToHex returns an hex string of b. 29 | func BytesToHex(b []byte) string { 30 | return fmt.Sprintf("%X", b) 31 | } 32 | -------------------------------------------------------------------------------- /identifiers/identifiers.go: -------------------------------------------------------------------------------- 1 | package identifiers 2 | 3 | import "errors" 4 | 5 | var ( 6 | GlobalPlatformDefaultKey = []byte{0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f} 7 | KeycardDevelopmentKey = []byte{0xc2, 0x12, 0xe0, 0x73, 0xff, 0x8b, 0x4b, 0xbf, 0xaf, 0xf4, 0xde, 0x8a, 0xb6, 0x55, 0x22, 0x1f} 8 | 9 | PackageAID = []byte{0xA0, 0x00, 0x00, 0x08, 0x04, 0x00, 0x01} 10 | 11 | KeycardAID = []byte{0xA0, 0x00, 0x00, 0x08, 0x04, 0x00, 0x01, 0x01} 12 | 13 | NdefAID = []byte{0xA0, 0x00, 0x00, 0x08, 0x04, 0x00, 0x01, 0x02} 14 | NdefInstanceAID = []byte{0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01} 15 | 16 | CashAID = []byte{0xA0, 0x00, 0x00, 0x08, 0x04, 0x00, 0x01, 0x03} 17 | CashInstanceAID = []byte{0xA0, 0x00, 0x00, 0x08, 0x04, 0x00, 0x01, 0x03, 0x01} 18 | 19 | KeycardDefaultInstanceIndex = 1 20 | 21 | ErrInvalidInstanceIndex = errors.New("instance index must be between 1 and 255") 22 | ) 23 | 24 | func KeycardInstanceAID(index int) ([]byte, error) { 25 | if index < 0x01 || index > 0xFF { 26 | return nil, ErrInvalidInstanceIndex 27 | } 28 | 29 | return append(KeycardAID, byte(index)), nil 30 | } 31 | -------------------------------------------------------------------------------- /io/normal_channel.go: -------------------------------------------------------------------------------- 1 | package io 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/log" 5 | "github.com/status-im/keycard-go/apdu" 6 | "github.com/status-im/keycard-go/globalplatform" 7 | "github.com/status-im/keycard-go/hexutils" 8 | ) 9 | 10 | var logger = log.New("package", "keycard-go/io") 11 | 12 | // Transmitter defines an interface with one method to transmit raw commands and receive raw responses. 13 | type Transmitter interface { 14 | Transmit([]byte) ([]byte, error) 15 | } 16 | 17 | // NormalChannel implements a normal channel to send apdu commands and receive apdu responses. 18 | type NormalChannel struct { 19 | t Transmitter 20 | } 21 | 22 | // NewNormalChannel returns a new NormalChannel that sends commands to Transmitter t. 23 | func NewNormalChannel(t Transmitter) *NormalChannel { 24 | return &NormalChannel{t} 25 | } 26 | 27 | // Send sends apdu commands to the current Transmitter. 28 | // Based on the smartcard transport protocol (T=0, T=1), it checks responses and sends a Get Response 29 | // command in case of T=0. 30 | func (c *NormalChannel) Send(cmd *apdu.Command) (*apdu.Response, error) { 31 | rawCmd, err := cmd.Serialize() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | logger.Debug("apdu command", "hex", hexutils.BytesToHexWithSpaces(rawCmd)) 37 | rawResp, err := c.t.Transmit(rawCmd) 38 | if err != nil { 39 | return nil, err 40 | } 41 | logger.Debug("apdu response", "hex", hexutils.BytesToHexWithSpaces(rawResp)) 42 | 43 | resp, err := apdu.ParseResponse(rawResp) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | if resp.Sw1 == globalplatform.Sw1ResponseDataIncomplete && (cmd.Cla != globalplatform.ClaISO7816 || cmd.Ins != globalplatform.InsGetResponse) { 49 | getResponse := globalplatform.NewCommandGetResponse(resp.Sw2) 50 | return c.Send(getResponse) 51 | } 52 | 53 | return apdu.ParseResponse(rawResp) 54 | } 55 | -------------------------------------------------------------------------------- /keycard.go: -------------------------------------------------------------------------------- 1 | package keycard 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/log" 5 | ) 6 | 7 | var logger = log.New("package", "keycard-go") 8 | -------------------------------------------------------------------------------- /secrets.go: -------------------------------------------------------------------------------- 1 | package keycard 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/sha256" 6 | "encoding/base64" 7 | "fmt" 8 | "math/big" 9 | 10 | "github.com/status-im/keycard-go/crypto" 11 | "golang.org/x/crypto/pbkdf2" 12 | "golang.org/x/text/unicode/norm" 13 | ) 14 | 15 | const ( 16 | maxPukNumber = int64(999999999999) 17 | maxPinNumber = int64(999999) 18 | ) 19 | 20 | // Secrets contains the secret data needed to pair a client with a card. 21 | type Secrets struct { 22 | pin string 23 | puk string 24 | pairingPass string 25 | pairingToken []byte 26 | } 27 | 28 | func NewSecrets(pin, puk, pairingPass string) *Secrets { 29 | return &Secrets{ 30 | pin: pin, 31 | puk: puk, 32 | pairingPass: pairingPass, 33 | pairingToken: generatePairingToken(pairingPass), 34 | } 35 | } 36 | 37 | // GenerateSecrets generate a new Secrets with random puk and pairing password. 38 | func GenerateSecrets() (*Secrets, error) { 39 | pairingPass, err := generatePairingPass() 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | puk, err := rand.Int(rand.Reader, big.NewInt(maxPukNumber)) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | pin, err := rand.Int(rand.Reader, big.NewInt(maxPinNumber)) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return &Secrets{ 55 | pin: fmt.Sprintf("%06d", pin.Int64()), 56 | puk: fmt.Sprintf("%012d", puk.Int64()), 57 | pairingPass: pairingPass, 58 | pairingToken: generatePairingToken(pairingPass), 59 | }, nil 60 | } 61 | 62 | // Pin returns the pin string. 63 | func (s *Secrets) Pin() string { 64 | return s.pin 65 | } 66 | 67 | // Puk returns the puk string. 68 | func (s *Secrets) Puk() string { 69 | return s.puk 70 | } 71 | 72 | // PairingPass returns the pairing password string. 73 | func (s *Secrets) PairingPass() string { 74 | return s.pairingPass 75 | } 76 | 77 | // PairingToken returns the pairing token generated from the random pairing password. 78 | func (s *Secrets) PairingToken() []byte { 79 | return s.pairingToken 80 | } 81 | 82 | func generatePairingPass() (string, error) { 83 | r := make([]byte, 12) 84 | _, err := rand.Read(r) 85 | if err != nil { 86 | return "", err 87 | } 88 | 89 | return base64.URLEncoding.EncodeToString(r), nil 90 | } 91 | 92 | func generatePairingToken(pass string) []byte { 93 | return pbkdf2.Key(norm.NFKD.Bytes([]byte(pass)), norm.NFKD.Bytes([]byte(crypto.PairingTokenSalt)), 50000, 32, sha256.New) 94 | } 95 | -------------------------------------------------------------------------------- /secure_channel.go: -------------------------------------------------------------------------------- 1 | package keycard 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "errors" 7 | 8 | ethcrypto "github.com/ethereum/go-ethereum/crypto" 9 | "github.com/status-im/keycard-go/apdu" 10 | "github.com/status-im/keycard-go/crypto" 11 | "github.com/status-im/keycard-go/globalplatform" 12 | "github.com/status-im/keycard-go/hexutils" 13 | "github.com/status-im/keycard-go/types" 14 | ) 15 | 16 | var ErrInvalidResponseMAC = errors.New("invalid response MAC") 17 | 18 | type SecureChannel struct { 19 | c types.Channel 20 | open bool 21 | secret []byte 22 | publicKey *ecdsa.PublicKey 23 | encKey []byte 24 | macKey []byte 25 | iv []byte 26 | } 27 | 28 | func NewSecureChannel(c types.Channel) *SecureChannel { 29 | return &SecureChannel{ 30 | c: c, 31 | } 32 | } 33 | 34 | func (sc *SecureChannel) GenerateSecret(cardPubKeyData []byte) error { 35 | key, err := ethcrypto.GenerateKey() 36 | if err != nil { 37 | return err 38 | } 39 | 40 | cardPubKey, err := ethcrypto.UnmarshalPubkey(cardPubKeyData) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | sc.publicKey = &key.PublicKey 46 | sc.secret = crypto.GenerateECDHSharedSecret(key, cardPubKey) 47 | 48 | return nil 49 | } 50 | 51 | func (sc *SecureChannel) Reset() { 52 | sc.open = false 53 | } 54 | 55 | func (sc *SecureChannel) Init(iv, encKey, macKey []byte) { 56 | sc.iv = iv 57 | sc.encKey = encKey 58 | sc.macKey = macKey 59 | sc.open = true 60 | } 61 | 62 | func (sc *SecureChannel) Secret() []byte { 63 | return sc.secret 64 | } 65 | 66 | func (sc *SecureChannel) PublicKey() *ecdsa.PublicKey { 67 | return sc.publicKey 68 | } 69 | 70 | func (sc *SecureChannel) RawPublicKey() []byte { 71 | return ethcrypto.FromECDSAPub(sc.publicKey) 72 | } 73 | 74 | func (sc *SecureChannel) Send(cmd *apdu.Command) (*apdu.Response, error) { 75 | if sc.open { 76 | encData, err := crypto.EncryptData(cmd.Data, sc.encKey, sc.iv) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | meta := []byte{cmd.Cla, cmd.Ins, cmd.P1, cmd.P2, byte(len(encData) + 16), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} 82 | if err = sc.updateIV(meta, encData); err != nil { 83 | return nil, err 84 | } 85 | 86 | newData := append(sc.iv, encData...) 87 | cmd.Data = newData 88 | } 89 | 90 | resp, err := sc.c.Send(cmd) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | if sc.open { 96 | if resp.Sw != globalplatform.SwOK { 97 | return nil, apdu.NewErrBadResponse(resp.Sw, "unexpected sw in secure channel") 98 | } 99 | 100 | rmeta := []byte{byte(len(resp.Data)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} 101 | rmac := resp.Data[:len(sc.iv)] 102 | rdata := resp.Data[len(sc.iv):] 103 | 104 | plainData, err := crypto.DecryptData(rdata, sc.encKey, sc.iv) 105 | 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | if err = sc.updateIV(rmeta, rdata); err != nil { 111 | return nil, err 112 | } 113 | 114 | if !bytes.Equal(sc.iv, rmac) { 115 | return nil, ErrInvalidResponseMAC 116 | } 117 | 118 | logger.Debug("apdu response decrypted", "hex", hexutils.BytesToHexWithSpaces(plainData)) 119 | return apdu.ParseResponse(plainData) 120 | } else { 121 | return resp, nil 122 | } 123 | 124 | } 125 | 126 | func (sc *SecureChannel) updateIV(meta, data []byte) error { 127 | mac, err := crypto.CalculateMac(meta, data, sc.macKey) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | sc.iv = mac 133 | 134 | return nil 135 | } 136 | 137 | func (sc *SecureChannel) OneShotEncrypt(secrets *Secrets) ([]byte, error) { 138 | pubKeyData := ethcrypto.FromECDSAPub(sc.publicKey) 139 | data := append([]byte(secrets.Pin()), []byte(secrets.Puk())...) 140 | data = append(data, secrets.PairingToken()...) 141 | 142 | return crypto.OneShotEncrypt(pubKeyData, sc.secret, data) 143 | } 144 | -------------------------------------------------------------------------------- /secure_channel_test.go: -------------------------------------------------------------------------------- 1 | package keycard 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/status-im/keycard-go/apdu" 8 | "github.com/status-im/keycard-go/hexutils" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | type fakeChannel struct { 13 | lastCmd *apdu.Command 14 | } 15 | 16 | func (fc *fakeChannel) Send(cmd *apdu.Command) (*apdu.Response, error) { 17 | fc.lastCmd = cmd 18 | return nil, errors.New("test error") 19 | } 20 | 21 | func TestSecureChannel_Send(t *testing.T) { 22 | c := &fakeChannel{} 23 | sc := &SecureChannel{ 24 | c: c, 25 | encKey: hexutils.HexToBytes("FDBCB1637597CF3F8F5E8263007D4E45F64C12D44066D4576EB1443D60AEF441"), 26 | macKey: hexutils.HexToBytes("2FB70219E6635EE0958AB3F7A428BA87E8CD6E6F873A5725A55F25B102D0F1F7"), 27 | iv: hexutils.HexToBytes("627E64358FA9BDCDAD4442BD8006E0A5"), 28 | open: true, 29 | } 30 | 31 | data := hexutils.HexToBytes("D545A5E95963B6BCED86A6AE826D34C5E06AC64A1217EFFA1415A96674A82500") 32 | 33 | cmd := NewCommandMutuallyAuthenticate(data) 34 | sc.Send(cmd) 35 | 36 | expectedData := "BA796BF8FAD1FD50407B87127B94F5023EF8903AE926EAD8A204F961B8A0EDAEE7CCCFE7F7F6380CE2C6F188E598E4468B7DEDD0E807C18CCBDA71A55F3E1F9A" 37 | assert.Equal(t, expectedData, hexutils.BytesToHex(c.lastCmd.Data)) 38 | 39 | expectedIV := "BA796BF8FAD1FD50407B87127B94F502" 40 | assert.Equal(t, expectedIV, hexutils.BytesToHex(sc.iv)) 41 | } 42 | -------------------------------------------------------------------------------- /types/application_info.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/status-im/keycard-go/apdu" 7 | ) 8 | 9 | var ErrWrongApplicationInfoTemplate = errors.New("wrong application info template") 10 | 11 | type Capability uint8 12 | 13 | const ( 14 | TagSelectResponsePreInitialized uint8 = 0x80 15 | TagApplicationStatusTemplate uint8 = 0xA3 16 | TagApplicationInfoTemplate uint8 = 0xA4 17 | TagApplicationInfoCapabilities uint8 = 0x8D 18 | ) 19 | 20 | const ( 21 | CapabilitySecureChannel Capability = 1 << iota 22 | CapabilityKeyManagement 23 | CapabilityCredentialsManagement 24 | CapabilityNDEF 25 | CapabilityFactoryReset 26 | 27 | CapabilityAll = CapabilitySecureChannel | 28 | CapabilityKeyManagement | 29 | CapabilityCredentialsManagement | 30 | CapabilityNDEF | 31 | CapabilityFactoryReset 32 | ) 33 | 34 | type ApplicationInfo struct { 35 | Installed bool 36 | Initialized bool 37 | InstanceUID []byte 38 | SecureChannelPublicKey []byte 39 | Version []byte 40 | AvailableSlots []byte 41 | // KeyUID is the sha256 of of the master public key on the card. 42 | // It's empty if the card doesn't contain any key. 43 | KeyUID []byte 44 | Capabilities Capability 45 | } 46 | 47 | func (a *ApplicationInfo) HasCapability(c Capability) bool { 48 | return a.Capabilities&c == c 49 | } 50 | 51 | func (a *ApplicationInfo) HasSecureChannelCapability() bool { 52 | return a.HasCapability(CapabilitySecureChannel) 53 | } 54 | 55 | func (a *ApplicationInfo) HasKeyManagementCapability() bool { 56 | return a.HasCapability(CapabilityKeyManagement) 57 | } 58 | 59 | func (a *ApplicationInfo) HasCredentialsManagementCapability() bool { 60 | return a.HasCapability(CapabilityCredentialsManagement) 61 | } 62 | 63 | func (a *ApplicationInfo) HasNDEFCapability() bool { 64 | return a.HasCapability(CapabilityNDEF) 65 | } 66 | 67 | func (a *ApplicationInfo) HasFactoryResetCapability() bool { 68 | return a.HasCapability(CapabilityFactoryReset) 69 | } 70 | 71 | func ParseApplicationInfo(data []byte) (*ApplicationInfo, error) { 72 | info := &ApplicationInfo{ 73 | Installed: true, 74 | } 75 | 76 | if data[0] == TagSelectResponsePreInitialized { 77 | info.SecureChannelPublicKey = data[2:] 78 | info.Capabilities = CapabilityCredentialsManagement 79 | 80 | if len(info.SecureChannelPublicKey) > 0 { 81 | info.Capabilities = info.Capabilities | CapabilitySecureChannel 82 | } 83 | 84 | return info, nil 85 | } 86 | 87 | info.Initialized = true 88 | 89 | if data[0] != TagApplicationInfoTemplate { 90 | return nil, ErrWrongApplicationInfoTemplate 91 | } 92 | 93 | instanceUID, err := apdu.FindTag(data, apdu.Tag{TagApplicationInfoTemplate}, apdu.Tag{0x8F}) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | pubKey, err := apdu.FindTag(data, apdu.Tag{TagApplicationInfoTemplate}, apdu.Tag{0x80}) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | appVersion, err := apdu.FindTag(data, apdu.Tag{TagApplicationInfoTemplate}, apdu.Tag{0x02}) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | availableSlots, err := apdu.FindTagN(data, 1, apdu.Tag{TagApplicationInfoTemplate}, apdu.Tag{0x02}) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | keyUID, err := apdu.FindTagN(data, 0, apdu.Tag{TagApplicationInfoTemplate}, apdu.Tag{0x8E}) 114 | if err != nil { 115 | return nil, err 116 | } 117 | 118 | capabilities := CapabilityAll 119 | capabilitiesBytes, err := apdu.FindTag(data, apdu.Tag{TagApplicationInfoCapabilities}) 120 | if err == nil && len(capabilitiesBytes) > 0 { 121 | capabilities = Capability(capabilitiesBytes[0]) 122 | } 123 | 124 | info.InstanceUID = instanceUID 125 | info.SecureChannelPublicKey = pubKey 126 | info.Version = appVersion 127 | info.AvailableSlots = availableSlots 128 | info.KeyUID = keyUID 129 | info.Capabilities = capabilities 130 | 131 | return info, nil 132 | } 133 | -------------------------------------------------------------------------------- /types/application_status.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | 7 | "github.com/status-im/keycard-go/apdu" 8 | "github.com/status-im/keycard-go/derivationpath" 9 | ) 10 | 11 | const hardenedStart = 0x80000000 // 2^31 12 | 13 | var ErrApplicationStatusTemplateNotFound = errors.New("application status template not found") 14 | 15 | type ApplicationStatus struct { 16 | PinRetryCount int 17 | PUKRetryCount int 18 | KeyInitialized bool 19 | Path string 20 | } 21 | 22 | func ParseApplicationStatus(data []byte) (*ApplicationStatus, error) { 23 | tpl, err := apdu.FindTag(data, apdu.Tag{TagApplicationStatusTemplate}) 24 | if err != nil { 25 | return parseKeyPathStatus(data) 26 | } 27 | 28 | appStatus := &ApplicationStatus{} 29 | 30 | if pinRetryCount, err := apdu.FindTag(tpl, apdu.Tag{0x02}); err == nil && len(pinRetryCount) == 1 { 31 | appStatus.PinRetryCount = int(pinRetryCount[0]) 32 | } 33 | 34 | if pukRetryCount, err := apdu.FindTagN(tpl, 1, apdu.Tag{0x02}); err == nil && len(pukRetryCount) == 1 { 35 | appStatus.PUKRetryCount = int(pukRetryCount[0]) 36 | } 37 | 38 | if keyInitialized, err := apdu.FindTag(tpl, apdu.Tag{0x01}); err == nil { 39 | if bytes.Equal(keyInitialized, []byte{0xFF}) { 40 | appStatus.KeyInitialized = true 41 | } 42 | } 43 | 44 | return appStatus, nil 45 | } 46 | 47 | func parseKeyPathStatus(data []byte) (*ApplicationStatus, error) { 48 | appStatus := &ApplicationStatus{} 49 | 50 | path, err := derivationpath.EncodeFromBytes(data) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | appStatus.Path = path 56 | 57 | return appStatus, nil 58 | } 59 | -------------------------------------------------------------------------------- /types/card_status.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/status-im/keycard-go/apdu" 7 | ) 8 | 9 | type lifeCycle byte 10 | 11 | var ( 12 | TagGetStatusTemplate = apdu.Tag{0xE3} 13 | TagGetStatusLifeCycleState = apdu.Tag{0x9F, 0x70} 14 | ) 15 | 16 | const ( 17 | LifeCycleOpReady lifeCycle = 0x01 18 | LifeCycleInitialized = 0x07 19 | LifeCycleSecured = 0x0F 20 | LifeCycleCardLocked = 0x7F 21 | LifeCycleTerminated = 0xFF 22 | ) 23 | 24 | func (lc lifeCycle) String() string { 25 | switch lc { 26 | case LifeCycleOpReady: 27 | return "OP_READY" 28 | case LifeCycleInitialized: 29 | return "INITIALIZED" 30 | case LifeCycleSecured: 31 | return "SECURED" 32 | case LifeCycleCardLocked: 33 | return "CARD_LOCKED" 34 | case LifeCycleTerminated: 35 | return "TERMINATED" 36 | default: 37 | return "UNKNOWN" 38 | } 39 | } 40 | 41 | type ErrInvalidLifeCycleValue struct { 42 | lc []byte 43 | } 44 | 45 | func (e *ErrInvalidLifeCycleValue) Error() string { 46 | return fmt.Sprintf("life cycle value must be 1 byte. got %d bytes: %x", len(e.lc), e.lc) 47 | } 48 | 49 | type CardStatus struct { 50 | lc lifeCycle 51 | } 52 | 53 | func (cs *CardStatus) LifeCycle() string { 54 | return cs.lc.String() 55 | } 56 | 57 | func ParseCardStatus(data []byte) (*CardStatus, error) { 58 | tpl, err := apdu.FindTag(data, TagGetStatusTemplate) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | lc, err := apdu.FindTag(tpl, TagGetStatusLifeCycleState) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | if len(lc) != 1 { 69 | return nil, &ErrInvalidLifeCycleValue{lc} 70 | } 71 | 72 | return &CardStatus{lifeCycle(lc[0])}, nil 73 | } 74 | -------------------------------------------------------------------------------- /types/cash_application_info.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/status-im/keycard-go/apdu" 4 | 5 | type CashApplicationInfo struct { 6 | Installed bool 7 | PublicKey []byte 8 | PublicData []byte 9 | Version []byte 10 | } 11 | 12 | func ParseCashApplicationInfo(data []byte) (*CashApplicationInfo, error) { 13 | info := &CashApplicationInfo{} 14 | 15 | if data[0] != TagApplicationInfoTemplate { 16 | return nil, ErrWrongApplicationInfoTemplate 17 | } 18 | 19 | info.Installed = true 20 | 21 | pubKey, err := apdu.FindTag(data, apdu.Tag{TagApplicationInfoTemplate}, apdu.Tag{0x80}) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | pubData, err := apdu.FindTag(data, apdu.Tag{TagApplicationInfoTemplate}, apdu.Tag{0x82}) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | appVersion, err := apdu.FindTag(data, apdu.Tag{TagApplicationInfoTemplate}, apdu.Tag{0x02}) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | info.PublicKey = pubKey 37 | info.PublicData = pubData 38 | info.Version = appVersion 39 | 40 | return info, nil 41 | } 42 | -------------------------------------------------------------------------------- /types/certificate.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "crypto/sha256" 5 | "errors" 6 | 7 | "github.com/status-im/keycard-go/apdu" 8 | ) 9 | 10 | type Certificate struct { 11 | identPub []byte 12 | signature *Signature 13 | } 14 | 15 | var ( 16 | TagCertificate = uint8(0x8A) 17 | ) 18 | 19 | func ParseCertificate(data []byte) (*Certificate, error) { 20 | if len(data) != 98 { 21 | return nil, errors.New("certificate must be 98 byte long") 22 | } 23 | 24 | identPub := data[0:33] 25 | sigData := data[33:98] 26 | msg := sha256.Sum256(identPub) 27 | 28 | sig, err := ParseRecoverableSignature(msg[:], sigData) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return &Certificate{ 34 | identPub: identPub, 35 | signature: sig, 36 | }, nil 37 | } 38 | 39 | func VerifyIdentity(challenge []byte, tlvData []byte) ([]byte, error) { 40 | template, err := apdu.FindTag(tlvData, apdu.Tag{TagSignatureTemplate}) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | certData, err := apdu.FindTag(template, apdu.Tag{TagCertificate}) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | cert, err := ParseCertificate(certData) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | r, s, err := DERSignatureToRS(template) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | // TODO: investigate why verify signature fails but recovery works 61 | _, err = calculateV(challenge, cert.identPub, r, s) 62 | 63 | if err != nil { 64 | return nil, errors.New("invalid signature") 65 | } 66 | 67 | return compressPublicKey(cert.signature.pubKey), nil 68 | } 69 | -------------------------------------------------------------------------------- /types/certificate_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func hexMustDecode(str string) []byte { 11 | out, _ := hex.DecodeString(str) 12 | return out 13 | } 14 | 15 | func TestVerifyIdentity(t *testing.T) { 16 | challenge := hexMustDecode("63acd6e02a8b5783551ff2836a9cbdf237c115c3ff018b943f044e6a69b19fe7") 17 | response := hexMustDecode("a081ab8a620365c18485fe7018e11cb992011426803aa8e843c63aab9657aed7d3ee4b85a62a11188ada267db3312a84e1be27c01c736a89da7a1fe4f7e90ce297e74f00008e2bfdb06058374abfc1c026386d16ead7bbc19bc0645d2e7acf7b953169bbc1ac0130450220364c5ca937b7ca42861978f086d206cc569ef0bb2ea4c7de08929c2fcca7434d022100c87699ce4f977e6a7a4800343db9b6842b91ca873e56dfe3327d19a2d01af14e") 18 | expectedKey := hexMustDecode("02fc929321aa94fea085b166994aa66590116252cf0235a03accaa2c8ab4595de5") 19 | 20 | pubkey, err := VerifyIdentity(challenge, response) 21 | assert.NoError(t, err) 22 | assert.Equal(t, expectedKey, pubkey) 23 | } 24 | -------------------------------------------------------------------------------- /types/exported_key.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | ethcrypto "github.com/ethereum/go-ethereum/crypto" 7 | "github.com/status-im/keycard-go/apdu" 8 | ) 9 | 10 | var ( 11 | TagExportKeyTemplate = apdu.Tag{0xA1} 12 | TagExportKeyPublic = apdu.Tag{0x80} 13 | TagExportKeyPrivate = apdu.Tag{0x81} 14 | TagExportKeyPublicChain = apdu.Tag{0x82} 15 | ) 16 | 17 | type ExportedKey struct { 18 | pubKey []byte 19 | privKey []byte 20 | chainCode []byte 21 | } 22 | 23 | func (k *ExportedKey) PubKey() []byte { 24 | return k.pubKey 25 | } 26 | 27 | func (k *ExportedKey) PrivKey() []byte { 28 | return k.privKey 29 | } 30 | 31 | func (k *ExportedKey) ChainCode() []byte { 32 | return k.chainCode 33 | } 34 | 35 | func ParseExportKeyResponse(data []byte) (*ExportedKey, error) { 36 | tpl, err := apdu.FindTag(data, TagExportKeyTemplate) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | pubKey := tryFindTag(tpl, TagExportKeyPublic) 42 | privKey := tryFindTag(tpl, TagExportKeyPrivate) 43 | chainCode := tryFindTag(tpl, TagExportKeyPublicChain) 44 | 45 | if len(pubKey) == 0 && len(privKey) > 0 { 46 | ecdsaKey, err := ethcrypto.HexToECDSA(fmt.Sprintf("%x", privKey)) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | pubKey = ethcrypto.FromECDSAPub(&ecdsaKey.PublicKey) 52 | } 53 | 54 | return &ExportedKey{pubKey, privKey, chainCode}, nil 55 | } 56 | 57 | func tryFindTag(tpl []byte, tags ...apdu.Tag) []byte { 58 | data, err := apdu.FindTag(tpl, tags...) 59 | if err != nil { 60 | return nil 61 | } 62 | 63 | return data 64 | } 65 | -------------------------------------------------------------------------------- /types/metadata.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "container/list" 6 | "errors" 7 | "io" 8 | 9 | "github.com/status-im/keycard-go/apdu" 10 | ) 11 | 12 | type Metadata struct { 13 | name string 14 | paths *list.List 15 | } 16 | 17 | func EmptyMetadata() *Metadata { 18 | return &Metadata{"", list.New()} 19 | } 20 | 21 | func NewMetadata(name string, paths []uint32) (*Metadata, error) { 22 | m := EmptyMetadata() 23 | 24 | if err := m.SetName(name); err != nil { 25 | return nil, err 26 | } 27 | 28 | for i := 0; i < len(paths); i++ { 29 | m.AddPath(paths[i]) 30 | } 31 | 32 | return m, nil 33 | } 34 | 35 | func ParseMetadata(data []byte) (*Metadata, error) { 36 | buf := bytes.NewBuffer(data) 37 | header, err := buf.ReadByte() 38 | 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | version := header >> 5 44 | 45 | if version != 1 { 46 | return nil, errors.New("invalid version") 47 | } 48 | 49 | namelen := int(header & 0x1f) 50 | cardName := string(buf.Next(namelen)) 51 | 52 | list := list.New() 53 | 54 | for { 55 | start, err := apdu.ParseLength(buf) 56 | 57 | if err == io.EOF { 58 | break 59 | } else if err != nil { 60 | return nil, err 61 | } 62 | 63 | count, err := apdu.ParseLength(buf) 64 | 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | for i := start; i <= (start + count); i++ { 70 | insertOrderedNoDups(list, i) 71 | } 72 | } 73 | 74 | return &Metadata{cardName, list}, nil 75 | } 76 | 77 | func insertOrderedNoDups(list *list.List, num uint32) { 78 | le := list.Back() 79 | 80 | for le != nil { 81 | val := le.Value.(uint32) 82 | 83 | if num > val { 84 | break 85 | } else if num == val { 86 | return 87 | } 88 | 89 | le = le.Prev() 90 | } 91 | 92 | if le == nil { 93 | list.PushFront(num) 94 | } else { 95 | list.InsertAfter(num, le) 96 | } 97 | } 98 | 99 | func (m *Metadata) Name() string { 100 | return m.name 101 | } 102 | 103 | func (m *Metadata) SetName(name string) error { 104 | if len(name) > 20 { 105 | return errors.New("name longer than 20 chars") 106 | } 107 | 108 | m.name = name 109 | return nil 110 | } 111 | 112 | func (m *Metadata) Paths() []uint32 { 113 | listlen := m.paths.Len() 114 | paths := make([]uint32, listlen) 115 | e := m.paths.Front() 116 | 117 | for i := 0; i < listlen; i++ { 118 | paths[i] = e.Value.(uint32) 119 | e = e.Next() 120 | } 121 | 122 | return paths 123 | } 124 | 125 | func (m *Metadata) AddPath(path uint32) { 126 | insertOrderedNoDups(m.paths, path) 127 | } 128 | 129 | func (m *Metadata) RemovePath(path uint32) { 130 | for le := m.paths.Front(); le != nil; le = le.Next() { 131 | if path == le.Value.(uint32) { 132 | m.paths.Remove(le) 133 | return 134 | } 135 | } 136 | } 137 | 138 | func (m *Metadata) Serialize() []byte { 139 | buf := new(bytes.Buffer) 140 | buf.WriteByte(0x20 | byte(len(m.name))) 141 | buf.WriteString(m.name) 142 | 143 | le := m.paths.Front() 144 | 145 | if le == nil { 146 | return buf.Bytes() 147 | } 148 | 149 | start := le.Value.(uint32) 150 | len := uint32(0) 151 | 152 | for le = le.Next(); le != nil; le = le.Next() { 153 | w := le.Value.(uint32) 154 | 155 | if w == (start + len + 1) { 156 | len++ 157 | } else { 158 | apdu.WriteLength(buf, start) 159 | apdu.WriteLength(buf, len) 160 | start = w 161 | len = 0 162 | } 163 | } 164 | 165 | apdu.WriteLength(buf, start) 166 | apdu.WriteLength(buf, len) 167 | 168 | return buf.Bytes() 169 | } 170 | -------------------------------------------------------------------------------- /types/metadata_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestParseMetadata(t *testing.T) { 10 | m, err := ParseMetadata([]byte{0x23, 0x31, 0x32, 0x33, 0x00, 0x00, 0x04, 0x03, 0x82, 0x7a, 0x28, 0x01}) 11 | assert.NoError(t, err) 12 | assert.Equal(t, "123", m.Name()) 13 | assert.Equal(t, []uint32{0x00, 0x04, 0x05, 0x06, 0x07, 0x7a28, 0x7a29}, m.Paths()) 14 | } 15 | 16 | func TestSerialize(t *testing.T) { 17 | m, err := NewMetadata("123", []uint32{0x00, 0x04, 0x05, 0x06, 0x07, 0x7a28, 0x7a29}) 18 | assert.NoError(t, err) 19 | assert.Equal(t, []byte{0x23, 0x31, 0x32, 0x33, 0x00, 0x00, 0x04, 0x03, 0x82, 0x7a, 0x28, 0x01}, m.Serialize()) 20 | } 21 | -------------------------------------------------------------------------------- /types/signature.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | 7 | "github.com/ethereum/go-ethereum/crypto" 8 | "github.com/status-im/keycard-go/apdu" 9 | ) 10 | 11 | var ( 12 | TagSignatureTemplate = uint8(0xA0) 13 | TagRawSignature = uint8(0x80) 14 | ) 15 | 16 | type Signature struct { 17 | pubKey []byte 18 | r []byte 19 | s []byte 20 | v byte 21 | } 22 | 23 | func ParseSignature(message, resp []byte) (*Signature, error) { 24 | // check for old template first because TagRawSignature matches the pubkey tag 25 | template, err := apdu.FindTag(resp, apdu.Tag{TagSignatureTemplate}) 26 | if err == nil { 27 | return parseLegacySignature(message, template) 28 | } 29 | 30 | sig, err := apdu.FindTag(resp, apdu.Tag{TagRawSignature}) 31 | 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return ParseRecoverableSignature(message, sig) 37 | } 38 | 39 | func ParseRecoverableSignature(message, sig []byte) (*Signature, error) { 40 | if len(sig) != 65 { 41 | return nil, errors.New("invalid signature") 42 | } 43 | 44 | pubKey, err := crypto.Ecrecover(message, sig) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return &Signature{ 50 | pubKey: pubKey, 51 | r: sig[0:32], 52 | s: sig[32:64], 53 | v: sig[64], 54 | }, nil 55 | } 56 | 57 | func DERSignatureToRS(tlv []byte) ([]byte, []byte, error) { 58 | r, err := apdu.FindTagN(tlv, 0, apdu.Tag{0x30}, apdu.Tag{0x02}) 59 | if err != nil { 60 | return nil, nil, err 61 | } 62 | 63 | if len(r) > 32 { 64 | r = r[len(r)-32:] 65 | } 66 | 67 | s, err := apdu.FindTagN(tlv, 1, apdu.Tag{0x30}, apdu.Tag{0x02}) 68 | if err != nil { 69 | return nil, nil, err 70 | } 71 | 72 | if len(s) > 32 { 73 | s = s[len(s)-32:] 74 | } 75 | 76 | return r, s, nil 77 | } 78 | 79 | func (s *Signature) PubKey() []byte { 80 | return s.pubKey 81 | } 82 | 83 | func (s *Signature) R() []byte { 84 | return s.r 85 | } 86 | 87 | func (s *Signature) S() []byte { 88 | return s.s 89 | } 90 | 91 | func (s *Signature) V() byte { 92 | return s.v 93 | } 94 | 95 | func parseLegacySignature(message, template []byte) (*Signature, error) { 96 | pubKey, err := apdu.FindTag(template, apdu.Tag{0x80}) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | r, s, err := DERSignatureToRS(template) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | v, err := calculateV(message, pubKey, r, s) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return &Signature{ 112 | pubKey: pubKey, 113 | r: r, 114 | s: s, 115 | v: v, 116 | }, nil 117 | } 118 | 119 | func calculateV(message, pubKey, r, s []byte) (v byte, err error) { 120 | rs := append(r, s...) 121 | for i := 0; i < 4; i++ { 122 | v = byte(i) 123 | sig := append(rs, v) 124 | rec, err := crypto.Ecrecover(message, sig) 125 | if err != nil { 126 | return v, err 127 | } 128 | 129 | if len(pubKey) == 33 { 130 | rec = compressPublicKey(rec) 131 | } 132 | 133 | if bytes.Equal(pubKey, rec) { 134 | return v, nil 135 | } 136 | } 137 | 138 | return v, err 139 | } 140 | 141 | func compressPublicKey(pubKey []byte) []byte { 142 | if len(pubKey) == 33 { 143 | return pubKey 144 | } 145 | 146 | if (pubKey[64] & 1) == 1 { 147 | pubKey[0] = 3 148 | } else { 149 | pubKey[0] = 2 150 | } 151 | 152 | return pubKey[0:33] 153 | } 154 | -------------------------------------------------------------------------------- /types/types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/status-im/keycard-go/apdu" 4 | 5 | // Channel is an interface with a Send method to send apdu commands and receive apdu responses. 6 | type Channel interface { 7 | Send(*apdu.Command) (*apdu.Response, error) 8 | } 9 | 10 | type PairingInfo struct { 11 | Key []byte 12 | Index int 13 | } --------------------------------------------------------------------------------