diff --git a/Makefile b/Makefile index 52102377..fcc42cf9 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,8 @@ mig-action-verifier: gpgme $(MKDIR) -p $(BINDIR) $(GO) build $(GOOPTS) -o $(BINDIR)/mig-action-verifier $(GOLDFLAGS) mig/clients/verifier +go_get_deps_into_system: + make GOGETTER="go get -u" go_get_deps go_get_deps: $(GOGETTER) code.google.com/p/go.crypto/openpgp @@ -123,4 +125,4 @@ clean: clean-all: clean rm -rf pkg -.PHONY: clean clean-all gpgme +.PHONY: clean clean-all gpgme go_get_deps_into_system diff --git a/conf/mig-agent-conf.go.inc b/conf/mig-agent-conf.go.inc index cc971155..2ac2ecfd 100644 --- a/conf/mig-agent-conf.go.inc +++ b/conf/mig-agent-conf.go.inc @@ -58,6 +58,37 @@ var HEARTBEATFREQ time.Duration = 300 * time.Second // timeout after which a module run is killed var MODULETIMEOUT time.Duration = 300 * time.Second +// Control modules permissions by PGP keys +var AGENTACL = [...]string{ +`{ + "default": { + "minimumweight": 2, + "investigators": { + "Bob Kelso": { + "fingerprint": "E60892BB....", + "weight": 2 + }, + "Morpheus": { + "fingerprint": "AD595634....", + "weight": 3 + } + } + } +}`, +`{ + "pidkill": { + "minimumweight": 1, + "investigators": { + "MIG Scheduler": { + "fingerprint": "E60892BB...", + "weight": 1 + } + } + } +}`, +} + + // PGP public keys that are authorized to sign actions // this is an array of strings, put each public key block // into its own array entry, as shown below diff --git a/doc/concepts.rst b/doc/concepts.rst index f58789b8..20fe63f6 100644 --- a/doc/concepts.rst +++ b/doc/concepts.rst @@ -112,20 +112,19 @@ The parameters are: Upon generation, additional fields are appended to the action: -* PGPSignature: all of the parameters above are concatenated into a string and +* PGPSignatures: all of the parameters above are concatenated into a string and signed with the investigator's private GPG key. The signature is part of the action, and used by agents to verify that an action comes from a trusted - investigator. -* PGPSignatureDate: is the date of the PGP signature, used as a timestamp of - the action creation. + investigator. `PGPSignatures` is an array that contains one or more signature + from authorized investigators. * ValidFrom and ExpireAt: two dates that constrains the validity of the action - to a time window. + to a UTC time window. -Actions files are submitted to the API or the Scheduler directly. Eventually, -the PGP signature will be verified by intermediary components, and in any case -by each agent before execution. +Actions files are submitted to the API or the Scheduler directly. The PGP +Signatures are always verified by the agents, and can optionally be verified by +other components along the way. Additional attributes are added to the action by the scheduler. Those are -defined as "MetaAction" and are used to track the action status. +defined as `ExtendedAction` and are used to track the action status. Commands ~~~~~~~~ @@ -186,6 +185,114 @@ While the result is negative, the command itself has succeeded. Had a failure happened on the agent, the scheduler would have been notified and the status would be one of "failed", "timeout" or "cancelled". +Access Control Lists +-------------------- + +Not all keys can perform all actions. The scheduler, for example, sometimes need +to issue specific actions to agents (such as during the upgrade protocol) but +shouldn't be able to perform more dangerous actions. This is enforced by +an Access Control List, or ACL, stored on the agents. An ACL describes who can +access what function of which module. It can be used to require multiple +signatures on specific actions, and limit the list of investigators allowed to +perform an action. + +An ACL is composed of permissions, which are JSON documents hardwired into +the agent configuration. In the future, MIG will dynamically ship permissions +to agents. + +Below is an example of a permission for the `filechecker` module: + +.. code:: json + + { + "filechecker": { + "minimumweight": 2, + "investigators": { + "Bob Kelso": { + "fingerprint": "E60892BB9BD...", + "weight": 2 + }, + "John Smith": { + "fingerprint": "9F759A1A0A3...", + "weight": 1 + } + } + } + } + +`investigators` contains a list of users with their PGP fingerprints, and their +weight, an integer that represents their access level. +When an agent receives an action that calls the filechecker module, it will +first verify the signatures of the action, and then validates that the signers +are authorized to perform the action. This is done by summing up the weights of +the signatures, and verifying that they equal or exceed the minimum required +weight. + +Thus, in the example above, investigator John Smith cannot issue a filechecker +action alone. His weight of 1 doesn't satisfy the minimum weight of 2 required +by the filechecker permission. Therefore, John will need to ask investigator Bob +Kelso to sign his action as well. The weight of both investigators are then +added, giving a total of 3, which satisfies the minimum weight of 2. + +This method gives ample flexibility to require multiple signatures on modules, +and ensure that one investigator cannot perform sensitive actions on remote +endpoints without the permissions of others. + +The default permission `default` can be used as a default for all modules. It +has the following syntax: + +.. code:: json + + { + "default": { + "minimumweight": 2, + "investigators": { ... } + ] + } + } + +The `default` permission is overridden by module specific permissions. + +The ACL is currently applied to modules. In the future, ACL will have finer +control to authorize access to specific functions of modules. For example, an +investigator could be authorized to call the `regex` function of filechecker +module, but only in `/etc`. This functionality is not implemented yet. + +Extracting PGP fingerprints from public keys +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +On Linux, the `gpg` command can easily display the fingerprint of a key using +`gpg --fingerprint `. For example: + +.. code:: bash + + $ gpg --fingerprint jvehent@mozilla.com + pub 2048R/3B763E8F 2013-04-30 + Key fingerprint = E608 92BB 9BD8 9A69 F759 A1A0 A3D6 5217 3B76 3E8F + uid Julien Vehent (personal) + uid Julien Vehent (ulfr) + sub 2048R/8026F39F 2013-04-30 + + +You should always verify the trustworthiness of a key before using it: + +.. code:: bash + + $ gpg --list-sigs jvehent@mozilla.com + pub 2048R/3B763E8F 2013-04-30 + uid Julien Vehent (personal) + sig 3 3B763E8F 2013-06-23 Julien Vehent (personal) + sig 3 28A860CE 2013-10-04 Curtis Koenig + ..... + +We want to extract the fingerprint, and obtain a 40 characters hexadecimal +string that can used in permissions. + +.. code:: bash + + $gpg --fingerprint --with-colons jvehent@mozilla.com |grep '^fpr'|cut -f 10 -d ':' + E60892BB9BD89A69F759A1A0A3D652173B763E8F + Agent registration process -------------------------- diff --git a/src/mig/acl.go b/src/mig/acl.go new file mode 100644 index 00000000..6f5102e6 --- /dev/null +++ b/src/mig/acl.go @@ -0,0 +1,73 @@ +/* Mozilla InvestiGator + +Version: MPL 1.1/GPL 2.0/LGPL 2.1 + +The contents of this file are subject to the Mozilla Public License Version +1.1 (the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at +http://www.mozilla.org/MPL/ + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the +License. + +The Initial Developer of the Original Code is +Mozilla Corporation +Portions created by the Initial Developer are Copyright (C) 2014 +the Initial Developer. All Rights Reserved. + +Contributor(s): +Julien Vehent jvehent@mozilla.com [:ulfr] + +Alternatively, the contents of this file may be used under the terms of +either the GNU General Public License Version 2 or later (the "GPL"), or +the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +in which case the provisions of the GPL or the LGPL are applicable instead +of those above. If you wish to allow use of your version of this file only +under the terms of either the GPL or the LGPL, and not to allow others to +use your version of this file under the terms of the MPL, indicate your +decision by deleting the provisions above and replace them with the notice +and other provisions required by the GPL or the LGPL. If you do not delete +the provisions above, a recipient may use your version of this file under +the terms of any one of the MPL, the GPL or the LGPL. +*/ + +package mig + +import ( + "fmt" + "strings" +) + +type ACL []Permission + +type Permission map[string]struct { + MinimumWeight int + Investigators map[string]struct{ + Fingerprint string + Weight int + } +} + +// verifyPermission controls that the PGP keys, identified by their fingerprints, that +// signed an operation are sufficient to allow this operation to run +func verifyPermission(operation operation, permName string, perm Permission, fingerprints []string) (err error) { + if perm[permName].MinimumWeight < 1 { + return fmt.Errorf("Invalid permission '%s'. Must require at least 1 signature, has %d", + permName, perm[permName].MinimumWeight) + } + signaturesWeight := 0 + for _, fp := range fingerprints { + for _, signer := range perm[permName].Investigators { + if strings.ToUpper(fp) == strings.ToUpper(signer.Fingerprint) { + signaturesWeight += signer.Weight + } + } + } + if signaturesWeight < perm[permName].MinimumWeight { + return fmt.Errorf("Permission denied for operation '%s'. Insufficient signatures weight. Need %d, got %d", + operation.Module, perm[permName].MinimumWeight, signaturesWeight) + } + return +} diff --git a/src/mig/action.go b/src/mig/action.go index bf40fff1..4dcea216 100644 --- a/src/mig/action.go +++ b/src/mig/action.go @@ -43,6 +43,7 @@ import ( "io" "io/ioutil" "math/rand" + "mig/pgp" "mig/pgp/verify" "strconv" "time" @@ -74,22 +75,16 @@ type counters struct { // an Action is the json object that is created by an investigator // and provided to the MIG platform. It must be PGP signed. type Action struct { - // meta - ID uint64 `json:"id"` - Name string `json:"name"` - Target string `json:"target"` - Description description `json:"description"` - Threat threat `json:"threat"` - // time window - ValidFrom time.Time `json:"validfrom"` - ExpireAfter time.Time `json:"expireafter"` - // operation to perform - Operations []operation `json:"operations"` - // action signature - PGPSignature string `json:"pgpsignature"` - PGPSignatureDate time.Time `json:"pgpsignaturedate"` - // action syntax version - SyntaxVersion int `json:"syntaxversion"` + ID uint64 `json:"id"` + Name string `json:"name"` + Target string `json:"target"` + Description description `json:"description"` + Threat threat `json:"threat"` + ValidFrom time.Time `json:"validfrom"` + ExpireAfter time.Time `json:"expireafter"` + Operations []operation `json:"operations"` + PGPSignatures []string `json:"pgpsignatures"` + SyntaxVersion int `json:"syntaxversion"` } // a description is a simple object that contains detail about the @@ -185,29 +180,29 @@ func (a Action) Validate() (err error) { if a.Operations == nil { return errors.New("Action.Operations is nil. Expecting string.") } - if a.PGPSignature == "" { - return errors.New("Action.PGPSignature is empty. Expecting string.") + if len(a.PGPSignatures) < 1 { + return errors.New("Action.PGPSignatures is empty. Expecting array of strings.") } return } -// Validate verifies that the Action received contained all the -// necessary fields, and returns an error when it doesn't. -func (a Action) VerifySignature(keyring io.Reader) (err error) { - // Verify the signature +// VerifySignatures verifies that the Action contains valid signatures from +// known investigators. It does not verify permissions. +func (a Action) VerifySignatures(keyring io.Reader) (err error) { astr, err := a.String() if err != nil { return errors.New("Failed to stringify action") } - valid, _, err := verify.Verify(astr, a.PGPSignature, keyring) - if err != nil { - return errors.New("Failed to verify PGP Signature") + for _, sig := range a.PGPSignatures { + valid, _, err := verify.Verify(astr, sig, keyring) + if err != nil { + return errors.New("Failed to verify PGP Signature") + } + if !valid { + return errors.New("Invalid PGP Signature") + } } - if !valid { - return errors.New("Invalid PGP Signature") - } - - return nil + return } // concatenates Action components into a string @@ -222,3 +217,51 @@ func (a Action) String() (str string, err error) { return } + +// VerifyACL controls that an action has been issued by investigators +// that have the right permissions. This function looks at each operation +// listed in the action, and find the corresponding permission. If no +// permission is found, the default one `default` is used. +// The first permission that is found to apply to an operation, but +// doesn't allow the operation to run, will fail the verification globally +func (a Action) VerifyACL(acl ACL, keyring io.Reader) (err error) { + // first, verify all signatures and get a list of PGP + // fingerprints of the signers + var fingerprints []string + astr, err := a.String() + if err != nil { + return errors.New("Failed to stringify action") + } + for _, sig := range a.PGPSignatures { + fp, err := pgp.GetFingerprintFromSignature(astr, sig, keyring) + if err != nil { + return fmt.Errorf("Failed to retrieve fingerprint from signatures: %v", err) + } + fingerprints = append(fingerprints, fp) + } + + // Then, for each operation contained in the action, look for + // a permission that apply to it, by comparing the operation name + // with permission name. If none is found, use the default permission. + for _, operation := range a.Operations { + for _, permission := range acl { + for permName, _ := range permission { + if permName == operation.Module { + return verifyPermission(operation, permName, permission, fingerprints) + } + } + } + // no specific permission found, apply the default permission + var defaultPerm Permission + for _, permission := range acl { + for permName, _ := range permission { + if permName == "default" { + defaultPerm = permission + break + } + } + } + return verifyPermission(operation, "default", defaultPerm, fingerprints) + } + return +} diff --git a/src/mig/agent/acl.go b/src/mig/agent/acl.go new file mode 100644 index 00000000..ac9ece36 --- /dev/null +++ b/src/mig/agent/acl.go @@ -0,0 +1,86 @@ +/* Mozilla InvestiGator Agent + +Version: MPL 1.1/GPL 2.0/LGPL 2.1 + +The contents of this file are subject to the Mozilla Public License Version +1.1 (the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at +http://www.mozilla.org/MPL/ + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the +License. + +The Initial Developer of the Original Code is +Mozilla Corporation +Portions created by the Initial Developer are Copyright (C) 2014 +the Initial Developer. All Rights Reserved. + +Contributor(s): +Julien Vehent jvehent@mozilla.com [:ulfr] +Guillaume Destuynder + +Alternatively, the contents of this file may be used under the terms of +either the GNU General Public License Version 2 or later (the "GPL"), or +the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +in which case the provisions of the GPL or the LGPL are applicable instead +of those above. If you wish to allow use of your version of this file only +under the terms of either the GPL or the LGPL, and not to allow others to +use your version of this file under the terms of the MPL, indicate your +decision by deleting the provisions above and replace them with the notice +and other provisions required by the GPL or the LGPL. If you do not delete +the provisions above, a recipient may use your version of this file under +the terms of any one of the MPL, the GPL or the LGPL. +*/ + +package main + +import ( + "fmt" + "mig" + "mig/pgp" + "time" +) + +// checkActionAuthorization verifies the PGP signatures of a given action +// against the Access Control List of the agent. +func checkActionAuthorization(a mig.Action, ctx Context) (err error) { + defer func() { + if e := recover(); e != nil { + err = fmt.Errorf("checkActionAuthorization() -> %v", e) + } + ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: "leaving checkActionAuthorization()"}.Debug() + }() + // get an io.Reader from the public pgp key + keyring, keycount, err := pgp.ArmoredPubKeysToKeyring(PUBLICPGPKEYS[0:]) + if err != nil { + panic(err) + } + ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: fmt.Sprintf("loaded %d keys", keycount)}.Debug() + + // Check the action syntax and signature + err = a.Validate() + if err != nil { + desc := fmt.Sprintf("action validation failed: %v", err) + ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: desc}.Err() + panic(desc) + } + // Validate() checks that the action hasn't expired, but we need to + // check the start time ourselves + if time.Now().Before(a.ValidFrom) { + ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: "action is scheduled for later"}.Err() + panic("Action ValidFrom date is in the future") + } + + // check ACLs, includes verifying signatures + err = a.VerifyACL(ctx.ACL, keyring) + if err != nil { + desc := fmt.Sprintf("action ACL verification failed: %v", err) + ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: desc}.Err() + panic(desc) + } + + ctx.Channels.Log <- mig.Log{ActionID: a.ID, Desc: "ACL verification succeeded."}.Debug() + return +} diff --git a/src/mig/agent/agent.go b/src/mig/agent/agent.go index 9cb56569..f9902e5e 100644 --- a/src/mig/agent/agent.go +++ b/src/mig/agent/agent.go @@ -47,7 +47,6 @@ import ( "mig" "mig/modules/connected" "mig/modules/filechecker" - "mig/pgp" "os" "os/exec" "strings" @@ -245,33 +244,12 @@ func parseCommands(ctx Context, msg []byte) (err error) { panic(err) } - // get an io.Reader from the public pgp key - keyring, keycount, err := pgp.ArmoredPubKeysToKeyring(PUBLICPGPKEYS[0:]) + // verify the PGP signature of the action, and verify that + // the signer is authorized to perform this action + err = checkActionAuthorization(cmd.Action, ctx) if err != nil { panic(err) } - ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: fmt.Sprintf("loaded %d keys", keycount)}.Debug() - - // Check the action syntax and signature - err = cmd.Action.Validate() - if err != nil { - desc := fmt.Sprintf("action validation failed: %v", err) - ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: desc}.Err() - panic(desc) - } - err = cmd.Action.VerifySignature(keyring) - if err != nil { - desc := fmt.Sprintf("action signature verification failed: %v", err) - ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: desc}.Err() - panic(desc) - } - - // Expiration is verified by the Validate() call above, but we need - // to verify the ScheduledDate ourselves - if time.Now().Before(cmd.Action.ValidFrom) { - ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: "action is scheduled for later"}.Err() - panic("ScheduledDateInFuture") - } // Each operation is ran separately by a module, a channel is created to receive the results from each module // a goroutine is created to read from the result channel, and when all modules are done, build the response diff --git a/src/mig/agent/context.go b/src/mig/agent/context.go index b5d24e87..0144af6f 100644 --- a/src/mig/agent/context.go +++ b/src/mig/agent/context.go @@ -39,6 +39,7 @@ import ( "crypto/rand" "crypto/tls" "crypto/x509" + "encoding/json" "fmt" "github.com/VividCortex/godaemon" "github.com/streadway/amqp" @@ -55,7 +56,7 @@ import ( // logs and channels // Context is intended as a single structure that can be passed around easily. type Context struct { - OpID uint64 // ID of the current operation, used for tracking + ACL mig.ACL Agent struct { Hostname, OS, QueueLoc, UID, BinPath string } @@ -77,6 +78,7 @@ type Context struct { Chan *amqp.Channel Bind mig.Binding } + OpID uint64 // ID of the current operation, used for tracking Sleeper time.Duration // timer used when the agent has to sleep for a while Stats struct { } @@ -146,6 +148,12 @@ func Init(foreground bool) (ctx Context, err error) { panic(err) } + // parse the ACLs + ctx, err = initACL(ctx) + if err != nil { + panic(err) + } + // connect to the message broker ctx, err = initMQ(ctx) if err != nil { @@ -277,6 +285,30 @@ func createIDFile(loc string) (id []byte, err error) { return } +// parse the permissions from the configuration into an ACL structure +func initACL(orig_ctx Context) (ctx Context, err error) { + ctx = orig_ctx + defer func() { + if e := recover(); e != nil { + err = fmt.Errorf("initACL() -> %v", e) + } + ctx.Channels.Log <- mig.Log{Desc: "leaving initACL()"}.Debug() + }() + for _, jsonPermission := range AGENTACL { + var parsedPermission mig.Permission + err = json.Unmarshal([]byte(jsonPermission), &parsedPermission) + if err != nil { + panic(err) + } + for permName, _ := range parsedPermission { + desc := fmt.Sprintf("Loading permission named '%s'", permName) + ctx.Channels.Log <- mig.Log{Desc: desc}.Debug() + } + ctx.ACL = append(ctx.ACL, parsedPermission) + } + return +} + func initMQ(orig_ctx Context) (ctx Context, err error) { ctx = orig_ctx defer func() { diff --git a/src/mig/api/api.go b/src/mig/api/api.go index 84c5d585..7938bab3 100644 --- a/src/mig/api/api.go +++ b/src/mig/api/api.go @@ -336,7 +336,7 @@ func createAction(respWriter http.ResponseWriter, request *http.Request) { if err != nil { panic(err) } - err = action.VerifySignature(keyring) + err = action.VerifySignatures(keyring) if err != nil { panic(err) } diff --git a/src/mig/clients/generator/generator.go b/src/mig/clients/generator/generator.go index 521c7eab..1e494893 100644 --- a/src/mig/clients/generator/generator.go +++ b/src/mig/clients/generator/generator.go @@ -115,16 +115,15 @@ func main() { if err != nil { panic(err) } - a.PGPSignature, err = sign.Sign(str, *key) + pgpsig, err := sign.Sign(str, *key) if err != nil { panic(err) } - - a.PGPSignatureDate = time.Now().UTC() - + a.PGPSignatures = append(a.PGPSignatures, pgpsig) var jsonAction []byte if *pretty { jsonAction, err = json.MarshalIndent(a, "", "\t") + fmt.Printf("%s\n", jsonAction) } else { jsonAction, err = json.Marshal(a) } @@ -179,7 +178,7 @@ func main() { } // syntax checking - err = a.VerifySignature(keyring) + err = a.VerifySignatures(keyring) if err != nil { panic(err) } diff --git a/src/mig/clients/verifier/verifier.go b/src/mig/clients/verifier/verifier.go index a937a714..3f6a3818 100644 --- a/src/mig/clients/verifier/verifier.go +++ b/src/mig/clients/verifier/verifier.go @@ -119,7 +119,7 @@ func main() { defer keyring.Close() // syntax checking - err = a.VerifySignature(keyring) + err = a.VerifySignatures(keyring) if err != nil { panic(err) } diff --git a/src/mig/pgp/pgp.go b/src/mig/pgp/pgp.go index 515fd712..d156b8e1 100644 --- a/src/mig/pgp/pgp.go +++ b/src/mig/pgp/pgp.go @@ -38,8 +38,10 @@ package pgp import ( "bytes" "code.google.com/p/go.crypto/openpgp" + "encoding/hex" "fmt" "io" + "mig/pgp/verify" ) // TransformArmoredPubKeysToKeyring takes a list of public PGP key in armored form and transforms @@ -72,3 +74,19 @@ func ArmoredPubKeysToKeyring(pubkeys []string) (keyring io.Reader, keycount int, keyring = bytes.NewReader(buf.Bytes()) return } + +// TransformArmoredPubKeysToKeyring takes a list of public PGP key in armored form and transforms +// it into a keyring that can be used in other openpgp's functions +func GetFingerprintFromSignature(data string, signature string, keyring io.Reader) (fingerprint string, err error) { + defer func() { + if e := recover(); e != nil { + err = fmt.Errorf("GetFingerprintFromSignature() -> %v", e) + } + }() + _, entity, err := verify.Verify(data, signature, keyring) + if err != nil { + panic(err) + } + fingerprint = hex.EncodeToString(entity.PrimaryKey.Fingerprint[:]) + return +} diff --git a/src/mig/pgp/verify/verify.go b/src/mig/pgp/verify/verify.go index 4e0b7156..43d84620 100644 --- a/src/mig/pgp/verify/verify.go +++ b/src/mig/pgp/verify/verify.go @@ -50,7 +50,7 @@ func Verify(data string, signature string, keyring io.Reader) (valid bool, entit valid = false // re-armor signature and transform into io.Reader - sigReader := strings.NewReader(reArmor(signature)) + sigReader := strings.NewReader(reArmorSignature(signature)) // decode armor sigBlock, err := armor.Decode(sigReader) @@ -82,9 +82,9 @@ func Verify(data string, signature string, keyring io.Reader) (valid bool, entit return } -// reArmor takes a single line armor and turns it back into an PGP-style +// reArmorSignature takes a single line armor and turns it back into an PGP-style // multi-line armored string (thank you, camlistore folks) -func reArmor(line string) string { +func reArmorSignature(line string) string { lastEq := strings.LastIndex(line, "=") if lastEq == -1 { return ""