github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/security/filesec/file_security.go (about) 1 // Copyright (c) 2020-2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 // Package filesec provides a manually configurable security Provider 6 // it allows you set every parameter like key paths etc manually without 7 // making any assumptions about your system 8 // 9 // It does not support any enrollment 10 package filesec 11 12 import ( 13 "bytes" 14 "context" 15 "crypto" 16 "crypto/rand" 17 "crypto/rsa" 18 "crypto/sha256" 19 "crypto/tls" 20 "crypto/x509" 21 "encoding/pem" 22 "errors" 23 "fmt" 24 "net/http" 25 "net/url" 26 "os" 27 "path/filepath" 28 "regexp" 29 "strings" 30 "sync" 31 "time" 32 33 "github.com/choria-io/go-choria/inter" 34 "github.com/choria-io/go-choria/internal/util" 35 "github.com/choria-io/go-choria/tlssetup" 36 37 "github.com/sirupsen/logrus" 38 ) 39 40 // used by tests to stub out uids etc, should probably be a class and use dependency injection, meh 41 var ( 42 useFakeUID = false 43 fakeUID = 0 44 useFakeOS = false 45 fakeOS = "fake" 46 callerIDRe = regexp.MustCompile(`^[a-z]+=([\w\.\-]+)`) 47 ) 48 49 // FileSecurity implements SecurityProvider using files on disk 50 type FileSecurity struct { 51 conf *Config 52 log *logrus.Entry 53 54 mu *sync.Mutex 55 } 56 57 // Config is the configuration for FileSecurity 58 type Config struct { 59 // Identity when not empty will force the identity to be used for validations etc 60 Identity string 61 62 // Certificate is the path to the public certificate 63 Certificate string 64 65 // Key is the path to the private key 66 Key string 67 68 // CA is the path to the Certificate Authority 69 CA string 70 71 // PrivilegedUsers is a list of regular expressions that identity privileged users 72 PrivilegedUsers []string 73 74 // AllowList is a list of regular expressions that identity valid users to allow in 75 AllowList []string 76 77 // DisableTLSVerify disables TLS verify in HTTP clients etc 78 DisableTLSVerify bool 79 80 // Is a URL where a remote signer is running 81 RemoteSignerURL string 82 83 // RemoteSignerTokenFile is a file with a token for access to the remote signer 84 RemoteSignerTokenFile string 85 86 // RemoteSignerSeedFile is a file with a seed related to RemoteSignerTokenFile 87 RemoteSignerSeedFile string 88 89 // TLSSetup is the shared TLS configuration state between security providers 90 TLSConfig *tlssetup.Config 91 92 // BackwardCompatVerification enables custom verification that allows legacy certificates without SANs 93 BackwardCompatVerification bool 94 95 // IdentitySuffix is the suffix to append to usernames when creating certnames and identities 96 IdentitySuffix string 97 98 // RemoteSigner is the signer used to sign requests using a remote like AAA Service 99 RemoteSigner inter.RequestSigner 100 } 101 102 // New creates a new instance of the File Security provider 103 func New(opts ...Option) (*FileSecurity, error) { 104 f := &FileSecurity{ 105 mu: &sync.Mutex{}, 106 } 107 108 for _, opt := range opts { 109 err := opt(f) 110 if err != nil { 111 return nil, err 112 } 113 } 114 115 if f.conf == nil { 116 return nil, errors.New("configuration not given") 117 } 118 119 if f.log == nil { 120 return nil, errors.New("logger not given") 121 } 122 123 if f.conf.Identity == "" { 124 return nil, errors.New("identity could not be determine automatically via Choria or was not supplied") 125 } 126 127 if f.conf.BackwardCompatVerification { 128 f.log.Infof("Enabling support for legacy SAN free certificates") 129 } 130 131 return f, nil 132 } 133 134 func (s *FileSecurity) BackingTechnology() inter.SecurityTechnology { 135 return inter.SecurityTechnologyX509 136 } 137 138 // Provider reports the name of the security provider 139 func (s *FileSecurity) Provider() string { 140 return "file" 141 } 142 143 func (s *FileSecurity) TokenBytes() ([]byte, error) { 144 return nil, fmt.Errorf("tokens not available for file security provider") 145 } 146 147 func (s *FileSecurity) RemoteSignerSeedFile() (string, error) { 148 if s.conf.RemoteSignerTokenFile != "" && s.conf.RemoteSignerSeedFile == "" { 149 // copies the behavior from framework SignerSeedFile() 150 s.conf.RemoteSignerSeedFile = fmt.Sprintf("%s.key", strings.TrimSuffix(s.conf.RemoteSignerTokenFile, filepath.Ext(s.conf.RemoteSignerTokenFile))) 151 } 152 153 if s.conf.RemoteSignerSeedFile == "" { 154 return "", fmt.Errorf("no seed file defined") 155 } 156 157 return s.conf.RemoteSignerSeedFile, nil 158 } 159 160 func (s *FileSecurity) RemoteSignerToken() ([]byte, error) { 161 if s.conf.RemoteSignerTokenFile == "" { 162 return nil, fmt.Errorf("no token file defined") 163 } 164 165 tb, err := os.ReadFile(s.conf.RemoteSignerTokenFile) 166 if err != nil { 167 return bytes.TrimSpace(tb), fmt.Errorf("could not read token file: %v", err) 168 } 169 170 return tb, err 171 } 172 173 func (s *FileSecurity) RemoteSignerURL() (*url.URL, error) { 174 if s.conf.RemoteSignerURL == "" { 175 return nil, fmt.Errorf("no remote url configured") 176 } 177 178 return url.Parse(s.conf.RemoteSignerURL) 179 } 180 181 // RemoteSignRequest signs a choria request using a remote signer and returns a secure request 182 func (s *FileSecurity) RemoteSignRequest(ctx context.Context, request []byte) (signed []byte, err error) { 183 if s.conf.RemoteSigner == nil { 184 return nil, fmt.Errorf("remote signing not configured") 185 } 186 187 s.log.Infof("Signing request using %s", s.conf.RemoteSigner.Kind()) 188 return s.conf.RemoteSigner.Sign(ctx, request, s) 189 } 190 191 // Validate determines if the node represents a valid SSL configuration 192 func (s *FileSecurity) Validate() ([]string, bool) { 193 var errors []string 194 195 if s.publicCertPath() != "" { 196 if !s.publicCertExists() { 197 errors = append(errors, fmt.Sprintf("public certificate %s does not exist", s.publicCertPath())) 198 } 199 } else { 200 errors = append(errors, "the public certificate path is not configured") 201 } 202 203 if s.privateKeyPath() != "" { 204 if !s.privateKeyExists() { 205 errors = append(errors, fmt.Sprintf("private key %s does not exist", s.privateKeyPath())) 206 } 207 } else { 208 errors = append(errors, "the private key path is not configured") 209 } 210 211 if s.caPath() != "" { 212 if !s.caExists() { 213 errors = append(errors, fmt.Sprintf("CA %s does not exist", s.caPath())) 214 } 215 } else { 216 errors = append(errors, "the CA path is not configured") 217 } 218 219 return errors, len(errors) == 0 220 } 221 222 // ChecksumBytes calculates a sha256 checksum for data 223 func (s *FileSecurity) ChecksumBytes(data []byte) []byte { 224 sum := sha256.Sum256(data) 225 226 return sum[:] 227 } 228 229 // SignBytes signs a message using a SHA256 PKCS1v15 protocol 230 func (s *FileSecurity) SignBytes(str []byte) ([]byte, error) { 231 sig := []byte{} 232 233 pkpem, err := s.privateKeyPEM() 234 if err != nil { 235 return sig, err 236 } 237 var parsedKey any 238 239 parsedKey, err = x509.ParsePKCS1PrivateKey(pkpem.Bytes) 240 if err != nil { 241 parsedKey, err = x509.ParsePKCS8PrivateKey(pkpem.Bytes) 242 if err != nil { 243 err = fmt.Errorf("could not parse private key PEM data: %s", err) 244 return sig, err 245 } 246 } 247 248 rng := rand.Reader 249 hashed := s.ChecksumBytes(str) 250 251 switch t := parsedKey.(type) { 252 case *rsa.PrivateKey: 253 sig, err = rsa.SignPKCS1v15(rng, t, crypto.SHA256, hashed[:]) 254 default: 255 return sig, fmt.Errorf("unhandled key type %T", t) 256 } 257 258 if err != nil { 259 err = fmt.Errorf("could not sign message: %s", err) 260 } 261 262 return sig, err 263 } 264 265 // VerifyByteSignature verify that dat matches signature sig made by the key, if pub cert is empty the active public key will be used 266 func (s *FileSecurity) VerifySignatureBytes(dat []byte, sig []byte, public ...[]byte) (should bool, signer string) { 267 if len(public) != 1 { 268 s.log.Errorf("Could not process public data: only single signer public data is supported") 269 return false, "" 270 } 271 272 pubcert := public[0] 273 274 var err error 275 276 if len(pubcert) == 0 { 277 pubcert, err = s.PublicCertBytes() 278 if err != nil { 279 s.log.Errorf("Could not load public cert: %v", err) 280 return false, "" 281 } 282 } 283 284 pkpem, _ := pem.Decode(pubcert) 285 if pkpem == nil { 286 s.log.Errorf("Could not decode PEM data in public key: invalid pem data") 287 return false, "" 288 } 289 290 cert, err := x509.ParseCertificate(pkpem.Bytes) 291 if err != nil { 292 s.log.Errorf("Could not parse decoded PEM data for public certificate: %s", err) 293 return false, "" 294 } 295 296 rsaPublicKey := cert.PublicKey.(*rsa.PublicKey) 297 hashed := s.ChecksumBytes(dat) 298 299 err = rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed[:], sig) 300 if err != nil { 301 s.log.Errorf("Signature verification failed: %s", err) 302 return false, "" 303 } 304 305 names := []string{cert.Subject.CommonName} 306 names = append(names, cert.DNSNames...) 307 308 if len(names) == 0 { 309 s.log.Errorf("Signature verification failed: no names found in signer certificate") 310 return false, "" 311 } 312 313 s.log.Debugf("Verified signature from %s", strings.Join(names, ", ")) 314 315 return true, names[0] 316 } 317 318 // CallerName creates a choria like caller name in the form of choria=identity 319 func (s *FileSecurity) CallerName() string { 320 return fmt.Sprintf("choria=%s", s.Identity()) 321 } 322 323 // CallerIdentity extracts the identity from a choria like caller name in the form of choria=identity 324 func (s *FileSecurity) CallerIdentity(caller string) (string, error) { 325 match := callerIDRe.FindStringSubmatch(caller) 326 327 if match == nil { 328 return "", fmt.Errorf("could not find a valid caller identity name in %s", caller) 329 } 330 331 return match[1], nil 332 } 333 334 // IsRemoteSigning determines if remote signer is set 335 func (s *FileSecurity) IsRemoteSigning() bool { 336 return s.conf.RemoteSigner != nil 337 } 338 339 // Identity determines the choria certname 340 func (s *FileSecurity) Identity() string { 341 return s.conf.Identity 342 } 343 344 // VerifyCertificate verifies a certificate is signed with the configured CA and if 345 // name is not "" that it matches the name given 346 func (s *FileSecurity) VerifyCertificate(certpem []byte, name string) error { 347 ca := s.caPath() 348 capem, err := os.ReadFile(ca) 349 if err != nil { 350 s.log.Errorf("Could not read CA '%s': %s", ca, err) 351 return err 352 } 353 354 roots := x509.NewCertPool() 355 if !roots.AppendCertsFromPEM(capem) { 356 s.log.Warnf("Could not use CA '%s' as PEM data: %s", ca, err) 357 return err 358 } 359 360 block, _ := pem.Decode(certpem) 361 if block == nil { 362 s.log.Warnf("Could not decode certificate '%s' PEM data: %s", name, err) 363 return err 364 } 365 366 cert, err := x509.ParseCertificate(block.Bytes) 367 if err != nil { 368 s.log.Warnf("Could not parse certificate '%s': %s", name, err) 369 return err 370 } 371 372 intermediates := x509.NewCertPool() 373 if !intermediates.AppendCertsFromPEM(certpem) { 374 s.log.Warnf("Could not add intermediates: %s", err) 375 return err 376 } 377 378 opts := x509.VerifyOptions{ 379 Roots: roots, 380 Intermediates: intermediates, 381 KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 382 } 383 384 _, err = cert.Verify(opts) 385 if err != nil { 386 s.log.Warnf("Certificate does not pass verification as '%s': %s", name, err) 387 return err 388 } 389 390 if len(cert.EmailAddresses) > 0 && strings.HasPrefix(name, "email:") { 391 s.log.Debug("Email addresses found in certificate, attempting verification") 392 for _, email := range cert.EmailAddresses { 393 if strings.TrimPrefix(name, "email:") == email { 394 return nil 395 } 396 } 397 398 return fmt.Errorf("email address not found in SAN: %s, %v", name, cert.EmailAddresses) 399 } 400 401 // ShouldAllowCaller passes in an empty name, we just want it to verify validity of the CA chain at this point 402 if name == "" { 403 return nil 404 } 405 406 if !findName(cert.DNSNames, name) { 407 if cert.Subject.CommonName != name { 408 return fmt.Errorf("x509: certificate is valid for %s, not %s", cert.Subject.CommonName, name) 409 } 410 } 411 412 return nil 413 } 414 415 func findName(names []string, name string) bool { 416 for _, n := range names { 417 if n == name { 418 return true 419 } 420 } 421 return false 422 } 423 424 // HTTPClient creates a standard HTTP client with optional security, it will 425 // be set to use the CA and client certs for auth. servername should match the 426 // remote hosts name for SNI 427 func (s *FileSecurity) HTTPClient(secure bool) (*http.Client, error) { 428 client := &http.Client{} 429 430 if secure { 431 tlsc, err := s.TLSConfig() 432 if err != nil { 433 return nil, fmt.Errorf("could not set up HTTP connection: %s", err) 434 } 435 436 client.Transport = &http.Transport{TLSClientConfig: tlsc} 437 } 438 439 return client, nil 440 } 441 442 func (s *FileSecurity) ClientTLSConfig() (*tls.Config, error) { 443 tlsc, err := s.TLSConfig() 444 if err != nil { 445 return nil, err 446 } 447 448 if s.conf.BackwardCompatVerification { 449 tlsc.InsecureSkipVerify = true 450 tlsc.VerifyConnection = s.constructCustomVerifier(tlsc.RootCAs) 451 } 452 453 return tlsc, nil 454 } 455 456 // TLSConfig creates a TLS configuration for use by NATS, HTTPS etc 457 func (s *FileSecurity) TLSConfig() (*tls.Config, error) { 458 pub := s.publicCertPath() 459 pri := s.privateKeyPath() 460 ca := s.caPath() 461 462 tlsc := &tls.Config{ 463 MinVersion: tls.VersionTLS12, 464 PreferServerCipherSuites: true, 465 CipherSuites: s.conf.TLSConfig.CipherSuites, 466 CurvePreferences: s.conf.TLSConfig.CurvePreferences, 467 } 468 469 if s.privateKeyExists() && s.publicCertExists() { 470 cert, err := tls.LoadX509KeyPair(pub, pri) 471 if err != nil { 472 err = fmt.Errorf("could not load certificate %s and key %s: %s", pub, pri, err) 473 return nil, err 474 } 475 476 cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 477 if err != nil { 478 err = fmt.Errorf("error parsing certificate: %v", err) 479 return nil, err 480 } 481 482 tlsc.Certificates = []tls.Certificate{cert} 483 } 484 485 if s.caExists() { 486 caCert, err := os.ReadFile(ca) 487 if err != nil { 488 return nil, err 489 } 490 491 caCertPool := x509.NewCertPool() 492 caCertPool.AppendCertsFromPEM(caCert) 493 494 tlsc.ClientCAs = caCertPool 495 tlsc.RootCAs = caCertPool 496 } 497 498 if s.conf.DisableTLSVerify { 499 tlsc.InsecureSkipVerify = true 500 } 501 502 return tlsc, nil 503 } 504 505 func (s *FileSecurity) constructCustomVerifier(pool *x509.CertPool) func(cs tls.ConnectionState) error { 506 return func(cs tls.ConnectionState) error { 507 s.log.Debug("Verifying connection using legacy SAN free certificate support") 508 opts := x509.VerifyOptions{ 509 Roots: pool, 510 Intermediates: x509.NewCertPool(), 511 } 512 // If there is no SAN, then fallback to using the CommonName 513 hasSanExtension := func(cert *x509.Certificate) bool { 514 // oid taken from crypt/x509/x509.go 515 var oidExtensionSubjectAltName = []int{2, 5, 29, 17} 516 for _, e := range cert.Extensions { 517 if e.Id.Equal(oidExtensionSubjectAltName) { 518 return true 519 } 520 } 521 return false 522 } 523 if !hasSanExtension(cs.PeerCertificates[0]) { 524 if !strings.EqualFold(cs.ServerName, cs.PeerCertificates[0].Subject.CommonName) { 525 return x509.HostnameError{Certificate: cs.PeerCertificates[0], Host: cs.ServerName} 526 } 527 } else { 528 opts.DNSName = cs.ServerName 529 } 530 for _, cert := range cs.PeerCertificates[1:] { 531 opts.Intermediates.AddCert(cert) 532 } 533 _, err := cs.PeerCertificates[0].Verify(opts) 534 return err 535 } 536 } 537 538 // publicCertPem retrieves the public certificate for this instance 539 func (s *FileSecurity) publicCertPem() (*pem.Block, error) { 540 path := s.publicCertPath() 541 542 return s.decodePEM(path) 543 } 544 545 // PublicCertBytes retrieves pem data in textual form for the public certificate of the current identity 546 func (s *FileSecurity) PublicCertBytes() ([]byte, error) { 547 path := s.publicCertPath() 548 549 return os.ReadFile(path) 550 } 551 552 // PublicCert is the parsed public certificate 553 func (s *FileSecurity) PublicCert() (*x509.Certificate, error) { 554 block, err := s.publicCertPem() 555 if err != nil { 556 return nil, err 557 } 558 559 cert, err := x509.ParseCertificate(block.Bytes) 560 if err != nil { 561 return nil, err 562 } 563 564 return cert, nil 565 } 566 567 // SSLContext creates a SSL context loaded with our certs and ca 568 func (s *FileSecurity) SSLContext() (*http.Transport, error) { 569 tlsConfig, err := s.ClientTLSConfig() 570 if err != nil { 571 return nil, err 572 } 573 574 transport := &http.Transport{TLSClientConfig: tlsConfig} 575 576 return transport, nil 577 } 578 579 // Enroll is not supported 580 func (s *FileSecurity) Enroll(ctx context.Context, wait time.Duration, cb func(digest string, try int)) error { 581 return errors.New("the file security provider does not support enrollment") 582 } 583 584 func (s *FileSecurity) decodePEM(certpath string) (*pem.Block, error) { 585 var err error 586 587 if certpath == "" { 588 return nil, errors.New("invalid certpath '' provided") 589 } 590 591 keydat, err := os.ReadFile(certpath) 592 if err != nil { 593 return nil, fmt.Errorf("could not read PEM data from %s: %s", certpath, err) 594 } 595 596 pb, _ := pem.Decode(keydat) 597 if pb == nil { 598 return nil, fmt.Errorf("failed to parse PEM data from key %s", certpath) 599 } 600 601 return pb, nil 602 } 603 604 func (s *FileSecurity) privateKeyPath() string { 605 return filepath.FromSlash(s.conf.Key) 606 } 607 608 func (s *FileSecurity) publicCertPath() string { 609 return filepath.FromSlash(s.conf.Certificate) 610 } 611 612 func (s *FileSecurity) caPath() string { 613 return filepath.FromSlash(s.conf.CA) 614 } 615 616 func (s *FileSecurity) privateKeyExists() bool { 617 return util.FileExist(s.privateKeyPath()) 618 } 619 620 func (s *FileSecurity) publicCertExists() bool { 621 return util.FileExist(s.publicCertPath()) 622 } 623 624 func (s *FileSecurity) caExists() bool { 625 return util.FileExist(s.caPath()) 626 } 627 628 func (s *FileSecurity) privateKeyPEM() (pb *pem.Block, err error) { 629 key := s.privateKeyPath() 630 631 keydat, err := os.ReadFile(key) 632 if err != nil { 633 return pb, fmt.Errorf("could not read Private Key %s: %s", key, err) 634 } 635 636 pb, _ = pem.Decode(keydat) 637 if pb == nil { 638 return pb, fmt.Errorf("failed to parse PEM data from key %s", key) 639 } 640 641 return 642 } 643 644 func (s *FileSecurity) ShouldAllowCaller(name string, callers ...[]byte) (privileged bool, err error) { 645 if len(callers) != 1 { 646 s.log.Warnf("Received multiple items of caller identity data in x509 security provider") 647 return false, fmt.Errorf("invalid public data") 648 } 649 650 data := callers[0] 651 652 privNames, err := s.certDNSNames(data) 653 if err != nil { 654 s.log.Warnf("Could not extract DNS Names from certificate") 655 return false, err 656 } 657 658 for _, privName := range privNames { 659 if MatchAnyRegex(privName, s.conf.PrivilegedUsers) { 660 privileged = true 661 break 662 } 663 } 664 665 if privileged { 666 // Checks if it was signed by a CA issued cert but without any name validation since privileged name wouldnt match 667 err = s.VerifyCertificate(data, "") 668 if err != nil { 669 s.log.Warnf("Received certificate '%s' certificate did not pass verification: %s", name, err) 670 return false, err 671 } 672 673 return true, nil 674 } else { 675 // Checks if it was signed by a CA issued cert that matches name since it must match when not privileged 676 err = s.VerifyCertificate(data, name) 677 if err != nil { 678 s.log.Warnf("Received certificate '%s' did not pass verification: %s", name, err) 679 return false, err 680 } 681 } 682 683 // Finally if its on the allow list 684 if MatchAnyRegex(name, s.conf.AllowList) { 685 return false, nil 686 } 687 688 s.log.Warnf("Received certificate '%s' does not match the allowed list '%s'", name, s.conf.AllowList) 689 690 return false, fmt.Errorf("not on allow list") 691 } 692 693 func (s *FileSecurity) certDNSNames(certpem []byte) (names []string, err error) { 694 block, _ := pem.Decode(certpem) 695 if block == nil { 696 s.log.Warnf("Could not decode certificate PEM data: %s", err) 697 return names, err 698 } 699 700 cert, err := x509.ParseCertificate(block.Bytes) 701 if err != nil { 702 s.log.Warnf("Could not parse certificate: %s", err) 703 return names, err 704 } 705 706 names = append(names, cert.Subject.CommonName) 707 names = append(names, cert.DNSNames...) 708 709 return names, nil 710 } 711 712 func (s *FileSecurity) ShouldSignReplies() bool { return false }