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

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