Support for "planned permanodes" in pkg/jsonsign, pkg/schema and cmd/camput.

A planned permanode involves setting the contents of the permanode
(instead of a random string) as well as the OpenPGP signing time so
the resultant bytes of the blob (and thus its blobref) is deterministic.

This allows multiple independent devices (my laptops) to create the
same permanodes for the same files (photos backed up from my phone)
when offline (airplane) and then when they sync later, still only have
one permanode per unique file.  This means that tagging and other
metadata applied to permanodes on one laptop merge cleanly with
metadata from the other.
This commit is contained in:
Brad Fitzpatrick 2012-07-28 16:32:31 -07:00
parent 8c293e34b6
commit 51e88cac65
5 changed files with 69 additions and 14 deletions

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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)