diff --git a/credentials.go b/credentials.go index 995eb31..60b95b0 100644 --- a/credentials.go +++ b/credentials.go @@ -33,7 +33,7 @@ type Credential struct { // ClientCert holds a client certificate for Kerberos or LDAP authentication if available. ClientCert *x509.Certificate // ClientCertKey holds the private key that corresponds to ClientCert. - ClientCertKey *rsa.PrivateKey + ClientCertKey any // CACerts holds CA certificates that were loaded alongside the ClientCert. CACerts []*x509.Certificate dc string diff --git a/dcerpcauth/dcerpcauth.go b/dcerpcauth/dcerpcauth.go index 9733587..04dcc2b 100644 --- a/dcerpcauth/dcerpcauth.go +++ b/dcerpcauth/dcerpcauth.go @@ -2,6 +2,7 @@ package dcerpcauth import ( "context" + "crypto/rsa" "encoding/hex" "fmt" "net" @@ -170,8 +171,13 @@ func DCERPCCredentials(ctx context.Context, creds *adauth.Credential, options *O dialer = &net.Dialer{Timeout: pkinit.DefaultKerberosRoundtripDeadline} } + rsaKey, ok := creds.ClientCertKey.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("cannot use %T because PKINIT requires an RSA key", creds.ClientCertKey) + } + ccache, err := pkinit.Authenticate(ctx, creds.Username, strings.ToUpper(creds.Domain), - creds.ClientCert, creds.ClientCertKey, krbConf, pkinit.WithDialer(adauth.AsContextDialer(dialer))) + creds.ClientCert, rsaKey, krbConf, pkinit.WithDialer(adauth.AsContextDialer(dialer))) if err != nil { return nil, fmt.Errorf("PKINIT: %w", err) } diff --git a/ldapauth/ldap.go b/ldapauth/ldap.go index 8fa91c3..c1fb4eb 100644 --- a/ldapauth/ldap.go +++ b/ldapauth/ldap.go @@ -425,8 +425,13 @@ func kerberosClient( case creds.ClientCert != nil: opts.Debug("authenticating using GSSAPI bind (PKINIT)") + rsaKey, ok := creds.ClientCertKey.(*rsa.PrivateKey) + if !ok { + return nil, fmt.Errorf("cannot use %T because PKINIT requires an RSA key", creds.ClientCertKey) + } + return newPKINITClient(ctx, creds.Username, strings.ToUpper(creds.Domain), - creds.ClientCert, creds.ClientCertKey, krbConf, opts.KerberosDialer) + creds.ClientCert, rsaKey, krbConf, opts.KerberosDialer) case creds.CCache != "": opts.Debug("authenticating using GSSAPI bind (ccache)") diff --git a/options.go b/options.go index f3acb0a..a9dc316 100644 --- a/options.go +++ b/options.go @@ -2,8 +2,9 @@ package adauth import ( "context" - "crypto/rsa" + "crypto/x509" "encoding/hex" + "encoding/pem" "fmt" "io" "net" @@ -28,8 +29,15 @@ type Options struct { CCache string DomainController string ForceKerberos bool - PFXFileName string - PFXPassword string + + // It is possible to specify a cert/key pair directly, as PEM files or as a + // single PFX file. + Certificate *x509.Certificate + CertificateKey any + PFXFileName string + PFXPassword string + PEMCertFileName string + PEMKeyFileName string credential *Credential flagset *pflag.FlagSet @@ -273,25 +281,31 @@ func (opts *Options) preliminaryCredential() (*Credential, error) { Resolver: opts.Resolver, } - if opts.PFXFileName != "" { - pfxData, err := os.ReadFile(opts.PFXFileName) + switch { + case opts.Certificate != nil && opts.CertificateKey == nil: + return nil, fmt.Errorf("specify a key file for the client certificate") + case opts.Certificate != nil && opts.CertificateKey != nil: + cred.ClientCert = opts.Certificate + cred.ClientCertKey = opts.CertificateKey + case opts.PFXFileName != "": + cert, key, caCerts, err := readPFX(opts.PFXFileName, opts.PFXPassword) if err != nil { - return nil, fmt.Errorf("read PFX: %w", err) - } - - key, cert, caCerts, err := pkcs12.DecodeChain(pfxData, opts.PFXPassword) - if err != nil { - return nil, fmt.Errorf("decode PFX: %w", err) - } - - rsaKey, ok := key.(*rsa.PrivateKey) - if !ok { - return nil, fmt.Errorf("PFX key is not an RSA private key but %T", rsaKey) + return nil, err } cred.ClientCert = cert - cred.ClientCertKey = rsaKey + cred.ClientCertKey = key cred.CACerts = caCerts + case opts.PEMCertFileName != "" && opts.PEMKeyFileName == "": + return nil, fmt.Errorf("specify a key file for the client certificate") + case opts.PEMCertFileName != "" && opts.PEMKeyFileName != "": + cert, key, err := readPEMCertAndKey(opts.PEMCertFileName, opts.PEMKeyFileName) + if err != nil { + return nil, err + } + + cred.ClientCert = cert + cred.ClientCertKey = key } //nolint:nestif @@ -313,6 +327,67 @@ func (opts *Options) preliminaryCredential() (*Credential, error) { return cred, nil } +func readPFX(fileName string, password string) (*x509.Certificate, any, []*x509.Certificate, error) { + pfxData, err := os.ReadFile(fileName) + if err != nil { + return nil, nil, nil, fmt.Errorf("read PFX: %w", err) + } + + key, cert, caCerts, err := pkcs12.DecodeChain(pfxData, password) + if err != nil { + return nil, nil, nil, fmt.Errorf("decode PFX: %w", err) + } + + return cert, key, caCerts, nil +} + +func readPEMCertAndKey(certFileName string, certKeyFileName string) (*x509.Certificate, any, error) { + certData, err := os.ReadFile(certFileName) + if err != nil { + return nil, nil, fmt.Errorf("read cert file: %w", err) + } + + block, _ := pem.Decode(certData) + if block == nil { + return nil, nil, fmt.Errorf("could not PEM-decode certificate") + } + + if block.Type != "" && !strings.Contains(strings.ToLower(block.Type), "certificate") { + return nil, nil, fmt.Errorf("unexpected block type for certificate: %q", block.Type) + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, nil, fmt.Errorf("parse certificate: %w", err) + } + + certKeyData, err := os.ReadFile(certKeyFileName) + if err != nil { + return nil, nil, fmt.Errorf("read cert key file: %w", err) + } + + block, _ = pem.Decode(certKeyData) + if block == nil { + return nil, nil, fmt.Errorf("could not PEM-decode certificate key") + } + + if block.Type != "" && !strings.Contains(strings.ToLower(block.Type), "key") { + return nil, nil, fmt.Errorf("unexpected block type for key: %q", block.Type) + } + + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + key, pkcs1Err := x509.ParsePKCS1PrivateKey(block.Bytes) + if pkcs1Err == nil { + return cert, key, nil + } + + return nil, nil, fmt.Errorf("parse private key: %w", err) + } + + return cert, key, nil +} + // NewDebugFunc creates a debug output handler. func NewDebugFunc(enabled *bool, writer io.Writer, colored bool) func(string, ...any) { return func(format string, a ...any) { diff --git a/pkinit/unpacthehash.go b/pkinit/unpacthehash.go index 2ba8228..a42ce1a 100644 --- a/pkinit/unpacthehash.go +++ b/pkinit/unpacthehash.go @@ -58,7 +58,12 @@ func UnPACTheHashFromPFXData( return nil, nil, fmt.Errorf("configure Kerberos: %w", err) } - return UnPACTheHash(ctx, cred.Username, cred.Domain, cred.ClientCert, cred.ClientCertKey, krbConf, opts...) + rsaKey, ok := cred.ClientCertKey.(*rsa.PrivateKey) + if !ok { + return nil, nil, fmt.Errorf("cannot use %T because PKINIT requires an RSA key", cred.ClientCertKey) + } + + return UnPACTheHash(ctx, cred.Username, cred.Domain, cred.ClientCert, rsaKey, krbConf, opts...) } // UnPACTheHash retrieves the user's NT hash via PKINIT using the provided