diff --git a/cmd/camput/permanode.go b/cmd/camput/permanode.go index 9b8588e03..ca438a4ef 100644 --- a/cmd/camput/permanode.go +++ b/cmd/camput/permanode.go @@ -19,15 +19,19 @@ package main import ( "errors" "flag" + "fmt" "strings" + "time" "camlistore.org/pkg/client" "camlistore.org/pkg/schema" ) type permanodeCmd struct { - name string - tag string + name string + tag string + key string // else random + sigTime string } func init() { @@ -35,6 +39,8 @@ func init() { cmd := new(permanodeCmd) flags.StringVar(&cmd.name, "name", "", "Optional name attribute to set on new permanode") flags.StringVar(&cmd.tag, "tag", "", "Optional tag(s) to set on new permanode; comma separated.") + flags.StringVar(&cmd.key, "key", "", "Optional key to create deterministic ('planned') permanodes. Must also use --sigtime.") + flags.StringVar(&cmd.sigTime, "sigtime", "", "Optional time to put in the OpenPGP signature packet instead of the current time. Required when producing a deterministic permanode (with --key). In format YYYY-MM-DD HH:MM:SS") return cmd }) } @@ -59,7 +65,20 @@ func (c *permanodeCmd) RunCommand(up *Uploader, args []string) error { permaNode *client.PutResult err error ) - permaNode, err = up.UploadNewPermanode() + if (c.key != "") != (c.sigTime != "") { + return errors.New("Both --key and --sigtime must be used to produce deterministic permanodes.") + } + if c.key == "" { + // Normal case, with a random permanode. + permaNode, err = up.UploadNewPermanode() + } else { + const format = "2006-01-02 15:04:05" + sigTime, err := time.Parse(format, c.sigTime) + if err != nil { + return fmt.Errorf("Error parsing time %q; expecting time of form %q", c.sigTime, format) + } + permaNode, err = up.UploadPlannedPermanode(c.key, sigTime) + } if handleResult("permanode", permaNode, err) != nil { return err } diff --git a/cmd/camput/uploader.go b/cmd/camput/uploader.go index 4ebc417f5..c22bf9708 100644 --- a/cmd/camput/uploader.go +++ b/cmd/camput/uploader.go @@ -20,6 +20,7 @@ import ( "errors" "log" "net/http" + "time" "camlistore.org/pkg/blobref" "camlistore.org/pkg/blobserver" @@ -55,7 +56,9 @@ type Uploader struct { fs http.FileSystem // virtual filesystem to read from; nil means OS filesystem. } -func (up *Uploader) SignMap(m map[string]interface{}) (string, error) { +// sigTime optionally specifies the signature time. +// If zero, the current time is used. +func (up *Uploader) SignMap(m map[string]interface{}, sigTime time.Time) (string, error) { camliSigBlobref := up.Client.SignerPublicKeyBlobref() if camliSigBlobref == nil { // TODO: more helpful error message @@ -71,6 +74,7 @@ func (up *Uploader) SignMap(m map[string]interface{}) (string, error) { UnsignedJSON: unsigned, Fetcher: up.Client.GetBlobFetcher(), EntityFetcher: up.entityFetcher, + SignatureTime: sigTime, } return sr.Sign() } @@ -84,7 +88,7 @@ func (up *Uploader) UploadMap(m map[string]interface{}) (*client.PutResult, erro } func (up *Uploader) UploadAndSignMap(m map[string]interface{}) (*client.PutResult, error) { - signed, err := up.SignMap(m) + signed, err := up.SignMap(m, time.Time{}) if err != nil { return nil, err } @@ -111,3 +115,12 @@ func (up *Uploader) UploadNewPermanode() (*client.PutResult, error) { unsigned := schema.NewUnsignedPermanode() return up.UploadAndSignMap(unsigned) } + +func (up *Uploader) UploadPlannedPermanode(key string, sigTime time.Time) (*client.PutResult, error) { + unsigned := schema.NewPlannedPermanode(key) + signed, err := up.SignMap(unsigned, sigTime) + if err != nil { + return nil, err + } + return up.uploadString(signed) +} diff --git a/pkg/jsonsign/sign.go b/pkg/jsonsign/sign.go index 12867fc2b..f53a4173a 100644 --- a/pkg/jsonsign/sign.go +++ b/pkg/jsonsign/sign.go @@ -28,6 +28,7 @@ import ( "path/filepath" "strings" "sync" + "time" "unicode" "camlistore.org/pkg/blobref" @@ -177,6 +178,9 @@ type SignRequest struct { Fetcher interface{} // blobref.Fetcher or blobref.StreamingFetcher ServerMode bool // if true, can't use pinentry or gpg-agent, etc. + // Optional signature time. If zero, time.Now() is used. + SignatureTime time.Time + // Optional function to return an entity (including decrypting // the PrivateKey, if necessary) EntityFetcher EntityFetcher @@ -269,7 +273,7 @@ func (sr *SignRequest) Sign() (signedJSON string, err error) { } var buf bytes.Buffer - err = openpgp.ArmoredDetachSign(&buf, signer, strings.NewReader(trimmedJSON)) + err = openpgp.ArmoredDetachSignAt(&buf, signer, sr.SignatureTime, strings.NewReader(trimmedJSON)) if err != nil { return "", err } diff --git a/third_party/code.google.com/p/go.crypto/openpgp/keys.go b/third_party/code.google.com/p/go.crypto/openpgp/keys.go index 7513b5efe..69ce8e3c5 100644 --- a/third_party/code.google.com/p/go.crypto/openpgp/keys.go +++ b/third_party/code.google.com/p/go.crypto/openpgp/keys.go @@ -29,6 +29,18 @@ type Entity struct { PrivateKey *packet.PrivateKey Identities map[string]*Identity // indexed by Identity.Name Subkeys []Subkey + + // Clock optionally specifies an alternate clock function to + // use when signing or encrypting using this Entity, instead + // of time.Now(). + Clock func() time.Time +} + +func (e *Entity) timeNow() time.Time { + if e.Clock != nil { + return e.Clock() + } + return time.Now() } // An Identity represents an identity claimed by an Entity and zero or more diff --git a/third_party/code.google.com/p/go.crypto/openpgp/write.go b/third_party/code.google.com/p/go.crypto/openpgp/write.go index 583023f08..5a62599cd 100644 --- a/third_party/code.google.com/p/go.crypto/openpgp/write.go +++ b/third_party/code.google.com/p/go.crypto/openpgp/write.go @@ -21,54 +21,61 @@ import ( // DetachSign signs message with the private key from signer (which must // already have been decrypted) and writes the signature to w. func DetachSign(w io.Writer, signer *Entity, message io.Reader) error { - return detachSign(w, signer, message, packet.SigTypeBinary) + return detachSign(w, signer, message, time.Time{}, packet.SigTypeBinary) } // ArmoredDetachSign signs message with the private key from signer (which // must already have been decrypted) and writes an armored signature to w. func ArmoredDetachSign(w io.Writer, signer *Entity, message io.Reader) (err error) { - return armoredDetachSign(w, signer, message, packet.SigTypeBinary) + return armoredDetachSign(w, signer, message, time.Time{}, packet.SigTypeBinary) +} + +func ArmoredDetachSignAt(w io.Writer, signer *Entity, sigTime time.Time, message io.Reader) (err error) { + return armoredDetachSign(w, signer, message, sigTime, packet.SigTypeBinary) } // DetachSignText signs message (after canonicalising the line endings) with // the private key from signer (which must already have been decrypted) and // writes the signature to w. func DetachSignText(w io.Writer, signer *Entity, message io.Reader) error { - return detachSign(w, signer, message, packet.SigTypeText) + return detachSign(w, signer, message, time.Time{}, packet.SigTypeText) } // ArmoredDetachSignText signs message (after canonicalising the line endings) // with the private key from signer (which must already have been decrypted) // and writes an armored signature to w. func ArmoredDetachSignText(w io.Writer, signer *Entity, message io.Reader) error { - return armoredDetachSign(w, signer, message, packet.SigTypeText) + return armoredDetachSign(w, signer, message, time.Time{}, packet.SigTypeText) } -func armoredDetachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.SignatureType) (err error) { +func armoredDetachSign(w io.Writer, signer *Entity, message io.Reader, sigTime time.Time, sigType packet.SignatureType) (err error) { out, err := armor.Encode(w, SignatureType, nil) if err != nil { return } - err = detachSign(out, signer, message, sigType) + err = detachSign(out, signer, message, sigTime, sigType) if err != nil { return } return out.Close() } -func detachSign(w io.Writer, signer *Entity, message io.Reader, sigType packet.SignatureType) (err error) { +func detachSign(w io.Writer, signer *Entity, message io.Reader, sigTime time.Time, sigType packet.SignatureType) (err error) { if signer.PrivateKey == nil { return errors.InvalidArgumentError("signing key doesn't have a private key") } if signer.PrivateKey.Encrypted { return errors.InvalidArgumentError("signing key is encrypted") } + if sigTime.IsZero() { + sigTime = time.Now() + } sig := new(packet.Signature) sig.SigType = sigType sig.PubKeyAlgo = signer.PrivateKey.PubKeyAlgo sig.Hash = crypto.SHA256 - sig.CreationTime = time.Now() + sig.CreationTime = sigTime sig.IssuerKeyId = &signer.PrivateKey.KeyId h, wrappedHash, err := hashForSignature(sig.Hash, sig.SigType)