###############################################################################
#
# sign.eagle --
#
# Extensible Adaptable Generalized Logic Engine (Eagle)
# Enterprise Edition Signing Tool
#
# Copyright (c) 2007-2012 by Joe Mistachkin. All rights reserved.
#
# See the file "license.terms" for information on usage and redistribution of
# this file, and for a DISCLAIMER OF ALL WARRANTIES.
#
# RCS: @(#) $Id: $
#
###############################################################################
proc usage { error } {
if {[string length $error] > 0} then {puts stdout $error}
puts stdout "usage:\
[file tail [info nameofexecutable]]\
[file tail [info script]] <fileName> \[vendor\] \[embed\]\
\[duration\] \[entityType\] \[encoding\] \[publicKeyFile\]\
\[privateKeyFile\]"
#
# NOTE: Indicate to the caller, if any, that we have failed.
#
exit 1
}
proc isScriptFile { fileName } {
return [expr {
[file extension $fileName] eq ".tcl" || \
[file extension $fileName] eq ".tk" || \
[file extension $fileName] eq ".test" || \
[file extension $fileName] eq ".eagle"
}]
}
proc isKeyRingFile { fileName } {
if {[file extension $fileName] ne ".eagle"} then {
return false
}
set rootName [file rootname [file tail $fileName]]
if {$rootName eq "keyRing" || \
[string match "keyRing*" $rootName]} then {
return true
}
return false
}
proc checkAndMatchKeyFile { varName {keyFile ""} } {
global env
if {[info exists env($varName)]} then {
set fileName $env($varName)
if {[string length $fileName] > 0} then {
if {[file exists $fileName]} then {
if {[catch {keypair token $fileName} token] == 0 && \
[string length $token] > 0} then {
if {[string length $keyFile] == 0 || \
[matchKeyFileTokens $fileName $keyFile]} then {
return true
} else {
puts stdout [appendArgs \
"file \"" $fileName \
" mismatches public key token from file \"" \
$keyFile \"]
}
} else {
puts stdout [appendArgs \
"file \"" $fileName "\" is probably not a key file"]
}
} else {
puts stdout [appendArgs \
"file \"" $fileName "\" does not exist"]
}
} else {
puts stdout [appendArgs \
"environment variable \"" $varName "\" has no value"]
}
} else {
puts stdout [appendArgs \
"environment variable \"" $varName "\" was not found"]
}
return false
}
proc matchKeyFileTokens { keyFile1 keyFile2 } {
if {[string length $keyFile1] == 0 || \
[string length $keyFile2] == 0} then {
return false
}
if {![file exists $keyFile1] || ![file exists $keyFile2]} then {
return false
}
set token1 [keypair token $keyFile1]
if {[string length $token1] == 0} then {
return false
}
set token2 [keypair token $keyFile2]
if {[string length $token2] == 0} then {
return false
}
return [expr {$token1 eq $token2}]
}
proc readEntityValue { fileName } {
return [readFile $fileName]
}
proc readCertificateFile { fileName } {
return [readFile $fileName]
}
proc writeCertificateFile { fileName data } {
return [writeFile $fileName $data]
}
proc removeEmbeddedCertificate { fileName } {
set prefixWithSpacing [appendArgs $::embedSpacing $::embedPrefix]
set data [readFile $fileName]
set beginIndex [string first $prefixWithSpacing $data]
if {$beginIndex == -1} then {
set prefixWithSpacing $::embedPrefix
set beginIndex [string first $prefixWithSpacing $data]
}
if {$beginIndex != -1} then {
set endIndex [string first $::embedSuffix $data \
[expr {$beginIndex + [string length $prefixWithSpacing]}]]
if {$endIndex != -1} then {
writeFile $fileName [string replace $data $beginIndex \
[expr {$endIndex + [string length $::embedSuffix]}]]
puts stdout [appendArgs \
"removed embedded certificate from file \"" $fileName \"]
}
}
}
if {[llength $argv] >= 1 && [llength $argv] <= 8} then {
#
# NOTE: This tool requires Eagle.
#
package require Eagle
#
# NOTE: Needed for the [getTemporaryPath] script procedure.
#
package require Eagle.Test
#
# NOTE: This tool requires the enterprise license features.
#
package require Licensing.Enterprise
#
# NOTE: If the tool base path does not already exist, set it
# to the directory where this script is running from.
#
if {![info exists path]} then {
set path [file normalize [file dirname [info script]]]
set path_set true
}
#
# NOTE: Grab the first argument to this tool (the name of the
# data file to sign).
#
set fileName [file normalize [lindex $argv 0]]
#
# NOTE: Grab the second argument to this tool (the name of the
# certificate vendor).
#
if {[llength $argv] >= 2} then {
set vendor [lindex $argv 1]
} else {
set vendor ""
}
#
# NOTE: Grab the third argument to this tool (the embed flag).
#
if {[llength $argv] >= 3} then {
set embed [lindex $argv 2]
} else {
set embed false; # TODO: Good default?
}
#
# NOTE: Grab the fourth argument to this tool (the valid duration
# of the certificate).
#
if {[llength $argv] >= 4} then {
set duration [lindex $argv 3]
} else {
set duration -1; # NOTE: Forever.
}
#
# NOTE: Grab the fifth argument to this tool (the entity type
# of the certificate).
#
if {[llength $argv] >= 5} then {
#
# NOTE: Use the specified entity type verbatim.
#
set entityType [lindex $argv 4]
} elseif {[isKeyRingFile $fileName]} then {
#
# NOTE: This should be a signed key ring script.
#
set entityType KeyRing; # NOTE: Could also be "Any".
} elseif {[isScriptFile $fileName] || \
[file extension $fileName] eq ".harpy"} then {
#
# NOTE: *LEGACY* This should be a signed script.
#
set entityType Script; # NOTE: Could also be "Any".
} else {
#
# NOTE: This is an arbitrary data file of some kind.
#
set entityType File; # NOTE: Could also be "Any".
}
#
# NOTE: Figure out which encoding to use.
#
if {[llength $argv] >= 6} then {
#
# NOTE: Use the specified encoding verbatim.
#
set encoding [lindex $argv 5]
} else {
#
# NOTE: Use the default encoding.
#
set encoding null
}
#
# NOTE: Figure out the file name for the private strong name key
# file that will be used to sign the data file. Now, this
# is checked first so the selected public key can be based
# on it.
#
if {[llength $argv] >= 8} then {
#
# NOTE: Use the private key file name supplied on the command
# line.
#
set privateKeyFile [string trim [lindex $argv 7]]
} elseif {[isKeyRingFile $fileName] && \
[checkAndMatchKeyFile EagleEnterpriseTrustRootPrivateKey]} then {
set privateKeyFile $env(EagleEnterpriseTrustRootPrivateKey)
} elseif {[checkAndMatchKeyFile EagleEnterpriseScriptPrivateKey]} then {
set privateKeyFile $env(EagleEnterpriseScriptPrivateKey)
} elseif {[checkAndMatchKeyFile EagleEnterprisePersonalPrivateKey]} then {
set privateKeyFile $env(EagleEnterprisePersonalPrivateKey)
} elseif {[checkAndMatchKeyFile EagleEnterprisePrivateKey]} then {
set privateKeyFile $env(EagleEnterprisePrivateKey)
} elseif {[info exists env(Eagle)]} then {
set privateKeyFile [file join \
$env(Eagle) Keys EagleEnterprisePluginRootPrivate.snk]
} else {
#
# NOTE: Default to "EagleEnterprisePluginRootPrivate.snk" in the current
# directory (which may not actually exist).
#
set privateKeyFile EagleEnterprisePluginRootPrivate.snk
}
#
# NOTE: Figure out the file name for the public strong name key
# file that will be used to verify the data file.
#
if {[llength $argv] >= 7} then {
#
# NOTE: Use the public key file name supplied on the command
# line.
#
set publicKeyFile [string trim [lindex $argv 6]]
} elseif {[checkAndMatchKeyFile EagleEnterpriseTrustRootPublicKey \
$privateKeyFile]} then {
set publicKeyFile $env(EagleEnterpriseTrustRootPublicKey)
} elseif {[checkAndMatchKeyFile EagleEnterpriseScriptPublicKey \
$privateKeyFile]} then {
set publicKeyFile $env(EagleEnterpriseScriptPublicKey)
} elseif {[checkAndMatchKeyFile EagleEnterprisePersonalPublicKey \
$privateKeyFile]} then {
set publicKeyFile $env(EagleEnterprisePersonalPublicKey)
} elseif {[checkAndMatchKeyFile EagleEnterprisePublicKey \
$privateKeyFile]} then {
set publicKeyFile $env(EagleEnterprisePublicKey)
} elseif {[info exists env(Eagle)]} then {
set publicKeyFile [file join \
$env(Eagle) Keys EagleEnterprisePluginRootPublic.snk]
} else {
#
# NOTE: Default to "EagleEnterprisePluginRootPublic.snk" in the current
# directory (which may not actually exist).
#
set publicKeyFile EagleEnterprisePluginRootPublic.snk
}
#
# NOTE: If the configuration file exists, load it now.
#
set configFileName [file join $path sign.settings.eagle]
if {[file exists $configFileName]} then {source $configFileName}
#
# NOTE: The spacing to use before the embedded certificate. This
# must match up with the number of blank lines used with the
# [linsert] command used to help produce the final embedded
# certificate string (below).
#
set embedSpacing [info newline]
#
# NOTE: Setup the prefix and suffix strings used for embedded
# certificates.
#
set embedPrefix "# <<CERTIFICATE-1.0>>"
set embedSuffix "# <</CERTIFICATE-1.0>>"
#
# NOTE: Grab the public key we need to verify that our signing
# process worked correctly.
#
set publicKey [keypair open -alias -public $publicKeyFile]
#
# NOTE: Grab the private key we need to actually create the
# detached certificate for the data file.
#
set privateKey [keypair open -alias -public -private $privateKeyFile]
#
# NOTE: Has embedded certificate handling been requested?
# If so, make sure we can actually do it.
#
if {$embed && [isScriptFile $fileName]} then {
set shouldEmbed true
} else {
set shouldEmbed false
}
#
# HOOK: After all arguments have been parsed and processed.
#
catch {certificate_hook phase0}
#
# NOTE: The existing embedded certificate, if any, must be
# removed prior to signing the (script?) file.
#
if {$shouldEmbed} then {
removeEmbeddedCertificate $fileName
}
#
# NOTE: If the file is an XML file, assume it is a license
# certificate that we need to manually re-sign.
#
if {[file extension $fileName] eq ".harpy"} then {
#
# NOTE: Set the file type for error messages.
#
set fileType "certificate file"
#
# NOTE: In this case, the certificate file to export is the same
# as the input file name.
#
set certificateFile $fileName
#
# NOTE: Import the license certificate.
#
set certificate [certificate import $fileName]
#
# HOOK: Post-certificate object creation (import).
#
catch {certificate_hook phase1}
#
# NOTE: Attempt to re-sign the license certificate file. Skip
# setting the Id as it should already be set correctly.
#
if {[certificate sign -encoding $encoding -settimestamp \
-setkey $certificate $privateKey] ne "SignedOk"} then {
error [appendArgs \
"failed to create signature for " $fileType " \"" $fileName \"]
}
#
# NOTE: Attempt to re-verify the license certificate file.
#
if {[certificate verify -encoding $encoding $certificate \
$privateKey] ne "VerifiedOk"} then {
error [appendArgs \
"failed to verify signature for " $fileType " \"" $fileName \"]
}
} else {
#
# NOTE: Set the file type for error messages.
#
if {[isKeyRingFile $fileName]} then {
set fileType "key ring file"
} elseif {[isScriptFile $fileName]} then {
set fileType "script file"
} else {
set fileType "data file"
}
#
# NOTE: Build the name of the certificate file name (i.e. the
# file where the detached certificate information will be
# placed) based on the data file name.
#
set certificateFile [appendArgs $fileName .harpy]
#
# NOTE: Create an empty certificate object.
#
set certificate [object create -alias \
Licensing.Components.Public.Certificate]
#
# HOOK: Post-certificate object creation (create).
#
catch {certificate_hook phase1}
#
# NOTE: If the certificate vendor is available, set it.
#
if {[string length $vendor] > 0} then {
$certificate Vendor $vendor
}
#
# NOTE: Always set the duration for new certificates since the
# policy code now checks for this. By default, non-license
# certificates are valid forever.
#
$certificate Duration $duration
#
# NOTE: Always set the entity type for new certificates since
# the policy code now checks for this. By default,
# non-license certificates are set to the "Script" entity
# type.
#
$certificate EntityType $entityType
#
# NOTE: When embedding, special handling is required when signing
# the certificate.
#
if {$shouldEmbed} then {
#
# NOTE: Set the entity value to the file contents that will, at
# some point, be seen by the script policy callback.
#
$certificate EntityValue [readEntityValue $fileName]
#
# HOOK: Post-certificate property setup (embedded).
#
catch {certificate_hook phase2}
#
# NOTE: Attempt to sign the embedded file certificate and place
# the Id, timestamp, public key token, and signature bytes
# into the certificate we created above.
#
if {[certificate sign -encoding $encoding -setid -settimestamp \
-setkey -hashflags {+Basic Embedded} $certificate \
$privateKey] ne "SignedOk"} then {
error [appendArgs \
"failed to create embedded signature for " $fileType " \"" \
$fileName \"]
}
#
# NOTE: Sanity check that the embedded file certificate we just
# created validates properly.
#
if {[certificate verify -encoding $encoding \
-hashflags {+Basic Embedded} $certificate \
$publicKey] ne "VerifiedOk"} then {
error [appendArgs \
"failed to verify embedded signature for " $fileType " \"" \
$fileName \"]
}
} else {
#
# HOOK: Post-certificate property setup (non-embedded).
#
catch {certificate_hook phase2}
#
# NOTE: Attempt to sign the data file and place the Id, timestamp,
# public key token, and signature bytes into the blank
# certificate we created above.
#
if {[certificate signfile -encoding $encoding -setid -settimestamp \
-setkey $certificate $privateKey $fileName] ne "SignedOk"} then {
error [appendArgs \
"failed to create signature for " $fileType " \"" $fileName \"]
}
#
# NOTE: Sanity check that the data file certificate we just
# created validates properly.
#
if {[certificate verifyfile -encoding $encoding \
$certificate $publicKey $fileName] ne "VerifiedOk"} then {
error [appendArgs \
"failed to verify signature for " $fileType " \"" $fileName \"]
}
}
}
#
# NOTE: When embedding, do not modify the external certificate
# file. Also, do not save out the entity value we set.
#
if {$shouldEmbed} then {
set certificateFile [file join [getTemporaryPath] \
[appendArgs [file tail $fileName] . [pid] .embed]]
$certificate EntityValue null
}
#
# NOTE: Delete any previous [and possibly stale] data file
# certificate.
#
catch {file delete -force $certificateFile}
#
# NOTE: Export the data file certificate to disk.
#
if {[certificate export $certificate $certificateFile] \
ne "ExportedOk"} then {
error [appendArgs \
"failed to export signature for " $fileType " \"" $fileName \"]
}
#
# NOTE: Add the standard XML comment to the file.
#
if {[certificate warning -type Script $certificateFile] \
ne "WarningOk"} then {
error [appendArgs \
"failed to add warning for " $fileType " \"" $fileName \"]
}
#
# HACK: Read the certificate file data into memory, fix the
# formatting how we want it, and then re-write the modified
# data back out to the same certificate file.
#
set data [readCertificateFile $certificateFile]
set xmlNs xmlns=\"https://eagle.to/2011/harpy\"
set xmlNsXsi xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
set xmlNsXsd xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"
set data [string map [list \
[appendArgs $xmlNsXsd " " $xmlNsXsi " " $xmlNs] \
[appendArgs $xmlNs " " $xmlNsXsi " " $xmlNsXsd]] $data]
set data [string map [list \
[appendArgs $xmlNsXsi " " $xmlNsXsd " " $xmlNs] \
[appendArgs $xmlNs " " $xmlNsXsi " " $xmlNsXsd]] $data]
set spaces " "
set data [string map [list \
"\" xmlns" [appendArgs \" [info newline] $spaces xmlns]] $data]
writeCertificateFile $certificateFile $data
#
# NOTE: Show that we signed it.
#
puts stdout [appendArgs \
"signed " $fileType " \"" $fileName "\" (as \"" $entityType \
"\") using encoding \"" $encoding "\" with key " [keypair token \
$privateKeyFile]]
#
# NOTE: Do we need to embed the certificate at the end of the script
# file?
#
if {$shouldEmbed} then {
#
# NOTE: Make sure any carriage-returns are removed as they impact
# the line splitting algorithm being used.
#
set data [string map [list [info newline] \n] $data]
#
# NOTE: Start out with the embedding prefix on a line by itself.
#
set lines [list $embedPrefix]
#
# NOTE: Add a comment character and a space to the start of each
# line, including any blank lines.
#
foreach line [split $data \n] {
lappend lines [string trim [appendArgs "# " $line]]
}
#
# NOTE: Finish off with the embedding suffix. Also, add one extra
# blank line just before the start of the embedding prefix.
#
lappend lines $embedSuffix; set lines [linsert $lines 0 ""]
#
# NOTE: Append the entire embedded certificate block to the script
# file itself, forcing the line-endings to be native for the
# platform.
#
appendFile $fileName [join $lines [info newline]]
#
# NOTE: Show that we embedded it.
#
puts stdout [appendArgs \
"added embedded certificate to " $fileType " \"" $fileName \"]
}
#
# HOOK: Script completion.
#
catch {certificate_hook phase3}
#
# NOTE: Play nice and cleanup all the variables we created during the
# whole the signing process.
#
unset -nocomplain fileName vendor embed duration entityType encoding \
privateKeyFile publicKeyFile configFileName embedSpacing embedPrefix \
embedSuffix publicKey privateKey shouldEmbed fileType certificate \
certificateFile data xmlNs xmlNsXsi xmlNsXsd spaces lines line
if {[info exists path_set]} then {
unset -nocomplain path path_set
}
} else {
usage ""
}