├── .gitignore ├── LICENSE ├── README.md └── go ├── agent.go ├── certprovider.go ├── certprovider_backend.go ├── io.go ├── main.go ├── signer.go └── storage_backend.go /.gitignore: -------------------------------------------------------------------------------- 1 | /go/go.js 2 | /go/go.js.map 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016- Duo Security, Inc. (https://duo.com) 4 | Copyright (c) 2015- Stripe, Inc. (https://stripe.com) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChromeOS SSH SmartCard Hack 2 | 3 | This repository contains some code to duct-tape an SSH agent to a 4 | Chrome extension that implements the [chrome.certificateProvider][] 5 | API. This can make it possible to use Smart Cards with the [Secure Shell][] 6 | app! 7 | 8 | This is forked from, and heavily based on, the [MacGyver][] extension 9 | developed by the good folks at Stripe. 10 | 11 | ## Background 12 | 13 | Recently, Chrome OS introduced full [Smart Card support][], along with 14 | the [chrome.certificateProvider][] API that allows Smart Card 15 | middleware extensions to provide certificates (and the ability to sign 16 | data) to ChromeOS for TLS client authentication. 17 | 18 | Separately, the [Secure Shell][] extension for Chrome (which is an 19 | OpenSSH compiled for [NaCl][]) [supports][chromium-hterm ssh-agent] 20 | using an external extension as a stand-in for an SSH agent. 21 | 22 | The interface offered by [chrome.certificateProvider][] extensions 23 | provides all the necessary cryptographic capabilities to implement a 24 | full-fledged SSH Agent extension that uses keypairs stored on smart 25 | cards. However, at this time, there is no clean way for another 26 | extension to access this functionality. Our solution is, instead, to 27 | inject a modified version of the [MacGyver][] code into a 28 | [chrome.certificateProvider][] extension. It turns out this is 29 | surprisingly straightforward! 30 | 31 | ## Usage 32 | 33 | After installing a properly-hacked extension, you can pass 34 | `--ssh-agent=extensionid` in the "relay options" field (not the "SSH 35 | Arguments"!) of the Secure Shell app. 36 | 37 | ## Performing the Hack 38 | 39 | The SSH Agent code is written in [Go][], and compiled to JavaScript 40 | using [GopherJS][]. Using Go lets us take advantage of packages like 41 | [x/crypto][], which already has an SSH agent implementation. 42 | 43 | You can compile the extension by running the following: 44 | 45 | * `go get -u github.com/gopherjs/gopherjs` 46 | * `cd go && gopherjs build` 47 | 48 | Then, take an existing [chrome.certificateProvider][] extension, 49 | unpack it if necessary, and do the following: 50 | 51 | * Copy the 'go' directory and all its contents into the unpacked 52 | extension directory 53 | * Edit manifest.json 54 | * Add `"go/go.js"` as a background script. 55 | 56 | That is, if manifest.json initially contains: 57 | 58 | ``` 59 | "app": { 60 | "background": { 61 | "persistent": false, 62 | "scripts": [ "background.js" ] 63 | } 64 | }, 65 | ``` 66 | 67 | Then modify it to contain: 68 | 69 | ``` 70 | "app": { 71 | "background": { 72 | "persistent": false, 73 | "scripts": [ "background.js", "go/go.js" ] 74 | } 75 | }, 76 | ``` 77 | 78 | * Allow messaging from the [Secure Shell][] app. 79 | 80 | That is, add: 81 | 82 | ``` 83 | "externally_connectable": { 84 | "ids": [ 85 | "pnhechapfaindjhompbnflcldabbghjo", 86 | "okddffdblfhhnmhodogpojmfkjmhinfp" 87 | ] 88 | }, 89 | ``` 90 | 91 | as a top-level key. (You may want to use the original 92 | manifest.json from [MacGyver][] as a reference.) 93 | 94 | 95 | ## Permissions 96 | 97 | In order to communicate with smart card hardware, middleware 98 | extensions (typically, those that implement the 99 | [chrome.certificateProvider][] interface) must use Google's [Smart 100 | Card Connector][] app. Unfortunately, this app has a highly 101 | restrictive model for [API permissions][Smart Card Connector API 102 | Permissions], in that it only accepts communications from whitelisted 103 | extensions. When you modify an extension as described above, you will 104 | typically end up with a new extension ID that is not on this 105 | whitelist. 106 | 107 | If you are deploying this extension onto enterprise-managed 108 | chromebooks, you can attach policy to the Smart Card Connector app to 109 | override this whitelist, as Google documents in their discussion of 110 | [API permissions][Smart Card Connector API Permissions]. 111 | 112 | Otherwise, with some JS hackery, it's possible to force-whitelist a 113 | Chrome extension. To do this: 114 | 115 | 1. Navigate to chrome://extensions/ 116 | 2. Ensure "Developer Mode" is checked 117 | 3. Beneath the "Smart Card Connector" app, click the link to inspect 118 | the 'background page' 119 | 4. In the JS console, type: 120 | `$jscomp.scope.permissionsChecker.userPromptingChecker_.storeUserSelection_('YOUR_EXTENSION_ID', true)` 121 | 122 | (Obviously, there's a risk this technique might break with a future 123 | update to the Smart Card Connector app!) 124 | 125 | ## Chrome SSH Agent Protocol 126 | 127 | The [Secure Shell][] extension for Chrome has 128 | [supported][chromium-hterm ssh-agent] relaying the SSH agent protocol 129 | to another extension since November 2014. The [protocol][nassh agent] 130 | is fairly straightforward, but undocumented. 131 | 132 | The [SSH agent protocol][ssh-agent] is based on a simple 133 | length-prefixed framing protocol. Each message is prefixed with a 134 | 4-byte network-encoded length. Messages are sent over a UNIX socket. 135 | 136 | By contrast, the [Secure Shell][] agent protocol uses [Chrome 137 | cross-extension messaging][Cross-extension messaging], connecting to 138 | the agent extension with [chrome.runtime.connect][]. Each frame of the 139 | SSH agent protocol is assembled, stripped of its length prefix, and 140 | sent as an array of numbers (not, say, an ArrayBuffer) in the "data" 141 | field of an object via `postMessage`. 142 | 143 | Here's an example message, representing the 144 | `SSH2_AGENTC_REQUEST_IDENTITIES` request (to list keys): 145 | 146 | ```json 147 | { 148 | "type": "auth-agent@openssh.com", 149 | "data": [11] 150 | } 151 | ``` 152 | 153 | SSH agents are expected to respond in the same format. 154 | 155 | ### macgyver.AgentPort 156 | 157 | Because [x/crypto][]'s [SSH agent 158 | implementation][x/crypto/ssh/agent.ServeAgent] expects an 159 | [io.ReadWriter][] that implements the standard (length-prefixed) 160 | protocol, MacGyver implements a wrapper around a `chrome.runtime.Port` 161 | that between [Secure Shell][]'s protocol and the native protocol 162 | (stripping or adding the length prefix and JSON object wrapper as 163 | necessary). 164 | 165 | ## Contributors 166 | 167 | MacGyver extension: 168 | 169 | * Evan Broder 170 | * Dan Benamy 171 | 172 | CertificateProvider modifications: 173 | 174 | * Adam Goodman 175 | 176 | [Cross-extension messaging]: https://developer.chrome.com/extensions/messaging#external 177 | [Go]: http://golang.org/ 178 | [Gopherjs]: http://www.gopherjs.org/ 179 | [MacGyver]: https://github.com/stripe/macgyver 180 | [NaCl]: https://en.wikipedia.org/wiki/Google_Native_Client 181 | [Secure Shell]: https://chrome.google.com/webstore/detail/secure-shell/pnhechapfaindjhompbnflcldabbghjo?hl=en 182 | [Smart Card support]: https://support.google.com/chrome/a/answer/7014689?hl=en 183 | [Smart Card Connector]: https://chrome.google.com/webstore/detail/smart-card-connector/khpfeaanjngmcnplbdlpegiifgpfgdco 184 | [Smart Card Connector API Permissions]: https://github.com/GoogleChrome/chromeos_smart_card_connector#smart-card-connector-app-api-permissions 185 | [chrome.certificateProvider]: https://developer.chrome.com/extensions/certificateProvider 186 | [chrome.runtime.connect]: https://developer.chrome.com/extensions/runtime#method-connect 187 | [chromium-hterm ssh-agent]: https://groups.google.com/a/chromium.org/d/msg/chromium-hterm/iq-AuvRJsYw/QVJdCw2wSM0J 188 | [io.ReadWriter]: https://godoc.org/io#ReadWriter 189 | [nassh agent]: https://github.com/libapps/libapps-mirror/blob/master/nassh/js/nassh_stream_sshagent_relay.js 190 | [ssh-agent]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.agent?rev=HEAD 191 | [x/crypto/ssh/agent.ServeAgent]: https://godoc.org/golang.org/x/crypto/ssh/agent#ServeAgent 192 | [x/crypto]: https://godoc.org/golang.org/x/crypto 193 | -------------------------------------------------------------------------------- /go/agent.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "errors" 7 | "log" 8 | "strings" 9 | 10 | "golang.org/x/crypto/ssh" 11 | "golang.org/x/crypto/ssh/agent" 12 | ) 13 | 14 | var ErrUnsupported = errors.New("unsupported operation") 15 | var ErrNotFound = errors.New("not found") 16 | 17 | // An interface that's a subset of agent.Agent. This abstraction is here so that 18 | // each backend doesn't have to stub out a bunch of methods and can share a bit 19 | // of code. 20 | type Backend interface { 21 | List() ([]*agent.Key, error) 22 | Signers() (signers []ssh.Signer, err error) 23 | } 24 | 25 | type Agent struct { 26 | backend Backend 27 | } 28 | 29 | func NewAgent(backend Backend) *Agent { 30 | return &Agent{backend} 31 | } 32 | 33 | // PubKeys exports the list of public keys in authorized_keys format. 34 | // 35 | // Expected to be used directly from javascript, so panic (i.e. throw) 36 | // rather than returning an error object. 37 | func (a *Agent) PubKeys() string { 38 | signers, err := a.Signers() 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | var keys []byte 44 | for _, signer := range signers { 45 | keys = append(keys, ssh.MarshalAuthorizedKey(signer.PublicKey())...) 46 | keys = append(keys, '\n') 47 | } 48 | 49 | return strings.TrimSpace(string(keys)) 50 | } 51 | 52 | func (a *Agent) List() ([]*agent.Key, error) { 53 | return a.backend.List() 54 | } 55 | 56 | func (a *Agent) Signers() (signers []ssh.Signer, err error) { 57 | return a.backend.Signers() 58 | } 59 | 60 | func (a *Agent) Sign(key ssh.PublicKey, data []byte) (*ssh.Signature, error) { 61 | wanted := key.Marshal() 62 | signers, err := a.Signers() 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | for _, signer := range signers { 68 | if bytes.Equal(signer.PublicKey().Marshal(), wanted) { 69 | log.Printf("Signing message: key=%s", ssh.MarshalAuthorizedKey(signer.PublicKey())) 70 | return signer.Sign(rand.Reader, data) 71 | } 72 | } 73 | 74 | return nil, ErrNotFound 75 | } 76 | 77 | func (a *Agent) Add(key agent.AddedKey) error { 78 | return ErrUnsupported 79 | } 80 | 81 | func (a *Agent) Remove(key ssh.PublicKey) error { 82 | return ErrUnsupported 83 | } 84 | 85 | func (a *Agent) RemoveAll() error { 86 | return ErrUnsupported 87 | } 88 | 89 | func (a *Agent) Lock(passphrase []byte) error { 90 | return ErrUnsupported 91 | } 92 | 93 | func (a *Agent) Unlock(passphrase []byte) error { 94 | return ErrUnsupported 95 | } 96 | -------------------------------------------------------------------------------- /go/certprovider.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | 5 | "github.com/gopherjs/gopherjs/js" 6 | ) 7 | 8 | // CertificateProvider is a wrapper for directly calling 9 | // CertificateProvider event listener methods (currently, those 10 | // provided by the Charismathics Smart Card Middleware extension) that 11 | // handles making the async API synchronous 12 | type CertificateProvider struct { 13 | } 14 | 15 | func (cp * CertificateProvider) ListClientCertificates() (matches [][]byte, err error) { 16 | // Uncaught exceptions in JS get translated into panics in Go 17 | defer func() { 18 | if r := recover(); r != nil { 19 | err = r.(error) 20 | } 21 | }() 22 | 23 | results := make(chan []*js.Object, 1) 24 | 25 | // find the event listener provided to Chrome via 26 | // chrome.certificateProvider.onCertificatesRequested.addListener(), 27 | // and then call that thing directly. 28 | // XXX or maybe we could actually just do 29 | // XXX chrome.certificateProvider.onCertificatesRequested.dispatch() ? 30 | backend := js.Global.Get("$jscomp").Get("scope").Get("certificateProviderBridgeBackend") 31 | backend.Call("boundCertificatesRequestListener_", func(matches []*js.Object) { 32 | go func() { results <- matches }() 33 | }) 34 | 35 | objects := <-results 36 | for _, obj := range objects { 37 | cert := obj.Get("certificate") 38 | matches = append(matches, js.Global.Get("Uint8Array").New(cert).Interface().([]byte)) 39 | } 40 | return 41 | } 42 | 43 | func (cp *CertificateProvider) Sign(request js.M) (sig []byte, err error) { 44 | // Uncaught exceptions in JS get translated into panics in Go 45 | defer func() { 46 | if r := recover(); r != nil { 47 | err = r.(error) 48 | } 49 | }() 50 | 51 | result := make(chan []byte); 52 | 53 | // find the event listener provided to chrome via 54 | // chrome.certificateProvider.onSignDigestRequested.addListener(), 55 | // and then call that thing directly. 56 | backend := js.Global.Get("$jscomp").Get("scope").Get("certificateProviderBridgeBackend") 57 | backend.Call("boundSignDigestRequestListener_", request, func(sig *js.Object) { 58 | go func() { result <- js.Global.Get("Uint8Array").New(sig).Interface().([]byte) }() 59 | }) 60 | sig = <-result 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /go/certprovider_backend.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/x509" 5 | "log" 6 | 7 | "golang.org/x/crypto/ssh" 8 | "golang.org/x/crypto/ssh/agent" 9 | ) 10 | 11 | type CertificateProviderBackend struct { 12 | cp *CertificateProvider 13 | } 14 | 15 | func NewCertificateProviderBackend() *CertificateProviderBackend { 16 | return &CertificateProviderBackend{ 17 | cp: &CertificateProvider{}, 18 | } 19 | } 20 | 21 | func (a *CertificateProviderBackend) List() ([]*agent.Key, error) { 22 | certs, err := a.listCertificates() 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | log.Printf("Listing keys: count=%d", len(certs)) 28 | 29 | keys := make([]*agent.Key, 0, len(certs)) 30 | for _, cert := range certs { 31 | pubkey, err := ssh.NewPublicKey(cert.PublicKey) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | keys = append(keys, &agent.Key{ 37 | Format: pubkey.Type(), 38 | Blob: pubkey.Marshal(), 39 | Comment: "", 40 | }) 41 | } 42 | return keys, nil 43 | } 44 | 45 | func (a *CertificateProviderBackend) Signers() (signers []ssh.Signer, err error) { 46 | certs, err := a.listCertificates() 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | for _, cert := range certs { 52 | signer, err := ssh.NewSignerFromSigner(NewCPSigner(a.cp, cert)) 53 | if err != nil { 54 | return nil, err 55 | } 56 | signers = append(signers, signer) 57 | } 58 | 59 | return 60 | } 61 | 62 | func (a *CertificateProviderBackend) listCertificates() ([]*x509.Certificate, error) { 63 | matches, err := a.cp.ListClientCertificates() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | certs := make([]*x509.Certificate, 0, len(matches)) 69 | for _, m := range matches { 70 | cert, err := x509.ParseCertificate(m) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | certs = append(certs, cert) 76 | } 77 | 78 | return certs, nil 79 | } 80 | -------------------------------------------------------------------------------- /go/io.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "io" 7 | "log" 8 | 9 | "github.com/gopherjs/gopherjs/js" 10 | ) 11 | 12 | var ErrInvalidMsg = errors.New("invalid message frame") 13 | 14 | type AgentPort struct { 15 | p *js.Object 16 | inReader *io.PipeReader 17 | inWriter *io.PipeWriter 18 | outReader *io.PipeReader 19 | outWriter *io.PipeWriter 20 | } 21 | 22 | func NewAgentPort(p *js.Object) *AgentPort { 23 | ir, iw := io.Pipe() 24 | or, ow := io.Pipe() 25 | ap := &AgentPort{ 26 | p: p, 27 | inReader: ir, 28 | inWriter: iw, 29 | outReader: or, 30 | outWriter: ow, 31 | } 32 | ap.p.Get("onDisconnect").Call("addListener", func() { 33 | go ap.OnDisconnect() 34 | }) 35 | ap.p.Get("onMessage").Call("addListener", func(msg js.M) { 36 | go ap.OnMessage(msg) 37 | }) 38 | 39 | go ap.SendMessages() 40 | 41 | return ap 42 | } 43 | 44 | func (ap *AgentPort) OnDisconnect() { 45 | ap.inWriter.Close() 46 | } 47 | 48 | func (ap *AgentPort) OnMessage(msg js.M) { 49 | d, ok := msg["data"].([]interface{}) 50 | if !ok { 51 | log.Printf("Message did not contain Array data field: %v", msg) 52 | ap.p.Call("disconnect") 53 | return 54 | } 55 | 56 | framed := make([]byte, 4+len(d)) 57 | binary.BigEndian.PutUint32(framed, uint32(len(d))) 58 | 59 | for i, raw := range d { 60 | n, ok := raw.(float64) 61 | if !ok { 62 | log.Printf("Message contained non-numeric data: %v", msg) 63 | ap.p.Call("disconnect") 64 | return 65 | } 66 | 67 | framed[i+4] = byte(n) 68 | } 69 | 70 | _, err := ap.inWriter.Write(framed) 71 | if err != nil { 72 | log.Printf("Error writing to pipe: %v", err) 73 | ap.p.Call("disconnect") 74 | } 75 | } 76 | 77 | func (ap *AgentPort) Read(p []byte) (n int, err error) { 78 | return ap.inReader.Read(p) 79 | } 80 | 81 | func (ap *AgentPort) SendMessages() { 82 | for { 83 | l := make([]byte, 4) 84 | _, err := io.ReadFull(ap.outReader, l) 85 | if err != nil { 86 | log.Printf("Error reading from pipe: %v", err) 87 | ap.outReader.Close() 88 | return 89 | } 90 | length := binary.BigEndian.Uint32(l) 91 | 92 | data := make([]byte, length) 93 | _, err = io.ReadFull(ap.outReader, data) 94 | if err != nil { 95 | log.Printf("Error reading from pipe: %v", err) 96 | ap.outReader.Close() 97 | return 98 | } 99 | 100 | encoded := make(js.S, length) 101 | for i, b := range data { 102 | encoded[i] = float64(b) 103 | } 104 | 105 | ap.p.Call("postMessage", js.M{ 106 | "data": encoded, 107 | }) 108 | } 109 | } 110 | 111 | func (ap *AgentPort) Write(p []byte) (n int, err error) { 112 | return ap.outWriter.Write(p) 113 | } 114 | -------------------------------------------------------------------------------- /go/main.go: -------------------------------------------------------------------------------- 1 | 2 | package main 3 | 4 | import ( 5 | "log" 6 | 7 | "github.com/gopherjs/gopherjs/js" 8 | "golang.org/x/crypto/ssh/agent" 9 | ) 10 | 11 | 12 | func main() { 13 | var backend Backend 14 | // if platformKeysSupported() { 15 | backend = NewCertificateProviderBackend() 16 | // } else { 17 | // var err error 18 | // backend, err = NewChromeStorageBackend() 19 | // if err != nil { 20 | // log.Printf("Failed to create ChromeStorageAgent: %v", err) 21 | // return 22 | // } 23 | // } 24 | launch(NewAgent(backend)) 25 | } 26 | 27 | func launch(mga *Agent) { 28 | js.Global.Set("agent", js.MakeWrapper(mga)) 29 | 30 | log.Printf("Starting agent") 31 | js.Global.Get("chrome"). 32 | Get("runtime"). 33 | Get("onConnectExternal"). 34 | Call("addListener", func(port *js.Object) { 35 | p := NewAgentPort(port) 36 | go agent.ServeAgent(mga, p) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /go/signer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto" 5 | "crypto/elliptic" 6 | "crypto/x509" 7 | "errors" 8 | "io" 9 | 10 | "github.com/gopherjs/gopherjs/js" 11 | ) 12 | 13 | var ErrUnsupportedHash = errors.New("unsupported hash") 14 | 15 | type CPSigner struct { 16 | cp *CertificateProvider 17 | cert *x509.Certificate 18 | } 19 | 20 | func NewCPSigner(cp *CertificateProvider, cert *x509.Certificate) crypto.Signer { 21 | return &CPSigner{ 22 | cp: cp, 23 | cert: cert, 24 | } 25 | } 26 | 27 | func (cps *CPSigner) Public() crypto.PublicKey { 28 | return cps.cert.PublicKey 29 | } 30 | 31 | // Limited to just the hashes supported by WebCrypto 32 | var hashPrefixes = map[crypto.Hash][]byte{ 33 | crypto.SHA1: {0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 0x04, 0x14}, 34 | crypto.SHA256: {0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20}, 35 | crypto.SHA384: {0x30, 0x41, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05, 0x00, 0x04, 0x30}, 36 | crypto.SHA512: {0x30, 0x51, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05, 0x00, 0x04, 0x40}, 37 | crypto.Hash(0): {}, // Special case in the golang interface to indicate that data is signed directly 38 | } 39 | 40 | var hashNames = map[crypto.Hash]string{ 41 | crypto.SHA1: "SHA1", 42 | crypto.SHA256: "SHA256", 43 | crypto.SHA384: "SHA384", 44 | crypto.SHA512: "SHA512", 45 | crypto.Hash(0): "none", 46 | } 47 | 48 | var curveNames = map[elliptic.Curve]string{ 49 | elliptic.P256(): "P-256", 50 | elliptic.P384(): "P-384", 51 | elliptic.P521(): "P-521", 52 | } 53 | 54 | func (cps *CPSigner) Sign(rand io.Reader, msg []byte, opts crypto.SignerOpts) (signature []byte, err error) { 55 | hash := hashNames[opts.HashFunc()] 56 | 57 | signRequest := js.M{ 58 | "digest": js.NewArrayBuffer(msg), 59 | "hash": hash, 60 | "certificate": js.NewArrayBuffer(cps.cert.Raw), 61 | } 62 | return cps.cp.Sign(signRequest) 63 | } 64 | -------------------------------------------------------------------------------- /go/storage_backend.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gopherjs/gopherjs/js" 7 | "golang.org/x/crypto/ssh" 8 | "golang.org/x/crypto/ssh/agent" 9 | ) 10 | 11 | type ChromeStorageBackend struct { 12 | signer ssh.Signer 13 | } 14 | 15 | func NewChromeStorageBackend() (*ChromeStorageBackend, error) { 16 | storage := js.Global.Get("window").Get("localStorage") 17 | rawPemStr := storage.Get("privateKey").String() 18 | if rawPemStr == "undefined" { 19 | return nil, errors.New("No key stored in local storage.") 20 | } 21 | rawPem := []byte(rawPemStr) 22 | signer, err := ssh.ParsePrivateKey(rawPem) 23 | if err != nil { 24 | return nil, err 25 | } 26 | agent := &ChromeStorageBackend{signer} 27 | return agent, nil 28 | } 29 | 30 | func (a *ChromeStorageBackend) List() ([]*agent.Key, error) { 31 | keys := make([]*agent.Key, 0, 1) 32 | pubkey := a.signer.PublicKey() 33 | keys = append(keys, &agent.Key{ 34 | Format: pubkey.Type(), 35 | Blob: pubkey.Marshal(), 36 | Comment: "", 37 | }) 38 | return keys, nil 39 | } 40 | 41 | func (a *ChromeStorageBackend) Signers() (signers []ssh.Signer, err error) { 42 | return []ssh.Signer{a.signer}, nil 43 | } 44 | --------------------------------------------------------------------------------