// Package config implements KRB5 client and service configuration as described at https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html
package config import ( ) // Config represents the KRB5 configuration. type Config struct { LibDefaults LibDefaults Realms []Realm DomainRealm DomainRealm //CaPaths //AppDefaults //Plugins } // WeakETypeList is a list of encryption types that have been deemed weak. const WeakETypeList = "des-cbc-crc des-cbc-md4 des-cbc-md5 des-cbc-raw des3-cbc-raw des-hmac-sha1 arcfour-hmac-exp rc4-hmac-exp arcfour-hmac-md5-exp des" // New creates a new config struct instance. func () *Config { := make(DomainRealm) return &Config{ LibDefaults: newLibDefaults(), DomainRealm: , } } // LibDefaults represents the [libdefaults] section of the configuration. type LibDefaults struct { AllowWeakCrypto bool //default false // ap_req_checksum_type int //unlikely to support this Canonicalize bool //default false CCacheType int //default is 4. unlikely to implement older Clockskew time.Duration //max allowed skew in seconds, default 300 //Default_ccache_name string // default /tmp/krb5cc_%{uid} //Not implementing as will hold in memory DefaultClientKeytabName string //default /usr/local/var/krb5/user/%{euid}/client.keytab DefaultKeytabName string //default /etc/krb5.keytab DefaultRealm string DefaultTGSEnctypes []string //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4 DefaultTktEnctypes []string //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4 DefaultTGSEnctypeIDs []int32 //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4 DefaultTktEnctypeIDs []int32 //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4 DNSCanonicalizeHostname bool //default true DNSLookupKDC bool //default false DNSLookupRealm bool ExtraAddresses []net.IP //Not implementing yet Forwardable bool //default false IgnoreAcceptorHostname bool //default false K5LoginAuthoritative bool //default false K5LoginDirectory string //default user's home directory. Must be owned by the user or root KDCDefaultOptions asn1.BitString //default 0x00000010 (KDC_OPT_RENEWABLE_OK) KDCTimeSync int //default 1 //kdc_req_checksum_type int //unlikely to implement as for very old KDCs NoAddresses bool //default true PermittedEnctypes []string //default aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 des3-cbc-sha1 arcfour-hmac-md5 camellia256-cts-cmac camellia128-cts-cmac des-cbc-crc des-cbc-md5 des-cbc-md4 PermittedEnctypeIDs []int32 //plugin_base_dir string //not supporting plugins PreferredPreauthTypes []int //default “17, 16, 15, 14”, which forces libkrb5 to attempt to use PKINIT if it is supported Proxiable bool //default false RDNS bool //default true RealmTryDomains int //default -1 RenewLifetime time.Duration //default 0 SafeChecksumType int //default 8 TicketLifetime time.Duration //default 1 day UDPPreferenceLimit int // 1 means to always use tcp. MIT krb5 has a default value of 1465, and it prevents user setting more than 32700. VerifyAPReqNofail bool //default false } // Create a new LibDefaults struct. func newLibDefaults() LibDefaults { := "0" var string , := user.Current() if != nil { = .Uid = .HomeDir } := asn1.BitString{} .Bytes, _ = hex.DecodeString("00000010") .BitLength = len(.Bytes) * 8 := LibDefaults{ CCacheType: 4, Clockskew: time.Duration(300) * time.Second, DefaultClientKeytabName: fmt.Sprintf("/usr/local/var/krb5/user/%s/client.keytab", ), DefaultKeytabName: "/etc/krb5.keytab", DefaultTGSEnctypes: []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "des3-cbc-sha1", "arcfour-hmac-md5", "camellia256-cts-cmac", "camellia128-cts-cmac", "des-cbc-crc", "des-cbc-md5", "des-cbc-md4"}, DefaultTktEnctypes: []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "des3-cbc-sha1", "arcfour-hmac-md5", "camellia256-cts-cmac", "camellia128-cts-cmac", "des-cbc-crc", "des-cbc-md5", "des-cbc-md4"}, DNSCanonicalizeHostname: true, K5LoginDirectory: , KDCDefaultOptions: , KDCTimeSync: 1, NoAddresses: true, PermittedEnctypes: []string{"aes256-cts-hmac-sha1-96", "aes128-cts-hmac-sha1-96", "des3-cbc-sha1", "arcfour-hmac-md5", "camellia256-cts-cmac", "camellia128-cts-cmac", "des-cbc-crc", "des-cbc-md5", "des-cbc-md4"}, RDNS: true, RealmTryDomains: -1, SafeChecksumType: 8, TicketLifetime: time.Duration(24) * time.Hour, UDPPreferenceLimit: 1465, PreferredPreauthTypes: []int{17, 16, 15, 14}, } .DefaultTGSEnctypeIDs = parseETypes(.DefaultTGSEnctypes, .AllowWeakCrypto) .DefaultTktEnctypeIDs = parseETypes(.DefaultTktEnctypes, .AllowWeakCrypto) .PermittedEnctypeIDs = parseETypes(.PermittedEnctypes, .AllowWeakCrypto) return } // Parse the lines of the [libdefaults] section of the configuration into the LibDefaults struct. func ( *LibDefaults) ( []string) error { for , := range { //Remove comments after the values if := strings.IndexAny(, "#;"); != -1 { = [:] } = strings.TrimSpace() if == "" { continue } if !strings.Contains(, "=") { return InvalidErrorf("libdefaults section line (%s)", ) } := strings.Split(, "=") := strings.TrimSpace(strings.ToLower([0])) switch { case "allow_weak_crypto": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .AllowWeakCrypto = case "canonicalize": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .Canonicalize = case "ccache_type": [1] = strings.TrimSpace([1]) , := strconv.ParseUint([1], 10, 32) if != nil || < 0 || > 4 { return InvalidErrorf("libdefaults section line (%s)", ) } .CCacheType = int() case "clockskew": , := parseDuration([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .Clockskew = case "default_client_keytab_name": .DefaultClientKeytabName = strings.TrimSpace([1]) case "default_keytab_name": .DefaultKeytabName = strings.TrimSpace([1]) case "default_realm": .DefaultRealm = strings.TrimSpace([1]) case "default_tgs_enctypes": .DefaultTGSEnctypes = strings.Fields([1]) case "default_tkt_enctypes": .DefaultTktEnctypes = strings.Fields([1]) case "dns_canonicalize_hostname": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .DNSCanonicalizeHostname = case "dns_lookup_kdc": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .DNSLookupKDC = case "dns_lookup_realm": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .DNSLookupRealm = case "extra_addresses": := strings.TrimSpace([1]) for , := range strings.Split(, ",") { if := net.ParseIP(); != nil { .ExtraAddresses = append(.ExtraAddresses, ) } } case "forwardable": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .Forwardable = case "ignore_acceptor_hostname": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .IgnoreAcceptorHostname = case "k5login_authoritative": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .K5LoginAuthoritative = case "k5login_directory": .K5LoginDirectory = strings.TrimSpace([1]) case "kdc_default_options": := strings.TrimSpace([1]) = strings.Replace(, "0x", "", -1) , := hex.DecodeString() if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .KDCDefaultOptions.Bytes = .KDCDefaultOptions.BitLength = len() * 8 case "kdc_timesync": [1] = strings.TrimSpace([1]) , := strconv.ParseInt([1], 10, 32) if != nil || < 0 { return InvalidErrorf("libdefaults section line (%s)", ) } .KDCTimeSync = int() case "noaddresses": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .NoAddresses = case "permitted_enctypes": .PermittedEnctypes = strings.Fields([1]) case "preferred_preauth_types": [1] = strings.TrimSpace([1]) := strings.Split([1], ",") var []int for , := range { , := strconv.ParseInt(, 10, 32) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } = append(, int()) } .PreferredPreauthTypes = case "proxiable": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .Proxiable = case "rdns": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .RDNS = case "realm_try_domains": [1] = strings.TrimSpace([1]) , := strconv.ParseInt([1], 10, 32) if != nil || < -1 { return InvalidErrorf("libdefaults section line (%s)", ) } .RealmTryDomains = int() case "renew_lifetime": , := parseDuration([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .RenewLifetime = case "safe_checksum_type": [1] = strings.TrimSpace([1]) , := strconv.ParseInt([1], 10, 32) if != nil || < 0 { return InvalidErrorf("libdefaults section line (%s)", ) } .SafeChecksumType = int() case "ticket_lifetime": , := parseDuration([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .TicketLifetime = case "udp_preference_limit": [1] = strings.TrimSpace([1]) , := strconv.ParseUint([1], 10, 32) if != nil || > 32700 { return InvalidErrorf("libdefaults section line (%s)", ) } .UDPPreferenceLimit = int() case "verify_ap_req_nofail": , := parseBoolean([1]) if != nil { return InvalidErrorf("libdefaults section line (%s): %v", , ) } .VerifyAPReqNofail = } } .DefaultTGSEnctypeIDs = parseETypes(.DefaultTGSEnctypes, .AllowWeakCrypto) .DefaultTktEnctypeIDs = parseETypes(.DefaultTktEnctypes, .AllowWeakCrypto) .PermittedEnctypeIDs = parseETypes(.PermittedEnctypes, .AllowWeakCrypto) return nil } // Realm represents an entry in the [realms] section of the configuration. type Realm struct { Realm string AdminServer []string //auth_to_local //Not implementing for now //auth_to_local_names //Not implementing for now DefaultDomain string KDC []string KPasswdServer []string //default admin_server:464 MasterKDC []string } // Parse the lines of a [realms] entry into the Realm struct. func ( *Realm) ( string, []string) ( error) { .Realm = var bool var bool var bool var bool var bool var int // counts the depth of blocks within brackets { } for , := range { if && > 0 && !strings.Contains(, "{") && !strings.Contains(, "}") { continue } //Remove comments after the values if := strings.IndexAny(, "#;"); != -1 { = [:] } = strings.TrimSpace() if == "" { continue } if !strings.Contains(, "=") && !strings.Contains(, "}") { return InvalidErrorf("realms section line (%s)", ) } if strings.Contains(, "v4_") { = true = UnsupportedDirective{"v4 configurations are not supported"} } if strings.Contains(, "{") { ++ if { continue } } if strings.Contains(, "}") { -- if < 0 { return InvalidErrorf("unpaired curly brackets") } if { if < 1 { = 0 = false } continue } } := strings.Split(, "=") := strings.TrimSpace(strings.ToLower([0])) := strings.TrimSpace([1]) switch { case "admin_server": appendUntilFinal(&.AdminServer, , &) case "default_domain": .DefaultDomain = case "kdc": if !strings.Contains(, ":") { // No port number specified default to 88 if strings.HasSuffix(, `*`) { = strings.TrimSpace(strings.TrimSuffix(, `*`)) + ":88*" } else { = strings.TrimSpace() + ":88" } } appendUntilFinal(&.KDC, , &) case "kpasswd_server": appendUntilFinal(&.KPasswdServer, , &) case "master_kdc": appendUntilFinal(&.MasterKDC, , &) } } //default for Kpasswd_server = admin_server:464 if len(.KPasswdServer) < 1 { for , := range .AdminServer { := strings.Split(, ":") .KPasswdServer = append(.KPasswdServer, [0]+":464") } } return } // Parse the lines of the [realms] section of the configuration into an slice of Realm structs. func parseRealms( []string) ( []Realm, error) { var string var int var int for , := range { //Remove comments after the values if := strings.IndexAny(, "#;"); != -1 { = [:] } = strings.TrimSpace() if == "" { continue } //if strings.Contains(l, "v4_") { // return nil, errors.New("v4 configurations are not supported in Realms section") //} if strings.Contains(, "{") { ++ if !strings.Contains(, "=") { return nil, fmt.Errorf("realm configuration line invalid: %s", ) } if == 1 { = := strings.Split(, "=") = strings.TrimSpace([0]) } } if strings.Contains(, "}") { if < 1 { // but not started a block!!! return nil, errors.New("invalid Realms section in configuration") } -- if == 0 { var Realm := .parseLines(, [+1:]) if != nil { if , := .(UnsupportedDirective); ! { = return } = } = append(, ) } } } return } // DomainRealm maps the domains to realms representing the [domain_realm] section of the configuration. type DomainRealm map[string]string // Parse the lines of the [domain_realm] section of the configuration and add to the mapping. func ( *DomainRealm) ( []string) error { for , := range { //Remove comments after the values if := strings.IndexAny(, "#;"); != -1 { = [:] } if strings.TrimSpace() == "" { continue } if !strings.Contains(, "=") { return InvalidErrorf("realm line (%s)", ) } := strings.Split(, "=") := strings.TrimSpace(strings.ToLower([0])) := strings.TrimSpace([1]) .addMapping(, ) } return nil } // Add a domain to realm mapping. func ( *DomainRealm) (, string) { (*)[] = } // Delete a domain to realm mapping. func ( *DomainRealm) (, string) { delete(*, ) } // ResolveRealm resolves the kerberos realm for the specified domain name from the domain to realm mapping. // The most specific mapping is returned. func ( *Config) ( string) string { = strings.TrimSuffix(, ".") // Try to match the entire hostname first if , := .DomainRealm[]; { return } // Try to match all DNS domain parts := strings.Count(, ".") + 1 for := 2; <= ; ++ { := strings.SplitN(, ".", ) if , := .DomainRealm["."+[len()-1]]; { return } } return "" } // Load the KRB5 configuration from the specified file path. func ( string) (*Config, error) { , := os.Open() if != nil { return nil, errors.New("configuration file could not be opened: " + + " " + .Error()) } defer .Close() := bufio.NewScanner() return NewFromScanner() } // NewFromString creates a new Config struct from a string. func ( string) (*Config, error) { := strings.NewReader() return NewFromReader() } // NewFromReader creates a new Config struct from an io.Reader. func ( io.Reader) (*Config, error) { := bufio.NewScanner() return NewFromScanner() } // NewFromScanner creates a new Config struct from a bufio.Scanner. func ( *bufio.Scanner) (*Config, error) { := New() var error := make(map[int]string) var []int var []string for .Scan() { // Skip comments and blank lines if , := regexp.MatchString(`^\s*(#|;|\n)`, .Text()); { continue } if , := regexp.MatchString(`^\s*\[libdefaults\]\s*`, .Text()); { [len()] = "libdefaults" = append(, len()) continue } if , := regexp.MatchString(`^\s*\[realms\]\s*`, .Text()); { [len()] = "realms" = append(, len()) continue } if , := regexp.MatchString(`^\s*\[domain_realm\]\s*`, .Text()); { [len()] = "domain_realm" = append(, len()) continue } if , := regexp.MatchString(`^\s*\[.*\]\s*`, .Text()); { [len()] = "unknown_section" = append(, len()) continue } = append(, .Text()) } for , := range { var int if +1 >= len() { = len() } else { = [+1] } switch := []; { case "libdefaults": := .LibDefaults.parseLines([:]) if != nil { if , := .(UnsupportedDirective); ! { return nil, fmt.Errorf("error processing libdefaults section: %v", ) } = } case "realms": , := parseRealms([:]) if != nil { if , := .(UnsupportedDirective); ! { return nil, fmt.Errorf("error processing realms section: %v", ) } = } .Realms = case "domain_realm": := .DomainRealm.parseLines([:]) if != nil { if , := .(UnsupportedDirective); ! { return nil, fmt.Errorf("error processing domaain_realm section: %v", ) } = } } } return , } // Parse a space delimited list of ETypes into a list of EType numbers optionally filtering out weak ETypes. func parseETypes( []string, bool) []int32 { var []int32 for , := range { if ! { var bool for , := range strings.Fields(WeakETypeList) { if == { = true break } } if { continue } } := etypeID.EtypeSupported() if != 0 { = append(, ) } } return } // Parse a time duration string in the configuration to a golang time.Duration. func parseDuration( string) (time.Duration, error) { = strings.Replace(strings.TrimSpace(), " ", "", -1) // handle Nd[NmNs] if strings.Contains(, "d") { := strings.SplitN(, "d", 2) , := strconv.ParseUint([0], 10, 32) if != nil { return time.Duration(0), errors.New("invalid time duration") } := time.Duration(*24) * time.Hour if [1] != "" { , := time.ParseDuration([1]) if != nil { return time.Duration(0), errors.New("invalid time duration") } = + } return , nil } // handle Nm[Ns] , := time.ParseDuration() if == nil { return , nil } // handle N , := strconv.ParseUint(, 10, 32) if == nil && > 0 { return time.Duration() * time.Second, nil } // handle h:m[:s] if strings.Contains(, ":") { := strings.Split(, ":") if 2 > len() || len() > 3 { return time.Duration(0), errors.New("invalid time duration value") } var []int for , := range { , := strconv.ParseInt(, 10, 16) if != nil { return time.Duration(0), errors.New("invalid time duration value") } = append(, int()) } := time.Duration([0])*time.Hour + time.Duration([1])*time.Minute if len() == 3 { = + time.Duration([2])*time.Second } return , nil } return time.Duration(0), errors.New("invalid time duration value") } // Parse possible boolean values to golang bool. func parseBoolean( string) (bool, error) { = strings.TrimSpace() , := strconv.ParseBool() if == nil { return , nil } switch strings.ToLower() { case "yes": return true, nil case "y": return true, nil case "no": return false, nil case "n": return false, nil } return false, errors.New("invalid boolean value") } // Parse array of strings but stop if an asterisk is placed at the end of a line. func appendUntilFinal( *[]string, string, *bool) { if * { return } if := len() - 1; >= 0 && [] == '*' { * = true = [:len()-1] } * = append(*, ) } // JSON return details of the config in a JSON format. func ( *Config) () (string, error) { , := json.MarshalIndent(, "", " ") if != nil { return "", } return string(), nil }