@@ -3,22 +3,35 @@ package outtune
3
3
import (
4
4
"bufio"
5
5
"context"
6
+ "crypto/rand"
7
+ "crypto/rsa"
8
+ "crypto/x509"
9
+ "encoding/pem"
10
+ "errors"
6
11
"fmt"
12
+ "io/fs"
7
13
"os"
8
14
"os/exec"
15
+ "path/filepath"
9
16
"regexp"
10
- "strings"
11
17
12
18
"github.com/nais/device/pkg/pb"
13
19
log "github.com/sirupsen/logrus"
14
20
)
15
21
16
- const aadLoginURL = "https://nav-no.managed.us2.access-control.cas.ms/aad_login"
22
+ const (
23
+ aadLoginURL = "https://nav-no.managed.us2.access-control.cas.ms/aad_login"
24
+ certPath = "Library/Application Support/naisdevice/browser_cert_pubkey.pem"
25
+ )
17
26
18
27
type darwin struct {
19
28
helper pb.DeviceHelperClient
20
29
}
21
30
31
+ type certresponse struct {
32
+ CertPem string `json:"cert_pem"`
33
+ }
34
+
22
35
func New (helper pb.DeviceHelperClient ) Outtune {
23
36
return & darwin {
24
37
helper : helper ,
@@ -32,71 +45,69 @@ func (o *darwin) Cleanup(ctx context.Context) error {
32
45
return err
33
46
}
34
47
35
- // find identities in Mac OS X keychain for this serial
36
- identities , err := identities (ctx , serial .GetSerial ())
48
+ // find certificates in Mac OS X keychain for this serial
49
+ certificates , err := certificates (ctx , serial .GetSerial ())
37
50
if err != nil {
38
51
return err
39
52
}
40
53
41
54
// remove identities
42
- for _ , certificateSerial := range identities {
43
- cmd := exec .CommandContext (ctx , "/usr/bin/security" , "delete-identity " , "-Z" , certificateSerial , "-t" )
55
+ for _ , certificateSerial := range certificates {
56
+ cmd := exec .CommandContext (ctx , "/usr/bin/security" , "delete-certificate " , "-Z" , certificateSerial )
44
57
err = cmd .Run ()
45
58
if err != nil {
46
59
log .Errorf ("unable to delete certificate and private key from keychain: %s" , err )
47
60
} else {
48
- log .Debugf ("deleted identity '%s' from keychain" , certificateSerial )
61
+ log .Debugf ("deleted certificate '%s' from keychain" , certificateSerial )
49
62
}
50
63
}
51
64
52
65
return nil
53
66
}
54
67
55
68
func (o * darwin ) Install (ctx context.Context ) error {
56
- o .Cleanup (ctx )
57
-
58
69
serial , err := o .helper .GetSerial (ctx , & pb.GetSerialRequest {})
59
70
if err != nil {
60
71
return err
61
72
}
62
73
63
- id , err := generateKeyAndCertificate ( ctx , serial . GetSerial () )
74
+ home , err := os . UserHomeDir ( )
64
75
if err != nil {
65
- return err
76
+ return fmt . Errorf ( "could not determine user home directory: %v" , err )
66
77
}
67
78
68
- w , err := os .CreateTemp (os .TempDir (), "naisdevice-" )
69
- if err != nil {
70
- return err
71
- }
72
- defer w .Close ()
73
- defer os .Remove (w .Name ())
79
+ pubKeyPath := filepath .Join (home , certPath )
74
80
75
- // Write key+certificate pair to disk in PEM format
76
- err = id . SerializePEM ( w )
81
+ var pk * rsa. PrivateKey
82
+ _ , err = os . Stat ( pubKeyPath )
77
83
if err != nil {
78
- return err
84
+ if ! errors .Is (err , fs .ErrNotExist ) {
85
+ return err
86
+ }
87
+
88
+ pk , err = o .generate (ctx , serial .GetSerial (), pubKeyPath )
89
+ if err != nil {
90
+ return err
91
+ }
92
+ } else {
93
+ o .Cleanup (ctx )
79
94
}
80
95
81
- // flush contents to disk
82
- err = w .Close ()
96
+ resp , err := o .download_cert (ctx , serial .GetSerial (), pubKeyPath )
83
97
if err != nil {
84
98
return err
85
99
}
86
-
87
- // run Mac OS X keychain import tool
88
- cmd := exec .CommandContext (ctx , "/usr/bin/security" , "import" , w .Name (), "-A" )
89
- err = cmd .Run ()
100
+ err = o .importIdentity (ctx , pk , resp .CertificatePEM )
90
101
if err != nil {
91
102
return err
92
103
}
93
104
94
- currentIdentities , err := identities (ctx , serial .GetSerial ())
95
- if err != nil {
105
+ currentIdentities , err := certificates (ctx , serial .GetSerial ())
106
+ if err != nil || len ( currentIdentities ) == 0 {
96
107
return fmt .Errorf ("unable to find identity in keychain: %s" , err )
97
108
}
98
109
99
- cmd = exec .CommandContext (ctx , "/usr/bin/security" , "set-identity-preference" , "-Z" , currentIdentities [0 ], "-s" , aadLoginURL )
110
+ cmd : = exec .CommandContext (ctx , "/usr/bin/security" , "set-identity-preference" , "-Z" , currentIdentities [0 ], "-s" , aadLoginURL )
100
111
err = cmd .Run ()
101
112
if err != nil {
102
113
log .Errorf ("set-identity-preference: %s" , err )
@@ -105,9 +116,70 @@ func (o *darwin) Install(ctx context.Context) error {
105
116
return nil
106
117
}
107
118
108
- func identities (ctx context.Context , serial string ) ([]string , error ) {
119
+ func (o * darwin ) download_cert (ctx context.Context , serial , pubKeyPath string ) (* response , error ) {
120
+ publicKey , err := os .ReadFile (pubKeyPath )
121
+ if err != nil {
122
+ return nil , err
123
+ }
124
+
125
+ return downloadWithPublicKey (ctx , serial , publicKey )
126
+ }
127
+
128
+ func (o * darwin ) generate (ctx context.Context , serial , pubKeyPath string ) (* rsa.PrivateKey , error ) {
129
+ privateKey , err := rsa .GenerateKey (rand .Reader , entropyBits )
130
+ if err != nil {
131
+ return nil , err
132
+ }
133
+
134
+ publicKey , err := x509 .MarshalPKIXPublicKey (& privateKey .PublicKey )
135
+ if err != nil {
136
+ return nil , err
137
+ }
138
+
139
+ return privateKey , os .WriteFile (pubKeyPath , publicKey , 0o644 )
140
+ }
141
+
142
+ func (o * darwin ) importIdentity (ctx context.Context , privateKey * rsa.PrivateKey , certificate string ) error {
143
+ w , err := os .CreateTemp (os .TempDir (), "naisdevice-" )
144
+ if err != nil {
145
+ return err
146
+ }
147
+ defer w .Close ()
148
+ defer os .Remove (w .Name ())
149
+
150
+ if privateKey != nil {
151
+ pem .Encode (w , & pem.Block {
152
+ Type : "RSA PRIVATE KEY" ,
153
+ Bytes : x509 .MarshalPKCS1PrivateKey (privateKey ),
154
+ })
155
+ }
156
+
157
+ block , rest := pem .Decode ([]byte (certificate ))
158
+ if len (rest ) > 0 {
159
+ log .Warnf ("certificate had remaining input which was ignored" )
160
+ }
161
+ cert , err := x509 .ParseCertificate (block .Bytes )
162
+ if err != nil {
163
+ return err
164
+ }
165
+ pem .Encode (w , & pem.Block {
166
+ Type : "CERTIFICATE" ,
167
+ Bytes : cert .Raw ,
168
+ })
169
+
170
+ // flush contents to disk
171
+ err = w .Close ()
172
+ if err != nil {
173
+ return err
174
+ }
175
+ // run Mac OS X keychain import tool
176
+ cmd := exec .CommandContext (ctx , "/usr/bin/security" , "import" , w .Name (), "-A" )
177
+ return cmd .Run ()
178
+ }
179
+
180
+ func certificates (ctx context.Context , serial string ) ([]string , error ) {
109
181
id := "naisdevice - " + serial
110
- cmd := exec .CommandContext (ctx , "/usr/bin/security" , "find-identity " , "-s " , id )
182
+ cmd := exec .CommandContext (ctx , "/usr/bin/security" , "find-certificate " , "-c " , id , "-Z" )
111
183
stdout , err := cmd .StdoutPipe ()
112
184
if err != nil {
113
185
return nil , err
@@ -120,15 +192,15 @@ func identities(ctx context.Context, serial string) ([]string, error) {
120
192
}
121
193
122
194
idMap := make (map [string ]struct {})
123
- re := regexp .MustCompile (" [A-Za-z0-9]{40}" )
195
+ re := regexp .MustCompile (`SHA-1 hash:\s( [A-Za-z0-9]{40})` )
124
196
scan := bufio .NewScanner (stdout )
125
197
for scan .Scan () {
126
198
line := scan .Text ()
127
- certificateID := re .FindString (line )
128
- if len (certificateID ) == 0 || ! strings . Contains ( line , "naisdevice" ) {
199
+ matches := re .FindAllStringSubmatch (line , 1 )
200
+ if len (matches ) == 0 {
129
201
continue
130
202
}
131
- idMap [certificateID ] = struct {}{}
203
+ idMap [matches [ 0 ][ 1 ] ] = struct {}{}
132
204
}
133
205
134
206
err = cmd .Wait ()
0 commit comments