auth: fetch tokens from client side
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>v0.8
parent
a2563079f7
commit
1f94445456
|
@ -6,7 +6,6 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
"github.com/moby/buildkit/cmd/buildctl/build"
|
||||
|
@ -15,7 +14,7 @@ import (
|
|||
"github.com/moby/buildkit/session/auth/authprovider"
|
||||
"github.com/moby/buildkit/session/sshforward/sshprovider"
|
||||
"github.com/moby/buildkit/solver/pb"
|
||||
"github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/moby/buildkit/util/progress/progresswriter"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
@ -204,7 +203,6 @@ func buildAction(clicontext *cli.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
ch := make(chan *client.SolveStatus)
|
||||
eg, ctx := errgroup.WithContext(bccommon.CommandContext(clicontext))
|
||||
|
||||
solveOpt := client.SolveOpt{
|
||||
|
@ -246,8 +244,46 @@ func buildAction(clicontext *cli.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
// not using shared context to not disrupt display but let is finish reporting errors
|
||||
pw, err := progresswriter.NewPrinter(context.TODO(), os.Stderr, clicontext.String("progress"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if traceEnc != nil {
|
||||
traceCh := make(chan *client.SolveStatus)
|
||||
pw = progresswriter.Tee(pw, traceCh)
|
||||
eg.Go(func() error {
|
||||
resp, err := c.Solve(ctx, def, solveOpt, ch)
|
||||
for s := range traceCh {
|
||||
if err := traceEnc.Encode(s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
mw := progresswriter.NewMultiWriter(pw)
|
||||
|
||||
var writers []progresswriter.Writer
|
||||
for _, at := range attachable {
|
||||
if s, ok := at.(interface {
|
||||
SetLogger(progresswriter.Logger)
|
||||
}); ok {
|
||||
w := mw.WithPrefix("", false)
|
||||
s.SetLogger(func(s *client.SolveStatus) {
|
||||
w.Status() <- s
|
||||
})
|
||||
writers = append(writers, w)
|
||||
}
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
defer func() {
|
||||
for _, w := range writers {
|
||||
close(w.Status())
|
||||
}
|
||||
}()
|
||||
resp, err := c.Solve(ctx, def, solveOpt, progresswriter.ResetTime(mw.WithPrefix("", false)).Status())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -257,42 +293,10 @@ func buildAction(clicontext *cli.Context) error {
|
|||
return err
|
||||
})
|
||||
|
||||
displayCh := ch
|
||||
if traceEnc != nil {
|
||||
displayCh = make(chan *client.SolveStatus)
|
||||
eg.Go(func() error {
|
||||
defer close(displayCh)
|
||||
for s := range ch {
|
||||
if err := traceEnc.Encode(s); err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
displayCh <- s
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
var c console.Console
|
||||
progressOpt := clicontext.String("progress")
|
||||
|
||||
switch progressOpt {
|
||||
case "auto", "tty":
|
||||
cf, err := console.ConsoleFromFile(os.Stderr)
|
||||
if err != nil && progressOpt == "tty" {
|
||||
return err
|
||||
}
|
||||
c = cf
|
||||
case "plain":
|
||||
default:
|
||||
return errors.Errorf("invalid progress value : %s", progressOpt)
|
||||
}
|
||||
|
||||
// not using shared context to not disrupt display but let is finish reporting errors
|
||||
return progressui.DisplaySolveStatus(context.TODO(), "", c, os.Stderr, displayCh)
|
||||
<-pw.Done()
|
||||
return pw.Err()
|
||||
})
|
||||
|
||||
err = eg.Wait()
|
||||
|
||||
return err
|
||||
return eg.Wait()
|
||||
}
|
||||
|
|
|
@ -2,12 +2,29 @@ package auth
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/moby/buildkit/session"
|
||||
"github.com/moby/buildkit/util/grpcerrors"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/nacl/sign"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var salt []byte
|
||||
var saltOnce sync.Once
|
||||
|
||||
// getSalt returns unique component per daemon restart to avoid persistent keys
|
||||
func getSalt() []byte {
|
||||
saltOnce.Do(func() {
|
||||
salt = make([]byte, 32)
|
||||
rand.Read(salt)
|
||||
})
|
||||
return salt
|
||||
}
|
||||
|
||||
func CredentialsFunc(sm *session.Manager, g session.Group) func(string) (session, username, secret string, err error) {
|
||||
return func(host string) (string, string, string, error) {
|
||||
var sessionID, user, secret string
|
||||
|
@ -34,3 +51,80 @@ func CredentialsFunc(sm *session.Manager, g session.Group) func(string) (session
|
|||
return sessionID, user, secret, nil
|
||||
}
|
||||
}
|
||||
|
||||
func FetchToken(req *FetchTokenRequest, sm *session.Manager, g session.Group) (resp *FetchTokenResponse, err error) {
|
||||
err = sm.Any(context.TODO(), g, func(ctx context.Context, id string, c session.Caller) error {
|
||||
client := NewAuthClient(c.Conn())
|
||||
|
||||
resp, err = client.FetchToken(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func VerifyTokenAuthority(host string, pubKey *[32]byte, sm *session.Manager, g session.Group) (sessionID string, ok bool, err error) {
|
||||
var verified bool
|
||||
err = sm.Any(context.TODO(), g, func(ctx context.Context, id string, c session.Caller) error {
|
||||
client := NewAuthClient(c.Conn())
|
||||
|
||||
payload := make([]byte, 32)
|
||||
rand.Read(payload)
|
||||
resp, err := client.VerifyTokenAuthority(ctx, &VerifyTokenAuthorityRequest{
|
||||
Host: host,
|
||||
Salt: getSalt(),
|
||||
Payload: payload,
|
||||
})
|
||||
if err != nil {
|
||||
if grpcerrors.Code(err) == codes.Unimplemented {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
var dt []byte
|
||||
dt, ok = sign.Open(nil, resp.Signed, pubKey)
|
||||
if ok && subtle.ConstantTimeCompare(dt, payload) == 1 {
|
||||
verified = true
|
||||
}
|
||||
sessionID = id
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return sessionID, verified, nil
|
||||
}
|
||||
|
||||
func GetTokenAuthority(host string, sm *session.Manager, g session.Group) (sessionID string, pubKey *[32]byte, err error) {
|
||||
err = sm.Any(context.TODO(), g, func(ctx context.Context, id string, c session.Caller) error {
|
||||
client := NewAuthClient(c.Conn())
|
||||
|
||||
resp, err := client.GetTokenAuthority(ctx, &GetTokenAuthorityRequest{
|
||||
Host: host,
|
||||
Salt: getSalt(),
|
||||
})
|
||||
if err != nil {
|
||||
if grpcerrors.Code(err) == codes.Unimplemented || grpcerrors.Code(err) == codes.Unavailable {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(resp.PublicKey) != 32 {
|
||||
return errors.Errorf("invalid pubkey length %d", len(pubKey))
|
||||
}
|
||||
|
||||
sessionID = id
|
||||
pubKey = new([32]byte)
|
||||
copy((*pubKey)[:], resp.PublicKey)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return sessionID, pubKey, nil
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,9 +6,11 @@ option go_package = "auth";
|
|||
|
||||
service Auth{
|
||||
rpc Credentials(CredentialsRequest) returns (CredentialsResponse);
|
||||
rpc FetchToken(FetchTokenRequest) returns (FetchTokenResponse);
|
||||
rpc GetTokenAuthority(GetTokenAuthorityRequest) returns (GetTokenAuthorityResponse);
|
||||
rpc VerifyTokenAuthority(VerifyTokenAuthorityRequest) returns (VerifyTokenAuthorityResponse);
|
||||
}
|
||||
|
||||
|
||||
message CredentialsRequest {
|
||||
string Host = 1;
|
||||
}
|
||||
|
@ -17,3 +19,36 @@ message CredentialsResponse {
|
|||
string Username = 1;
|
||||
string Secret = 2;
|
||||
}
|
||||
|
||||
message FetchTokenRequest {
|
||||
string ClientID = 1;
|
||||
string Host = 2;
|
||||
string Realm = 3;
|
||||
string Service = 4;
|
||||
repeated string Scopes = 5;
|
||||
}
|
||||
|
||||
message FetchTokenResponse {
|
||||
string Token = 1;
|
||||
int64 ExpiresIn = 2; // seconds
|
||||
int64 IssuedAt = 3; // timestamp
|
||||
}
|
||||
|
||||
message GetTokenAuthorityRequest {
|
||||
string Host = 1;
|
||||
bytes Salt = 2;
|
||||
}
|
||||
|
||||
message GetTokenAuthorityResponse {
|
||||
bytes PublicKey = 1;
|
||||
}
|
||||
|
||||
message VerifyTokenAuthorityRequest {
|
||||
string Host = 1;
|
||||
bytes Payload = 2;
|
||||
bytes Salt = 3;
|
||||
}
|
||||
|
||||
message VerifyTokenAuthorityResponse {
|
||||
bytes Signed = 1;
|
||||
}
|
||||
|
|
|
@ -2,24 +2,46 @@ package authprovider
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
authutil "github.com/containerd/containerd/remotes/docker/auth"
|
||||
remoteserrors "github.com/containerd/containerd/remotes/errors"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/config/configfile"
|
||||
"github.com/moby/buildkit/session"
|
||||
"github.com/moby/buildkit/session/auth"
|
||||
"github.com/moby/buildkit/util/progress/progresswriter"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/nacl/sign"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func NewDockerAuthProvider(stderr io.Writer) session.Attachable {
|
||||
return &authProvider{
|
||||
config: config.LoadDefaultConfigFile(stderr),
|
||||
seeds: &tokenSeeds{dir: config.Dir()},
|
||||
loggerCache: map[string]struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
type authProvider struct {
|
||||
config *configfile.ConfigFile
|
||||
seeds *tokenSeeds
|
||||
logger progresswriter.Logger
|
||||
loggerCache map[string]struct{}
|
||||
|
||||
// The need for this mutex is not well understood.
|
||||
// Without it, the docker cli on OS X hangs when
|
||||
|
@ -28,17 +50,80 @@ type authProvider struct {
|
|||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (ap *authProvider) SetLogger(l progresswriter.Logger) {
|
||||
ap.mu.Lock()
|
||||
ap.logger = l
|
||||
ap.mu.Unlock()
|
||||
}
|
||||
|
||||
func (ap *authProvider) Register(server *grpc.Server) {
|
||||
auth.RegisterAuthServer(server, ap)
|
||||
}
|
||||
|
||||
func (ap *authProvider) Credentials(ctx context.Context, req *auth.CredentialsRequest) (*auth.CredentialsResponse, error) {
|
||||
func (ap *authProvider) FetchToken(ctx context.Context, req *auth.FetchTokenRequest) (rr *auth.FetchTokenResponse, err error) {
|
||||
creds, err := ap.credentials(req.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
to := authutil.TokenOptions{
|
||||
Realm: req.Realm,
|
||||
Service: req.Service,
|
||||
Scopes: req.Scopes,
|
||||
Username: creds.Username,
|
||||
Secret: creds.Secret,
|
||||
}
|
||||
|
||||
if creds.Secret != "" {
|
||||
done := func(progresswriter.SubLogger) error {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err = errors.Wrap(err, "failed to fetch oauth token")
|
||||
}()
|
||||
ap.mu.Lock()
|
||||
name := fmt.Sprintf("[auth] %v token for %s", strings.Join(trimScopePrefix(req.Scopes), " "), req.Host)
|
||||
if _, ok := ap.loggerCache[name]; !ok {
|
||||
progresswriter.Wrap(name, ap.logger, done)
|
||||
}
|
||||
ap.mu.Unlock()
|
||||
// try GET first because Docker Hub does not support POST
|
||||
// switch once support has landed
|
||||
resp, err := authutil.FetchToken(ctx, http.DefaultClient, nil, to)
|
||||
if err != nil {
|
||||
var errStatus remoteserrors.ErrUnexpectedStatus
|
||||
if errors.As(err, &errStatus) {
|
||||
// retry with POST request
|
||||
// As of September 2017, GCR is known to return 404.
|
||||
// As of February 2018, JFrog Artifactory is known to return 401.
|
||||
if (errStatus.StatusCode == 405 && to.Username != "") || errStatus.StatusCode == 404 || errStatus.StatusCode == 401 {
|
||||
resp, err := authutil.FetchTokenWithOAuth(ctx, http.DefaultClient, nil, "buildkit-client", to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return toTokenResponse(resp.AccessToken, resp.IssuedAt, resp.ExpiresIn), nil
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return toTokenResponse(resp.Token, resp.IssuedAt, resp.ExpiresIn), nil
|
||||
}
|
||||
// do request anonymously
|
||||
resp, err := authutil.FetchToken(ctx, http.DefaultClient, nil, to)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to fetch anonymous token")
|
||||
}
|
||||
return toTokenResponse(resp.Token, resp.IssuedAt, resp.ExpiresIn), nil
|
||||
}
|
||||
|
||||
func (ap *authProvider) credentials(host string) (*auth.CredentialsResponse, error) {
|
||||
ap.mu.Lock()
|
||||
defer ap.mu.Unlock()
|
||||
if req.Host == "registry-1.docker.io" {
|
||||
req.Host = "https://index.docker.io/v1/"
|
||||
if host == "registry-1.docker.io" {
|
||||
host = "https://index.docker.io/v1/"
|
||||
}
|
||||
ac, err := ap.config.GetAuthConfig(req.Host)
|
||||
ac, err := ap.config.GetAuthConfig(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -51,3 +136,85 @@ func (ap *authProvider) Credentials(ctx context.Context, req *auth.CredentialsRe
|
|||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (ap *authProvider) Credentials(ctx context.Context, req *auth.CredentialsRequest) (*auth.CredentialsResponse, error) {
|
||||
resp, err := ap.credentials(req.Host)
|
||||
if err != nil || resp.Secret != "" {
|
||||
ap.mu.Lock()
|
||||
defer ap.mu.Unlock()
|
||||
_, ok := ap.loggerCache[req.Host]
|
||||
ap.loggerCache[req.Host] = struct{}{}
|
||||
if !ok {
|
||||
return resp, progresswriter.Wrap(fmt.Sprintf("[auth] sharing credentials for %s", req.Host), ap.logger, func(progresswriter.SubLogger) error {
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
|
||||
func (ap *authProvider) GetTokenAuthority(ctx context.Context, req *auth.GetTokenAuthorityRequest) (*auth.GetTokenAuthorityResponse, error) {
|
||||
key, err := ap.getAuthorityKey(req.Host, req.Salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &auth.GetTokenAuthorityResponse{PublicKey: key[32:]}, nil
|
||||
}
|
||||
|
||||
func (ap *authProvider) VerifyTokenAuthority(ctx context.Context, req *auth.VerifyTokenAuthorityRequest) (*auth.VerifyTokenAuthorityResponse, error) {
|
||||
key, err := ap.getAuthorityKey(req.Host, req.Salt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
priv := new([64]byte)
|
||||
copy((*priv)[:], key)
|
||||
|
||||
return &auth.VerifyTokenAuthorityResponse{Signed: sign.Sign(nil, req.Payload, priv)}, nil
|
||||
}
|
||||
|
||||
func (ap *authProvider) getAuthorityKey(host string, salt []byte) (ed25519.PrivateKey, error) {
|
||||
if v, err := strconv.ParseBool(os.Getenv("BUILDKIT_NO_CLIENT_TOKEN")); err == nil && v {
|
||||
return nil, status.Errorf(codes.Unavailable, "client side tokens disabled")
|
||||
}
|
||||
|
||||
creds, err := ap.credentials(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
seed, err := ap.seeds.getSeed(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mac := hmac.New(sha256.New, salt)
|
||||
if creds.Secret != "" {
|
||||
mac.Write(seed)
|
||||
enc := json.NewEncoder(mac)
|
||||
enc.Encode(creds)
|
||||
}
|
||||
|
||||
sum := mac.Sum(nil)
|
||||
|
||||
return ed25519.NewKeyFromSeed(sum[:ed25519.SeedSize]), nil
|
||||
}
|
||||
|
||||
func toTokenResponse(token string, issuedAt time.Time, expires int) *auth.FetchTokenResponse {
|
||||
resp := &auth.FetchTokenResponse{
|
||||
Token: token,
|
||||
ExpiresIn: int64(expires),
|
||||
}
|
||||
if !issuedAt.IsZero() {
|
||||
resp.IssuedAt = issuedAt.Unix()
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func trimScopePrefix(scopes []string) []string {
|
||||
out := make([]string, len(scopes))
|
||||
for i, s := range scopes {
|
||||
out[i] = strings.TrimPrefix(s, "repository:")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
package authprovider
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/gofrs/flock"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type tokenSeeds struct {
|
||||
mu sync.Mutex
|
||||
dir string
|
||||
}
|
||||
|
||||
type seed struct {
|
||||
Seed []byte
|
||||
}
|
||||
|
||||
func (ts *tokenSeeds) getSeed(host string) ([]byte, error) {
|
||||
ts.mu.Lock()
|
||||
defer ts.mu.Unlock()
|
||||
|
||||
if err := os.MkdirAll(ts.dir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := flock.New(filepath.Join(ts.dir, ".token_seed.lock"))
|
||||
if err := l.Lock(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer l.Unlock()
|
||||
|
||||
// we include client side randomness to avoid chosen plaintext attack from the daemon side
|
||||
fp := filepath.Join(ts.dir, ".token_seed")
|
||||
dt, err := ioutil.ReadFile(fp)
|
||||
m := map[string]seed{}
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := json.Unmarshal(dt, &m); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse %s", fp)
|
||||
}
|
||||
}
|
||||
v, ok := m[host]
|
||||
if !ok {
|
||||
v = seed{Seed: newSeed()}
|
||||
}
|
||||
|
||||
m[host] = v
|
||||
|
||||
dt, err = json.MarshalIndent(m, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(fp, dt, 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.Seed, nil
|
||||
}
|
||||
|
||||
func newSeed() []byte {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return b
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package progresswriter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/moby/buildkit/client"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type MultiWriter struct {
|
||||
w Writer
|
||||
eg *errgroup.Group
|
||||
once sync.Once
|
||||
ready chan struct{}
|
||||
}
|
||||
|
||||
func (mw *MultiWriter) WithPrefix(pfx string, force bool) Writer {
|
||||
in := make(chan *client.SolveStatus)
|
||||
out := mw.w.Status()
|
||||
p := &prefixed{
|
||||
main: mw.w,
|
||||
in: in,
|
||||
}
|
||||
mw.eg.Go(func() error {
|
||||
mw.once.Do(func() {
|
||||
close(mw.ready)
|
||||
})
|
||||
for {
|
||||
select {
|
||||
case v, ok := <-in:
|
||||
if ok {
|
||||
if force {
|
||||
for _, v := range v.Vertexes {
|
||||
v.Name = addPrefix(pfx, v.Name)
|
||||
}
|
||||
}
|
||||
out <- v
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
case <-mw.Done():
|
||||
return mw.Err()
|
||||
}
|
||||
}
|
||||
})
|
||||
return p
|
||||
}
|
||||
|
||||
func (mw *MultiWriter) Done() <-chan struct{} {
|
||||
return mw.w.Done()
|
||||
}
|
||||
|
||||
func (mw *MultiWriter) Err() error {
|
||||
return mw.w.Err()
|
||||
}
|
||||
|
||||
func (mw *MultiWriter) Status() chan *client.SolveStatus {
|
||||
return nil
|
||||
}
|
||||
|
||||
type prefixed struct {
|
||||
main Writer
|
||||
in chan *client.SolveStatus
|
||||
}
|
||||
|
||||
func (p *prefixed) Done() <-chan struct{} {
|
||||
return p.main.Done()
|
||||
}
|
||||
|
||||
func (p *prefixed) Err() error {
|
||||
return p.main.Err()
|
||||
}
|
||||
|
||||
func (p *prefixed) Status() chan *client.SolveStatus {
|
||||
return p.in
|
||||
}
|
||||
|
||||
func NewMultiWriter(pw Writer) *MultiWriter {
|
||||
if pw == nil {
|
||||
return nil
|
||||
}
|
||||
eg, _ := errgroup.WithContext(context.TODO())
|
||||
|
||||
ready := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
<-ready
|
||||
eg.Wait()
|
||||
close(pw.Status())
|
||||
}()
|
||||
|
||||
return &MultiWriter{
|
||||
w: pw,
|
||||
eg: eg,
|
||||
ready: ready,
|
||||
}
|
||||
}
|
||||
|
||||
func addPrefix(pfx, name string) string {
|
||||
if strings.HasPrefix(name, "[") {
|
||||
return "[" + pfx + " " + name[1:]
|
||||
}
|
||||
return "[" + pfx + "] " + name
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package progresswriter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/util/progress/progressui"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type printer struct {
|
||||
status chan *client.SolveStatus
|
||||
done <-chan struct{}
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *printer) Done() <-chan struct{} {
|
||||
return p.done
|
||||
}
|
||||
|
||||
func (p *printer) Err() error {
|
||||
return p.err
|
||||
}
|
||||
|
||||
func (p *printer) Status() chan *client.SolveStatus {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return p.status
|
||||
}
|
||||
|
||||
type tee struct {
|
||||
Writer
|
||||
status chan *client.SolveStatus
|
||||
}
|
||||
|
||||
func (t *tee) Status() chan *client.SolveStatus {
|
||||
return t.status
|
||||
}
|
||||
|
||||
func Tee(w Writer, ch chan *client.SolveStatus) Writer {
|
||||
st := make(chan *client.SolveStatus)
|
||||
t := &tee{
|
||||
status: st,
|
||||
Writer: w,
|
||||
}
|
||||
go func() {
|
||||
for v := range st {
|
||||
w.Status() <- v
|
||||
ch <- v
|
||||
}
|
||||
close(w.Status())
|
||||
close(ch)
|
||||
}()
|
||||
return t
|
||||
}
|
||||
|
||||
func NewPrinter(ctx context.Context, out console.File, mode string) (Writer, error) {
|
||||
statusCh := make(chan *client.SolveStatus)
|
||||
doneCh := make(chan struct{})
|
||||
|
||||
pw := &printer{
|
||||
status: statusCh,
|
||||
done: doneCh,
|
||||
}
|
||||
|
||||
if v := os.Getenv("BUILDKIT_PROGRESS"); v != "" && mode == "auto" {
|
||||
mode = v
|
||||
}
|
||||
|
||||
var c console.Console
|
||||
switch mode {
|
||||
case "auto", "tty", "":
|
||||
if cons, err := console.ConsoleFromFile(out); err == nil {
|
||||
c = cons
|
||||
} else {
|
||||
if mode == "tty" {
|
||||
return nil, errors.Wrap(err, "failed to get console")
|
||||
}
|
||||
}
|
||||
case "plain":
|
||||
default:
|
||||
return nil, errors.Errorf("invalid progress mode %s", mode)
|
||||
}
|
||||
|
||||
go func() {
|
||||
// not using shared context to not disrupt display but let is finish reporting errors
|
||||
pw.err = progressui.DisplaySolveStatus(ctx, "", c, out, statusCh)
|
||||
close(doneCh)
|
||||
}()
|
||||
return pw, nil
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package progresswriter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
type Logger func(*client.SolveStatus)
|
||||
|
||||
type SubLogger interface {
|
||||
Wrap(name string, fn func() error) error
|
||||
Log(stream int, dt []byte)
|
||||
}
|
||||
|
||||
func Wrap(name string, l Logger, fn func(SubLogger) error) (err error) {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
dgst := digest.FromBytes([]byte(identity.NewID()))
|
||||
tm := time.Now()
|
||||
l(&client.SolveStatus{
|
||||
Vertexes: []*client.Vertex{{
|
||||
Digest: dgst,
|
||||
Name: name,
|
||||
Started: &tm,
|
||||
}},
|
||||
})
|
||||
|
||||
defer func() {
|
||||
tm2 := time.Now()
|
||||
errMsg := ""
|
||||
if err != nil {
|
||||
errMsg = err.Error()
|
||||
}
|
||||
l(&client.SolveStatus{
|
||||
Vertexes: []*client.Vertex{{
|
||||
Digest: dgst,
|
||||
Name: name,
|
||||
Started: &tm,
|
||||
Completed: &tm2,
|
||||
Error: errMsg,
|
||||
}},
|
||||
})
|
||||
}()
|
||||
|
||||
return fn(&subLogger{dgst, l})
|
||||
}
|
||||
|
||||
type subLogger struct {
|
||||
dgst digest.Digest
|
||||
logger Logger
|
||||
}
|
||||
|
||||
func (sl *subLogger) Wrap(name string, fn func() error) (err error) {
|
||||
tm := time.Now()
|
||||
sl.logger(&client.SolveStatus{
|
||||
Statuses: []*client.VertexStatus{{
|
||||
Vertex: sl.dgst,
|
||||
ID: name,
|
||||
Timestamp: time.Now(),
|
||||
Started: &tm,
|
||||
}},
|
||||
})
|
||||
|
||||
defer func() {
|
||||
tm2 := time.Now()
|
||||
sl.logger(&client.SolveStatus{
|
||||
Statuses: []*client.VertexStatus{{
|
||||
Vertex: sl.dgst,
|
||||
ID: name,
|
||||
Timestamp: time.Now(),
|
||||
Started: &tm,
|
||||
Completed: &tm2,
|
||||
}},
|
||||
})
|
||||
}()
|
||||
|
||||
return fn()
|
||||
}
|
||||
|
||||
func (sl *subLogger) Log(stream int, dt []byte) {
|
||||
sl.logger(&client.SolveStatus{
|
||||
Logs: []*client.VertexLog{{
|
||||
Vertex: sl.dgst,
|
||||
Stream: stream,
|
||||
Data: dt,
|
||||
Timestamp: time.Now(),
|
||||
}},
|
||||
})
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package progresswriter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/moby/buildkit/client"
|
||||
)
|
||||
|
||||
func ResetTime(in Writer) Writer {
|
||||
w := &pw{Writer: in, status: make(chan *client.SolveStatus), tm: time.Now()}
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-in.Done():
|
||||
return
|
||||
case st, ok := <-w.status:
|
||||
if !ok {
|
||||
close(in.Status())
|
||||
return
|
||||
}
|
||||
if w.diff == nil {
|
||||
for _, v := range st.Vertexes {
|
||||
if v.Started != nil {
|
||||
d := v.Started.Sub(w.tm)
|
||||
w.diff = &d
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.diff != nil {
|
||||
for _, v := range st.Vertexes {
|
||||
if v.Started != nil {
|
||||
d := v.Started.Add(-*w.diff)
|
||||
v.Started = &d
|
||||
}
|
||||
if v.Completed != nil {
|
||||
d := v.Completed.Add(-*w.diff)
|
||||
v.Completed = &d
|
||||
}
|
||||
}
|
||||
for _, v := range st.Statuses {
|
||||
if v.Started != nil {
|
||||
d := v.Started.Add(-*w.diff)
|
||||
v.Started = &d
|
||||
}
|
||||
if v.Completed != nil {
|
||||
d := v.Completed.Add(-*w.diff)
|
||||
v.Completed = &d
|
||||
}
|
||||
v.Timestamp = v.Timestamp.Add(-*w.diff)
|
||||
}
|
||||
for _, v := range st.Logs {
|
||||
v.Timestamp = v.Timestamp.Add(-*w.diff)
|
||||
}
|
||||
}
|
||||
in.Status() <- st
|
||||
}
|
||||
}
|
||||
}()
|
||||
return w
|
||||
}
|
||||
|
||||
type pw struct {
|
||||
Writer
|
||||
tm time.Time
|
||||
diff *time.Duration
|
||||
status chan *client.SolveStatus
|
||||
}
|
||||
|
||||
func (p *pw) Status() chan *client.SolveStatus {
|
||||
return p.status
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package progresswriter
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/moby/buildkit/client"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
type Writer interface {
|
||||
Done() <-chan struct{}
|
||||
Err() error
|
||||
Status() chan *client.SolveStatus
|
||||
}
|
||||
|
||||
func Write(w Writer, name string, f func() error) {
|
||||
status := w.Status()
|
||||
dgst := digest.FromBytes([]byte(identity.NewID()))
|
||||
tm := time.Now()
|
||||
|
||||
vtx := client.Vertex{
|
||||
Digest: dgst,
|
||||
Name: name,
|
||||
Started: &tm,
|
||||
}
|
||||
|
||||
status <- &client.SolveStatus{
|
||||
Vertexes: []*client.Vertex{&vtx},
|
||||
}
|
||||
|
||||
var err error
|
||||
if f != nil {
|
||||
err = f()
|
||||
}
|
||||
|
||||
tm2 := time.Now()
|
||||
vtx2 := vtx
|
||||
vtx2.Completed = &tm2
|
||||
if err != nil {
|
||||
vtx2.Error = err.Error()
|
||||
}
|
||||
status <- &client.SolveStatus{
|
||||
Vertexes: []*client.Vertex{&vtx2},
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -63,6 +64,14 @@ func (a *authHandlerNS) get(host string, sm *session.Manager, g session.Group) *
|
|||
continue
|
||||
}
|
||||
if parts[0] == host {
|
||||
if h.authority != nil {
|
||||
session, ok, err := sessionauth.VerifyTokenAuthority(host, h.authority, sm, g)
|
||||
if err == nil && ok {
|
||||
a.handlers[host+"/"+session] = h
|
||||
h.lastUsed = time.Now()
|
||||
return h
|
||||
}
|
||||
} else {
|
||||
session, username, password, err := sessionauth.CredentialsFunc(sm, g)(host)
|
||||
if err == nil {
|
||||
if username == h.common.Username && password == h.common.Secret {
|
||||
|
@ -73,6 +82,7 @@ func (a *authHandlerNS) get(host string, sm *session.Manager, g session.Group) *
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -117,7 +127,7 @@ func (a *dockerAuthorizer) Authorize(ctx context.Context, req *http.Request) err
|
|||
return nil
|
||||
}
|
||||
|
||||
auth, err := ah.authorize(ctx, a.session)
|
||||
auth, err := ah.authorize(ctx, a.sm, a.session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -141,17 +151,17 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
|
|||
|
||||
for _, c := range auth.ParseAuthHeader(last.Header) {
|
||||
if c.Scheme == auth.BearerAuth {
|
||||
var oldScopes []string
|
||||
if err := invalidAuthorization(c, responses); err != nil {
|
||||
a.handlers.delete(handler)
|
||||
|
||||
oldScope := ""
|
||||
if handler != nil {
|
||||
oldScope = strings.Join(handler.common.Scopes, " ")
|
||||
oldScopes = handler.common.Scopes
|
||||
}
|
||||
handler = nil
|
||||
|
||||
// this hacky way seems to be best method to detect that error is fatal and should not be retried with a new token
|
||||
if c.Parameters["error"] == "insufficient_scope" && c.Parameters["scope"] == oldScope {
|
||||
if c.Parameters["error"] == "insufficient_scope" && parseScopes(oldScopes).contains(parseScopes(strings.Split(c.Parameters["scope"], " "))) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -166,17 +176,25 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
|
|||
return nil
|
||||
}
|
||||
|
||||
session, username, secret, err := a.getCredentials(host)
|
||||
var username, secret string
|
||||
session, pubKey, err := sessionauth.GetTokenAuthority(host, a.sm, a.session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pubKey == nil {
|
||||
session, username, secret, err = a.getCredentials(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
common, err := auth.GenerateTokenOptions(ctx, host, username, secret, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
common.Scopes = parseScopes(append(common.Scopes, oldScopes...)).normalize()
|
||||
|
||||
a.handlers.set(host, session, newAuthHandler(a.client, c.Scheme, common))
|
||||
a.handlers.set(host, session, newAuthHandler(host, a.client, c.Scheme, pubKey, common))
|
||||
|
||||
return nil
|
||||
} else if c.Scheme == auth.BasicAuth {
|
||||
|
@ -191,7 +209,7 @@ func (a *dockerAuthorizer) AddResponses(ctx context.Context, responses []*http.R
|
|||
Secret: secret,
|
||||
}
|
||||
|
||||
a.handlers.set(host, session, newAuthHandler(a.client, c.Scheme, common))
|
||||
a.handlers.set(host, session, newAuthHandler(host, a.client, c.Scheme, nil, common))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -225,24 +243,30 @@ type authHandler struct {
|
|||
scopedTokens map[string]*authResult
|
||||
|
||||
lastUsed time.Time
|
||||
|
||||
host string
|
||||
|
||||
authority *[32]byte
|
||||
}
|
||||
|
||||
func newAuthHandler(client *http.Client, scheme auth.AuthenticationScheme, opts auth.TokenOptions) *authHandler {
|
||||
func newAuthHandler(host string, client *http.Client, scheme auth.AuthenticationScheme, authority *[32]byte, opts auth.TokenOptions) *authHandler {
|
||||
return &authHandler{
|
||||
host: host,
|
||||
client: client,
|
||||
scheme: scheme,
|
||||
common: opts,
|
||||
scopedTokens: map[string]*authResult{},
|
||||
lastUsed: time.Now(),
|
||||
authority: authority,
|
||||
}
|
||||
}
|
||||
|
||||
func (ah *authHandler) authorize(ctx context.Context, g session.Group) (string, error) {
|
||||
func (ah *authHandler) authorize(ctx context.Context, sm *session.Manager, g session.Group) (string, error) {
|
||||
switch ah.scheme {
|
||||
case auth.BasicAuth:
|
||||
return ah.doBasicAuth(ctx)
|
||||
case auth.BearerAuth:
|
||||
return ah.doBearerAuth(ctx)
|
||||
return ah.doBearerAuth(ctx, sm, g)
|
||||
default:
|
||||
return "", errors.Wrapf(errdefs.ErrNotImplemented, "failed to find supported auth scheme: %s", string(ah.scheme))
|
||||
}
|
||||
|
@ -259,11 +283,11 @@ func (ah *authHandler) doBasicAuth(ctx context.Context) (string, error) {
|
|||
return fmt.Sprintf("Basic %s", auth), nil
|
||||
}
|
||||
|
||||
func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err error) {
|
||||
func (ah *authHandler) doBearerAuth(ctx context.Context, sm *session.Manager, g session.Group) (token string, err error) {
|
||||
// copy common tokenOptions
|
||||
to := ah.common
|
||||
|
||||
to.Scopes = docker.GetTokenScopes(ctx, to.Scopes)
|
||||
to.Scopes = parseScopes(docker.GetTokenScopes(ctx, to.Scopes)).normalize()
|
||||
|
||||
// Docs: https://docs.docker.com/registry/spec/auth/scope
|
||||
scoped := strings.Join(to.Scopes, " ")
|
||||
|
@ -300,6 +324,21 @@ func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err erro
|
|||
r.Done()
|
||||
}()
|
||||
|
||||
if ah.authority != nil {
|
||||
resp, err := sessionauth.FetchToken(&sessionauth.FetchTokenRequest{
|
||||
ClientID: "buildkit-client",
|
||||
Host: ah.host,
|
||||
Realm: to.Realm,
|
||||
Service: to.Service,
|
||||
Scopes: to.Scopes,
|
||||
}, sm, g)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
issuedAt, expires = time.Unix(resp.IssuedAt, 0), int(resp.ExpiresIn)
|
||||
return resp.Token, nil
|
||||
}
|
||||
|
||||
// fetch token for the resource scope
|
||||
if to.Secret != "" {
|
||||
defer func() {
|
||||
|
@ -315,7 +354,7 @@ func (ah *authHandler) doBearerAuth(ctx context.Context) (token string, err erro
|
|||
// As of September 2017, GCR is known to return 404.
|
||||
// As of February 2018, JFrog Artifactory is known to return 401.
|
||||
if (errStatus.StatusCode == 405 && to.Username != "") || errStatus.StatusCode == 404 || errStatus.StatusCode == 401 {
|
||||
resp, err := auth.FetchTokenWithOAuth(ctx, ah.client, nil, "containerd-client", to)
|
||||
resp, err := auth.FetchTokenWithOAuth(ctx, ah.client, nil, "buildkit-client", to)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -365,3 +404,68 @@ func sameRequest(r1, r2 *http.Request) bool {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type scopes map[string]map[string]struct{}
|
||||
|
||||
func parseScopes(s []string) scopes {
|
||||
// https://docs.docker.com/registry/spec/auth/scope/
|
||||
m := map[string]map[string]struct{}{}
|
||||
for _, scope := range s {
|
||||
parts := strings.SplitN(scope, ":", 3)
|
||||
names := []string{parts[0]}
|
||||
if len(parts) > 1 {
|
||||
names = append(names, parts[1])
|
||||
}
|
||||
var actions []string
|
||||
if len(parts) == 3 {
|
||||
actions = append(actions, strings.Split(parts[2], ",")...)
|
||||
}
|
||||
name := strings.Join(names, ":")
|
||||
ma, ok := m[name]
|
||||
if !ok {
|
||||
ma = map[string]struct{}{}
|
||||
m[name] = ma
|
||||
}
|
||||
|
||||
for _, a := range actions {
|
||||
ma[a] = struct{}{}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s scopes) normalize() []string {
|
||||
names := make([]string, 0, len(s))
|
||||
for n := range s {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
out := make([]string, 0, len(s))
|
||||
|
||||
for _, n := range names {
|
||||
actions := make([]string, 0, len(s[n]))
|
||||
for a := range s[n] {
|
||||
actions = append(actions, a)
|
||||
}
|
||||
sort.Strings(actions)
|
||||
|
||||
out = append(out, n+":"+strings.Join(actions, ","))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s scopes) contains(s2 scopes) bool {
|
||||
for n := range s2 {
|
||||
v, ok := s[n]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for a := range s2[n] {
|
||||
if _, ok := v[a]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright 2018 The Go 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 sign signs small messages using public-key cryptography.
|
||||
//
|
||||
// Sign uses Ed25519 to sign messages. The length of messages is not hidden.
|
||||
// Messages should be small because:
|
||||
// 1. The whole message needs to be held in memory to be processed.
|
||||
// 2. Using large messages pressures implementations on small machines to process
|
||||
// plaintext without verifying the signature. This is very dangerous, and this API
|
||||
// discourages it, but a protocol that uses excessive message sizes might present
|
||||
// some implementations with no other choice.
|
||||
// 3. Performance may be improved by working with messages that fit into data caches.
|
||||
// Thus large amounts of data should be chunked so that each message is small.
|
||||
//
|
||||
// This package is not interoperable with the current release of NaCl
|
||||
// (https://nacl.cr.yp.to/sign.html), which does not support Ed25519 yet. However,
|
||||
// it is compatible with the NaCl fork libsodium (https://www.libsodium.org), as well
|
||||
// as TweetNaCl (https://tweetnacl.cr.yp.to/).
|
||||
package sign
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/internal/subtle"
|
||||
)
|
||||
|
||||
// Overhead is the number of bytes of overhead when signing a message.
|
||||
const Overhead = 64
|
||||
|
||||
// GenerateKey generates a new public/private key pair suitable for use with
|
||||
// Sign and Open.
|
||||
func GenerateKey(rand io.Reader) (publicKey *[32]byte, privateKey *[64]byte, err error) {
|
||||
pub, priv, err := ed25519.GenerateKey(rand)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
publicKey, privateKey = new([32]byte), new([64]byte)
|
||||
copy((*publicKey)[:], pub)
|
||||
copy((*privateKey)[:], priv)
|
||||
return publicKey, privateKey, nil
|
||||
}
|
||||
|
||||
// Sign appends a signed copy of message to out, which will be Overhead bytes
|
||||
// longer than the original and must not overlap it.
|
||||
func Sign(out, message []byte, privateKey *[64]byte) []byte {
|
||||
sig := ed25519.Sign(ed25519.PrivateKey((*privateKey)[:]), message)
|
||||
ret, out := sliceForAppend(out, Overhead+len(message))
|
||||
if subtle.AnyOverlap(out, message) {
|
||||
panic("nacl: invalid buffer overlap")
|
||||
}
|
||||
copy(out, sig)
|
||||
copy(out[Overhead:], message)
|
||||
return ret
|
||||
}
|
||||
|
||||
// Open verifies a signed message produced by Sign and appends the message to
|
||||
// out, which must not overlap the signed message. The output will be Overhead
|
||||
// bytes smaller than the signed message.
|
||||
func Open(out, signedMessage []byte, publicKey *[32]byte) ([]byte, bool) {
|
||||
if len(signedMessage) < Overhead {
|
||||
return nil, false
|
||||
}
|
||||
if !ed25519.Verify(ed25519.PublicKey((*publicKey)[:]), signedMessage[Overhead:], signedMessage[:Overhead]) {
|
||||
return nil, false
|
||||
}
|
||||
ret, out := sliceForAppend(out, len(signedMessage)-Overhead)
|
||||
if subtle.AnyOverlap(out, signedMessage) {
|
||||
panic("nacl: invalid buffer overlap")
|
||||
}
|
||||
copy(out, signedMessage[Overhead:])
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// sliceForAppend takes a slice and a requested number of bytes. It returns a
|
||||
// slice with the contents of the given slice followed by that many bytes and a
|
||||
// second slice that aliases into it and contains only the extra bytes. If the
|
||||
// original slice has sufficient capacity then no allocation is performed.
|
||||
func sliceForAppend(in []byte, n int) (head, tail []byte) {
|
||||
if total := len(in) + n; cap(in) >= total {
|
||||
head = in[:total]
|
||||
} else {
|
||||
head = make([]byte, total)
|
||||
copy(head, in)
|
||||
}
|
||||
tail = head[len(in):]
|
||||
return
|
||||
}
|
|
@ -342,6 +342,7 @@ golang.org/x/crypto/curve25519
|
|||
golang.org/x/crypto/ed25519
|
||||
golang.org/x/crypto/ed25519/internal/edwards25519
|
||||
golang.org/x/crypto/internal/subtle
|
||||
golang.org/x/crypto/nacl/sign
|
||||
golang.org/x/crypto/poly1305
|
||||
golang.org/x/crypto/ssh
|
||||
golang.org/x/crypto/ssh/agent
|
||||
|
|
Loading…
Reference in New Issue