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 }