Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(793)

Side by Side Diff: bin/entdwife.sh

Issue 3541011: entd: Add policy signatures (Closed) Base URL: http://git.chromium.org/git/entd.git
Patch Set: Address review comments Created 10 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « base_policy/policy-utils.js ('k') | bin/fix_pkcs11_token.sh » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 #!/bin/sh
2 # Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 # This script is temporary front-end to entd. It validates the policy's
7 # signature before starting the daemon. If the signing certificate or
8 # signature to not validate, then this script will log an error to syslog
9 # and exit without starting entd.
10 #
11
12 if [ -z "$HOME" ]; then
13 HOME=/home/chronos/user
14 fi
15
16 SCRIPT="$(basename $0)"
17 SIGNING_CERT="signing-cert.pem"
18 SIGNED_DIGEST="signed-digest.sha1"
19 ROOT_CA_FILE="root-ca.pem"
20 REQUIRED_TO_SIGN="isa-cros-policy manifest.json policy.js $SIGNING_CERT"
21 REQUIRED_TO_START="$REQUIRED_FOR_SIGN $SIGNED_DIGEST"
22
23 . "/usr/lib/shflags" || (echo "error loading shflags"; exit 1)
24
25 DEFINE_string command "" \
26 "Command to run; One of: sign, verify, or start" "c"
27 DEFINE_boolean verify_signature $FLAGS_TRUE \
28 "Verify the authenticity of the signature" "v"
29 DEFINE_boolean allow_self_signed $FLAGS_FALSE \
30 "Allow self signed certificates" "S"
31 DEFINE_string extension "" \
32 "Location of the enterprise extension" "x"
33 DEFINE_string extension_path "${HOME}/Extensions" \
34 "Location to search for extensions when --extension is not provided" "p"
35 DEFINE_string signing_key "" \
36 "Location of the private key to use when signing." "K"
37 DEFINE_string crx_key "" \
38 "Location of the private key to use when creating the crx file." "k"
39 DEFINE_string entd "/usr/sbin/entd" \
40 "Path to the entd binary" "d"
41 DEFINE_string utility "/etc/entd/base-policy.js" \
42 "Path to policy utility file" "t"
43 DEFINE_string username "$CHROMEOS_USER" \
44 "ChromeOS Username (full email address)." "u"
45 DEFINE_string output "$(pwd)" \
46 "Destination directory or filename for crx files" "o"
47
48 FLAGS "$@" || exit $?
49 eval set -- "${FLAGS_ARGV}"
50
51 main() {
52 local extension="$FLAGS_extension"
53
54 if [ -z "$FLAGS_command" ]; then
55 log "Missing option: --command"
56 exit 1
57 fi
58
59 if [ -z "$extension" ]; then
60 # User didn't pick a particular extension, search one out.
61 extension="$(find_extension "$FLAGS_extension_path")"
62 if [ -z "$extension" ]; then
63 return 1
64 fi
65 fi
66
67 log "Using extension: $extension"
68
69 local result
70 if [ "$FLAGS_command" = "makecrx" ]; then
71 # Sign an extension.
72 cmd_makecrx "$extension" "$FLAGS_crx_key" "$FLAGS_signing_key"
73 result=$?
74
75 elif [ "$FLAGS_command" = "verify" ]; then
76 # Verify a signature without starting entd.
77 cmd_verify "$extension"
78 result=$?
79 if [ "$result" = "0" ]; then
80 log "Extension verified: $extension"
81 fi
82
83 elif [ "$FLAGS_command" = "start" ]; then
84 # Verify the signature, then start entd.
85 cmd_start "$extension"
86 result=$?
87
88 else
89 log "Unknown command: $FLAGS_command"
90 result=1
91 fi
92
93 return $result
94 }
95
96 cmd_makecrx() {
97 local extension="$1"
98 local crx_key="$2"
99 local signing_key="$3"
100
101 if [ ! -d "$extension" ]; then
102 log "Missing or invalid extension: $extension"
103 return 1
104 fi
105
106 if [ ! -f "$crx_key" ]; then
107 log "Missing or invalid crx key: $crx_key"
108 return 1
109 fi
110
111 if [ ! -f "$signing_key" ]; then
112 log "Missing or invalid signing key: $signing_key"
113 return 1
114 fi
115
116 local chrome="$(which google-chrome)"
117 if [ ! -x "$chrome" ]; then
118 log "Unable to locate google-chrome executable"
119 return 1
120 fi
121
122 local workdir="$(mktemp -d)"
123 echo "workdir: $workdir"
124
125 local signed="$workdir/signed"
126 mkdir -p "$signed"
127 cp -rL $extension/* "$signed"
128 rm $(find "$signed" -name '*~')
129
130 if ! cmd_sign "$signed" "$signing_key"; then
131 log "Error signing"
132 return 1
133 fi
134
135 local userdata="$workdir/userdata"
136 mkdir -p "$userdata"
137
138 "$chrome" --pack-extension="$signed" \
139 --pack-extension-key="$crx_key" \
140 --user-data-dir="$userdata" --no-message-box --no-first-run
141
142 if [ ! -f "$workdir/signed.crx" ]; then
143 log "Error creating crx"
144 return 1
145 fi
146
147 local output="$FLAGS_output"
148
149 if [ -d "$output" ]; then
150 local cn="$(get_cn "$extension")"
151 local version="$(get_version "$extension")"
152 output="$output/$cn-policy-$version.crx"
153 fi
154
155 mv "$workdir/signed.crx" "$output"
156 chmod a+r "$output"
157
158 log "CRX file written to: $output"
159
160 rm -rf "$workdir"
161 }
162
163 cmd_sign() {
164 local extension="$1"
165 local private_key="$2"
166
167 log "Signing: $extension"
168
169 if [ -z "$private_key" ]; then
170 log "Missing private key"
171 return 1
172 fi
173
174 if ! verify_files "$extension" "$REQUIRED_TO_SIGN"; then
175 log "Invalid extension: $extension"
176 return 1
177 fi
178
179 local catfile="$(make_catfile "$extension")"
180 if [ -z "$catfile" ]; then
181 return 1
182 fi
183
184 local output
185 output="$(openssl dgst -sha1 -sign "$private_key" \
186 -out "$extension/$SIGNED_DIGEST" "$catfile" 2>&1)"
187 local result=$?
188
189 rm "$catfile"
190
191 # If openssl exited with code 0 and the signature file is not zero length,
192 # then were probably successful.
193 if [ $result = 0 -a -s "$extension/$SIGNED_DIGEST" ]; then
194 cmd_verify "$extension"
195 return $?
196 fi
197
198 log "Error signing extension: $output"
199 return 1
200 }
201
202 cmd_verify() {
203 local extension="$1"
204
205 log "Verifying: $extension"
206
207 if ! (verify_files "$extension" "$REQUIRED_TO_START"); then
208 log "Invalid extension: $extension"
209 return 1
210 fi
211
212 if [ "$FLAGS_verify_signature" = "$FLAGS_TRUE" ]; then
213 if ! verify_cert "$extension"; then
214 return 1
215 fi
216
217 if ! verify_signature "$extension"; then
218 return 1
219 fi
220
221 else
222 log "WARNING: Not verifying extension certificate"
223 fi
224 }
225
226 cmd_start() {
227 local extension="$1"
228
229 if ! cmd_verify "$extension"; then
230 return 1
231 fi
232
233 local cn="$(get_cn "$extension")"
234
235 local hostname="$(echo "$FLAGS_username" | cut -f2 -d@)"
236
237 if [ "$cn" != "$hostname" ]; then
238 log "Certificate CN '$cn' does not match expected hostname '$hostname'"
239 return 1
240 fi
241
242 local root_ca_option=""
243 if [ -f "$extension/$ROOT_CA_FILE" ]; then
244 root_ca_option="--root-ca-file=$ROOT_CA_FILE"
245 fi
246
247 exec "$FLAGS_entd" --utility="$FLAGS_utility" "$root_ca_option" \
248 --policy="$extension/policy.js" --manifest="$extension/manifest.json" \
249 --username="$FLAGS_username"
250 }
251
252 log() {
253 if [ -t 2 ]; then
254 echo "$@" >&2
255 else
256 logger -t SCRIPT -- "$@"
257 fi
258 }
259
260 abspath() {
261 if [ -d "$1" ]; then
262 echo "$(cd $1; pwd)"
263 else
264 echo "$(cd $(dirname "$1"); pwd)/$(basename $1)"
265 fi
266 }
267
268 get_cn() {
269 local extension="$1"
270
271 # Extract the CN from the certificate.
272 openssl x509 -in "$extension/$SIGNING_CERT" -noout -subject | \
273 sed 's:.*/CN=\([^/]\+\).*:\1:'
274 }
275
276 get_version() {
277 local extension="$1"
278
279 cat "$extension/manifest.json" | sed -n 's/\s*"version":\s*"\([^"]\+\).*/\1/p'
280 }
281
282 # Search through the given path for one and only one enterprise extension.
283 find_extension() {
284 local path="$1"
285
286 if [ ! -d "$path" ]; then
287 log "Extension path is not a directory: $path"
288 return
289 fi
290
291 local candidates="$(find -L "$path" -mindepth 3 -maxdepth 3 \
292 -name isa-cros-policy)"
293
294 if [ -z "$candidates" ]; then
295 log "No enterprise policy extension found in $path";
296 return
297 fi
298
299
300 if [ "$(echo "$candidates" | wc -w)" != "1" ]; then
301 log "Multiple enterprise policies found in $path: $candidates";
302 return
303 fi
304
305 dirname "$candidates"
306 }
307
308 # Verifies that the extension contains a list of files. If the list is not
309 # specified, assume they mean the list of files require to start entd with
310 # this extension.
311 verify_files() {
312 local extension="$1"
313 local required_files="$2"
314
315 if [ -z "$required_files" ]; then
316 required_files="$REQUIRED_TO_START"
317 fi
318
319 local err=0
320
321 for file in $required_files; do
322 if [ ! -f "$extension/$file" ]; then
323 log "Missing $extension/$file";
324 err=1
325 fi
326 done
327
328 return $err
329 }
330
331 # The "catfile" is the concatenation of all of the files from the extension
332 # into a single file.
333 #
334 # We locate all regular files in the extension, excluding any previous
335 # signature file, sort them in ascending order. Then for each file, we
336 # append the filename and contents to the catfile.
337 #
338 # The hash of this file is what is actually signed.
339 #
340 make_catfile() {
341 local extension="$1"
342
343 local normalized_manifest="$(normalize_manifest "$extension")"
344 if [ -z "$normalized_manifest" ]; then
345 return 1
346 fi
347
348 cd "$extension" > /dev/null
349 local filenames="$(find . -type f -not -wholename "./$SIGNED_DIGEST" | sort)"
350 cd - > /dev/null
351
352 if [ -z "$filenames" ]; then
353 log "Error enumerating files"
354 return 1
355 fi
356
357 local catfile="$(mktemp)"
358 for filename in $filenames; do
359 echo $filename >> "$catfile"
360 if [ "$filename" = "./manifest.json" ]; then
361 echo "$normalized_manifest" >> "$catfile"
362 else
363 cat "$extension/$filename" >> "$catfile"
364 fi
365 done
366
367
368 echo "$catfile"
369 }
370
371 # Verify that a certificate is valid.
372 verify_cert() {
373 local extension="$1"
374
375 local output="$(openssl verify "$extension/$SIGNING_CERT")"
376
377 if [ "$FLAGS_allow_self_signed" = "$FLAGS_TRUE" ]; then
378 log "WARNING: Allowing self signed certificates"
379 output="$(echo $output | grep -v "self signed certificate")"
380 fi
381
382 if [ ! -z "$(echo "$output" | grep "error")" ]; then
383 # `openssl verify` exits with a success code even if verification fails,
384 # so we have to grep the output to determine the result of verification.
385 log "Unable to verify certificate: $output"
386 return 1
387 fi
388 }
389
390 # Verify the signature of a policy.
391 verify_signature() {
392 local extension="$1"
393
394 # This file contains the signature of the extension.
395 local sigfile="$extension/$SIGNED_DIGEST"
396
397 local catfile="$(make_catfile "$extension")"
398
399 if [ -z "$catfile" ]; then
400 return 1
401 fi
402
403 # Extract the public key from the certificate.
404 local keyfile="$(mktemp)"
405
406 log "Extracting public key from $extension/$SIGNING_CERT"
407 openssl x509 -in "$extension/$SIGNING_CERT" -pubkey -noout > "$keyfile"
408 local result=$?
409
410 if [ "$result" != "0" ]; then
411 log "Error extracting public key from certificate"
412 rm "$catfile"
413 return $result
414 fi
415
416 if [ ! -f "$keyfile" ]; then
417 log "Unknown error extracting public key from certificate"
418 return 1
419 fi
420
421 # Declare $output before assigning, so we don't lose the return code of
422 # the openssl command.
423 local output
424
425 # Verify the signature.
426 output="$(openssl dgst -sha1 -verify "$keyfile" \
427 -signature "$extension/$SIGNED_DIGEST" "$catfile" 2>&1)"
428 result=$?
429
430 # Clean up the temporary files.
431 rm "$catfile" "$keyfile"
432
433 if [ "$result" != "0" ]; then
434 log "Signature $sigfile did not validate: $output"
435 else
436 log "Signature $sigfile is valid"
437 fi
438
439 return $result
440 }
441
442 # Chrome takes liberties with the manifest file when it installs an extension.
443 # It deserializes it, adds the "key" if it is missing, and then re-serializes
444 # it. There is no guaranteed ordering for the keys when the file is finally
445 # written to disk. This royally screws up our recalculation of the signature.
446 #
447 # So, instead of signing the actual manifest source file, we pass it through
448 # a normalization procedure where we impose a guaranteed ordering on the keys,
449 # and remove the extra key (called "key") that Chrome adds during installation.
450 #
451 # For example, given { "name": "Foo", "browser": { "hello": "world" } }, we
452 # return:
453 #
454 # manifest.browser.hello = "world"
455 # manifest.name = "Foo"
456 #
457 # If we can, we use a small Python script to do the normalization. If Python
458 # isn't available (say, on the device) then we use entd itself.
459 #
460 #
461 # TODO(rginda): Do better.
462 normalize_manifest() {
463 local python="$(which python)"
464
465 if [ -x "$python" ]; then
466 normalize_manifest_py "$@"
467 elif [ -x "$FLAGS_entd" ]; then
468 normalize_manifest_js "$@"
469 else
470 log "Can't find python or entd, unable to normalize extension manifest"
471 return 1
472 fi
473 }
474
475 normalize_manifest_py() {
476 local extension="$1"
477
478 local scriptfile="$(mktemp)"
479 cat <<EOF > "$scriptfile"
480 import sys
481
482 try:
483 import json
484 except ImportError:
485 json = None
486
487 if not json or not hasattr(json, 'loads'):
488 # The contemporary version of the json module wasn't included until
489 # Python 2.6. If you want to build a policy extension on an earlier version,
490 # you need to install the simplejson module.
491 import simplejson
492 json = simplejson
493
494 def to_str(name, value, ary = []):
495 if type(value) == list:
496 ary.append(name + ": []");
497 for i in xrange(len(value)):
498 to_str(name + "." + str(i), value[i], ary);
499 elif type(value) == dict:
500 ary.append(name + ": {}");
501 for key in value.keys():
502 to_str(name + "." + key, value[key], ary);
503 else:
504 ary.append(name + ": " + json.dumps(value));
505
506 return ary
507
508 if __name__ == '__main__':
509 fp = open(sys.argv[1])
510 d = json.load(fp)
511 # Remove the "key" possibly added by Chrome.
512 if 'key' in d:
513 del d['key']
514 ary = to_str('manifest', d)
515 ary.sort()
516 ary.reverse()
517 print '\n'.join(ary)
518
519 EOF
520
521 python "$scriptfile" "$extension/manifest.json"
522
523 rm "$scriptfile"
524 }
525
526 normalize_manifest_js() {
527 local extension="$1"
528
529 local scriptfile="$(mktemp)"
530 cat <<EOF > "$scriptfile"
531 entd.onLoad = function(m) {
532 // Remove the "key" possibly added by Chrome.
533 delete m.key;
534 var ary = str("manifest", m);
535 // Sort the results so we can determine an ordering.
536 println(ary.sort().reverse().join("\n"));
537 }
538
539 function str(name, value, ary) {
540 ary = ary || [];
541
542 if (value instanceof Array) {
543 ary.push(name + ": []");
544 for (var i = 0; i < value.length; ++i)
545 str(name + "." + i, value[i], ary);
546 } else if (value instanceof Object) {
547 ary.push(name + ": {}");
548 for (var key in value)
549 str(name + "." + key, value[key], ary);
550 } else {
551 ary.push(name + ": " + JSON.stringify(value));
552 }
553
554 return ary
555 }
556 EOF
557
558 "$FLAGS_entd" --policy="$scriptfile" --manifest="$extension/manifest.json" \
559 --username=user@example.com --allow-dirty-exit 2>/dev/null
560
561 rm "$scriptfile"
562 }
563
564 main "$@"
OLDNEW
« no previous file with comments | « base_policy/policy-utils.js ('k') | bin/fix_pkcs11_token.sh » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698