ref: Remote REST signer implementation.
MultiPart: 2/2
Change-Id: I8de2eb4def9f857695b0ef8334aae20af60bcb45
diff --git a/services/identity/internal/rest_signer_test/main.go b/services/identity/internal/rest_signer_test/main.go
new file mode 100644
index 0000000..03995d6
--- /dev/null
+++ b/services/identity/internal/rest_signer_test/main.go
@@ -0,0 +1,25 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+package main
+
+import (
+ "fmt"
+
+ "v.io/x/ref/services/identity/internal/server"
+)
+
+func main() {
+ signer, err := server.NewRestSigner()
+ if err != nil {
+ fmt.Printf("NewRestSigner error: %v\n", err)
+ return
+ }
+ sig, err := signer.Sign([]byte("purpose"), []byte("message"))
+ if err != nil {
+ fmt.Printf("Sign error: %v\n", err)
+ return
+ }
+ ok := sig.Verify(signer.PublicKey(), []byte("message"))
+ fmt.Printf("Verified: %v\n", ok)
+}
diff --git a/services/identity/internal/server/rest_signer.go b/services/identity/internal/server/rest_signer.go
new file mode 100644
index 0000000..5a8efbf
--- /dev/null
+++ b/services/identity/internal/server/rest_signer.go
@@ -0,0 +1,69 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server
+
+import (
+ "crypto/ecdsa"
+ "crypto/x509"
+ "encoding/base64"
+ "fmt"
+ "math/big"
+
+ "golang.org/x/oauth2"
+ "golang.org/x/oauth2/google"
+ "v.io/v23/security"
+ "v.io/x/ref/services/identity/internal/signer/v1"
+)
+
+func DecodePublicKey(k *signer.PublicKey) (*ecdsa.PublicKey, error) {
+ bytes, err := base64.URLEncoding.DecodeString(k.Base64)
+ if err != nil {
+ return nil, err
+ }
+ key, err := x509.ParsePKIXPublicKey(bytes)
+ if err != nil {
+ return nil, err
+ }
+ pub, ok := key.(*ecdsa.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("Not an ECDSA public key")
+ }
+ return pub, nil
+}
+
+func DecodeSignature(sig *signer.VSignature) (r, s *big.Int, err error) {
+ r, s = new(big.Int), new(big.Int)
+ if _, ok := r.SetString(sig.R, 0); !ok {
+ return nil, nil, fmt.Errorf("unable to parse big.Int %s", sig.R)
+ }
+ if _, ok := s.SetString(sig.S, 0); !ok {
+ return nil, nil, fmt.Errorf("unable to parse big.Int %s", sig.S)
+ }
+ return
+}
+
+func NewRestSigner() (security.Signer, error) {
+ client, err := signer.New(oauth2.NewClient(oauth2.NoContext, google.ComputeTokenSource("")))
+ if err != nil {
+ return nil, err
+ }
+ jkey, err := client.PublicKey().Do()
+ if err != nil {
+ return nil, err
+ }
+ key, err := DecodePublicKey(jkey)
+ if err != nil {
+ return nil, err
+ }
+ sign := func(message []byte) (r, s *big.Int, err error) {
+ msgBase64 := base64.URLEncoding.EncodeToString(message)
+ jsig, err := client.Sign(msgBase64).Do()
+ if err != nil {
+ return nil, nil, err
+ }
+ return DecodeSignature(jsig)
+ }
+ return security.NewECDSASigner(key, sign), nil
+}
diff --git a/services/identity/internal/server/rest_signer_test.go b/services/identity/internal/server/rest_signer_test.go
new file mode 100644
index 0000000..ef4cd11
--- /dev/null
+++ b/services/identity/internal/server/rest_signer_test.go
@@ -0,0 +1,35 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package server_test
+
+import (
+ "math/big"
+ "testing"
+
+ "v.io/v23/security"
+ "v.io/x/ref/services/identity/internal/server"
+ "v.io/x/ref/services/identity/internal/signer/v1"
+)
+
+func TestDecode(t *testing.T) {
+ encodedKey := &signer.PublicKey{Base64: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPqDbuT2B9Bb3JMcOGd2mm4bQuKSREeSKRt8_oofo0jRYiKFQ2ZVuCqssA-IUvFArT5KfXc6B9BNesgS10rPKrg=="}
+ encodedSig := &signer.VSignature{R: "0x42bca58e435f906c874536789cfc31656dd8f9ffbd3b7be84181611cc04eaf74", S: "0xa6f57e858a9f36b559e9cd9f13854b90fad49e0c5591ed66033fd286682b2078"}
+
+ key, err := server.DecodePublicKey(encodedKey)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ s := security.NewECDSASigner(key, func(message []byte) (r, s *big.Int, err error) {
+ return server.DecodeSignature(encodedSig)
+ })
+ sig, err := s.Sign([]byte("purpose"), []byte("message"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !sig.Verify(s.PublicKey(), []byte("message")) {
+ t.Fatal("Signature does not verify")
+ }
+}
diff --git a/services/identity/internal/signer/v1/README b/services/identity/internal/signer/v1/README
new file mode 100644
index 0000000..23fa070
--- /dev/null
+++ b/services/identity/internal/signer/v1/README
@@ -0,0 +1,12 @@
+Auto generated client for the signer service.
+
+The service is implemented using Cloud Endpoints
+https://cloud.google.com/endpoints/
+
+The signer-api.json file is auto generated from the server implementation.
+
+The signer-gen.go file is generated using
+google.golang.org/api/google-api-go-generator:
+
+google-api-go-generator --api_json_file=signer-api.json -output=signer-gen.go
+
diff --git a/services/identity/internal/signer/v1/signer-api.json b/services/identity/internal/signer/v1/signer-api.json
new file mode 100644
index 0000000..3a4e012
--- /dev/null
+++ b/services/identity/internal/signer/v1/signer-api.json
@@ -0,0 +1,130 @@
+{
+ "kind": "discovery#restDescription",
+ "etag": "\"u_zXkMELIlX4ktyNbM2XKD4vK8E/fv51HUFpIp2SZ4FxWW-y5odvVuE\"",
+ "discoveryVersion": "v1",
+ "id": "signer:v1",
+ "name": "signer",
+ "version": "v1",
+ "description": "Vanadium remote signer",
+ "icons": {
+ "x16": "http://www.google.com/images/icons/product/search-16.gif",
+ "x32": "http://www.google.com/images/icons/product/search-32.gif"
+ },
+ "protocol": "rest",
+ "baseUrl": "https://vanadium-production.appspot.com/_ah/api/signer/v1/",
+ "basePath": "/_ah/api/signer/v1/",
+ "rootUrl": "https://vanadium-production.appspot.com/_ah/api/",
+ "servicePath": "signer/v1/",
+ "batchPath": "batch",
+ "parameters": {
+ "alt": {
+ "type": "string",
+ "description": "Data format for the response.",
+ "default": "json",
+ "enum": [
+ "json"
+ ],
+ "enumDescriptions": [
+ "Responses with Content-Type of application/json"
+ ],
+ "location": "query"
+ },
+ "fields": {
+ "type": "string",
+ "description": "Selector specifying which fields to include in a partial response.",
+ "location": "query"
+ },
+ "key": {
+ "type": "string",
+ "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.",
+ "location": "query"
+ },
+ "oauth_token": {
+ "type": "string",
+ "description": "OAuth 2.0 token for the current user.",
+ "location": "query"
+ },
+ "prettyPrint": {
+ "type": "boolean",
+ "description": "Returns response with indentations and line breaks.",
+ "default": "true",
+ "location": "query"
+ },
+ "quotaUser": {
+ "type": "string",
+ "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.",
+ "location": "query"
+ },
+ "userIp": {
+ "type": "string",
+ "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.",
+ "location": "query"
+ }
+ },
+ "auth": {
+ "oauth2": {
+ "scopes": {
+ "https://www.googleapis.com/auth/userinfo.email": {
+ "description": "View your email address"
+ }
+ }
+ }
+ },
+ "schemas": {
+ "PublicKey": {
+ "id": "PublicKey",
+ "type": "object",
+ "properties": {
+ "base64": {
+ "type": "string"
+ }
+ }
+ },
+ "VSignature": {
+ "id": "VSignature",
+ "type": "object",
+ "properties": {
+ "r": {
+ "type": "string"
+ },
+ "s": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "methods": {
+ "publicKey": {
+ "id": "signer.publicKey",
+ "path": "publicKey",
+ "httpMethod": "POST",
+ "response": {
+ "$ref": "PublicKey"
+ },
+ "scopes": [
+ "https://www.googleapis.com/auth/userinfo.email"
+ ]
+ },
+ "sign": {
+ "id": "signer.sign",
+ "path": "sign/{base64}",
+ "httpMethod": "POST",
+ "parameters": {
+ "base64": {
+ "type": "string",
+ "required": true,
+ "location": "path"
+ }
+ },
+ "parameterOrder": [
+ "base64"
+ ],
+ "response": {
+ "$ref": "VSignature"
+ },
+ "scopes": [
+ "https://www.googleapis.com/auth/userinfo.email"
+ ]
+ }
+ }
+}
diff --git a/services/identity/internal/signer/v1/signer-gen.go b/services/identity/internal/signer/v1/signer-gen.go
new file mode 100644
index 0000000..98308ed
--- /dev/null
+++ b/services/identity/internal/signer/v1/signer-gen.go
@@ -0,0 +1,207 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package signer provides access to the .
+//
+// Usage example:
+//
+// import "google.golang.org/api/signer/v1"
+// ...
+// signerService, err := signer.New(oauthHttpClient)
+package signer
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "golang.org/x/net/context"
+ "google.golang.org/api/googleapi"
+ "io"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+// Always reference these packages, just in case the auto-generated code
+// below doesn't.
+var _ = bytes.NewBuffer
+var _ = strconv.Itoa
+var _ = fmt.Sprintf
+var _ = json.NewDecoder
+var _ = io.Copy
+var _ = url.Parse
+var _ = googleapi.Version
+var _ = errors.New
+var _ = strings.Replace
+var _ = context.Background
+
+const apiId = "signer:v1"
+const apiName = "signer"
+const apiVersion = "v1"
+const basePath = "https://vanadium-production.appspot.com/_ah/api/signer/v1/"
+
+// OAuth2 scopes used by this API.
+const (
+ // View your email address
+ UserinfoEmailScope = "https://www.googleapis.com/auth/userinfo.email"
+)
+
+func New(client *http.Client) (*Service, error) {
+ if client == nil {
+ return nil, errors.New("client is nil")
+ }
+ s := &Service{client: client, BasePath: basePath}
+ return s, nil
+}
+
+type Service struct {
+ client *http.Client
+ BasePath string // API endpoint base URL
+}
+
+type PublicKey struct {
+ Base64 string `json:"base64,omitempty"`
+}
+
+type VSignature struct {
+ R string `json:"r,omitempty"`
+
+ S string `json:"s,omitempty"`
+}
+
+// method id "signer.publicKey":
+
+type PublicKeyCall struct {
+ s *Service
+ opt_ map[string]interface{}
+}
+
+// PublicKey:
+func (s *Service) PublicKey() *PublicKeyCall {
+ c := &PublicKeyCall{s: s, opt_: make(map[string]interface{})}
+ return c
+}
+
+// Fields allows partial responses to be retrieved.
+// See https://developers.google.com/gdata/docs/2.0/basics#PartialResponse
+// for more information.
+func (c *PublicKeyCall) Fields(s ...googleapi.Field) *PublicKeyCall {
+ c.opt_["fields"] = googleapi.CombineFields(s)
+ return c
+}
+
+func (c *PublicKeyCall) Do() (*PublicKey, error) {
+ var body io.Reader = nil
+ params := make(url.Values)
+ params.Set("alt", "json")
+ if v, ok := c.opt_["fields"]; ok {
+ params.Set("fields", fmt.Sprintf("%v", v))
+ }
+ urls := googleapi.ResolveRelative(c.s.BasePath, "publicKey")
+ urls += "?" + params.Encode()
+ req, _ := http.NewRequest("POST", urls, body)
+ googleapi.SetOpaque(req.URL)
+ req.Header.Set("User-Agent", "google-api-go-client/0.5")
+ res, err := c.s.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer googleapi.CloseBody(res)
+ if err := googleapi.CheckResponse(res); err != nil {
+ return nil, err
+ }
+ var ret *PublicKey
+ if err := json.NewDecoder(res.Body).Decode(&ret); err != nil {
+ return nil, err
+ }
+ return ret, nil
+ // {
+ // "httpMethod": "POST",
+ // "id": "signer.publicKey",
+ // "path": "publicKey",
+ // "response": {
+ // "$ref": "PublicKey"
+ // },
+ // "scopes": [
+ // "https://www.googleapis.com/auth/userinfo.email"
+ // ]
+ // }
+
+}
+
+// method id "signer.sign":
+
+type SignCall struct {
+ s *Service
+ base64 string
+ opt_ map[string]interface{}
+}
+
+// Sign:
+func (s *Service) Sign(base64 string) *SignCall {
+ c := &SignCall{s: s, opt_: make(map[string]interface{})}
+ c.base64 = base64
+ return c
+}
+
+// Fields allows partial responses to be retrieved.
+// See https://developers.google.com/gdata/docs/2.0/basics#PartialResponse
+// for more information.
+func (c *SignCall) Fields(s ...googleapi.Field) *SignCall {
+ c.opt_["fields"] = googleapi.CombineFields(s)
+ return c
+}
+
+func (c *SignCall) Do() (*VSignature, error) {
+ var body io.Reader = nil
+ params := make(url.Values)
+ params.Set("alt", "json")
+ if v, ok := c.opt_["fields"]; ok {
+ params.Set("fields", fmt.Sprintf("%v", v))
+ }
+ urls := googleapi.ResolveRelative(c.s.BasePath, "sign/{base64}")
+ urls += "?" + params.Encode()
+ req, _ := http.NewRequest("POST", urls, body)
+ googleapi.Expand(req.URL, map[string]string{
+ "base64": c.base64,
+ })
+ req.Header.Set("User-Agent", "google-api-go-client/0.5")
+ res, err := c.s.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer googleapi.CloseBody(res)
+ if err := googleapi.CheckResponse(res); err != nil {
+ return nil, err
+ }
+ var ret *VSignature
+ if err := json.NewDecoder(res.Body).Decode(&ret); err != nil {
+ return nil, err
+ }
+ return ret, nil
+ // {
+ // "httpMethod": "POST",
+ // "id": "signer.sign",
+ // "parameterOrder": [
+ // "base64"
+ // ],
+ // "parameters": {
+ // "base64": {
+ // "location": "path",
+ // "required": true,
+ // "type": "string"
+ // }
+ // },
+ // "path": "sign/{base64}",
+ // "response": {
+ // "$ref": "VSignature"
+ // },
+ // "scopes": [
+ // "https://www.googleapis.com/auth/userinfo.email"
+ // ]
+ // }
+
+}