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 }