From 6a75179d9b71ffee9836934d670f5bcd99a31e83 Mon Sep 17 00:00:00 2001 From: Roger Peppe Date: Thu, 30 Oct 2014 09:47:25 +0000 Subject: [PATCH] Opinionated and specific bakery refactoring. --- bakery/codec.go | 147 +++++++++ bakery/discharge.go | 21 +- bakery/example/authservice.go | 6 +- bakery/example/example_test.go | 28 +- bakery/example/idservice/idservice.go | 2 +- bakery/example/idservice/idservice_test.go | 7 +- .../example/idservice/targetservice_test.go | 13 +- bakery/example/main.go | 13 +- bakery/example/targetservice.go | 14 +- bakery/keys.go | 144 +++++++++ bakery/service.go | 65 ++-- httpbakery/caveatid.go | 295 ------------------ httpbakery/client.go | 7 + httpbakery/discharge.go | 5 + httpbakery/service.go | 81 +---- 15 files changed, 419 insertions(+), 429 deletions(-) create mode 100644 bakery/codec.go create mode 100644 bakery/keys.go delete mode 100644 httpbakery/caveatid.go diff --git a/bakery/codec.go b/bakery/codec.go new file mode 100644 index 0000000..a8506f7 --- /dev/null +++ b/bakery/codec.go @@ -0,0 +1,147 @@ +package bakery + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + + "code.google.com/p/go.crypto/nacl/box" +) + +type caveatIdRecord struct { + RootKey []byte + Condition string +} + +// caveatId defines the format of a third party caveat id. +type caveatId struct { + ThirdPartyPublicKey []byte + FirstPartyPublicKey []byte + Nonce []byte + Id string +} + +// boxEncoder encodes caveat ids confidentially to a third-party service using +// authenticated public key encryption compatible with NaCl box. +type boxEncoder struct { + locator PublicKeyLocator + key *KeyPair +} + +// newBoxEncoder creates a new boxEncoder with the given public key pair and +// third-party public key locator function. +func newBoxEncoder(locator PublicKeyLocator, key *KeyPair) *boxEncoder { + return &boxEncoder{ + key: key, + locator: locator, + } +} + +func (enc *boxEncoder) encodeCaveatId(cav Caveat, rootKey []byte) (string, error) { + if cav.Location == "" { + return "", fmt.Errorf("cannot make caveat id for first party caveat") + } + thirdPartyPub, err := enc.locator.PublicKeyForLocation(cav.Location) + if err != nil { + return "", err + } + id, err := enc.newCaveatId(cav, rootKey, thirdPartyPub) + if err != nil { + return "", err + } + data, err := json.Marshal(id) + if err != nil { + return "", fmt.Errorf("cannot marshal %#v: %v", id, err) + } + return base64.StdEncoding.EncodeToString(data), nil +} + +func (enc *boxEncoder) newCaveatId(cav Caveat, rootKey []byte, thirdPartyPub *PublicKey) (*caveatId, error) { + var nonce [NonceLen]byte + if _, err := rand.Read(nonce[:]); err != nil { + return nil, fmt.Errorf("cannot generate random number for nonce: %v", err) + } + plain := caveatIdRecord{ + RootKey: rootKey, + Condition: cav.Condition, + } + plainData, err := json.Marshal(&plain) + if err != nil { + return nil, fmt.Errorf("cannot marshal %#v: %v", &plain, err) + } + sealed := box.Seal(nil, plainData, &nonce, (*[32]byte)(thirdPartyPub), (*[32]byte)(enc.key.PrivateKey())) + return &caveatId{ + ThirdPartyPublicKey: thirdPartyPub[:], + FirstPartyPublicKey: enc.key.PublicKey()[:], + Nonce: nonce[:], + Id: base64.StdEncoding.EncodeToString(sealed), + }, nil +} + +// boxDecoder decodes caveat ids for third-party service that were encoded to +// the third-party with authenticated public key encryption compatible with +// NaCl box. +type boxDecoder struct { + key *KeyPair +} + +// newBoxDecoder creates a new BoxDecoder using the given key pair. +func newBoxDecoder(key *KeyPair) *boxDecoder { + return &boxDecoder{ + key: key, + } +} + +func (d *boxDecoder) decodeCaveatId(id string) (rootKey []byte, condition string, err error) { + data, err := base64.StdEncoding.DecodeString(id) + if err != nil { + return nil, "", fmt.Errorf("cannot base64-decode caveat id: %v", err) + } + var tpid caveatId + if err := json.Unmarshal(data, &tpid); err != nil { + return nil, "", fmt.Errorf("cannot unmarshal caveat id %q: %v", data, err) + } + var recordData []byte + + recordData, err = d.encryptedCaveatId(tpid) + if err != nil { + return nil, "", err + } + var record caveatIdRecord + if err := json.Unmarshal(recordData, &record); err != nil { + return nil, "", fmt.Errorf("cannot decode third party caveat record: %v", err) + } + return record.RootKey, record.Condition, nil +} + +func (d *boxDecoder) encryptedCaveatId(id caveatId) ([]byte, error) { + if d.key == nil { + return nil, fmt.Errorf("no public key for caveat id decryption") + } + if !bytes.Equal(d.key.PublicKey()[:], id.ThirdPartyPublicKey) { + return nil, fmt.Errorf("public key mismatch") + } + var nonce [NonceLen]byte + if len(id.Nonce) != len(nonce) { + return nil, fmt.Errorf("bad nonce length") + } + copy(nonce[:], id.Nonce) + + var firstPartyPublicKey [KeyLen]byte + if len(id.FirstPartyPublicKey) != len(firstPartyPublicKey) { + return nil, fmt.Errorf("bad public key length") + } + copy(firstPartyPublicKey[:], id.FirstPartyPublicKey) + + sealed, err := base64.StdEncoding.DecodeString(id.Id) + if err != nil { + return nil, fmt.Errorf("cannot base64-decode encrypted caveat id: %v", err) + } + out, ok := box.Open(nil, sealed, &nonce, (*[KeyLen]byte)(&firstPartyPublicKey), (*[KeyLen]byte)(d.key.PrivateKey())) + if !ok { + return nil, fmt.Errorf("decryption of public-key encrypted caveat id %#v failed", id) + } + return out, nil +} diff --git a/bakery/discharge.go b/bakery/discharge.go index d9b2fa6..1c096a7 100644 --- a/bakery/discharge.go +++ b/bakery/discharge.go @@ -19,25 +19,22 @@ type Discharger struct { // Checker is used to check the caveat's condition. Checker ThirdPartyChecker - // Decoder is used to decode the caveat id. - Decoder CaveatIdDecoder - // Factory is used to create the macaroon. // Note that *Service implements NewMacarooner. Factory NewMacarooner + + // boxDecoder is used to decode the caveat id. + decoder *boxDecoder } -// Discharge creates a macaroon that discharges the third party -// caveat with the given id. The id should have been created -// earlier with a matching CaveatIdEncoder. -// The condition implicit in the id is checked for validity -// using d.Checker, and then if valid, a new macaroon -// is minted which discharges the caveat, and -// can eventually be associated with a client request using -// AddClientMacaroon. +// Discharge creates a macaroon that discharges the third party caveat with the +// given id. The id should have been created earlier by a Service. The +// condition implicit in the id is checked for validity using d.Checker, and +// then if valid, a new macaroon is minted which discharges the caveat, and can +// eventually be associated with a client request using AddClientMacaroon. func (d *Discharger) Discharge(id string) (*macaroon.Macaroon, error) { logf("server attempting to discharge %q", id) - rootKey, condition, err := d.Decoder.DecodeCaveatId(id) + rootKey, condition, err := d.decoder.decodeCaveatId(id) if err != nil { return nil, fmt.Errorf("discharger cannot decode caveat id: %v", err) } diff --git a/bakery/example/authservice.go b/bakery/example/authservice.go index 0192cb1..2ef7171 100644 --- a/bakery/example/authservice.go +++ b/bakery/example/authservice.go @@ -10,9 +10,11 @@ import ( // authService implements an authorization service, // that can discharge third-party caveats added // to other macaroons. -func authService(endpoint string) (http.Handler, error) { - svc, err := httpbakery.NewService(httpbakery.NewServiceParams{ +func authService(endpoint string, key *bakery.KeyPair) (http.Handler, error) { + svc, err := httpbakery.NewService(bakery.NewServiceParams{ Location: endpoint, + Key: key, + Locator: bakery.NewPublicKeyRing(), }) if err != nil { return nil, err diff --git a/bakery/example/example_test.go b/bakery/example/example_test.go index 0d36cba..aa51c43 100644 --- a/bakery/example/example_test.go +++ b/bakery/example/example_test.go @@ -5,24 +5,36 @@ import ( "testing" gc "gopkg.in/check.v1" + + "github.com/rogpeppe/macaroon/bakery" ) func TestPackage(t *testing.T) { gc.TestingT(t) } -type exampleSuite struct{} +type exampleSuite struct { + authEndpoint string + authPublicKey *bakery.PublicKey +} var _ = gc.Suite(&exampleSuite{}) -func (*exampleSuite) TestExample(c *gc.C) { - authEndpoint, err := serve(authService) +func (s *exampleSuite) SetUpSuite(c *gc.C) { + key, err := bakery.GenerateKey() c.Assert(err, gc.IsNil) - serverEndpoint, err := serve(func(endpoint string) (http.Handler, error) { - return targetService(endpoint, authEndpoint) + s.authPublicKey = key.PublicKey() + s.authEndpoint, err = serve(func(endpoint string) (http.Handler, error) { + return authService(endpoint, key) }) c.Assert(err, gc.IsNil) +} +func (s *exampleSuite) TestExample(c *gc.C) { + serverEndpoint, err := serve(func(endpoint string) (http.Handler, error) { + return targetService(endpoint, s.authEndpoint, s.authPublicKey) + }) + c.Assert(err, gc.IsNil) c.Logf("gold request") resp, err := clientRequest(serverEndpoint + "/gold") c.Assert(err, gc.IsNil) @@ -34,11 +46,9 @@ func (*exampleSuite) TestExample(c *gc.C) { c.Assert(resp, gc.Equals, "every cloud has a silver lining") } -func (*exampleSuite) BenchmarkExample(c *gc.C) { - authEndpoint, err := serve(authService) - c.Assert(err, gc.IsNil) +func (s *exampleSuite) BenchmarkExample(c *gc.C) { serverEndpoint, err := serve(func(endpoint string) (http.Handler, error) { - return targetService(endpoint, authEndpoint) + return targetService(endpoint, s.authEndpoint, s.authPublicKey) }) c.Assert(err, gc.IsNil) c.ResetTimer() diff --git a/bakery/example/idservice/idservice.go b/bakery/example/idservice/idservice.go index 071c458..25f2bea 100644 --- a/bakery/example/idservice/idservice.go +++ b/bakery/example/idservice/idservice.go @@ -41,7 +41,7 @@ type UserInfo struct { // Params holds parameters for New. type Params struct { - Service httpbakery.NewServiceParams + Service bakery.NewServiceParams Users map[string]*UserInfo } diff --git a/bakery/example/idservice/idservice_test.go b/bakery/example/idservice/idservice_test.go index 2c30f5f..20fe3ca 100644 --- a/bakery/example/idservice/idservice_test.go +++ b/bakery/example/idservice/idservice_test.go @@ -20,13 +20,13 @@ import ( type suite struct { authEndpoint string - authPublicKey *[32]byte + authPublicKey *bakery.PublicKey } var _ = gc.Suite(&suite{}) func (s *suite) SetUpSuite(c *gc.C) { - key, err := httpbakery.GenerateKey() + key, err := bakery.GenerateKey() c.Assert(err, gc.IsNil) s.authPublicKey = key.PublicKey() s.authEndpoint = serve(c, func(endpoint string) (http.Handler, error) { @@ -42,10 +42,11 @@ func (s *suite) SetUpSuite(c *gc.C) { }, }, }, - Service: httpbakery.NewServiceParams{ + Service: bakery.NewServiceParams{ Location: endpoint, Store: bakery.NewMemStorage(), Key: key, + Locator: bakery.NewPublicKeyRing(), }, }) }) diff --git a/bakery/example/idservice/targetservice_test.go b/bakery/example/idservice/targetservice_test.go index aa1de54..295bab2 100644 --- a/bakery/example/idservice/targetservice_test.go +++ b/bakery/example/idservice/targetservice_test.go @@ -21,15 +21,22 @@ type targetServiceHandler struct { // targetService implements a "target service", representing // an arbitrary web service that wants to delegate authorization // to third parties. -func targetService(endpoint, authEndpoint string, authPK *[32]byte) (http.Handler, error) { - svc, err := httpbakery.NewService(httpbakery.NewServiceParams{ +func targetService(endpoint, authEndpoint string, authPK *bakery.PublicKey) (http.Handler, error) { + key, err := bakery.GenerateKey() + if err != nil { + return nil, err + } + pkLocator := bakery.NewPublicKeyRing() + svc, err := httpbakery.NewService(bakery.NewServiceParams{ + Key: key, Location: endpoint, + Locator: pkLocator, }) if err != nil { return nil, err } log.Printf("adding public key for location %s: %x", authEndpoint, authPK[:]) - svc.AddPublicKeyForLocation(authEndpoint, true, authPK) + pkLocator.AddPublicKeyForLocation(authEndpoint, true, authPK) mux := http.NewServeMux() srv := &targetServiceHandler{ svc: svc, diff --git a/bakery/example/main.go b/bakery/example/main.go index ac6cc14..ab8ef33 100644 --- a/bakery/example/main.go +++ b/bakery/example/main.go @@ -22,12 +22,21 @@ import ( "log" "net" "net/http" + + "github.com/rogpeppe/macaroon/bakery" ) func main() { - authEndpoint := mustServe(authService) + key, err := bakery.GenerateKey() + if err != nil { + log.Fatalf("cannot generate auth service key pair: %v", err) + } + authPublicKey := key.PublicKey() + authEndpoint := mustServe(func(endpoint string) (http.Handler, error) { + return authService(endpoint, key) + }) serverEndpoint := mustServe(func(endpoint string) (http.Handler, error) { - return targetService(endpoint, authEndpoint) + return targetService(endpoint, authEndpoint, authPublicKey) }) resp, err := clientRequest(serverEndpoint) if err != nil { diff --git a/bakery/example/targetservice.go b/bakery/example/targetservice.go index 09cb949..a8d4a6b 100644 --- a/bakery/example/targetservice.go +++ b/bakery/example/targetservice.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log" "net" "net/http" "time" @@ -22,13 +23,22 @@ type targetServiceHandler struct { // an arbitrary web service that wants to delegate authorization // to third parties. // -func targetService(endpoint, authEndpoint string) (http.Handler, error) { - svc, err := httpbakery.NewService(httpbakery.NewServiceParams{ +func targetService(endpoint, authEndpoint string, authPK *bakery.PublicKey) (http.Handler, error) { + key, err := bakery.GenerateKey() + if err != nil { + return nil, err + } + pkLocator := bakery.NewPublicKeyRing() + svc, err := httpbakery.NewService(bakery.NewServiceParams{ + Key: key, Location: endpoint, + Locator: pkLocator, }) if err != nil { return nil, err } + log.Printf("adding public key for location %s: %x", authEndpoint, authPK[:]) + pkLocator.AddPublicKeyForLocation(authEndpoint, true, authPK) mux := http.NewServeMux() srv := &targetServiceHandler{ svc: svc, diff --git a/bakery/keys.go b/bakery/keys.go new file mode 100644 index 0000000..71448b6 --- /dev/null +++ b/bakery/keys.go @@ -0,0 +1,144 @@ +package bakery + +import ( + "crypto/rand" + "encoding/hex" + "strings" + "sync" + + "code.google.com/p/go.crypto/nacl/box" +) + +// KeyLen is the byte length of the Ed25519 public and private keys used for +// caveat id encryption. +const KeyLen = 32 + +// NonceLen is the byte length of the nonce values used for caveat id +// encryption. +const NonceLen = 24 + +// PublicKey is a 256-bit Ed25519 public key. +type PublicKey [KeyLen]byte + +// Key is a 256-bit Ed25519 private key. +type Key [KeyLen]byte + +// PublicKeyLocator is used to find the public key for a given +// caveat or macaroon location. +type PublicKeyLocator interface { + // PublicKeyForLocation returns the public key matching the caveat or + // macaroon location. Returns ErrNotFound if no match is found. + PublicKeyForLocation(loc string) (*PublicKey, error) +} + +// PublicKeyLocatorMap implements PublicKeyLocator for a map. +// Each entry in the map holds a public key value for +// a location named by the map key. +type PublicKeyLocatorMap map[string]*PublicKey + +// PublicKeyForLocation implements the PublicKeyLocator interface. +func (m PublicKeyLocatorMap) PublicKeyForLocation(loc string) (*PublicKey, error) { + if pk, ok := m[loc]; ok { + return pk, nil + } + return nil, ErrNotFound +} + +// KeyPair holds a public/private pair of keys. +// TODO(rog) marshal/unmarshal functions for KeyPair +type KeyPair struct { + public PublicKey + private Key +} + +// GenerateKey generates a new key pair. +func GenerateKey() (*KeyPair, error) { + var key KeyPair + pub, priv, err := box.GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + key.public = PublicKey(*pub) + key.private = *priv + return &key, nil +} + +// PublicKey returns the public part of the key pair. +func (key *KeyPair) PublicKey() *PublicKey { + return &key.public +} + +// PrivateKey returns the private part of the key pair. +func (key *KeyPair) PrivateKey() *Key { + return &key.private +} + +// Zeroize sets the private key material to all zeros. +func (key *KeyPair) Zeroize() { + key.private = Key{} +} + +// String implements the fmt.Stringer interface. +func (key *KeyPair) String() string { + return hex.EncodeToString(key.public[:]) +} + +type publicKeyRecord struct { + location string + prefix bool + key PublicKey +} + +// PublicKeyRing stores public keys for third-party services, accessible by +// location string. +type PublicKeyRing struct { + // mu guards the fields following it. + mu sync.Mutex + + // TODO(rog) use a more efficient data structure + publicKeys []publicKeyRecord +} + +// NewPublicKeyRing returns a new PublicKeyRing instance. +func NewPublicKeyRing() *PublicKeyRing { + return &PublicKeyRing{} +} + +// AddPublicKeyForLocation adds a public key to the keyring for the given +// location or location prefix. +func (kr *PublicKeyRing) AddPublicKeyForLocation(loc string, prefix bool, key *PublicKey) { + kr.mu.Lock() + defer kr.mu.Unlock() + kr.publicKeys = append(kr.publicKeys, publicKeyRecord{ + location: loc, + prefix: prefix, + key: *key, + }) +} + +// PublicKeyForLocation implements the PublicKeyLocator interface. +func (kr *PublicKeyRing) PublicKeyForLocation(loc string) (*PublicKey, error) { + kr.mu.Lock() + defer kr.mu.Unlock() + var ( + longestPrefix string + longestPrefixKey *PublicKey // public key associated with longest prefix + ) + for i := len(kr.publicKeys) - 1; i >= 0; i-- { + k := kr.publicKeys[i] + if k.location == loc && !k.prefix { + return &k.key, nil + } + if !k.prefix { + continue + } + if strings.HasPrefix(loc, k.location) && len(k.location) > len(longestPrefix) { + longestPrefix = k.location + longestPrefixKey = &k.key + } + } + if len(longestPrefix) == 0 { + return nil, ErrNotFound + } + return longestPrefixKey, nil +} diff --git a/bakery/service.go b/bakery/service.go index 50d2070..dcf52ad 100644 --- a/bakery/service.go +++ b/bakery/service.go @@ -24,10 +24,10 @@ func logf(f string, a ...interface{}) { // Service represents a service which can use macaroons // to check authorization. type Service struct { - location string - store storage - checker FirstPartyChecker - caveatIdEncoder CaveatIdEncoder + location string + store storage + checker FirstPartyChecker + encoder *boxEncoder } // NewServiceParams holds the parameters for a NewService call. @@ -41,21 +41,38 @@ type NewServiceParams struct { // an in-memory storage will be used. Store Storage - // CaveatIdEncoder is used to create third-party caveats. - CaveatIdEncoder CaveatIdEncoder + // Key is the public key pair used by the service for + // third-party caveat encryption. + Key *KeyPair + + // Locator provides public keys for third-party services + // by location when adding a third-party caveat. + Locator PublicKeyLocator } // NewService returns a new service that can mint new // macaroons and store their associated root keys. -func NewService(p NewServiceParams) *Service { +func NewService(p NewServiceParams) (*Service, error) { if p.Store == nil { p.Store = NewMemStorage() } - return &Service{ - location: p.Location, - store: storage{p.Store}, - caveatIdEncoder: p.CaveatIdEncoder, + svc := &Service{ + location: p.Location, + store: storage{p.Store}, + } + + var err error + if p.Key == nil { + p.Key, err = GenerateKey() + if err != nil { + return nil, err + } + } + if p.Locator == nil { + p.Locator = make(PublicKeyLocatorMap) } + svc.encoder = newBoxEncoder(p.Locator, p.Key) + return svc, nil } // Store returns the store used by the service. @@ -68,18 +85,6 @@ func (svc *Service) Location() string { return svc.location } -// CaveatIdDecoder decodes caveat ids created by a CaveatIdEncoder. -type CaveatIdDecoder interface { - DecodeCaveatId(id string) (rootKey []byte, condition string, err error) -} - -// CaveatIdEncoder can create caveat ids for -// third parties. It is left abstract to allow location-dependent -// caveat id creation. -type CaveatIdEncoder interface { - EncodeCaveatId(caveat Caveat, rootKey []byte) (string, error) -} - // Caveat represents a condition that must be true for a check to // complete successfully. If Location is non-empty, the caveat must be // discharged by a third party at the given location. @@ -205,7 +210,7 @@ func (svc *Service) AddCaveat(m *macaroon.Macaroon, cav Caveat) error { if err != nil { return fmt.Errorf("cannot generate third party secret: %v", err) } - id, err := svc.caveatIdEncoder.EncodeCaveatId(cav, rootKey) + id, err := svc.encoder.encodeCaveatId(cav, rootKey) if err != nil { return fmt.Errorf("cannot create third party caveat id at %q: %v", cav.Location, err) } @@ -215,6 +220,18 @@ func (svc *Service) AddCaveat(m *macaroon.Macaroon, cav Caveat) error { return nil } +// Discharger returns a Discharger that uses the receiving service +// to create its macaroons and to decode third-party caveat ids. +// The decoded caveat ids are checked using the provided +// checker. +func (svc *Service) Discharger(checker ThirdPartyChecker) *Discharger { + return &Discharger{ + Checker: checker, + Factory: svc, + decoder: newBoxDecoder(svc.encoder.key), + } +} + func randomBytes(n int) ([]byte, error) { b := make([]byte, n) _, err := rand.Read(b) diff --git a/httpbakery/caveatid.go b/httpbakery/caveatid.go deleted file mode 100644 index 29857a2..0000000 --- a/httpbakery/caveatid.go +++ /dev/null @@ -1,295 +0,0 @@ -package httpbakery - -import ( - "bytes" - "crypto/rand" - "encoding/base64" - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - "sync" - - "code.google.com/p/go.crypto/nacl/box" - - "github.com/rogpeppe/macaroon/bakery" -) - -const keyLen = 32 - -// caveatIdEncoder implements bakery.CaveatIdEncoder. It -// knows how to make caveat ids by communicating -// with the caveat id creation service served by DischargeHandler, -// and also how to create caveat ids using public key -// cryptography (also recognised by the DischargeHandler -// service). -type caveatIdEncoder struct { - key KeyPair - - // mu guards the fields following it. - mu sync.Mutex - - // TODO(rog) use a more efficient data structure - publicKeys []publicKeyRecord - - httpClient *http.Client -} - -type publicKeyRecord struct { - location string - prefix bool - key [32]byte -} - -// KeyPair holds a public/private pair of keys. -// TODO(rog) marshal/unmarshal functions for KeyPair -type KeyPair struct { - public [32]byte - private [32]byte -} - -// GenerateKey generates a new key pair. -func GenerateKey() (*KeyPair, error) { - var key KeyPair - pub, priv, err := box.GenerateKey(rand.Reader) - if err != nil { - return nil, err - } - key.public = *pub - key.private = *priv - return &key, nil -} - -// PublicKey returns the public part of the key pair. -func (key *KeyPair) PublicKey() *[32]byte { - return &key.public -} - -// newCaveatIdEncoder returns a new caveatIdEncoder using key, which should -// have been created using GenerateKey. -func newCaveatIdEncoder(c *http.Client, key *KeyPair) *caveatIdEncoder { - return &caveatIdEncoder{ - key: *key, - httpClient: c, - } -} - -type caveatIdResponse struct { - CaveatId string - Error string -} - -type caveatIdSealed struct { - Condition string - Secret []byte -} - -// EncodeCaveatId implements bakery.CaveatIdEncoder.EncodeCaveatId. -// This is the client side of DischargeHandler's /create endpoint. -func (enc *caveatIdEncoder) EncodeCaveatId(cav bakery.Caveat, rootKey []byte) (string, error) { - if cav.Location == "" { - return "", fmt.Errorf("cannot make caveat id for first party caveat") - } - var id *thirdPartyCaveatId - var err error - thirdPartyPub := enc.publicKeyForLocation(cav.Location) - if thirdPartyPub != nil { - id, err = enc.newEncryptedCaveatId(cav, rootKey, thirdPartyPub) - } else { - id, err = enc.newStoredCaveatId(cav, rootKey) - } - if err != nil { - return "", err - } - data, err := json.Marshal(id) - if err != nil { - return "", fmt.Errorf("cannot marshal %#v: %v", id, err) - } - return base64.StdEncoding.EncodeToString(data), nil -} - -func (enc *caveatIdEncoder) newEncryptedCaveatId(cav bakery.Caveat, rootKey []byte, thirdPartyPub *[32]byte) (*thirdPartyCaveatId, error) { - var nonce [24]byte - if _, err := rand.Read(nonce[:]); err != nil { - return nil, fmt.Errorf("cannot generate random number for nonce: %v", err) - } - plain := thirdPartyCaveatIdRecord{ - RootKey: rootKey, - Condition: cav.Condition, - } - plainData, err := json.Marshal(&plain) - if err != nil { - return nil, fmt.Errorf("cannot marshal %#v: %v", &plain, err) - } - sealed := box.Seal(nil, plainData, &nonce, thirdPartyPub, &enc.key.private) - return &thirdPartyCaveatId{ - ThirdPartyPublicKey: thirdPartyPub[:], - FirstPartyPublicKey: enc.key.public[:], - Nonce: nonce[:], - Id: base64.StdEncoding.EncodeToString(sealed), - }, nil -} - -func (enc *caveatIdEncoder) newStoredCaveatId(cav bakery.Caveat, rootKey []byte) (*thirdPartyCaveatId, error) { - // TODO(rog) fetch public key from service here, and use public - // key encryption if available? - - // TODO(rog) check that the URL is https? - // Is that really just smoke and mirrors though? - // Are there advantages to having an unrestricted protocol? - u := appendURLElem(cav.Location, "create") - - // TODO(rog) should we use the logic found in clientContext.do? - var resp caveatIdResponse - if err := postFormJSON(u, url.Values{ - "condition": {cav.Condition}, - "root-key": {base64.StdEncoding.EncodeToString(rootKey)}, - }, &resp, enc.httpClient.PostForm); err != nil { - return nil, fmt.Errorf("cannot create caveat id through %q: %v", u, err) - } - if resp.Error != "" { - return nil, fmt.Errorf("remote error from %q: %v", u, resp.Error) - } - if resp.CaveatId == "" { - return nil, fmt.Errorf("empty caveat id returned from %q", u) - } - return &thirdPartyCaveatId{ - Id: resp.CaveatId, - }, nil -} - -func appendURLElem(u, elem string) string { - if strings.HasSuffix(u, "/") { - return u + elem - } - return u + "/" + elem -} - -// thirdPartyCaveatId defines the format -// of a third party caveat id. If ThirdPartyPublicKey -// is non-empty, then both FirstPartyPublicKey -// and Nonce must be set, and the id will have -// been encrypted with the third party public key -// and base64-encoded. -// -// If not, the Id holds an id that was created -// by the third party. -type thirdPartyCaveatId struct { - ThirdPartyPublicKey []byte `json:",omitempty"` - FirstPartyPublicKey []byte `json:",omitempty"` - Nonce []byte `json:",omitempty"` - Id string -} - -func (enc *caveatIdEncoder) addPublicKeyForLocation(loc string, prefix bool, key *[32]byte) { - enc.mu.Lock() - defer enc.mu.Unlock() - enc.publicKeys = append(enc.publicKeys, publicKeyRecord{ - location: loc, - prefix: prefix, - key: *key, - }) -} - -func (enc *caveatIdEncoder) publicKeyForLocation(loc string) *[32]byte { - enc.mu.Lock() - defer enc.mu.Unlock() - var ( - longestPrefix string - longestPrefixKey *[32]byte // public key associated with longest prefix - ) - for i := len(enc.publicKeys) - 1; i >= 0; i-- { - k := enc.publicKeys[i] - if k.location == loc && !k.prefix { - return &k.key - } - if !k.prefix { - continue - } - if strings.HasPrefix(loc, k.location) && len(k.location) > len(longestPrefix) { - longestPrefix = k.location - longestPrefixKey = &k.key - } - } - if len(longestPrefix) == 0 { - return nil - } - return longestPrefixKey -} - -type caveatIdDecoder struct { - store bakery.Storage - key *KeyPair -} - -func newCaveatIdDecoder(store bakery.Storage, key *KeyPair) bakery.CaveatIdDecoder { - return &caveatIdDecoder{ - store: store, - key: key, - } -} - -func (d *caveatIdDecoder) DecodeCaveatId(id string) (rootKey []byte, condition string, err error) { - data, err := base64.StdEncoding.DecodeString(id) - if err != nil { - return nil, "", fmt.Errorf("cannot base64-decode caveat id: %v", err) - } - var tpid thirdPartyCaveatId - if err := json.Unmarshal(data, &tpid); err != nil { - return nil, "", fmt.Errorf("cannot unmarshal caveat id %q: %v", data, err) - } - var recordData []byte - - if tpid.ThirdPartyPublicKey != nil { - recordData, err = d.encryptedCaveatId(tpid) - } else { - recordData, err = d.storedCaveatId(tpid.Id) - } - if err != nil { - return nil, "", err - } - var record thirdPartyCaveatIdRecord - if err := json.Unmarshal(recordData, &record); err != nil { - return nil, "", fmt.Errorf("cannot decode third party caveat record: %v", err) - } - return record.RootKey, record.Condition, nil -} - -func (d *caveatIdDecoder) encryptedCaveatId(id thirdPartyCaveatId) ([]byte, error) { - if d.key == nil { - return nil, fmt.Errorf("no public key for caveat id decryption") - } - if !bytes.Equal(d.key.public[:], id.ThirdPartyPublicKey) { - return nil, fmt.Errorf("public key mismatch") - } - var nonce [24]byte - if len(id.Nonce) != len(nonce) { - return nil, fmt.Errorf("bad nonce length") - } - copy(nonce[:], id.Nonce) - - var firstPartyPublicKey [32]byte - if len(id.FirstPartyPublicKey) != len(firstPartyPublicKey) { - return nil, fmt.Errorf("bad public key length") - } - copy(firstPartyPublicKey[:], id.FirstPartyPublicKey) - - sealed, err := base64.StdEncoding.DecodeString(id.Id) - if err != nil { - return nil, fmt.Errorf("cannot base64-decode encrypted caveat id", err) - } - out, ok := box.Open(nil, sealed, &nonce, &firstPartyPublicKey, &d.key.private) - if !ok { - return nil, fmt.Errorf("decryption of public-key encrypted caveat id %#v failed", id) - } - return out, nil -} - -func (d *caveatIdDecoder) storedCaveatId(id string) ([]byte, error) { - str, err := d.store.Get(id) - if err != nil { - return nil, err - } - return []byte(str), nil -} diff --git a/httpbakery/client.go b/httpbakery/client.go index 84c8306..5eefe10 100644 --- a/httpbakery/client.go +++ b/httpbakery/client.go @@ -145,6 +145,13 @@ func (ctxt *clientContext) addCookies(req *http.Request, ms []*macaroon.Macaroon return nil } +func appendURLElem(u, elem string) string { + if strings.HasSuffix(u, "/") { + return u + elem + } + return u + "/" + elem +} + func (ctxt *clientContext) obtainThirdPartyDischarge(originalLocation string, cav macaroon.Caveat) (*macaroon.Macaroon, error) { var resp dischargeResponse loc := appendURLElem(cav.Location, "discharge") diff --git a/httpbakery/discharge.go b/httpbakery/discharge.go index ff652de..e025307 100644 --- a/httpbakery/discharge.go +++ b/httpbakery/discharge.go @@ -129,6 +129,11 @@ type thirdPartyCaveatIdRecord struct { Condition string } +type caveatIdResponse struct { + CaveatId string + Error string +} + func (d *dischargeHandler) serveCreate(h http.Header, req *http.Request) (interface{}, error) { req.ParseForm() condition := req.Form.Get("condition") diff --git a/httpbakery/service.go b/httpbakery/service.go index dece625..a427db2 100644 --- a/httpbakery/service.go +++ b/httpbakery/service.go @@ -24,13 +24,6 @@ import ( // to create third-party caveats. type Service struct { *bakery.Service - caveatIdEncoder *caveatIdEncoder - key KeyPair -} - -// Key returns the service's private/public key pair. -func (svc *Service) Key() *KeyPair { - return &svc.key } // DefaultHTTPClient is an http.Client that ensures that @@ -75,77 +68,13 @@ func (j *cookieLogger) SetCookies(u *url.URL, cookies []*http.Cookie) { j.CookieJar.SetCookies(u, cookies) } -// NewServiceParams holds parameters for the NewService call. -type NewServiceParams struct { - // Location holds the location of the service. - // Macaroons minted by the service will have this location. - Location string - - // Store defines where macaroons are stored. - Store bakery.Storage - - // Key holds the private/public key pair for - // the service to use. If it is nil, a new key pair - // will be generated. - Key *KeyPair - - // HTTPClient holds the http client to use when - // creating new third party caveats for third - // parties. If it is nil, DefaultHTTPClient will be used. - HTTPClient *http.Client -} - // NewService returns a new Service. -func NewService(p NewServiceParams) (*Service, error) { - if p.Key == nil { - key, err := GenerateKey() - if err != nil { - return nil, fmt.Errorf("cannot generate key: %v", err) - } - p.Key = key - } - log.Printf("new service at %s with public key %x", p.Location, p.Key.public[:]) - if p.HTTPClient == nil { - p.HTTPClient = DefaultHTTPClient - } - enc := newCaveatIdEncoder(p.HTTPClient, p.Key) - return &Service{ - Service: bakery.NewService(bakery.NewServiceParams{ - Location: p.Location, - Store: p.Store, - CaveatIdEncoder: enc, - }), - caveatIdEncoder: enc, - key: *p.Key, - }, nil -} - -// AddPublicKeyForLocation specifies that third party caveats -// for the given location will be encrypted with the given public -// key. If prefix is true, any locations with loc as a prefix will -// be also associated with the given key. The longest prefix -// match will be chosen. -// TODO(rog) perhaps string might be a better representation -// of public keys? -// TODO(rog) strict string prefix is bad when locations -// are URLs. We should probably parse them as URLs -// and dispatch in a more intelligent way (for example -// by matching host name exactly and the path by -// full path name elements only.) -func (svc *Service) AddPublicKeyForLocation(loc string, prefix bool, publicKey *[32]byte) { - svc.caveatIdEncoder.addPublicKeyForLocation(loc, prefix, publicKey) -} - -// Discharger returns a discharger that uses the receiving service -// to create its macaroons and to decode third-party caveat ids. -// The decoded caveat ids are checked using the provided -// checker. -func (svc *Service) Discharger(checker bakery.ThirdPartyChecker) *bakery.Discharger { - return &bakery.Discharger{ - Checker: checker, - Decoder: newCaveatIdDecoder(svc.Store(), svc.Key()), - Factory: svc, +func NewService(p bakery.NewServiceParams) (*Service, error) { + svc, err := bakery.NewService(p) + if err != nil { + return nil, err } + return &Service{Service: svc}, nil } // NewRequest returns a new request, converting cookies from the