Home Download Docs Code Community
     1	/*
     2	Copyright 2011 The Perkeep Authors
     3	
     4	Licensed under the Apache License, Version 2.0 (the "License");
     5	you may not use this file except in compliance with the License.
     6	You may obtain a copy of the License at
     7	
     8	     http://www.apache.org/licenses/LICENSE-2.0
     9	
    10	Unless required by applicable law or agreed to in writing, software
    11	distributed under the License is distributed on an "AS IS" BASIS,
    12	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13	See the License for the specific language governing permissions and
    14	limitations under the License.
    15	*/
    16	
    17	package jsonsign
    18	
    19	import (
    20		"bytes"
    21		"context"
    22		"encoding/json"
    23		"errors"
    24		"fmt"
    25		"strings"
    26		"sync"
    27		"time"
    28		"unicode"
    29	
    30		"perkeep.org/internal/osutil"
    31		"perkeep.org/pkg/blob"
    32	
    33		"go4.org/wkfs"
    34		"golang.org/x/crypto/openpgp"
    35		"golang.org/x/crypto/openpgp/packet"
    36	)
    37	
    38	type EntityFetcher interface {
    39		FetchEntity(fingerprint string) (*openpgp.Entity, error)
    40	}
    41	
    42	type FileEntityFetcher struct {
    43		File string
    44	}
    45	
    46	func FlagEntityFetcher() *FileEntityFetcher {
    47		return &FileEntityFetcher{File: osutil.SecretRingFile()}
    48	}
    49	
    50	type CachingEntityFetcher struct {
    51		Fetcher EntityFetcher
    52	
    53		lk sync.Mutex
    54		m  map[string]*openpgp.Entity
    55	}
    56	
    57	func (ce *CachingEntityFetcher) FetchEntity(fingerprint string) (*openpgp.Entity, error) {
    58		ce.lk.Lock()
    59		if ce.m != nil {
    60			e := ce.m[fingerprint]
    61			if e != nil {
    62				ce.lk.Unlock()
    63				return e, nil
    64			}
    65		}
    66		ce.lk.Unlock()
    67	
    68		e, err := ce.Fetcher.FetchEntity(fingerprint)
    69		if err == nil {
    70			ce.lk.Lock()
    71			defer ce.lk.Unlock()
    72			if ce.m == nil {
    73				ce.m = make(map[string]*openpgp.Entity)
    74			}
    75			ce.m[fingerprint] = e
    76		}
    77	
    78		return e, err
    79	}
    80	
    81	func (fe *FileEntityFetcher) FetchEntity(fingerprint string) (*openpgp.Entity, error) {
    82		f, err := wkfs.Open(fe.File)
    83		if err != nil {
    84			return nil, fmt.Errorf("jsonsign: FetchEntity: %v", err)
    85		}
    86		defer f.Close()
    87		el, err := readKeyRing(f)
    88		if err != nil {
    89			return nil, fmt.Errorf("jsonsign: readKeyRing of %q: %v", fe.File, err)
    90		}
    91		for _, e := range el {
    92			pubk := &e.PrivateKey.PublicKey
    93			if fingerprintString(pubk) != fingerprint {
    94				continue
    95			}
    96			if e.PrivateKey.Encrypted {
    97				if err := fe.decryptEntity(e); err == nil {
    98					return e, nil
    99				}
   100				return nil, err
   101			}
   102			return e, nil
   103		}
   104		return nil, fmt.Errorf("jsonsign: entity for fingerprint %q not found in %q", fingerprint, fe.File)
   105	}
   106	
   107	type SignRequest struct {
   108		UnsignedJSON string
   109		Fetcher      blob.Fetcher
   110		ServerMode   bool // if true, can't use pinentry or gpg-agent, etc.
   111	
   112		// Optional signature time. If zero, time.Now() is used.
   113		SignatureTime time.Time
   114	
   115		// Optional function to return an entity (including decrypting
   116		// the PrivateKey, if necessary)
   117		EntityFetcher EntityFetcher
   118	
   119		// SecretKeyringPath is only used if EntityFetcher is nil,
   120		// in which case SecretKeyringPath is used if non-empty.
   121		// As a final resort, we default to osutil.SecretRingFile().
   122		SecretKeyringPath string
   123	}
   124	
   125	func (sr *SignRequest) secretRingPath() string {
   126		if sr.SecretKeyringPath != "" {
   127			return sr.SecretKeyringPath
   128		}
   129		return osutil.SecretRingFile()
   130	}
   131	
   132	func (sr *SignRequest) Sign(ctx context.Context) (signedJSON string, err error) {
   133		trimmedJSON := strings.TrimRightFunc(sr.UnsignedJSON, unicode.IsSpace)
   134	
   135		// TODO: make sure these return different things
   136		inputfail := func(msg string) (string, error) {
   137			return "", errors.New(msg)
   138		}
   139		execfail := func(msg string) (string, error) {
   140			return "", errors.New(msg)
   141		}
   142	
   143		jmap := make(map[string]interface{})
   144		if err := json.Unmarshal([]byte(trimmedJSON), &jmap); err != nil {
   145			return inputfail("json parse error")
   146		}
   147	
   148		camliSigner, hasSigner := jmap["camliSigner"]
   149		if !hasSigner {
   150			return inputfail("json lacks \"camliSigner\" key with public key blobref")
   151		}
   152	
   153		camliSignerStr, _ := camliSigner.(string)
   154		signerBlob, ok := blob.Parse(camliSignerStr)
   155		if !ok {
   156			return inputfail("json \"camliSigner\" key is malformed or unsupported")
   157		}
   158	
   159		pubkeyReader, _, err := sr.Fetcher.Fetch(ctx, signerBlob)
   160		if err != nil {
   161			// TODO: not really either an inputfail or an execfail.. but going
   162			// with exec for now.
   163			return execfail(fmt.Sprintf("failed to find public key %s: %v", signerBlob.String(), err))
   164		}
   165	
   166		pubk, err := openArmoredPublicKeyFile(pubkeyReader)
   167		pubkeyReader.Close()
   168		if err != nil {
   169			return execfail(fmt.Sprintf("failed to parse public key from blobref %s: %v", signerBlob.String(), err))
   170		}
   171	
   172		// This check should be redundant if the above JSON parse succeeded, but
   173		// for explicitness...
   174		if len(trimmedJSON) == 0 || trimmedJSON[len(trimmedJSON)-1] != '}' {
   175			return inputfail("json parameter lacks trailing '}'")
   176		}
   177		trimmedJSON = trimmedJSON[0 : len(trimmedJSON)-1]
   178	
   179		// sign it
   180		entityFetcher := sr.EntityFetcher
   181		if entityFetcher == nil {
   182			file := sr.secretRingPath()
   183			if file == "" {
   184				return "", errors.New("jsonsign: no EntityFetcher, and no secret ring file defined")
   185			}
   186			secring, err := wkfs.Open(sr.secretRingPath())
   187			if err != nil {
   188				return "", fmt.Errorf("jsonsign: failed to open secret ring file %q: %v", sr.secretRingPath(), err)
   189			}
   190			secring.Close() // just opened to see if it's readable
   191			entityFetcher = &FileEntityFetcher{File: file}
   192		}
   193		signer, err := entityFetcher.FetchEntity(fingerprintString(pubk))
   194		if err != nil {
   195			return "", err
   196		}
   197	
   198		var buf bytes.Buffer
   199		err = openpgp.ArmoredDetachSign(
   200			&buf,
   201			signer,
   202			strings.NewReader(trimmedJSON),
   203			&packet.Config{Time: func() time.Time { return sr.SignatureTime }},
   204		)
   205		if err != nil {
   206			return "", err
   207		}
   208	
   209		output := buf.String()
   210	
   211		index1 := strings.Index(output, "\n\n")
   212		index2 := strings.Index(output, "\n-----")
   213		if index1 == -1 || index2 == -1 {
   214			return execfail("Failed to parse signature from gpg.")
   215		}
   216		inner := output[index1+2 : index2]
   217		signature := strings.Replace(inner, "\n", "", -1)
   218	
   219		return fmt.Sprintf("%s,\"camliSig\":\"%s\"}\n", trimmedJSON, signature), nil
   220	}
Website layout inspired by memcached.
Content by the authors.