github.com/vmware/govmomi@v0.37.1/sts/signer.go (about)

     1  /*
     2  Copyright (c) 2018 VMware, Inc. All Rights Reserved.
     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 sts
    18  
    19  import (
    20  	"bytes"
    21  	"compress/gzip"
    22  	"crypto"
    23  	"crypto/rand"
    24  	"crypto/rsa"
    25  	"crypto/sha256"
    26  	"crypto/tls"
    27  	"encoding/base64"
    28  	"errors"
    29  	"fmt"
    30  	"io"
    31  	mrand "math/rand"
    32  	"net"
    33  	"net/http"
    34  	"net/url"
    35  	"strings"
    36  	"time"
    37  
    38  	"github.com/google/uuid"
    39  
    40  	"github.com/vmware/govmomi/sts/internal"
    41  	"github.com/vmware/govmomi/vim25/methods"
    42  	"github.com/vmware/govmomi/vim25/soap"
    43  	"github.com/vmware/govmomi/vim25/xml"
    44  )
    45  
    46  // Signer implements the soap.Signer interface.
    47  type Signer struct {
    48  	Token       string           // Token is a SAML token
    49  	Certificate *tls.Certificate // Certificate is used to sign requests
    50  	Lifetime    struct {
    51  		Created time.Time
    52  		Expires time.Time
    53  	}
    54  	user  *url.Userinfo // user contains the credentials for bearer token request
    55  	keyID string        // keyID is the Signature UseKey ID, which is referenced in both the soap body and header
    56  }
    57  
    58  // signedEnvelope is similar to soap.Envelope, but with namespace and Body as innerxml
    59  type signedEnvelope struct {
    60  	XMLName xml.Name    `xml:"soap:Envelope"`
    61  	NS      string      `xml:"xmlns:soap,attr"`
    62  	Header  soap.Header `xml:"soap:Header"`
    63  	Body    string      `xml:",innerxml"`
    64  }
    65  
    66  // newID returns a unique Reference ID, with a leading underscore as required by STS.
    67  func newID() string {
    68  	return "_" + uuid.New().String()
    69  }
    70  
    71  func (s *Signer) setTokenReference(info *internal.KeyInfo) error {
    72  	var token struct {
    73  		ID       string `xml:",attr"`     // parse saml2:Assertion ID attribute
    74  		InnerXML string `xml:",innerxml"` // no need to parse the entire token
    75  	}
    76  	if err := xml.Unmarshal([]byte(s.Token), &token); err != nil {
    77  		return err
    78  	}
    79  
    80  	info.SecurityTokenReference = &internal.SecurityTokenReference{
    81  		WSSE11:    "http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd",
    82  		TokenType: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0",
    83  		KeyIdentifier: &internal.KeyIdentifier{
    84  			ID:        token.ID,
    85  			ValueType: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID",
    86  		},
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  // Sign is a soap.Signer implementation which can be used to sign RequestSecurityToken and LoginByTokenBody requests.
    93  func (s *Signer) Sign(env soap.Envelope) ([]byte, error) {
    94  	var key *rsa.PrivateKey
    95  	hasKey := false
    96  	if s.Certificate != nil {
    97  		key, hasKey = s.Certificate.PrivateKey.(*rsa.PrivateKey)
    98  		if !hasKey {
    99  			return nil, errors.New("sts: rsa.PrivateKey is required")
   100  		}
   101  	}
   102  
   103  	created := time.Now().UTC()
   104  	header := &internal.Security{
   105  		WSU:  internal.WSU,
   106  		WSSE: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
   107  		Timestamp: internal.Timestamp{
   108  			NS:      internal.WSU,
   109  			ID:      newID(),
   110  			Created: created.Format(internal.Time),
   111  			Expires: created.Add(time.Minute).Format(internal.Time), // If STS receives this request after this, it is assumed to have expired.
   112  		},
   113  	}
   114  	env.Header.Security = header
   115  
   116  	info := internal.KeyInfo{XMLName: xml.Name{Local: "ds:KeyInfo"}}
   117  	var c14n, body string
   118  	type requestToken interface {
   119  		RequestSecurityToken() *internal.RequestSecurityToken
   120  	}
   121  
   122  	switch x := env.Body.(type) {
   123  	case requestToken:
   124  		if hasKey {
   125  			// We need c14n for all requests, as its digest is included in the signature and must match on the server side.
   126  			// We need the body in original form when using an ActAs or RenewTarget token, where the token and its signature are embedded in the body.
   127  			req := x.RequestSecurityToken()
   128  			c14n = req.C14N()
   129  			body = req.String()
   130  
   131  			if len(s.Certificate.Certificate) == 0 {
   132  				header.Assertion = s.Token
   133  				if err := s.setTokenReference(&info); err != nil {
   134  					return nil, err
   135  				}
   136  			} else {
   137  				id := newID()
   138  
   139  				header.BinarySecurityToken = &internal.BinarySecurityToken{
   140  					EncodingType: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary",
   141  					ValueType:    "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3",
   142  					ID:           id,
   143  					Value:        base64.StdEncoding.EncodeToString(s.Certificate.Certificate[0]),
   144  				}
   145  
   146  				info.SecurityTokenReference = &internal.SecurityTokenReference{
   147  					Reference: &internal.SecurityReference{
   148  						URI:       "#" + id,
   149  						ValueType: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3",
   150  					},
   151  				}
   152  			}
   153  		}
   154  		// When requesting HoK token for interactive user, request will have both priv. key and username/password.
   155  		if s.user.Username() != "" {
   156  			header.UsernameToken = &internal.UsernameToken{
   157  				Username: s.user.Username(),
   158  			}
   159  			header.UsernameToken.Password, _ = s.user.Password()
   160  		}
   161  	case *methods.LoginByTokenBody:
   162  		header.Assertion = s.Token
   163  
   164  		if hasKey {
   165  			if err := s.setTokenReference(&info); err != nil {
   166  				return nil, err
   167  			}
   168  
   169  			c14n = internal.Marshal(x.Req)
   170  		}
   171  	default:
   172  		// We can end up here via ssoadmin.SessionManager.Login().
   173  		// No other known cases where a signed request is needed.
   174  		header.Assertion = s.Token
   175  		if hasKey {
   176  			if err := s.setTokenReference(&info); err != nil {
   177  				return nil, err
   178  			}
   179  			type Req interface {
   180  				C14N() string
   181  			}
   182  			c14n = env.Body.(Req).C14N()
   183  		}
   184  	}
   185  
   186  	if !hasKey {
   187  		return xml.Marshal(env) // Bearer token without key to sign
   188  	}
   189  
   190  	id := newID()
   191  	tmpl := `<soap:Body xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsu="%s" wsu:Id="%s">%s</soap:Body>`
   192  	c14n = fmt.Sprintf(tmpl, internal.WSU, id, c14n)
   193  	if body == "" {
   194  		body = c14n
   195  	} else {
   196  		body = fmt.Sprintf(tmpl, internal.WSU, id, body)
   197  	}
   198  
   199  	header.Signature = &internal.Signature{
   200  		XMLName: xml.Name{Local: "ds:Signature"},
   201  		NS:      internal.DSIG,
   202  		ID:      s.keyID,
   203  		KeyInfo: info,
   204  		SignedInfo: internal.SignedInfo{
   205  			XMLName: xml.Name{Local: "ds:SignedInfo"},
   206  			NS:      internal.DSIG,
   207  			CanonicalizationMethod: internal.Method{
   208  				XMLName:   xml.Name{Local: "ds:CanonicalizationMethod"},
   209  				Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#",
   210  			},
   211  			SignatureMethod: internal.Method{
   212  				XMLName:   xml.Name{Local: "ds:SignatureMethod"},
   213  				Algorithm: internal.SHA256,
   214  			},
   215  			Reference: []internal.Reference{
   216  				internal.NewReference(header.Timestamp.ID, header.Timestamp.C14N()),
   217  				internal.NewReference(id, c14n),
   218  			},
   219  		},
   220  	}
   221  
   222  	sum := sha256.Sum256([]byte(header.Signature.SignedInfo.C14N()))
   223  	sig, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, sum[:])
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	header.Signature.SignatureValue = internal.Value{
   229  		XMLName: xml.Name{Local: "ds:SignatureValue"},
   230  		Value:   base64.StdEncoding.EncodeToString(sig),
   231  	}
   232  
   233  	return xml.Marshal(signedEnvelope{
   234  		NS:     "http://schemas.xmlsoap.org/soap/envelope/",
   235  		Header: *env.Header,
   236  		Body:   body,
   237  	})
   238  }
   239  
   240  // SignRequest is a rest.Signer implementation which can be used to sign rest.Client.LoginByTokenBody requests.
   241  func (s *Signer) SignRequest(req *http.Request) error {
   242  	type param struct {
   243  		key, val string
   244  	}
   245  	var params []string
   246  	add := func(p param) {
   247  		params = append(params, fmt.Sprintf(`%s="%s"`, p.key, p.val))
   248  	}
   249  
   250  	var buf bytes.Buffer
   251  	gz := gzip.NewWriter(&buf)
   252  	if _, err := io.WriteString(gz, s.Token); err != nil {
   253  		return fmt.Errorf("zip token: %s", err)
   254  	}
   255  	if err := gz.Close(); err != nil {
   256  		return fmt.Errorf("zip token: %s", err)
   257  	}
   258  	add(param{
   259  		key: "token",
   260  		val: base64.StdEncoding.EncodeToString(buf.Bytes()),
   261  	})
   262  
   263  	if s.Certificate != nil {
   264  		nonce := fmt.Sprintf("%d:%d", time.Now().UnixNano()/1e6, mrand.Int())
   265  		var body []byte
   266  		if req.GetBody != nil {
   267  			r, rerr := req.GetBody()
   268  			if rerr != nil {
   269  				return fmt.Errorf("sts: getting http.Request body: %s", rerr)
   270  			}
   271  			defer r.Close()
   272  			body, rerr = io.ReadAll(r)
   273  			if rerr != nil {
   274  				return fmt.Errorf("sts: reading http.Request body: %s", rerr)
   275  			}
   276  		}
   277  		bhash := sha256.Sum256(body)
   278  
   279  		port := req.URL.Port()
   280  		if port == "" {
   281  			port = "80" // Default port for the "Host" header on the server side
   282  		}
   283  
   284  		var buf bytes.Buffer
   285  		host := req.URL.Hostname()
   286  
   287  		// Check if the host IP is in IPv6 format. If yes, add the opening and closing square brackets.
   288  		if isIPv6(host) {
   289  			host = fmt.Sprintf("%s%s%s", "[", host, "]")
   290  		}
   291  
   292  		msg := []string{
   293  			nonce,
   294  			req.Method,
   295  			req.URL.Path,
   296  			strings.ToLower(host),
   297  			port,
   298  		}
   299  		for i := range msg {
   300  			buf.WriteString(msg[i])
   301  			buf.WriteByte('\n')
   302  		}
   303  		buf.Write(bhash[:])
   304  		buf.WriteByte('\n')
   305  
   306  		sum := sha256.Sum256(buf.Bytes())
   307  		key, ok := s.Certificate.PrivateKey.(*rsa.PrivateKey)
   308  		if !ok {
   309  			return errors.New("sts: rsa.PrivateKey is required to sign http.Request")
   310  		}
   311  		sig, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, sum[:])
   312  		if err != nil {
   313  			return err
   314  		}
   315  
   316  		add(param{
   317  			key: "signature_alg",
   318  			val: "RSA-SHA256",
   319  		})
   320  		add(param{
   321  			key: "signature",
   322  			val: base64.StdEncoding.EncodeToString(sig),
   323  		})
   324  		add(param{
   325  			key: "nonce",
   326  			val: nonce,
   327  		})
   328  		add(param{
   329  			key: "bodyhash",
   330  			val: base64.StdEncoding.EncodeToString(bhash[:]),
   331  		})
   332  	}
   333  
   334  	req.Header.Set("Authorization", fmt.Sprintf("SIGN %s", strings.Join(params, ", ")))
   335  
   336  	return nil
   337  }
   338  
   339  func (s *Signer) NewRequest() TokenRequest {
   340  	return TokenRequest{
   341  		Token:       s.Token,
   342  		Certificate: s.Certificate,
   343  		Userinfo:    s.user,
   344  		KeyID:       s.keyID,
   345  	}
   346  }
   347  
   348  func isIPv6(s string) bool {
   349  	ip := net.ParseIP(s)
   350  	if ip == nil {
   351  		return false
   352  	}
   353  	return ip.To4() == nil
   354  }