diff --git a/doc/standardisation/rfc6717.txt b/doc/standardisation/rfc6717.txt new file mode 100644 index 000000000..8bd72ffc3 --- /dev/null +++ b/doc/standardisation/rfc6717.txt @@ -0,0 +1,731 @@ + + + + + + +Independent Submission H. Hotz +Request for Comments: 6717 Jet Propulsion Lab, Caltech +Category: Informational R. Allbery +ISSN: 2070-1721 Stanford University + August 2012 + + + kx509 Kerberized Certificate Issuance Protocol in Use in 2012 + +Abstract + + This document describes a protocol, called kx509, for using Kerberos + tickets to acquire X.509 certificates. These certificates may be + used for many of the same purposes as X.509 certificates acquired by + other means, but if a Kerberos infrastructure already exists, then + the overhead of using kx509 may be much less. + + While not standardized, this protocol is already in use at several + large organizations, and certificates issued with this protocol are + recognized by the International Grid Trust Federation. + +Status of This Memo + + This document is not an Internet Standards Track specification; it is + published for informational purposes. + + This is a contribution to the RFC Series, independently of any other + RFC stream. The RFC Editor has chosen to publish this document at + its discretion and makes no statement about its value for + implementation or deployment. Documents approved for publication by + the RFC Editor are not a candidate for any level of Internet + Standard; see Section 2 of RFC 5741. + + Information about the current status of this document, any errata, + and how to provide feedback on it may be obtained at + http://www.rfc-editor.org/info/rfc6717. + +Copyright Notice + + Copyright (c) 2012 IETF Trust and the persons identified as the + document authors. All rights reserved. + + This document is subject to BCP 78 and the IETF Trust's Legal + Provisions Relating to IETF Documents + (http://trustee.ietf.org/license-info) in effect on the date of + publication of this document. Please review these documents + carefully, as they describe your rights and restrictions with respect + to this document. + + + +Hotz & Allbery Informational [Page 1] + +RFC 6717 kx509 August 2012 + + +Table of Contents + + 1. Introduction ....................................................2 + 1.1. Requirements Language ......................................3 + 2. Protocol Data ...................................................3 + 2.1. Request Packet .............................................3 + 2.2. Reply Packet ...............................................4 + 3. Protocol Operation ..............................................7 + 4. Acknowledgements ................................................8 + 5. IANA Considerations .............................................8 + 6. Security Considerations .........................................9 + 7. References .....................................................10 + 7.1. Normative References ......................................10 + 7.2. Informative References ....................................10 + Appendix A. Certificate Caching and Deployment Considerations ....12 + Appendix B. Historic Extensions ..................................12 + Appendix C. Example Exchange .....................................12 + +1. Introduction + + The two primary ways of providing cryptographically secure + identification on the Internet are Kerberos tickets [RFC4120] and + X.509 [RFC5280] [X.509] certificates. + + In practical IT infrastructure where both are in use, it's highly + desirable to deploy their support in a way that guarantees they both + authoritatively refer to the same entities. There is already a + widely adopted standard for using X.509 certificates to acquire + corresponding Kerberos tickets called Public Key Cryptography for + Initial Authentication in Kerberos (PKINIT) [RFC4556]. This document + describes the kx509 protocol for supporting the symmetric operation + of acquiring X.509 certificates using Kerberos tickets. + + Preparing and reviewing this document exposed a number of issues that + are discussed in the security considerations. Unfortunately, some of + them can only be addressed with an incompatible upgrade to this + protocol. The IETF's Kerberos working group has an expected work + item to address these issues. + + The International Grid Trust Federation [IGTF] supports the use of + Short Lived Credential Services [SLCS] as a means to authenticate for + resource usage based on other, native identity stores that an + organization maintains. X.509 certificates issued using the kx509 + protocol based on a Kerberos identity is one of the recognized + credential services. The certificate profile for that use is outside + the scope of this RFC but is described in [GRID-prof]. + + + + + +Hotz & Allbery Informational [Page 2] + +RFC 6717 kx509 August 2012 + + + In normal operation, kx509 can be used after a Kerberos ticket- + granting-ticket (TGT) is acquired, which is most likely during user + login. First, the client generates an RSA public/private key pair. + Next, using the Kerberos ticket-granting-ticket, it acquires a + Kerberos service ticket for the KCA (Kerberized Certificate + Authority) and uses this to send the public half of its key pair. + The KCA will decrypt the service ticket, verify the integrity of the + incoming packet, determine the identity of the user, and use the + session key to send back a corresponding X.509 certificate. + +1.1. Requirements Language + + The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + document are to be interpreted as described in RFC 2119 [RFC2119]. + +2. Protocol Data + + The protocol consists of a single request/reply exchange using UDP. + + Both the request and the reply packet begin with four bytes of + version ID information, followed by a DER-encoded ASN.1 message. The + first two bytes of the version ID are reserved. They MUST be set to + zero when sent and SHOULD be ignored when received. The third and + fourth bytes are the major and minor version numbers, respectively. + The version of the protocol described in this document is designated + 2.0, so the first four bytes of the packet are 0, 0, 2, and 0. + + Incompatible variations of this protocol MUST use a different major + version number. + +2.1. Request Packet + + The request consists of a version ID, followed by a DER-encoded ASN.1 + message containing a Kerberos AP-REQ, integrity check data on the + request, and public key generated by the client. The ASN.1 encoding + is: + + KX509Request ::= SEQUENCE { + AP-REQ OCTET STRING, + pk-hash OCTET STRING, + pk-key OCTET STRING + } + + The AP-REQ is as described in [RFC4120], Section 5.5.1. + + + + + + +Hotz & Allbery Informational [Page 3] + +RFC 6717 kx509 August 2012 + + + The pk-hash is Hashed Message Authentication Code (HMAC) using SHA-1 + as the underlying hash. All 160 bits are sent. The key used is the + Kerberos session key. The data to be hashed is the concatenation of + the following: + + o 4-byte version ID at the beginning of the packet. + + o OCTET STRING of the AP-REQ. + + o OCTET STRING of the pk-key. + + The pk-key contains a public key. This key and its corresponding + private key are generated by the client before contacting the server. + Implementations of this protocol MUST support RSA keys, in which case + the key is a DER-encoded RSAPublicKey as defined in [RFC3447], + Section A.1.1, and then it is stored in this octet string in the + request. Its encoding as an OCTET STRING starts with the 0x30 byte + sequence at the beginning of a DER-encoded RSAPublicKey. Use of + other public-key types is not defined. + + Appendix C shows an example request packet. + +2.2. Reply Packet + + The reply consists of a version ID, followed by a DER-encoded ASN.1 + message containing an error code, an optional authentication hash, + optional certificate, and optional error text. The service SHOULD + return replies of the same version as the request where possible. + + KX509Response ::= SEQUENCE { + error-code[0] INTEGER DEFAULT 0, + hash[1] OCTET STRING OPTIONAL, + certificate[2] OCTET STRING OPTIONAL, + e-text[3] VisibleString OPTIONAL + } + + Although the format of the reply contains independently optional + objects, the server MUST only generate replies with one of the + following allowed combinations. + + +------------+------+-------------+--------+ + | | hash | certificate | | + | error-code | hash | | e-text | + | error-code | | | e-text | + +------------+------+-------------+--------+ + + + + + + +Hotz & Allbery Informational [Page 4] + +RFC 6717 kx509 August 2012 + + + The first case is returned when the server successfully generates a + certificate for the user. The certificate is a DER-encoded + certificate as defined in [RFC5280], Appendix A, page 116. Its + encoding as an OCTET STRING starts with the 0x30 byte sequence that + is at the beginning of a DER-encoded certificate. + + The second case is returned when the server successfully + authenticates the user and their key, but is unable for some other + reason to generate a certificate. + + The third case MAY be returned if the server is unable to + successfully authenticate the user and intends to return some + unauthenticated information to the client. + + The hash on a response is computed using SHA-1 HMAC as for the + request. + + The data that is hashed is the concatenation of the following things: + + o 4-byte version ID at the beginning of the packet. + + o DER representation of the error-code exclusive of the tag and + length, if it is present. + + o OCTET STRING of the certificate, if it is present. + + o VisibleString representation of the e-text exclusive of the tag + and length, if it is present. + + In other words, the hash is computed on the data in the fields that + are present, exclusive of the overall ASN.1 wrapping. + + The e-text MAY be translated into other character sets for display + purposes, but the hash is computed on the e-text in its VisibleString + representation. If the e-text contains NUL characters, the client + MAY ignore any part of the error message after the first NUL + character for display purposes. + + As implied by the above table, if the reply does not contain a + certificate, it MUST contain an error message and a non-zero error + code. Conversely, if a certificate is returned, then the error-code + MUST be zero. The server SHOULD use the DEFAULT encoding for a zero + error-code value by omitting any explicit error-code from the reply. + + + + + + + + +Hotz & Allbery Informational [Page 5] + +RFC 6717 kx509 August 2012 + + + The defined values for error-code are as follows: + + +------------+-----------------------------+------------------------+ + | error-code | Condition | Example | + +------------+-----------------------------+------------------------+ + | 1 | Permanent problem with | Incompatible version | + | | client request | | + | 2 | Solvable problem with | Expired Kerberos | + | | client request | credentials | + | 3 | Temporary problem with | Packet loss | + | | client request | | + | 4 | Permanent problem with the | Internal | + | | server | misconfiguration | + | 5 | Temporary problem with the | Server overloaded | + | | server | | + +------------+-----------------------------+------------------------+ + + If a client error is returned, the client SHOULD NOT retry the + request unless some remedial action is first taken, although if + error-code 3 is returned, the client MAY retry with other servers + before giving up. + + If a server error is returned, it is RECOMMENDED that the client + retry the request with a different server if one is known. + + Since all KCAs serving a Kerberos realm are intended to be + equivalent, in accordance with Section 4.1.2.2 of [RFC5280], the + certificates returned from different KCAs serving the same Kerberos + realm MUST NOT contain duplicate serial numbers. + + This protocol and document do not address certificate verification or + path construction. There are no provisions for returning any + additional certificates that might be needed. Any application using + a returned certificate must be configured independently to address + these issues. An incompatible upgrade to this protocol will provide + options to address this issue. + + The returned certificate MUST identify the Kerberos client principal + from the AP-REQ in the original KX509Request in the subject of the + certificate or in a subjectAltName extension. The identification + MUST be unique within the organization's deployed infrastructure. It + is RECOMMENDED that a subjectAltName extension be included of type + id-pkinit-san as described in [RFC4556], Section 3.2.2. Note that + the id-pkinit-san is simply a standard representation of a Kerberos + principal and has no other implications with respect to PKINIT. + + + + + + +Hotz & Allbery Informational [Page 6] + +RFC 6717 kx509 August 2012 + + + Other extensions MAY be added according to local policy. + + Appendix C shows an example reply packet. + +3. Protocol Operation + + Absent errors, the protocol consists of a single request, sent via + UDP, and a single reply, also sent via UDP. + + There is no special provision for requests or replies that exceed the + allowable size of a UDP packet. Also, some implementations have + imposed hard size limits that are smaller than a typical UDP MTU and + will limit the use of extensions and the supportable key size. Even + without hard limits, if the request or reply exceeds the MTU size of + a UDP packet for the infrastructure in use, then the reliability of + the exchange will decrease significantly. + + For "normal" Kerberos AP-REQ structures, and "normal" X.509 + certificates, this is unlikely unless the Kerberos service ticket + contains large amounts of authorization data. For this reason, it is + RECOMMENDED that service tickets for the KCA be issued without + authorization data. If the KCA performs authorization, it should do + so by other means. + + Before constructing the request, the client must know the canonical + name(s) and port(s) of the server(s) to contact. It MAY determine + them by looking up the service's SRV record as described in + [RFC2782]. The entry to be used is _kca._udp._realm_, where _realm_ + is the Kerberos realm, used as part of the DNS name. + + The client has to acquire a service ticket in order to construct the + AP-REQ for the service. Conventionally, the Kerberos service + principal name to use for this service has a first component of + "kca_service". Absent local configuration or other external + knowledge of the correct principal to use, the second and final + component is conventionally the canonical name of the KCA server + being contacted, and the realm of the principal is determined + following normal Kerberos domain-to-realm mapping conventions, as + discussed in [RFC4120], Section 1.3. + + When the server receives a request, it MUST verify the following + properties of the request before issuing a certificate: + + o The AP-REQ can be decoded and is not expired. + + o If the request uses cross-realm authentication, then it satisfies + the requirements of local policy and [RFC4120], Sections 1.2 and + 2.7. + + + +Hotz & Allbery Informational [Page 7] + +RFC 6717 kx509 August 2012 + + + o The request's hash is valid. + + The server SHOULD make other sanity checks, such as a minimum public + key length, to the extent feasible. + + The server MAY decline to respond to an erroneous request. If it + does not receive a response, a client MAY retry its request, but the + client SHOULD wait at least one second before doing so. + + The client MUST verify any hash in the reply and MUST NOT use any + certificate in a reply whose hash does not verify. The client MAY + display the e-text if the hash is absent or does not verify but + SHOULD indicate the message is not authenticated. + +4. Acknowledgements + + The original version of kx509 was implemented using Kerberos 4 at the + University of Michigan and was nicely documented in [KX509]. Many + thanks to them for their original work, as well as the subsequent + updates. + + While developing this document, important corrections and comments + were provided by Jeffrey Altman and Love Hornquist Astrand. The + following people also provided many helpful comments and corrections: + Doug Engert, Jeffrey Hutzelman, Sam Hartman, Timothy J. Miller, + Chaskiel Grundman, and Jim Schaad. Alan Sill provided the references + to the International Grid Trust Federation and its acceptable + credential services. Example network traffic was provided by Doug + Engert, Marcus Watts, Matt Crawford, and Chaskiel Grundman from their + deployments and was extremely useful for verifying the reality of + this specification. + +5. IANA Considerations + + This service is conventionally run on UDP port 9878. IANA has + registered that port in the Service Name and Transport Port Number + Registry as follows: + + Service Name: kca-service + Transport Protocol: UDP + Assignee: IESG + Contact: IETF Chair + Description: The kx509 Kerberized Certificate Issuance + Protocol in Use in 2012 + Reference: RFC 6717 + Port Number: 9878 + Assignment Notes: Historically, this service has been referred to + as "kca_service", but this service name does + + + +Hotz & Allbery Informational [Page 8] + +RFC 6717 kx509 August 2012 + + + not meet the registry requirements. + + The Generic Security Service Application Program Interface (GSS-API) + / Kerberos / Simple Authentication and Security Layer (SASL) service + name currently in use for this protocol is "kca_service". This does + not meet the naming requirements for IANA's GSS-API/Kerberos/SASL + service name registry, so no registration has been requested. The + conflict between the conventional service name and the registry rules + is expected to be addressed in a future version of this protocol. + Appropriate registrations will be requested at that time. + +6. Security Considerations + + The only encrypted information in the protocol is that used by + Kerberos itself. The considerations for any Kerberized service apply + here. + + The public key in the request is sent in the clear and without any + guarantees that the requester actually possesses the corresponding + private key. Therefore, the only appropriate uses of the returned + certificate are those where the identity of the requester is + unimportant or the subsequent use independently guarantees that the + user possesses the private key. This issue is expected to be + addressed in a future version of this protocol. + + For example, if the kx509-issued certificate is used for a digital + signature in a way that does not independently demonstrate proof-of- + possession of the private key, then an eavesdropper could request + their own valid certificate via kx509 and claim to have originated + material signed by the legitimate, original requester. [RFC4211], + Appendix C, contains a more detailed discussion of why proof-of- + possession is important. + + An example that should be safe is initial client authentication with + Transport Layer Security (TLS) [RFC5246] connections. If a client + certificate is used, then a Certificate Verify message (Section 7.4.8 + of [RFC5246]) is added to the handshake exchange. It includes a + signature of the handshake messages to that point. Those messages + depend on data known only to the client and server ends of the + specific connection, so computing the signature proves possession of + the private key. This application was the original intended use case + for kx509. + + Some information, such as the public key and certificate, is + transmitted in the clear but (as the name implies) is generally + intended to be publicly available. However, its visibility could + still raise privacy concerns. The hash is used to protect the + integrity of this information. + + + +Hotz & Allbery Informational [Page 9] + +RFC 6717 kx509 August 2012 + + + The policies for issuing Kerberos tickets and X.509 certificates are + usually expressed very differently. An implementation of this + protocol should not provide a mechanism for bypassing ticket or + certificate policies. + + In particular, if the issued certificate can be used with PKINIT, + this authentication loop SHOULD NOT bypass policy limits for either + X.509 certificates or Kerberos tickets. + + X.509 certificates are usually issued with considerably longer + validity times than Kerberos tickets. Care should be taken that the + issued certificate is not valid for longer than the intended policy + should allow. Note that Section 3.2.3 of [RFC4556] REQUIRES that the + lifetime of an issued ticket not exceed the lifetime of the + predecessor certificate. By analogy, it is RECOMMENDED that the + lifetime of an issued certificate not exceed the lifetime of the + predecessor Kerberos ticket unless the implications with respect to + local policy and supporting infrastructure are clearly understood and + allow it. + +7. References + +7.1. Normative References + + [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate + Requirement Levels", BCP 14, RFC 2119, March 1997. + + [RFC3447] Jonsson, J. and B. Kaliski, "Public-Key Cryptography + Standards (PKCS) #1: RSA Cryptography Specifications + Version 2.1", RFC 3447, February 2003. + + [RFC4120] Neuman, C., Yu, T., Hartman, S., and K. Raeburn, "The + Kerberos Network Authentication Service (V5)", RFC 4120, + July 2005. + + [RFC5280] Cooper, D., Santesson, S., Farrell, S., Boeyen, S., + Housley, R., and W. Polk, "Internet X.509 Public Key + Infrastructure Certificate and Certificate Revocation + List (CRL) Profile", RFC 5280, May 2008. + +7.2. Informative References + + [GRID-prof] "Grid Certificate Profile", March 2008, + . + + [IGTF] "The International Grid Trust Federation", + . + + + + +Hotz & Allbery Informational [Page 10] + +RFC 6717 kx509 August 2012 + + + [KX509] Doster, W., Watts, M., and D. Hyde, "The KX.509 + Protocol", September 2001, . + + [RFC2782] Gulbrandsen, A., Vixie, P., and L. Esibov, "A DNS RR for + specifying the location of services (DNS SRV)", + RFC 2782, February 2000. + + [RFC4211] Schaad, J., "Internet X.509 Public Key Infrastructure + Certificate Request Message Format (CRMF)", RFC 4211, + September 2005. + + [RFC4556] Zhu, L. and B. Tung, "Public Key Cryptography for + Initial Authentication in Kerberos (PKINIT)", RFC 4556, + June 2006. + + [RFC5246] Dierks, T. and E. Rescorla, "The Transport Layer + Security (TLS) Protocol Version 1.2", RFC 5246, + August 2008. + + [SLCS] "Short Lived Credential Services", February 2009, + . + + [X.509] International Telecommunications Union, "Recommendation + X.509: The Directory: Public-key and attribute + certificate framework", November 2008. + + + + + + + + + + + + + + + + + + + + + + + + + +Hotz & Allbery Informational [Page 11] + +RFC 6717 kx509 August 2012 + + +Appendix A. Certificate Caching and Deployment Considerations + + As noted in the Security Considerations section, the functional + lifetime of the acquired X.509 certificate should usually match the + lifetime of its predecessor Kerberos ticket. Therefore, it is likely + that X.509 certificates issued with this protocol should be deleted + when the supporting Kerberos tickets are deleted. That makes the + Kerberos ticket cache a reasonable location to store the certificate + (and its private key). + + On the other hand, applications, such as web browsers, probably + expect certificates in different stores. + + A widely used solution to this dichotomy is to implement a PKCS11 + library that supports the kx509-acquired credentials. The + credentials remain stored in the Kerberos credentials cache, but full + PKI functionality is still available via a standard interface for PKI + credentials. + +Appendix B. Historic Extensions + + This appendix documents extensions to the kx509 protocol that are + either no longer in use or are expected to be dropped. + + A subjectAltName othername extension of type kcaAuthRealm (OID value + 1.3.6.1.4.1.250.42.1) is frequently used to include the client's + realm as an ASN.1 octet string. + + The Microsoft-defined userPrincipalName has frequently been used for + the same purpose as the id-pkinit-san. + + The historic implementations of this protocol included provisions for + DSA keys in place of RSA. DSA does not appear to be in use. A + future version of this protocol will use a standard certificate + request structure that will provide algorithm agility. + + The historic implementations of this protocol allowed an optional + client-version field (at the end of the request) of type + VisibleString. If present, the KCA copied it into the issued + certificate as an extension with the OID 1.3.6.1.4.1.250.42.2. This + feature does not appear to be in use. + +Appendix C. Example Exchange + + The request and reply are from the same exchange. The Ethernet, IP, + and UDP headers, and the 4-byte version string at the beginning of + the request and reply packets are all omitted. Only the ASN.1- + encoded portions are shown. + + + +Hotz & Allbery Informational [Page 12] + +RFC 6717 kx509 August 2012 + + + 0:d=0 hl=4 l= 678 cons: SEQUENCE + 4:d=1 hl=4 l= 509 prim: OCTET STRING + [HEX DUMP]:6E8201F9308201F5A003... (AP-REQ) + 517:d=1 hl=2 l= 20 prim: OCTET STRING + [HEX DUMP]:ECFF1C922300D0E9DD02... (pk-hash) + 539:d=1 hl=3 l= 140 prim: OCTET STRING + [HEX DUMP]:30818902818100B70F46... (pk-key) + + Request Packet ASN.1 Decode + + 0:d=0 hl=4 l= 870 cons: SEQUENCE + 4:d=1 hl=2 l= 22 cons: cont [ 1 ] + 6:d=2 hl=2 l= 20 prim: OCTET STRING + [HEX DUMP]:F3A844834C26D843B6FD... (hash) + 28:d=1 hl=4 l= 842 cons: cont [ 2 ] + 32:d=2 hl=4 l= 838 prim: OCTET STRING + [HEX DUMP]:308203423082022AA003... (certificate) + + Reply Packet ASN.1 Decode + +Authors' Addresses + + Henry B. Hotz + Jet Propulsion Laboratory, California Institute of Technology + 4800 Oak Grove Dr. + Pasadena, CA 91109 + USA + + Phone: +01 818 354-4880 + EMail: hotz@jpl.nasa.gov + + + Russ Allbery + Stanford University + P.O. Box 20066 + Stanford, CA 94309 + USA + + EMail: rra@stanford.edu + URI: http://www.eyrie.org/~eagle/ + + + + + + + + + + + +Hotz & Allbery Informational [Page 13] + diff --git a/kdc/default_config.c b/kdc/default_config.c index 064e1f505..3b621cec6 100644 --- a/kdc/default_config.c +++ b/kdc/default_config.c @@ -113,17 +113,13 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config) "kdc", "enable-kx509", NULL); if (c->enable_kx509) { + /* These are global defaults. There are also per-realm defaults. */ c->kx509_template = krb5_config_get_string(context, NULL, "kdc", "kx509_template", NULL); c->kx509_ca = krb5_config_get_string(context, NULL, "kdc", "kx509_ca", NULL); - if (c->kx509_ca == NULL || c->kx509_template == NULL) { - kdc_log(context, c, 0, - "missing kx509 configuration, turning off"); - c->enable_kx509 = FALSE; - } } #endif diff --git a/kdc/kx509.c b/kdc/kx509.c index 6f61a8ea3..9dd097c08 100644 --- a/kdc/kx509.c +++ b/kdc/kx509.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006 - 2007 Kungliga Tekniska Högskolan + * Copyright (c) 2006 - 2019 Kungliga Tekniska Högskolan * (Royal Institute of Technology, Stockholm, Sweden). * All rights reserved. * @@ -35,29 +35,84 @@ #include #include #include +#include + +#include + +/* + * This file implements the kx509 service. + * + * The protocol, its shortcomings, and its future are described in + * lib/krb5/hx509.c. + * + * The service handles requests, decides whether to issue a certificate, and + * does so by populating a "template" to generate a TBSCertificate and signing + * it with a configured CA issuer certificate and private key. + * + * A "template" is a Certificate that has ${variable} references in its + * subjectName, and may have EKUs. + * + * Some SANs may be included in issued certificates. See below. + * + * Besides future protocol improvements described in lib/krb5/hx509.c, here is + * a list of KDC functionality we'd like to add: + * + * - support templates as strings in configuration? + * - lookup an hx509 template for the client principal in its HDB entry + * - lookup subjectName, SANs for a principal in its HDB entry + * - lookup a host-based client principal's HDB entry and add its canonical + * name / aliases as dNSName SANs + * (this would have to be if requested by the client, perhaps; see + * commentary about the protocol in lib/krb5/kx509.c) + * - add code to build a template on the fly + * + * (just SANs, with empty subjectName? + * or + * CN=component0,CN=component1,..,CN=componentN,DC= + * and set KU and EKUs) + */ #ifdef KX509 -/* - * - */ +static const unsigned char version_2_0[4] = {0 , 0, 2, 0}; +/* + * Taste the request to see if it's a kx509 request. + */ krb5_error_code -_kdc_try_kx509_request(void *ptr, size_t len, struct Kx509Request *req, size_t *size) +_kdc_try_kx509_request(void *ptr, size_t len, struct Kx509Request *req) { - if (len < 4) + const unsigned char *p = (const void *)(uintptr_t)ptr; + size_t sz; + + if (len < sizeof(version_2_0)) return -1; - if (memcmp("\x00\x00\x02\x00", ptr, 4) != 0) + if (memcmp(version_2_0, p, sizeof(version_2_0)) != 0) return -1; - return decode_Kx509Request(((unsigned char *)ptr) + 4, len - 4, req, size); + p += sizeof(version_2_0); + len -= sizeof(version_2_0); + if (len == 0) + return -1; + return decode_Kx509Request(p, len, req, &sz); +} + +static krb5_boolean +get_bool_param(krb5_context context, krb5_boolean def, + const char *crealm, const char *name) +{ + krb5_boolean global_default; + + global_default = krb5_config_get_bool_default(context, NULL, def, "kdc", + name, NULL); + if (!crealm) + return global_default; + return krb5_config_get_bool_default(context, NULL, global_default, + "kdc", "realms", crealm, name, NULL); } /* - * + * Verify the HMAC in the request. */ - -static const unsigned char version_2_0[4] = {0 , 0, 2, 0}; - static krb5_error_code verify_req_hash(krb5_context context, const Kx509Request *req, @@ -80,18 +135,24 @@ verify_req_hash(krb5_context context, if (sizeof(digest) != HMAC_size(&ctx)) krb5_abortx(context, "runtime error, hmac buffer wrong size in kx509"); HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); - HMAC_Update(&ctx, req->pk_key.data, req->pk_key.length); + if (req->pk_key.length) + HMAC_Update(&ctx, req->pk_key.data, req->pk_key.length); + else + HMAC_Update(&ctx, req->authenticator.data, req->authenticator.length); HMAC_Final(&ctx, digest, 0); HMAC_CTX_cleanup(&ctx); - if (memcmp(req->pk_hash.data, digest, sizeof(digest)) != 0) { + if (ct_memcmp(req->pk_hash.data, digest, sizeof(digest)) != 0) { krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED, - "pk-hash is not correct"); + "kx509 request MAC mismatch"); return KRB5KDC_ERR_PREAUTH_FAILED; } return 0; } +/* + * Set the HMAC in the response. + */ static krb5_error_code calculate_reply_hash(krb5_context context, krb5_keyblock *key, @@ -112,13 +173,27 @@ calculate_reply_hash(krb5_context context, } HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); - if (rep->error_code) { - int32_t t = *rep->error_code; - do { - unsigned char p = (t & 0xff); - HMAC_Update(&ctx, &p, 1); - t >>= 8; - } while (t); + { + int32_t t = rep->error_code; + unsigned char encint[sizeof(t) + 1]; + size_t k; + + /* + * RFC6717 says this about how the error-code is included in the HMAC: + * + * o DER representation of the error-code exclusive of the tag and + * length, if it is present. + * + * So we use der_put_integer(), which encodes from the right. + * + * RFC6717 does not constrain the error-code's range. We assume it to + * be a 32-bit, signed integer, for which we'll need no more than 5 + * bytes. + */ + ret = der_put_integer(&encint[sizeof(encint) - 1], + sizeof(encint), &t, &k); + if (ret == 0) + HMAC_Update(&ctx, &encint[sizeof(encint)] - k, k); } if (rep->certificate) HMAC_Update(&ctx, rep->certificate->data, rep->certificate->length); @@ -131,10 +206,163 @@ calculate_reply_hash(krb5_context context, return 0; } +/* + * Finds a template in the configuration that is appropriate to the form of the + * client principal. Also sets some variables in `env' and adds some SANs to + * `tbs'` as appropriate (others are added in build_certificate()). + * + * TODO: + * - support templates as strings in configuration? + * - lookup a template for the client principal in its HDB entry + * - lookup subjectName, SANs for a principal in its HDB entry + * - lookup a host-based client principal's HDB entry and add its canonical + * name / aliases as dNSName SANs + * (this would have to be if requested by the client, perhaps) + * - add code to build a template on the fly + */ +static krb5_error_code +get_template(krb5_context context, + krb5_kdc_configuration *config, + krb5_principal principal, + const char *princ_no_realm, + const char *princ, + const char **template, + hx509_env *env, + hx509_ca_tbs tbs) +{ + unsigned int ncomp = krb5_principal_get_num_comp(context, principal); + const char *crealm = krb5_principal_get_realm(context, principal); + const char *kx509_template = NULL; + const char *comp0, *comp1, *comp2; + char *domain = NULL; + char *email = NULL; + krb5_error_code ret = KRB5KDC_ERR_POLICY; + + if (ncomp == 1) { + /* 1-component, user principal */ + + /* Find the template */ + kx509_template = krb5_config_get_string(context, NULL, "kdc", "realms", + crealm, "kx509_template", + NULL); + if (kx509_template == NULL) + kx509_template = config->kx509_template; + if (kx509_template == NULL) + goto out; + + /* + * Add [some of] the "env" variables we support for 1-component + * pincipals. Others are added in build_certificate(). + */ + ret = hx509_env_add(context->hx509ctx, env, "principal-component0", + princ_no_realm); + + /* + * If configured, include an rfc822Name that's just the client's + * principal name @ down-cased realm. + * + * XXX Dicey feature! Maybe this should be a string param whose value + * is the domainname to use for the email address. + */ + if (ret == 0 && + get_bool_param(context, FALSE, crealm, "kx509_include_email_san")) { + char *p; + + if ((domain = strdup(crealm)) == NULL) { + ret = ENOMEM; + goto out; + } + for (p = domain; *p; p++) + *p = isupper(*p) ? tolower(*p) : *p; + if (asprintf(&email, "%s@%s", princ_no_realm, domain) == -1 || + email == NULL) { + ret = ENOMEM; + goto out; + } + ret = hx509_ca_tbs_add_san_rfc822name(context->hx509ctx, tbs, email); + } + } else if (ncomp == 2 || ncomp == 3) { + const char *config_label; + + /* + * 2- and 3-component principal name. + * + * We do not have a reliable name-type indicator. If the second + * component has a '.' in it then we'll assume that the name is a + * host-based (2-component) or domain-based (3-component) service + * principal name. Else we'll assume it's a two-component admin-style + * username. + */ + + comp0 = krb5_principal_get_comp_string(context, principal, 0); + comp1 = krb5_principal_get_comp_string(context, principal, 1); + comp2 = ncomp == 3 ? + krb5_principal_get_comp_string(context, principal, 2) : NULL; + + ret = hx509_env_add(context->hx509ctx, env, "principal-component0", + comp0); + if (ret == 0) + ret = hx509_env_add(context->hx509ctx, env, "principal-component1", + comp1); + if (ret == 0 && ncomp == 3) + ret = hx509_env_add(context->hx509ctx, env, "principal-component2", + comp2); + + if (ret == 0 && strchr(comp1, '.')) { + /* Looks like host-based or domain-based service */ + config_label = ncomp == 2 ? "hostbased" : "domainbased"; + + ret = hx509_env_add(context->hx509ctx, env, + "principal-service-name", comp0); + if (ret == 0) + ret = hx509_env_add(context->hx509ctx, env, "principal-host-name", comp1); + if (ret == 0 && ncomp == 3) + ret = hx509_env_add(context->hx509ctx, env, "principal-domain-name", comp2); + + if (ret == 0 && + get_bool_param(context, FALSE, crealm, + "kx509_include_dnsname_san")) { + ret = hx509_ca_tbs_add_san_hostname(context->hx509ctx, tbs, comp1); + } + } else if (ret == 0 && ncomp == 2) { + /* Looks like a kadmin/username or similar name */ + config_label = "two_component_user"; + } else { + if (ret == 0) + ret = KRB5KDC_ERR_POLICY; + goto out; + } + + /* Find the template */ + kx509_template = krb5_config_get_string(context, NULL, "kdc", "realms", + crealm, "kx509_templates", + config_label, comp0, NULL); + if (kx509_template == NULL) + kx509_template = krb5_config_get_string(context, NULL, "kdc", + "kx509_templates", + config_label, comp0, NULL); + if (kx509_template == NULL) { + kdc_log(context, config, 0, "kx509 template not found for %s", + princ); + ret = KRB5KDC_ERR_POLICY; + goto out; + } + } else { + kdc_log(context, config, 0, "kx509 client %s has too many components!", + princ); + ret = KRB5KDC_ERR_POLICY; + } + +out: + *template = kx509_template; + free(domain); + free(email); + return ret; +} + /* * Build a certifate for `principal´ that will expire at `endtime´. */ - static krb5_error_code build_certificate(krb5_context context, krb5_kdc_configuration *config, @@ -143,14 +371,32 @@ build_certificate(krb5_context context, krb5_principal principal, krb5_data *certificate) { - char *name = NULL; + const char *crealm = krb5_principal_get_realm(context, principal); const char *kx509_ca; + const char *kx509_template; + char *princ = NULL; + char *name = NULL; hx509_ca_tbs tbs = NULL; hx509_env env = NULL; hx509_cert cert = NULL; hx509_cert signer = NULL; - krb5_boolean def_bool; - int ret; + krb5_error_code ret; + + ret = hx509_ca_tbs_init(context->hx509ctx, &tbs); + if (ret) + goto out; + + /* Pick an issuer based on the crealm if we can */ + kx509_ca = krb5_config_get_string(context, NULL, "kdc", "realms", crealm, + "kx509_ca", NULL); + if (kx509_ca == NULL) + kx509_ca = config->kx509_ca; + if (kx509_ca == NULL) { + ret = KRB5KDC_ERR_POLICY; + kdc_log(context, config, 0, "No kx509 CA credential specified for " + "realm %s", crealm); + goto out; + } ret = krb5_unparse_name_flags(context, principal, KRB5_PRINCIPAL_UNPARSE_NO_REALM, @@ -158,48 +404,45 @@ build_certificate(krb5_context context, if (ret) goto out; - ret = hx509_env_add(context->hx509ctx, &env, "principal-name-without-realm", - name); - krb5_xfree(name); - name = NULL; + ret = krb5_unparse_name(context, principal, &princ); if (ret) goto out; + /* Get a template and set things in `env' and `tbs' as appropriate */ + ret = get_template(context, config, principal, name, princ, + &kx509_template, &env, tbs); + if (ret) + goto out; + if (kx509_template == NULL) { + kdc_log(context, config, 0, "No kx509 certificate template specified"); + ret = KRB5KDC_ERR_POLICY; + goto out; + } + + kdc_log(context, config, 0, "Issuing kx509 certificate to %s using " + "template %s", princ, kx509_template); + /* - * Include the realm in the principal-name env var; the template - * might not use $principal-name-realm after all. + * Populate additional template "env" variables */ - ret = krb5_unparse_name(context, principal, &name); + ret = hx509_env_add(context->hx509ctx, &env, + "principal-name-without-realm", name); + if (ret == 0) + ret = hx509_env_add(context->hx509ctx, &env, "principal-name", princ); + if (ret == 0) + ret = hx509_env_add(context->hx509ctx, &env, "principal-name-realm", + crealm); if (ret) goto out; - ret = hx509_env_add(context->hx509ctx, &env, "principal-name", - name); - if (ret) - goto out; - - ret = hx509_env_add(context->hx509ctx, &env, "principal-name-realm", - krb5_principal_get_realm(context, principal)); - if (ret) - goto out; - - /* Pick an issuer based on the crealm if we can */ - kx509_ca = krb5_config_get_string(context, NULL, "kdc", - krb5_principal_get_realm(context, - principal), - "kx509_ca", NULL); - if (kx509_ca == NULL) - kx509_ca = config->kx509_ca; - + /* Load the issuer certificate and private key */ { hx509_certs certs; hx509_query *q; - ret = hx509_certs_init(context->hx509ctx, config->kx509_ca, 0, - NULL, &certs); + ret = hx509_certs_init(context->hx509ctx, kx509_ca, 0, NULL, &certs); if (ret) { - kdc_log(context, config, 0, "Failed to load CA %s", - config->kx509_ca); + kdc_log(context, config, 0, "Failed to load CA %s", kx509_ca); goto out; } ret = hx509_query_alloc(context->hx509ctx, &q); @@ -215,16 +458,12 @@ build_certificate(krb5_context context, hx509_query_free(context->hx509ctx, q); hx509_certs_free(&certs); if (ret) { - kdc_log(context, config, 0, "Failed to find a CA in %s", - config->kx509_ca); + kdc_log(context, config, 0, "Failed to find a CA in %s", kx509_ca); goto out; } } - ret = hx509_ca_tbs_init(context->hx509ctx, &tbs); - if (ret) - goto out; - + /* Populate the subject public key in the TBS context */ { SubjectPublicKeyInfo spki; heim_any any; @@ -247,24 +486,26 @@ build_certificate(krb5_context context, goto out; } + /* Load the template into the TBS context */ { hx509_certs certs; hx509_cert template; - ret = hx509_certs_init(context->hx509ctx, config->kx509_template, 0, + ret = hx509_certs_init(context->hx509ctx, kx509_template, 0, NULL, &certs); - if (ret) { - kdc_log(context, config, 0, "Failed to load template %s", - config->kx509_template); - goto out; - } - ret = hx509_get_one_cert(context->hx509ctx, certs, &template); + if (ret == 0) + ret = hx509_get_one_cert(context->hx509ctx, certs, &template); hx509_certs_free(&certs); if (ret) { - kdc_log(context, config, 0, "Failed to find template in %s", - config->kx509_template); + kdc_log(context, config, 0, "Failed to load template from %s", + kx509_template); goto out; } + + /* + * Only take the subjectName, the keyUsage, and EKUs from the template + * certificate. + */ ret = hx509_ca_tbs_set_template(context->hx509ctx, tbs, HX509_CA_TEMPLATE_SUBJECT| HX509_CA_TEMPLATE_KU| @@ -275,64 +516,120 @@ build_certificate(krb5_context context, goto out; } - def_bool = krb5_config_get_bool_default(context, NULL, TRUE, "kdc", - "kx509_include_pkinit_san", - NULL); - if (krb5_config_get_bool_default(context, NULL, def_bool, "kdc", - krb5_principal_get_realm(context, - principal), - "kx509_include_pkinit_san", - NULL)) { - ret = hx509_ca_tbs_add_san_pkinit(context->hx509ctx, tbs, name); + /* + * Add other SANs. + * + * Adding an id-pkinit-san means the client can use the certificate to + * initiate PKINIT. That might seem odd, but it enables a sort of PKIX + * credential delegation by allowing forwarded Kerberos tickets to be + * used to acquire PKIX credentials. Thus this can work: + * + * PKIX (w/ HW token) -> Kerberos -> + * PKIX (w/ softtoken) -> Kerberos -> + * PKIX (w/ softtoken) -> Kerberos -> + * ... + */ + if (ret == 0 && + get_bool_param(context, TRUE, crealm, "kx509_include_pkinit_san")) { + ret = hx509_ca_tbs_add_san_pkinit(context->hx509ctx, tbs, princ); if (ret) goto out; } hx509_ca_tbs_set_notAfter(context->hx509ctx, tbs, endtime); + /* Finally, expand the subjectName in the TBS context and sign to issue */ hx509_ca_tbs_subject_expand(context->hx509ctx, tbs, env); hx509_env_free(&env); - ret = hx509_ca_sign(context->hx509ctx, tbs, signer, &cert); - hx509_cert_free(signer); if (ret) goto out; - hx509_ca_tbs_free(&tbs); - + /* Encode and output the certificate */ ret = hx509_cert_binary(context->hx509ctx, cert, certificate); - hx509_cert_free(cert); - if (ret) - goto out; - /* cleanup on success */ - krb5_xfree(name); - - return 0; out: - if (name) - krb5_xfree(name); + if (ret) + kdc_log(context, config, 0, "Failed to build a certificate for %s", + princ); + krb5_xfree(name); + krb5_xfree(princ); if (env) hx509_env_free(&env); if (tbs) hx509_ca_tbs_free(&tbs); + if (cert) + hx509_cert_free(cert); if (signer) hx509_cert_free(signer); - krb5_set_error_message(context, ret, "cert creation failed"); return ret; } +/* Check that a krbtgt's second component is a local realm */ +static krb5_error_code +is_local_realm(krb5_context context, + krb5_kdc_configuration *config, + const char *realm) +{ + krb5_error_code ret; + krb5_principal tgs; + hdb_entry_ex *ent = NULL; + + ret = krb5_make_principal(context, &tgs, realm, KRB5_TGS_NAME, realm, + NULL); + if (ret) + return ret; + if (ret == 0) + ret = _kdc_db_fetch(context, config, tgs, HDB_F_GET_KRBTGT, NULL, NULL, + &ent); + if (ent) + _kdc_free_ent(context, ent); + krb5_free_principal(context, tgs); + if (ret == HDB_ERR_NOENTRY || ret == HDB_ERR_NOT_FOUND_HERE) + return KRB5KRB_AP_ERR_NOT_US; + return ret; +} + +/* + * Since we're using the HDB as a keytab we have to check that the client used + * an acceptable name for the kx509 service. + * + * We accept two names: kca_service/hostname and krbtgt/REALM. + * + * We allow cross-realm requests. + * + * XXX Maybe x-realm support should be configurable. Requiring INITIAL tickets + * does NOT preclude x-realm support! (Cross-realm TGTs can be INITIAL.) + * + * Support for specific client realms is configurable by configuring issuer + * credentials and TBS templates on a per-realm basis and configuring no + * default. But maybe we should have an explicit configuration parameter + * to enable support for clients from different realms than the service. + */ krb5_error_code kdc_kx509_verify_service_principal(krb5_context context, + krb5_kdc_configuration *config, const char *cname, krb5_principal sprincipal) { - krb5_error_code ret, aret; - krb5_boolean bret; + krb5_error_code ret = 0; krb5_principal principal = NULL; char *expected = NULL; char localhost[MAXHOSTNAMELEN]; + if (krb5_principal_get_num_comp(context, sprincipal) != 2) + goto err; + + /* Check if sprincipal is a krbtgt/REALM name */ + if (strcmp(krb5_principal_get_comp_string(context, sprincipal, 0), + KRB5_TGS_NAME) == 0) { + const char *r = krb5_principal_get_comp_string(context, sprincipal, 1); + if ((ret = is_local_realm(context, config, r))) + kdc_log(context, config, 0, "client used wrong krbtgt for kx509"); + goto out; + } + + /* Must be hostbased kca_service name then */ ret = gethostname(localhost, sizeof(localhost) - 1); if (ret != 0) { ret = errno; @@ -347,30 +644,133 @@ kdc_kx509_verify_service_principal(krb5_context context, if (ret) goto out; - bret = krb5_principal_compare_any_realm(context, sprincipal, principal); - if (bret == TRUE) + if (krb5_principal_compare_any_realm(context, sprincipal, principal)) goto out; /* found a match */ - ret = KRB5KDC_ERR_SERVER_NOMATCH; - - aret = krb5_unparse_name(context, sprincipal, &expected); - if (aret) +err: + ret = krb5_unparse_name(context, sprincipal, &expected); + if (ret) goto out; + ret = KRB5KDC_ERR_SERVER_NOMATCH; krb5_set_error_message(context, ret, "User %s used wrong Kx509 service " "principal, expected: %s", cname, expected); - out: +out: krb5_xfree(expected); krb5_free_principal(context, principal); return ret; } +static krb5_error_code +encode_reply(krb5_context context, + krb5_kdc_configuration *config, + krb5_data *reply, + Kx509Response *r) +{ + krb5_error_code ret; + krb5_data data; + size_t size; + + reply->data = NULL; + reply->length = 0; + ASN1_MALLOC_ENCODE(Kx509Response, data.data, data.length, r, &size, ret); + if (ret) { + kdc_log(context, config, 0, "Failed to encode kx509 reply"); + return ret; + } + if (size != data.length) + krb5_abortx(context, "ASN1 internal error"); + + ret = krb5_data_alloc(reply, data.length + sizeof(version_2_0)); + if (ret == 0) { + memcpy(reply->data, version_2_0, sizeof(version_2_0)); + memcpy(((unsigned char *)reply->data) + sizeof(version_2_0), + data.data, data.length); + } + free(data.data); + return ret; +} + +static krb5_error_code +mk_error_response(krb5_context context, + krb5_kdc_configuration *config, + krb5_keyblock *key, + krb5_data *reply, + int32_t code, + const char *fmt, + ...) +{ + krb5_error_code ret = code; + krb5_error_code ret2; + Kx509Response rep; + const char *msg; + char *freeme0 = NULL; + char *freeme1 = NULL; + va_list ap; + + if (!config->enable_kx509) + code = KRB5KDC_ERR_POLICY; + + /* Make sure we only send RFC4120 and friends wire protocol error codes */ + if (code) { + if (code == KX509_ERR_NONE) { + code = 0; + } else if (code > KX509_ERR_NONE && code <= KX509_ERR_SRV_OVERLOADED) { + code -= KX509_ERR_NONE; + } else { + if (code < KRB5KDC_ERR_NONE || code >= KRB5_ERR_RCSID) + code = KRB5KRB_ERR_GENERIC; + code -= KRB5KDC_ERR_NONE; + code += kx509_krb5_error_base; + } + } + + va_start(ap, fmt); + if (vasprintf(&freeme0, fmt, ap) == -1 || freeme0 == NULL) + msg = "Could not format error message (out of memory)"; + else + msg = freeme0; + va_end(ap); + + if (!config->enable_kx509 && + asprintf(&freeme1, "kx509 service is disabled (%s)", msg) > -1 && + freeme1 != NULL) { + msg = freeme1; + } + + kdc_log(context, config, 0, "%s", msg); + + rep.hash = NULL; + rep.certificate = NULL; + rep.error_code = code; + if (ALLOC(rep.e_text)) + *rep.e_text = (void *)(uintptr_t)msg; + + if (key) { + if (ALLOC(rep.hash) != NULL && + calculate_reply_hash(context, key, &rep)) { + free(rep.hash); + rep.hash = NULL; + } + } + + if ((ret2 = encode_reply(context, config, reply, &rep))) + ret = ret2; + if (rep.hash) + krb5_data_free(rep.hash); + free(rep.e_text); + free(rep.hash); + free(freeme0); + free(freeme1); + return ret; +} + /* - * + * Process a request, produce a reply. */ krb5_error_code @@ -385,26 +785,40 @@ _kdc_do_kx509(krb5_context context, krb5_auth_context ac = NULL; krb5_keytab id = NULL; krb5_principal sprincipal = NULL, cprincipal = NULL; + char *sname = NULL; char *cname = NULL; Kx509Response rep; - size_t size; krb5_keyblock *key = NULL; - krb5_boolean def_bool; + + /* + * In order to support authenticated error messages we defer checking + * whether the kx509 service is enabled until after accepting the AP-REQ. + */ krb5_data_zero(reply); memset(&rep, 0, sizeof(rep)); - if(!config->enable_kx509) { - kdc_log(context, config, 0, - "Rejected kx509 request (disabled) from %s", from); - return KRB5KDC_ERR_POLICY; - } - kdc_log(context, config, 0, "Kx509 request from %s", from); + if (req->authenticator.length == 0) { + /* + * Unauthenticated kx509 service availability probe. + * + * mk_error_response() will check whether the service is enabled and + * possibly change the error code and message. + */ + ret = mk_error_response(context, config, key, reply, + KRB5KDC_ERR_NULL_KEY, + "kx509 service is available"); + goto out; + } + + /* Consume the AP-REQ */ ret = krb5_kt_resolve(context, "HDBGET:", &id); if (ret) { - kdc_log(context, config, 0, "Can't open database for digest"); + mk_error_response(context, config, key, reply, + KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, + "Can't open database for digest"); goto out; } @@ -415,114 +829,148 @@ _kdc_do_kx509(krb5_context context, id, &ap_req_options, &ticket); - if (ret) + if (ret == 0) + ret = krb5_auth_con_getkey(context, ac, &key); + if (ret == 0 && key == NULL) + ret = KRB5KDC_ERR_NULL_KEY; + /* + * Provided we got the session key, errors past this point will be + * authenticated. + */ + if (ret == 0) + ret = krb5_ticket_get_client(context, ticket, &cprincipal); + if (ret) { + mk_error_response(context, config, key, reply, ret, + "Could not get Ticket client principal from %s", + from); goto out; + } - ret = krb5_ticket_get_client(context, ticket, &cprincipal); - if (ret) - goto out; - - def_bool = krb5_config_get_bool_default(context, NULL, TRUE, "kdc", - "require_initial_kca_tickets", - NULL); - if (!ticket->ticket.flags.initial && - krb5_config_get_bool_default(context, NULL, def_bool, "kdc", - krb5_principal_get_realm(context, - cprincipal), - "require_initial_kca_tickets", NULL)) { - ret = KRB5KDC_ERR_POLICY; + /* Optional: check if Ticket is INITIAL */ + if (ret == 0 && + !ticket->ticket.flags.initial && + !get_bool_param(context, TRUE, + krb5_principal_get_realm(context, cprincipal), + "require_initial_kca_tickets")) { + ret = mk_error_response(context, config, key, reply, + KRB5KDC_ERR_POLICY, /* XXX */ + "kx509 client %s used non-INITIAL tickets, " + "but kx509 service is configured to require " + "INITIAL tickets", from); goto out; } ret = krb5_unparse_name(context, cprincipal, &cname); - if (ret) - goto out; - ret = krb5_ticket_get_server(context, ticket, &sprincipal); - if (ret) - goto out; - - ret = kdc_kx509_verify_service_principal(context, cname, sprincipal); - if (ret) - goto out; - - ret = krb5_auth_con_getkey(context, ac, &key); - if (ret == 0 && key == NULL) - ret = KRB5KDC_ERR_NULL_KEY; + /* Check that the service name is a valid kx509 service name */ + if (ret == 0) + ret = krb5_ticket_get_server(context, ticket, &sprincipal); + if (ret == 0) + ret = krb5_unparse_name(context, sprincipal, &sname); + if (ret == 0) + if (ret == 0) + ret = kdc_kx509_verify_service_principal(context, config, cname, + sprincipal); if (ret) { - krb5_set_error_message(context, ret, "Kx509 can't get session key"); + mk_error_response(context, config, key, reply, ret, "kx509 client %s from " + "%s used incorrect service name (%s) for kx509 " + "service", + cname ? cname : "", from, + sname ? sname : ""); goto out; } + /* Authenticate the rest of the request */ ret = verify_req_hash(context, req, key); - if (ret) + if (ret) { + mk_error_response(context, config, key, reply, ret, "Incorrect HMAC " + "for kx509 request for client %s from %s for %s", + cname, from, sname); goto out; + } - /* Verify that the key is encoded RSA key */ + if (req->pk_key.length == 0) { + /* + * The request is a kx509 service availability probe. + * + * mk_error_response() will check whether the service is enabled and + * possibly change the error code and message. + */ + ret = mk_error_response(context, config, key, reply, 0, + "kx509 probe request"); + goto out; + } + + /* + * Verify that the key is a DER-encoded RSA key + * + * TODO: Try decoding `req->pk_key' as a DER-encoded CSR, or as a + * DER-encoded Certificate (and check that the subject key signed the + * thing). + * + * That will add proof-of-possesion. + * + * That will also add algorithm agility. + */ { RSAPublicKey rsapkey; size_t rsapkeysize; ret = decode_RSAPublicKey(req->pk_key.data, req->pk_key.length, &rsapkey, &rsapkeysize); - if (ret) - goto out; free_RSAPublicKey(&rsapkey); - if (rsapkeysize != req->pk_key.length) { - ret = ASN1_EXTRA_DATA; - goto out; - } + if (ret || rsapkeysize != req->pk_key.length) { + ret = KRB5KDC_ERR_NULL_KEY; + mk_error_response(context, config, key, reply, ret, + "Could not decode RSA subject public key for " + "kx509 client %s from %s for %s", cname, from, + sname); + goto out; + } } - ALLOC(rep.certificate); - if (rep.certificate == NULL) - goto out; - krb5_data_zero(rep.certificate); ALLOC(rep.hash); - if (rep.hash == NULL) - goto out; - krb5_data_zero(rep.hash); - - ret = build_certificate(context, config, &req->pk_key, - krb5_ticket_get_endtime(context, ticket), - cprincipal, rep.certificate); - if (ret) - goto out; - - ret = calculate_reply_hash(context, key, &rep); - if (ret) - goto out; - - /* - * Encode reply, [ version | Kx509Response ] - */ - - { - krb5_data data; - - ASN1_MALLOC_ENCODE(Kx509Response, data.data, data.length, &rep, - &size, ret); - if (ret) { - krb5_set_error_message(context, ret, "Failed to encode kx509 reply"); - goto out; - } - if (size != data.length) - krb5_abortx(context, "ASN1 internal error"); - - ret = krb5_data_alloc(reply, data.length + sizeof(version_2_0)); - if (ret) { - free(data.data); - goto out; - } - memcpy(reply->data, version_2_0, sizeof(version_2_0)); - memcpy(((unsigned char *)reply->data) + sizeof(version_2_0), - data.data, data.length); - free(data.data); + ALLOC(rep.certificate); + if (rep.certificate == NULL || rep.hash == NULL) { + ret = mk_error_response(context, config, key, reply, ENOMEM, + "Could allocate memory for response for kx509 " + "client %s from %s for %s", + cname, from, sname); + goto out; } - kdc_log(context, config, 0, "Successful Kx509 request for %s", cname); + /* Issue the certificate */ + krb5_data_zero(rep.hash); + krb5_data_zero(rep.certificate); + ret = build_certificate(context, config, &req->pk_key, + krb5_ticket_get_endtime(context, ticket), + cprincipal, rep.certificate); + if (ret) { + mk_error_response(context, config, key, reply, ret, "Failed to build " + "certificate for kx509 client %s from %s for %s", + cname, from, sname); + goto out; + } + + /* Authenticate the response */ + ret = calculate_reply_hash(context, key, &rep); + if (ret) { + mk_error_response(context, config, key, reply, ret, "Failed to HMAC " + "certificate for kx509 client %s from %s for %s", + cname, from, sname); + goto out; + } + + /* Encode and output reply */ + ret = encode_reply(context, config, reply, &rep); + if (ret) + mk_error_response(context, config, key, reply, ret, "Could not encode " + "kx509 response to client %s from %s for %s", cname, + from, sname); out: + if (ret == 0) + kdc_log(context, config, 0, "Successful Kx509 request for %s", cname); if (ac) krb5_auth_con_free(context, ac); if (ret) @@ -537,11 +985,13 @@ out: krb5_free_principal(context, cprincipal); if (key) krb5_free_keyblock (context, key); + if (sname) + free(sname); if (cname) free(cname); free_Kx509Response(&rep); - return 0; + return ret; } #endif /* KX509 */ diff --git a/kdc/process.c b/kdc/process.c index f91d4174a..64d78e65c 100644 --- a/kdc/process.c +++ b/kdc/process.c @@ -150,10 +150,9 @@ kdc_kx509(krb5_context context, { Kx509Request kx509req; krb5_error_code ret; - size_t len; ret = _kdc_try_kx509_request(req_buffer->data, req_buffer->length, - &kx509req, &len); + &kx509req); if (ret) return ret; diff --git a/kuser/Makefile.am b/kuser/Makefile.am index 0e82c8ea6..75ba9c19c 100644 --- a/kuser/Makefile.am +++ b/kuser/Makefile.am @@ -37,10 +37,13 @@ kdestroy_LDADD = $(kinit_LDADD) kimpersonate_LDADD = $(kinit_LDADD) +LIB_hx509 = ../lib/hx509/libhx509.la + heimtools_LDADD = \ $(top_builddir)/lib/sl/libsl.la \ $(kinit_LDADD) \ - $(LIB_readline) + $(LIB_readline) \ + $(LIB_hx509) dist_heimtools_SOURCES = heimtools.c klist.c kswitch.c copy_cred_cache.c nodist_heimtools_SOURCES = heimtools-commands.c diff --git a/kuser/NTMakefile b/kuser/NTMakefile index f9106c391..3b12ffd44 100644 --- a/kuser/NTMakefile +++ b/kuser/NTMakefile @@ -57,6 +57,7 @@ NOINSTPROGRAMS=\ BINLIBS=\ $(LIBHEIMDAL) \ $(LIBHEIMNTLM) \ + $(LIBHX509) \ !if !defined(NO_AFS) $(LIBKAFS) \ !endif diff --git a/kuser/heimtools-commands.in b/kuser/heimtools-commands.in index cf3bf2fd7..3d8a20684 100644 --- a/kuser/heimtools-commands.in +++ b/kuser/heimtools-commands.in @@ -114,6 +114,11 @@ command = { type = "flag" help = "Verbose output" } + option = { + long = "extract-kx509-cert" + type = "string" + help = "hx509 store for kx509 certificate and private key" + } } command = { name = "kgetcred" diff --git a/kuser/klist.1 b/kuser/klist.1 index 8ebad7d1b..17d888961 100644 --- a/kuser/klist.1 +++ b/kuser/klist.1 @@ -44,6 +44,8 @@ .Fl Fl cache= Ns Ar cache .Xc .Oc +.Oo Fl Fl extract-kx509-cert= Ns Ar hx509-store +.Oc .Op Fl s | Fl t | Fl Fl test .Op Fl T | Fl Fl tokens .Op Fl 5 | Fl Fl v5 @@ -65,6 +67,17 @@ credential cache to list .It Fl s , Fl t , Fl Fl test Test for there being an active and valid TGT for the local realm of the user in the credential cache. +.It Fl Fl extract-kx509-cert= Ns Ar hx509-store +An hx509 store specification, such as +.Va DER-FILE:/path/to/der/file , +.Va PEM-FILE:/path/to/PEM/file , +.Va FILE:/path/to/PEM/file , +or +.Va PKCS12:/path/to/PKCS#12/file +into which to store any PKIX certificate and private key +(unencrypted) that may have been acquired with the kx509 protocol +and stored in the +.Ns Ar ccache. .It Fl T , Fl Fl tokens display AFS tokens .It Fl 5 , Fl Fl v5 diff --git a/kuser/klist.c b/kuser/klist.c index 955ca139b..73c2d90ed 100644 --- a/kuser/klist.c +++ b/kuser/klist.c @@ -36,6 +36,10 @@ #include "kuser_locl.h" #include "parse_units.h" #include "heimtools-commands.h" +#include +#undef HC_DEPRECATED_CRYPTO +#include "../lib/hx509/hx_locl.h" +#include "hx509-private.h" static char* printable_time_internal(time_t t, int x) @@ -173,6 +177,7 @@ print_cred_verbose(krb5_context context, krb5_creds *cred, int do_json) strcmp(s, "FriendlyName") == 0 || strcmp(s, "fast_avail") == 0 || strcmp(s, "kx509store") == 0 || + strcmp(s, "kx509_service_realm") == 0 || strcmp(s, "kx509_service_status") == 0) printf(N_("Configuration item payload: %.*s\n", ""), (int)cred->ticket.length, @@ -253,22 +258,34 @@ print_cred_verbose(krb5_context context, krb5_creds *cred, int do_json) */ static void -print_tickets (krb5_context context, - krb5_ccache ccache, - krb5_principal principal, - int do_verbose, - int do_flags, - int do_hidden, - int do_json) +print_tickets(krb5_context context, + krb5_ccache ccache, + krb5_principal principal, + int do_verbose, + int do_flags, + int do_hidden, + int do_json, + const char *hx509_store) { char *str, *name, *fullname; + char *kx509_realm = NULL; + hx509_private_key key = NULL; + hx509_context hx509ctx = NULL; + hx509_certs certs = NULL; + hx509_cert cert = NULL; + krb5_error_code kx509_ret = 0; krb5_error_code ret; krb5_cc_cursor cursor; krb5_creds creds; krb5_deltat sec; - int print_comma = 0; - rtbl_t ct = NULL; + int print_comma = 0; + int kx509_disabled = 0; + int cert_stored = 0; + int cert_seen = 0; + + if (hx509_store) + kx509_ret = hx509_context_init(&hx509ctx); ret = krb5_unparse_name (context, principal, &str); if (ret) @@ -341,6 +358,45 @@ print_tickets (krb5_context context, if (do_verbose && do_json) printf("\"tickets\" : ["); while ((ret = krb5_cc_next_cred(context, ccache, &cursor, &creds)) == 0) { + if (krb5_is_config_principal(context, creds.server) && + krb5_principal_get_num_comp(context, creds.server) == 2 && + hx509_store) { + const char *s; + + s = krb5_principal_get_comp_string(context, creds.server, 1); + if (strcmp(s, "kx509_service_status") == 0) { + kx509_disabled = 1; + } else if (strcmp(s, "kx509_service_realm") == 0) { + kx509_realm = strndup(creds.ticket.data, creds.ticket.length); + } else if (strcmp(s, "kx509cert") == 0) { + cert = hx509_cert_init_data(hx509ctx, creds.ticket.data, + creds.ticket.length, NULL); + } else if (strcmp(s, "kx509key") == 0) { + (void) hx509_parse_private_key(hx509ctx, NULL, + creds.ticket.data, + creds.ticket.length, + HX509_KEY_FORMAT_PKCS8, &key); + } + if (hx509ctx && cert && key && !cert_seen) { + /* Now store the cert and key into the given hx509 store */ + (void) _hx509_cert_assign_key(cert, key); + kx509_ret = hx509_certs_init(hx509ctx, hx509_store, + HX509_CERTS_CREATE, NULL, &certs); + if (kx509_ret == 0) + kx509_ret = hx509_certs_add(hx509ctx, certs, cert); + if (kx509_ret == 0) + kx509_ret = hx509_certs_store(hx509ctx, certs, 0, NULL); + + /* + * Wait till we're done listing the ccache to complain about + * failing to extract the cert and priv key. + */ + cert_seen = 1; + if (kx509_ret == 0) + cert_stored = 1; + } + } + if (!do_hidden && krb5_is_config_principal(context, creds.server)) { ; } else if (do_verbose) { @@ -353,12 +409,47 @@ print_tickets (krb5_context context, } krb5_free_cred_contents(context, &creds); } - print_comma = 0; - if(ret != KRB5_CC_END) + if (ret != KRB5_CC_END) krb5_err(context, 1, ret, "krb5_cc_get_next"); ret = krb5_cc_end_seq_get (context, ccache, &cursor); if (ret) - krb5_err (context, 1, ret, "krb5_cc_end_seq_get"); + krb5_err(context, 1, ret, "krb5_cc_end_seq_get"); + + /* Finish kx509 extraction error checking */ + if (hx509_store && cert_seen && !cert_stored) { + if (!hx509ctx) + krb5_err(context, 1, kx509_ret, + N_("Failed to store certificate and private key " + "in %s due to failure to initialize context: %s", ""), + hx509_store, hx509_get_error_string(hx509ctx, kx509_ret)); + if (!cert || !key) + krb5_err(context, 1, kx509_ret, + N_("Failed to store certificate and private key " + "in %s due to failure to parse them: %s", ""), + hx509_store, hx509_get_error_string(hx509ctx, kx509_ret)); + krb5_err(context, 1, kx509_ret, N_("Failed to store certificate and " + "private key in %s", ""), + hx509_store); + } + if (hx509_store && !cert_seen) { + /* No PKIX creds in ccache, but maybe we can run kx509 now */ + if (kx509_disabled) + krb5_errx(context, 1, N_("The kx509 protocol is disabled at the " + "KDC for realm %s", ""), + kx509_realm ? kx509_realm : ""); + ret = krb5_kx509_ext(context, ccache, NULL, NULL, NULL, 0, hx509_store, + NULL); + if (ret) + krb5_err(context, 1, ret, N_("Failed to acquire certificate and " + "store it and private key in %s", ""), + hx509_store); + } + hx509_private_key_free(&key); + hx509_certs_free(&certs); + hx509_cert_free(cert); + hx509_context_free(&hx509ctx); + + print_comma = 0; if(!do_verbose) { rtbl_format(ct, stdout); rtbl_destroy(ct); @@ -368,6 +459,7 @@ print_tickets (krb5_context context, printf("]"); printf("}"); } + free(fullname); } /* @@ -474,7 +566,7 @@ static int display_v5_ccache (krb5_context context, krb5_ccache ccache, int do_test, int do_verbose, int do_flags, int do_hidden, - int do_json) + int do_json, const char *hx509_store) { krb5_error_code ret; krb5_principal principal; @@ -499,7 +591,7 @@ display_v5_ccache (krb5_context context, krb5_ccache ccache, exit_status = check_expiration(context, ccache, NULL); else print_tickets (context, ccache, principal, do_verbose, - do_flags, do_hidden, do_json); + do_flags, do_hidden, do_json, hx509_store); ret = krb5_cc_close (context, ccache); if (ret) @@ -646,7 +738,8 @@ klist(struct klist_options *opt, int argc, char **argv) exit_status |= display_v5_ccache(heimtools_context, id, do_test, do_verbose, opt->flags_flag, - opt->hidden_flag, opt->json_flag); + opt->hidden_flag, opt->json_flag, + opt->extract_kx509_cert_string); if (!opt->json_flag) printf("\n\n"); @@ -667,7 +760,8 @@ klist(struct klist_options *opt, int argc, char **argv) } exit_status = display_v5_ccache(heimtools_context, id, do_test, do_verbose, opt->flags_flag, - opt->hidden_flag, opt->json_flag); + opt->hidden_flag, opt->json_flag, + opt->extract_kx509_cert_string); } } diff --git a/lib/asn1/kx509.asn1 b/lib/asn1/kx509.asn1 index 14ebf50ec..9357ab970 100644 --- a/lib/asn1/kx509.asn1 +++ b/lib/asn1/kx509.asn1 @@ -1,5 +1,7 @@ -- $Id$ +-- The kx509 protocol is documented in RFC6717. + KX509 DEFINITIONS ::= BEGIN @@ -12,20 +14,134 @@ KX509-ERROR-CODE ::= INTEGER { KX509-STATUS-SERVER-TEMP(5), -- 6 is used internally in the umich client, avoid that KX509-STATUS-SERVER-KEY(7) + -- Let us reserve 1000+ for Kebreros protocol wire error codes -Nico } +-- Version 2, which has no proof of possession Kx509Request ::= SEQUENCE { authenticator OCTET STRING, - pk-hash OCTET STRING, - pk-key OCTET STRING + pk-hash OCTET STRING, -- HMAC(ticket_session_key, pk-key) + pk-key OCTET STRING -- the public key, DER-encoded (RSA, basically) } +-- Kx509ErrorCode is a Heimdal-specific enhancement with no change on the wire, +-- and really only just so the error-code field below can fit on one line. +Kx509ErrorCode ::= INTEGER (-2147483648..2147483647) + Kx509Response ::= SEQUENCE { - error-code[0] INTEGER (-2147483648..2147483647) - OPTIONAL -- DEFAULT 0 --, - hash[1] OCTET STRING OPTIONAL, - certificate[2] OCTET STRING OPTIONAL, + error-code[0] Kx509ErrorCode DEFAULT 0, + hash[1] OCTET STRING OPTIONAL, -- HMAC(session_key, ...) + certificate[2] OCTET STRING OPTIONAL, -- Certificates (plural) + -- if client used a + -- Kx509CSRPlus e-text[3] VisibleString OPTIONAL } +-- Offset for Kerberos protocol errors when error-code set to one: +kx509-krb5-error-base INTEGER ::= 1000 + +-- RFC6717 says this about error codes: +-- +-- +------------+-----------------------------+------------------------+ +-- | error-code | Condition | Example | +-- +------------+-----------------------------+------------------------+ +-- | 1 | Permanent problem with | Incompatible version | +-- | | client request | | +-- | 2 | Solvable problem with | Expired Kerberos | +-- | | client request | credentials | +-- | 3 | Temporary problem with | Packet loss | +-- | | client request | | +-- | 4 | Permanent problem with the | Internal | +-- | | server | misconfiguration | +-- | 5 | Temporary problem with the | Server overloaded | +-- | | server | | +-- +------------+-----------------------------+------------------------+ +-- +-- Looking at UMich CITI's kca (server-side of kx509) implementation, it always +-- sends 0 as the status code, and the UMich CITI kx509 client never checks it. +-- All of these error codes are local only in the UMich CITI implementation. +-- +-- Meanwhile, Heimdal used to never send error responses at all. +-- +-- As a result we can use whatever error codes we want. We'll send Kerberos +-- protocol errors + 1000. And we'll never use RFC6717 error codes at all. +-- +-- Looking at umich source... +-- +-- #define KX509_STATUS_GOOD 0 /* No problems handling client request */ +-- #define KX509_STATUS_CLNT_BAD 1 /* Client-side permanent problem */ +-- /* ex. version incompatible */ +-- #define KX509_STATUS_CLNT_FIX 2 /* Client-side solvable problem */ +-- /* ex. re-authenticate */ +-- #define KX509_STATUS_CLNT_TMP 3 /* Client-side temporary problem */ +-- /* ex. packet loss */ +-- #define KX509_STATUS_SRVR_BAD 4 /* Server-side permanent problem */ +-- /* ex. server broken */ +-- #define KX509_STATUS_SRVR_TMP 5 /* Server-side temporary problem */ +-- /* ex. server overloaded */ +-- #define KX509_STATUS_SRVR_CANT_CLNT_VERS 6 /* Server-side doesn't handle */ +-- /* existence of client_version */ +-- /* field in KX509_REQUEST */ +-- +-- The umich server uses these errors in these situations: +-- +-- - KX509_STATUS_SRVR_TMP is for: +-- - request decode errors +-- - krb5_is_ap_req() errors +-- - wrong Kerberos protocol vno in AP-REQ +-- - some ENOMEMs +-- - UDP read errors (??) +-- - LDAP issues (they use LDAP to map realm-chopped user princ names to +-- full names) +-- - pk decode errors +-- - KX509_STATUS_CLNT_TMP is for: +-- - HMAC mismatch +-- - some ENOMEMs +-- - failure to accept AP-REQ +-- - failure to unparse princ names from AP-REQ's Ticket +-- - KX509_STATUS_SRVR_BAD is for: +-- - configuration issues (missing issuer creds) +-- - serial number transaction issues (we should randomize) +-- - subjectName construction issues +-- - certificate construction issues (ENOMEM, say) +-- - failure to authenticate (never happens, since KX509_STATUS_CLNT_TMP is +-- used earlier when krb5_rd_req() fails) +-- - KX509_STATUS_CLNT_FIX is for: +-- - more than one component client principals +-- - client princ name component zero string length shorter than 3 or +-- longer than 8 (WTF) +-- - other policy issues +-- - KX509_STATUS_CLNT_BAD +-- - wrong protocol version number (version_2_0) + +-- Possible new version designs: +-- +-- - keep the protocol the same but use a CSR instead of a raw RSA public key +-- - on the server try decoding first a CSR, then a raw RSA public key +-- +-- - keep the protocol the same but use either a CSR or a self-signed cert +-- - on the server try decoding first a Certificate, then a CSR, then a raw +-- RSA public key +-- +-- CSRs are a pain to deal with. Self-signed certificates can act as a +-- CSR of a sort. Use notBefore == 1970-01-01T00:00:00Z and an EKU +-- denoting "this certificate is really a much-easier-to-work-with CSR +-- alternative". +-- +-- - keep the protocol similar, but use the checksum field of the +-- Authenticator to authenticate the request data; use a KRB-PRIV for the +-- reply +-- +-- - extend the KDC/AS/TGS protocols to support certificate issuance, either +-- at the same time as ticket acquisition, or as an alternative +-- - send a CSR as a authz-data element +-- - expect an EncryptedData with the issued Certificate inside as the +-- Ticket in the result (again, ugly hack) +-- - or maybe just add new messages, but, the thing is that the existing +-- "AP-REP + stuff" kx509 protocol is a fine design pattern, there's no +-- need to radically change it, just slightly. +-- +-- The main benefit of using an extension to the KDC/AS/TGS protocols is that +-- we could then use FAST for confidentiality protection. + END diff --git a/lib/krb5/Makefile.am b/lib/krb5/Makefile.am index d2f72d834..ab82d3df0 100644 --- a/lib/krb5/Makefile.am +++ b/lib/krb5/Makefile.am @@ -71,7 +71,7 @@ libkrb5_la_LIBADD = \ $(top_builddir)/lib/ipc/libheim-ipcc.la \ $(top_builddir)/lib/wind/libwind.la \ $(top_builddir)/lib/base/libheimbase.la \ - $(LIB_pkinit) \ + $(top_builddir)/lib/hx509/libhx509.la \ $(LIB_openssl_crypto) \ $(use_sqlite) \ $(LIB_com_err) \ @@ -100,7 +100,7 @@ librfc3961_la_LIBADD = \ lib_LTLIBRARIES = libkrb5.la -ERR_FILES = krb5_err.c krb_err.c heim_err.c k524_err.c k5e1_err.c +ERR_FILES = krb5_err.c krb_err.c heim_err.c k524_err.c k5e1_err.c kx509_err.c libkrb5_la_CPPFLAGS = \ -DBUILD_KRB5_LIB \ @@ -191,6 +191,7 @@ dist_libkrb5_la_SOURCES = \ krbhst.c \ kuserok.c \ kuserok_plugin.h \ + kx509.c \ log.c \ mcache.c \ misc.c \ @@ -281,7 +282,8 @@ ALL_OBJECTS += $(test_renew_OBJECTS) ALL_OBJECTS += $(test_rfc3961_OBJECTS) $(ALL_OBJECTS): $(srcdir)/krb5-protos.h $(srcdir)/krb5-private.h -$(ALL_OBJECTS): krb5_err.h heim_err.h k524_err.h k5e1_err.h krb_err.h k524_err.h +$(ALL_OBJECTS): krb5_err.h heim_err.h k524_err.h k5e1_err.h \ + krb_err.h k524_err.h kx509_err.h librfc3961_la_SOURCES = \ crc.c \ @@ -385,7 +387,7 @@ dist_include_HEADERS = \ noinst_HEADERS = $(srcdir)/krb5-private.h -nodist_include_HEADERS = krb5_err.h heim_err.h k524_err.h k5e1_err.h +nodist_include_HEADERS = krb5_err.h heim_err.h k524_err.h k5e1_err.h kx509_err.h # XXX use nobase_include_HEADERS = krb5/locate_plugin.h krb5dir = $(includedir)/krb5 @@ -409,9 +411,10 @@ CLEANFILES = \ krb_err.c krb_err.h \ heim_err.c heim_err.h \ k524_err.c k524_err.h \ - k5e1_err.c k5e1_err.h + k5e1_err.c k5e1_err.h \ + kx509_err.c kx509_err.h -$(libkrb5_la_OBJECTS): krb5_err.h krb_err.h heim_err.h k524_err.h k5e1_err.h +$(libkrb5_la_OBJECTS): krb5_err.h krb_err.h heim_err.h k524_err.h k5e1_err.h kx509_err.h test_config_strings.out: test_config_strings.cfg $(CP) $(srcdir)/test_config_strings.cfg test_config_strings.out @@ -427,6 +430,7 @@ EXTRA_DIST = \ heim_err.et \ k524_err.et \ k5e1_err.et \ + kx509_err.et \ $(man_MANS) \ version-script.map \ test_config_strings.cfg \ @@ -445,3 +449,5 @@ heim_err.h: heim_err.et k524_err.h: k524_err.et k5e1_err.h: k5e1_err.et + +kx509_err.h: kx509_err.et diff --git a/lib/krb5/NTMakefile b/lib/krb5/NTMakefile index 5965c0cd7..4e1d903db 100644 --- a/lib/krb5/NTMakefile +++ b/lib/krb5/NTMakefile @@ -103,6 +103,7 @@ libkrb5_OBJS = \ $(OBJ)\keytab_memory.obj \ $(OBJ)\krbhst.obj \ $(OBJ)\kuserok.obj \ + $(OBJ)\kx509.obj \ $(OBJ)\log.obj \ $(OBJ)\mcache.obj \ $(OBJ)\misc.obj \ @@ -173,6 +174,7 @@ INCFILES= \ $(INCDIR)\heim_err.h \ $(INCDIR)\k524_err.h \ $(INCDIR)\k5e1_err.h \ + $(INCDIR)\kx509_err.h \ $(INCDIR)\kcm.h \ $(INCDIR)\krb_err.h \ $(INCDIR)\krb5.h \ @@ -268,6 +270,7 @@ dist_libkrb5_la_SOURCES = \ krb5-v4compat.h \ krbhst.c \ kuserok.c \ + kx509.c \ log.c \ mcache.c \ misc.c \ @@ -360,6 +363,11 @@ $(OBJ)\k5e1_err.c $(OBJ)\k5e1_err.h: k5e1_err.et $(BINDIR)\compile_et.exe $(SRCDIR)\k5e1_err.et cd $(SRCDIR) +$(OBJ)\kx509_err.c $(OBJ)\kx509_err.h: kx509_err.et + cd $(OBJ) + $(BINDIR)\compile_et.exe $(SRCDIR)\kx509_err.et + cd $(SRCDIR) + #---------------------------------------------------------------------- # libkrb5 diff --git a/lib/krb5/cache.c b/lib/krb5/cache.c index 5a7bf6758..4fb60d996 100644 --- a/lib/krb5/cache.c +++ b/lib/krb5/cache.c @@ -716,8 +716,12 @@ krb5_cc_initialize(krb5_context context, krb5_error_code ret; ret = (*id->ops->init)(context, id, primary_principal); - if (ret == 0) - id->initialized = 1; + if (ret == 0) { + id->cc_kx509_done = 0; + id->cc_initialized = 1; + id->cc_need_start_realm = 1; + id->cc_start_tgt_stored = 0; + } return ret; } @@ -735,11 +739,32 @@ KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL krb5_cc_destroy(krb5_context context, krb5_ccache id) { + krb5_error_code ret2 = 0; krb5_error_code ret; + krb5_data d; + + /* + * Destroy associated hx509 PKIX credential store created by krb5_kx509*(). + */ + if ((ret = krb5_cc_get_config(context, id, NULL, "kx509store", &d)) == 0) { + char *name; + + if ((name = strndup(d.data, d.length)) == NULL) { + ret2 = krb5_enomem(context); + } else { + hx509_certs certs; + ret = hx509_certs_init(context->hx509ctx, name, 0, NULL, &certs); + if (ret == 0) + ret2 = hx509_certs_destroy(context->hx509ctx, &certs); + else + hx509_certs_free(&certs); + free(name); + } + } ret = (*id->ops->destroy)(context, id); - krb5_cc_close (context, id); - return ret; + (void) krb5_cc_close(context, id); + return ret ? ret : ret2; } /** @@ -756,6 +781,44 @@ krb5_cc_close(krb5_context context, krb5_ccache id) { krb5_error_code ret; + + /* + * We want to automatically acquire a PKIX credential using kx509. + * + * This can be slow if we're generating an RSA key. Plus it means talking + * to the KDC. + * + * We only want to do this when: + * + * - krb5_cc_initialize() was called on this ccache handle, + * - a start TGT was stored (actually, a cross-realm TGT would do), + * + * and + * + * - we aren't creating a gss_cred_id_t for a delegated credential. + * + * We only have a heuristic for the last condition: that `id' is not a + * MEMORY ccache, which is what's used for delegated credentials. + * + * We really only want to do this when storing a credential in a user's + * default ccache, but we leave it to krb5_kx509() to do that check. + * + * XXX Perhaps we should do what krb5_kx509() does here, and just call + * krb5_kx509_ext() (renamed to krb5_kx509()). Then we wouldn't need + * the delegated cred handle heuristic. + */ + if (id->cc_initialized && id->cc_start_tgt_stored && !id->cc_kx509_done && + strcmp("MEMORY", krb5_cc_get_type(context, id)) != 0) { + _krb5_debug(context, 2, "attempting to fetch a certificate using " + "kx509"); + ret = krb5_kx509(context, id, NULL); + if (ret) + _krb5_debug(context, 2, "failed to fetch a certificate"); + else + _krb5_debug(context, 2, "fetched a certificate"); + ret = 0; + } + ret = (*id->ops->close)(context, id); free(id); return ret; @@ -777,31 +840,49 @@ krb5_cc_store_cred(krb5_context context, { krb5_error_code ret; krb5_data realm; + const char *cfg = ""; ret = (*id->ops->store)(context, id, creds); + if (ret) + return ret; - /* Look for and mark the first root TGT's realm as the start realm */ - if (ret == 0 && id->initialized && + /* Automatic cc_config-setting and other actions */ + if (krb5_principal_get_num_comp(context, creds->server) > 1 && + krb5_is_config_principal(context, creds->server)) + cfg = krb5_principal_get_comp_string(context, creds->server, 1); + + if (id->cc_initialized && !id->cc_start_tgt_stored && krb5_principal_is_root_krbtgt(context, creds->server)) { - - id->initialized = 0; + /* Mark the first root TGT's realm as the start realm */ + id->cc_start_tgt_stored = 1; + id->cc_need_start_realm = 0; realm.length = strlen(creds->server->realm); realm.data = creds->server->realm; (void) krb5_cc_set_config(context, id, NULL, "start_realm", &realm); - } else if (ret == 0 && id->initialized && - krb5_is_config_principal(context, creds->server) && - strcmp(creds->server->name.name_string.val[1], "start_realm") == 0) { - + } else if (id->cc_initialized && id->cc_start_tgt_stored && + !id->cc_kx509_done && strcmp(cfg, "kx509cert") == 0) { /* - * But if the caller is storing a start_realm ccconfig, then - * stop looking for root TGTs to mark as the start_realm. - * - * By honoring any start_realm cc config stored, we interop - * both, with ccache implementations that don't preserve - * insertion order, and Kerberos implementations that store this - * cc config before the TGT. + * Do not attempt kx509 at cc close time -- we're copying a ccache and + * we've already got a cert (and private key). */ - id->initialized = 0; + id->cc_kx509_done = 1; + } else if (id->cc_initialized && id->cc_start_tgt_stored && + !id->cc_kx509_done && strcmp(cfg, "kx509_service_status") == 0) { + /* + * Do not attempt kx509 at cc close time -- we're copying a ccache and + * we know the kx509 service is not available. + */ + id->cc_kx509_done = 1; + } else if (id->cc_initialized && strcmp(cfg, "start_realm") == 0) { + /* + * If the caller is storing a start_realm ccconfig, then stop looking + * for root TGTs to mark as the start_realm. + * + * By honoring any start_realm cc config stored, we interop both, with + * ccache implementations that don't preserve insertion order, and + * Kerberos implementations that store this cc config before the TGT. + */ + id->cc_need_start_realm = 0; } return ret; } diff --git a/lib/krb5/krb5.conf.5 b/lib/krb5/krb5.conf.5 index 3a452ec56..fd9e6c4ec 100644 --- a/lib/krb5/krb5.conf.5 +++ b/lib/krb5/krb5.conf.5 @@ -342,6 +342,28 @@ be allowed to run. .It Li fcache_strict_checking strict checking in FILE credential caches that owner, no symlink and permissions is correct. +.It Li enable-kx509 = Va boolean +Enable use of kx509 so that every TGT that can has a corresponding +PKIX certificate. Default: false. +.It Li kx509_gen_key_type = Va public-key-type +Type of public key for kx509 private key generation. Defaults to +.Va rsa +and currently only +.Va rsa +is supported. +.It Li kx509_gen_rsa_key_size = Va number-of-bits +RSA key size for kx509. Defaults to 2048. +.It Li kx509_store = path +A file path into which to write a certificate obtained with +kx509, and its private key, when attempting kx509 optimistically +using credentials from a default ccache. Tokens will be +expanded. +.It Li kx509_hostname = Va hostname +If set, then the kx509 client will use this hostname for the +kx509 service. This can also be set in the +.Li [realm] +section on a per-realm basis. If not set then a TGS name will be +used. .It Li name_canon_rules = Va rules One or more service principal name canonicalization rules. Each rule consists of one or more tokens separated by colon (':'). Currently @@ -706,6 +728,8 @@ Should the kdc answer digest requests. The default is FALSE. .It Li digests_allowed = Va list of digests Specifies the digests the kdc will reply to. The default is .Li ntlm-v2 . +.It Li enable-kx509 = Va boolean +Enables kx509 service. .It Li kx509_ca = Va file Specifies the PEM credentials for the kx509 certification authority. .It Li require_initial_kca_tickets = Va boolean @@ -718,13 +742,31 @@ Defaults to true for the global setting. If true then the kx509 client principal's name and realm will be included in an .Li id-pkinit-san -certificate extension. +subject alternative name certificate extension. This can be set on a per-realm basis as well as globally. Defaults to true for the global setting. +.It Li kx509_include_email_san = Va boolean +If true then the kx509 client user principal's name and realm will be +included in an +.Li rfc822Name +subject alternative name certificate extension, with the downcased +realm as the domainname. +This can be set on a per-realm basis as well as globally. +Defaults to false for the global setting. +.It Li kx509_include_dnsname_san = Va boolean +If true then the kx509 host-based or domain-based client principal's +hostname will be included in an +.Li dNSName +subject alternative name certificate extension, with the +downcased realm as the domainname. This can be set on a +per-realm basis as well as +globally. Defaults to false for the global setting. .It Li kx509_template = Va file Specifies the PEM file with a template for the certificates to be -issued. -The following variables can be interpolated in the subject name using +issued to kx509 clients whose principal names have one component +(i.e., are user principals). A template is a certificate with +variables to be interpolated in the subjectName. The following +variables can be interpolated in the subject name using ${variable} syntax: .Bl -tag -width "xxx" -offset indent .It principal-name @@ -734,6 +776,53 @@ The full name of the kx509 client principal, excluding the realm name. .It principal-name-realm The name of the client principal's realm. .El +.It Li kx509_templates = { +.Bl -tag -width "xxx" -offset indent +.It Li two_component_user = { +.Bl -tag -width "xxx" -offset indent +.It Va first-component-of-principal-name = Va file +.It ... +.It Li } +.El +.It Li hostbased = { +.Bl -tag -width "xxx" -offset indent +.It Va service = Va file +.It ... +.It Li } +.El +.It Li domainbased = { +.Bl -tag -width "xxx" -offset indent +.It Va service = Va file +.It ... +.It Li } +.El +.It Li } +.El +Specifies the PEM files with templates for the certificates to be +issued to clients with principal names with two or three name +components. This is useful for issuing server certificates to +host-based principals. The following variables can be +interpolated in the subject name using +.Va ${variable} +syntax: +.Bl -tag -width "xxx" -offset indent +.It principal-name +The full name of the kx509 client principal. +.It principal-name-without-realm +The full name of the kx509 client principal, excluding the realm name. +.It principal-name-realm +The name of the client principal's realm. +.It principal-component0 +The first component of the client principal. +.It principal-component1 +The second component of the client principal. +.It principal-component2 +The third component of the client principal. +.It principal-service-name +The name of the service. +.It principal-host-name +The name of the host. +.El .El The .Li kx509 , diff --git a/lib/krb5/krb5.h b/lib/krb5/krb5.h index 7c941673d..1fbbc00f4 100644 --- a/lib/krb5/krb5.h +++ b/lib/krb5/krb5.h @@ -386,7 +386,10 @@ typedef struct krb5_cccol_cursor_data *krb5_cccol_cursor; typedef struct krb5_ccache_data { const struct krb5_cc_ops *ops; krb5_data data; - int initialized; /* if non-zero: krb5_cc_initialize() called, now empty */ + unsigned int cc_initialized:1; /* if 1: krb5_cc_initialize() called */ + unsigned int cc_need_start_realm:1; + unsigned int cc_start_tgt_stored:1; + unsigned int cc_kx509_done:1; }krb5_ccache_data; typedef struct krb5_ccache_data *krb5_ccache; diff --git a/lib/krb5/kx509.c b/lib/krb5/kx509.c new file mode 100644 index 000000000..19a1f1abd --- /dev/null +++ b/lib/krb5/kx509.c @@ -0,0 +1,796 @@ +/* + * Copyright (c) 2019 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the Institute nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "krb5_locl.h" +#include +#include +#include "../hx509/hx_locl.h" /* XXX find a better way */ +#include "hx509-private.h" + +/* + * This file implements a client for the kx509 protocol -- a Kerberized online + * CA that can issue a Certificate to a client that authenticates using + * Kerberos. + * + * The kx509 protocol is the inverse of PKINIT. Whereas PKINIT allows users + * with PKIX credentials to acquire Kerberos credentials, the kx509 protocol + * allows users with Kerberos credentials to acquire PKIX credentials. + * + * I.e., kx509 is a bridge, just like PKINIT. + * + * The kx509 protocol is very simple, and very limited. + * + * A request consists of a DER-encoded Kx509Request message prefixed with four + * bytes identifying the protocol (see `version_2_0' below). + * + * A Kx509Request message contains an AP-REQ, a public key, and an HMAC of the + * public key made with the session key of the AP-REQ's ticket. + * + * The service principal can be either kca_service/hostname.fqdn or + * krbtgt/REALM (a Heimdal innovation). + * + * If a request is missing a public key, then the request is a probe intended + * to discover whether the service is enabled, thus helping the client avoid + * a possibly-slow private key generation operation. + * + * The response is a DER-encoded Kx509Response also prefixed with + * `version_2_0', and contains: an optional error code and error text, an + * optional certificate (for the success case), and an optional HMAC of those + * fields that is present when the service was able to verify the AP-REQ. + * + * Limitations: + * + * - no proof of possession for the public key + * - only RSA keys are supported + * - no way to express options (e.g., what KUs, EKUs, or SANs are desired) + * - no sub-session key usage + * - no reflection protection other than the HMAC's forgery protection and the + * fact that the client could tell that a reflected attack isn't success + * + * Future directions: + * + * - Since the public key field of the request is an OCTET STRING, we could + * send a CSR, or even an expired certificate (possibly self-signed, + * possibly one issued earlier) that can serve as a template. + * + * This solves the first three limitations, as it allows the client to + * demonstrate proof of possession, allows arbitrary public key types, and + * allows the client to express desires about the to-be-issued certificate. + * + * - Use the AP-REQ's Authenticator's sub-session key for the HMAC, and derive + * per-direction sub-sub-keys. + * + * - We might design a new protocol that better fits the RFC4120 KDC message + * framework. + */ + +static const unsigned char version_2_0[4] = {0 , 0, 2, 0}; + +struct kx509_ctx_data { + char *send_to_realm; /* realm to which to send request */ + krb5_keyblock *hmac_key; /* For HMAC validation */ + hx509_private_key *keys; + hx509_private_key priv_key; +}; + +static krb5_error_code +load_priv_key(krb5_context context, + struct kx509_ctx_data *kx509_ctx, + const char *fn) +{ + hx509_private_key *keys = NULL; + hx509_certs certs = NULL; + krb5_error_code ret; + + ret = hx509_certs_init(context->hx509ctx, fn, 0, NULL, &certs); + if (ret == ENOENT) + return 0; + if (ret == 0) + ret = _hx509_certs_keys_get(context->hx509ctx, certs, &keys); + if (ret == 0 && keys[0] == NULL) + ret = ENOENT; + if (ret == 0) + kx509_ctx->priv_key = _hx509_private_key_ref(keys[0]); + if (ret) + krb5_set_error_message(context, ret, "Could not load private key " + "from %s for kx509: %s", fn, + hx509_get_error_string(context->hx509ctx, ret)); + hx509_certs_free(&certs); + return ret; +} + +static krb5_error_code +gen_priv_key(krb5_context context, + const char *gen_type, + unsigned long gen_bits, + hx509_private_key *key) +{ + struct hx509_generate_private_context *key_gen_ctx = NULL; + krb5_error_code ret; + + _krb5_debug(context, 1, "kx509: gen priv key"); + if (strcmp(gen_type, "rsa")) { + krb5_set_error_message(context, ENOTSUP, "Key type %s is not " + "supported for kx509; only \"rsa\" is " + "supported for kx509 at this time", + gen_type); + return ENOTSUP; + } + + ret = _hx509_generate_private_key_init(context->hx509ctx, + ASN1_OID_ID_PKCS1_RSAENCRYPTION, + &key_gen_ctx); + if (ret == 0) + ret = _hx509_generate_private_key_bits(context->hx509ctx, key_gen_ctx, gen_bits); + + if (ret == 0) + ret = _hx509_generate_private_key(context->hx509ctx, key_gen_ctx, key); + _hx509_generate_private_key_free(&key_gen_ctx); + if (ret) + krb5_set_error_message(context, ret, + "Could not generate a private key: %s", + hx509_get_error_string(context->hx509ctx, ret)); + return ret; +} + +/* Set a cc config entry indicating that the kx509 service is not available */ +static void +store_kx509_disabled(krb5_context context, const char *realm, krb5_ccache cc) +{ + krb5_data data; + + if (!cc) + return; + + data.data = (void *)(uintptr_t)realm; + data.length = strlen(realm); + krb5_cc_set_config(context, cc, NULL, "kx509_service_realm", &data); + data.data = "disabled"; + data.length = strlen(data.data); + krb5_cc_set_config(context, cc, NULL, "kx509_service_status", &data); +} + +/* Store the private key and certificate where requested */ +static krb5_error_code +store(krb5_context context, + const char *hx509_store, + const char *realm, + krb5_ccache cc, + hx509_private_key key, + hx509_cert cert) +{ + heim_octet_string hdata; + krb5_error_code ret = 0; + krb5_data data; + + krb5_clear_error_message(context); + + if (cc) { + /* Record the realm we used */ + data.data = (void *)(uintptr_t)realm; + data.length = strlen(realm); + krb5_cc_set_config(context, cc, NULL, "kx509_service_realm", &data); + + /* Serialize and store the certificate in the ccache */ + ret = hx509_cert_binary(context->hx509ctx, cert, &hdata); + data.data = hdata.data; + data.length = hdata.length; + if (ret == 0) + ret = krb5_cc_set_config(context, cc, NULL, "kx509cert", &data); + free(hdata.data); + + /* + * Serialized and store the key in the ccache. Use PKCS#8 so that we + * store the algorithm OID too, which is needed in order to be able to + * read the private key back. + */ + if (ret == 0) + ret = _hx509_private_key_export(context->hx509ctx, key, + HX509_KEY_FORMAT_PKCS8, &hdata); + data.data = hdata.data; + data.length = hdata.length; + if (ret == 0) + ret = krb5_cc_set_config(context, cc, NULL, "kx509key", &data); + free(hdata.data); + if (ret) + krb5_set_error_message(context, ret, "Could not store kx509 " + "private key and certificate in ccache %s", + krb5_cc_get_name(context, cc)); + } + + + /* Store the private key and cert in an hx509 store */ + if (hx509_store != NULL) { + hx509_certs certs; + + _hx509_cert_assign_key(cert, key); /* store both in the same store */ + + ret = hx509_certs_init(context->hx509ctx, hx509_store, + HX509_CERTS_CREATE, NULL, &certs); + if (ret == 0) + ret = hx509_certs_add(context->hx509ctx, certs, cert); + if (ret == 0) + ret = hx509_certs_store(context->hx509ctx, certs, 0, NULL); + hx509_certs_free(&certs); + if (ret) + krb5_prepend_error_message(context, ret, "Could not store kx509 " + "private key and certificate in key " + "store %s", hx509_store); + } + + /* Store the name of the hx509 store in the ccache too */ + if (cc && hx509_store) { + data.data = (void *)(uintptr_t)hx509_store; + data.length = strlen(hx509_store); + (void) krb5_cc_set_config(context, cc, NULL, "kx509store", &data); + } + return ret; +} + +static void +init_kx509_ctx(struct kx509_ctx_data *ctx) +{ + memset(ctx, 0, sizeof(*ctx)); + ctx->send_to_realm = NULL; + ctx->hmac_key = NULL; + ctx->keys = NULL; + ctx->priv_key = NULL; +} + +static void +free_kx509_ctx(krb5_context context, struct kx509_ctx_data *ctx) +{ + krb5_free_keyblock(context, ctx->hmac_key); + free(ctx->send_to_realm); + hx509_private_key_free(&ctx->priv_key); + if (ctx->keys) + _hx509_certs_keys_free(context->hx509ctx, ctx->keys); + init_kx509_ctx(ctx); +} + +/* + * Make a request, which is a DER-encoded Kx509Request with version_2_0 + * prefixed to it. + * + * If no private key is given, then a probe request will be made. + */ +static krb5_error_code +mk_kx509_req(krb5_context context, + struct kx509_ctx_data *kx509_ctx, + krb5_ccache incc, + const char *realm, + hx509_private_key private_key, + krb5_data *req) +{ + unsigned char digest[SHA_DIGEST_LENGTH]; + SubjectPublicKeyInfo spki; + struct Kx509Request kx509_req; + krb5_data pre_req; + krb5_auth_context ac = NULL; + krb5_error_code ret = 0; + krb5_creds this_cred; + krb5_creds *cred = NULL; + HMAC_CTX ctx; + const char *hostname; + size_t len; + + krb5_data_zero(&pre_req); + memset(&spki, 0, sizeof(spki)); + memset(&this_cred, 0, sizeof(this_cred)); + memset(&kx509_req, 0, sizeof(kx509_req)); + kx509_req.pk_hash.data = digest; + kx509_req.pk_hash.length = SHA_DIGEST_LENGTH; + + if (private_key) { + /* Encode the public key for use in the request */ + ret = hx509_private_key2SPKI(context->hx509ctx, private_key, &spki); + kx509_req.pk_key.data = spki.subjectPublicKey.data; + kx509_req.pk_key.length = spki.subjectPublicKey.length >> 3; + } else { + /* Probe */ + kx509_req.pk_key.data = NULL; + kx509_req.pk_key.length = 0; + } + + if (ret == 0) + ret = krb5_auth_con_init(context, &ac); + if (ret == 0) + ret = krb5_cc_get_principal(context, incc, &this_cred.client); + if (ret == 0) { + /* + * The kx509 protocol as deployed uses kca_service/kdc_hostname, but + * this is inconvenient in libkrb5: we want to be able to use the + * send_to_kdc machinery, and since the Heimdal KDC is also the kx509 + * service, we want not to have to specify kx509 hosts separately from + * KDCs. + * + * We'd much rather use krbtgt/CLIENT_REALM@REQUESTED_REALM. What + * we do is assume all KDCs for `realm' support the kx509 service and + * then sendto the KDCs for that realm while using a hostbased service + * if still desired. + * + * Note that upstairs we try to get the start_realm cc config, so if + * realm wasn't given to krb5_kx509_ext(), then it should be set to + * that already unless there's no start_realm cc config, in which case + * we'll use the ccache's default client principal's realm. + */ + realm = realm ? realm : this_cred.client->realm; + hostname = krb5_config_get_string(context, NULL, "realm", realm, + "kx509_hostname", NULL); + if (hostname == NULL) + hostname = krb5_config_get_string(context, NULL, "libdefaults", + "kx509_hostname", NULL); + if (hostname) { + ret = krb5_sname_to_principal(context, hostname, "kca_service", + KRB5_NT_SRV_HST, &this_cred.server); + if (ret == 0) + ret = krb5_principal_set_realm(context, this_cred.server, + realm); + } else { + ret = krb5_make_principal(context, &this_cred.server, realm, + KRB5_TGS_NAME, this_cred.client->realm, + NULL); + } + } + + /* Make the AP-REQ and extract the HMAC key */ + if (ret == 0) + ret = krb5_get_credentials(context, 0, incc, &this_cred, &cred); + if (ret == 0) + ret = krb5_mk_req_extended(context, &ac, AP_OPTS_USE_SUBKEY, NULL, cred, + &kx509_req.authenticator); + krb5_free_keyblock(context, kx509_ctx->hmac_key); + kx509_ctx->hmac_key = NULL; + if (ret == 0) + ret = krb5_auth_con_getkey(context, ac, &kx509_ctx->hmac_key); + + /* Save the realm to send to */ + free(kx509_ctx->send_to_realm); + kx509_ctx->send_to_realm = NULL; + if (ret == 0 && + (kx509_ctx->send_to_realm = + strdup(krb5_principal_get_realm(context, cred->server))) == NULL) + ret = krb5_enomem(context); + + if (ret) + goto out; + + /* Add the the key and HMAC to the message */ + HMAC_CTX_init(&ctx); + HMAC_Init_ex(&ctx, kx509_ctx->hmac_key->keyvalue.data, + kx509_ctx->hmac_key->keyvalue.length, EVP_sha1(), NULL); + HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); + if (private_key) { + HMAC_Update(&ctx, kx509_req.pk_key.data, kx509_req.pk_key.length); + } else { + /* Probe */ + HMAC_Update(&ctx, kx509_req.authenticator.data, kx509_req.authenticator.length); + } + HMAC_Final(&ctx, kx509_req.pk_hash.data, 0); + HMAC_CTX_cleanup(&ctx); + + /* Encode the message, prefix `version_2_0', output the result */ + ASN1_MALLOC_ENCODE(Kx509Request, pre_req.data, pre_req.length, &kx509_req, &len, ret); + ret = krb5_data_alloc(req, pre_req.length + sizeof(version_2_0)); + if (ret == 0) { + memcpy(req->data, version_2_0, sizeof(version_2_0)); + memcpy(((unsigned char *)req->data) + sizeof(version_2_0), + pre_req.data, pre_req.length); + } + +out: + free(pre_req.data); + krb5_free_creds(context, cred); + krb5_xfree(kx509_req.authenticator.data); + free_SubjectPublicKeyInfo(&spki); + krb5_free_cred_contents(context, &this_cred); + krb5_auth_con_free(context, ac); + if (ret == 0 && req->length != len + sizeof(version_2_0)) { + krb5_data_free(req); + krb5_set_error_message(context, ret = ERANGE, + "Could not make a kx509 request"); + } + return ret; +} + +/* Parse and validate a kx509 reply */ +static krb5_error_code +rd_kx509_resp(krb5_context context, + struct kx509_ctx_data *kx509_ctx, + krb5_data *rep, + hx509_cert *cert) +{ + unsigned char digest[SHA_DIGEST_LENGTH]; + Kx509Response r; + krb5_error_code code = 0; + krb5_error_code ret = 0; + heim_string_t hestr; + heim_error_t herr = NULL; + const char *estr; + HMAC_CTX ctx; + size_t hdr_len = sizeof(version_2_0); + size_t len; + + *cert = NULL; + + /* Strip `version_2_0' prefix */ + if (rep->length < hdr_len || memcmp(rep->data, version_2_0, hdr_len)) { + krb5_set_error_message(context, ENOTSUP, + "KDC does not support kx509 protocol"); + return ENOTSUP; /* XXX */ + } + + /* Decode */ + ret = decode_Kx509Response(((unsigned char *)rep->data) + 4, + rep->length - 4, &r, &len); + if (ret == 0 && len + hdr_len != rep->length) + ret = EINVAL; /* XXX */ + if (ret) { + krb5_set_error_message(context, ret, "kx509 response is not valid"); + return ret; + } + + HMAC_CTX_init(&ctx); + HMAC_Init_ex(&ctx, kx509_ctx->hmac_key->keyvalue.data, + kx509_ctx->hmac_key->keyvalue.length, EVP_sha1(), NULL); + HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); + + { + int32_t t = r.error_code; + unsigned char encint[sizeof(t) + 1]; + size_t k; + + /* + * RFC6717 says this about how the error-code is included in the HMAC: + * + * o DER representation of the error-code exclusive of the tag and + * length, if it is present. + * + * So we use der_put_integer(), which encodes from the right. + * + * RFC6717 does not constrain the error-code's range. We assume it to + * be a 32-bit, signed integer, for which we'll need no more than 5 + * bytes. + */ + ret = der_put_integer(&encint[sizeof(encint) - 1], + sizeof(encint), &t, &k); + if (ret == 0) + HMAC_Update(&ctx, &encint[sizeof(encint)] - k, k); + + /* Normalize error code */ + if (r.error_code == 0) { + code = 0; /* No error */ + } else if (r.error_code < 0) { + code = KRB5KRB_ERR_GENERIC; /* ??? */ + } else if (r.error_code <= KX509_ERR_SRV_OVERLOADED) { + /* + * RFC6717 (kx509) error code. These are actually not used on the + * wire in any existing implementations that we are aware of. Just + * in case, however, we'll map these. + */ + code = KX509_ERR_CLNT_FATAL + r.error_code; + } else if (r.error_code < kx509_krb5_error_base) { + /* Unknown error codes */ + code = KRB5KRB_ERR_GENERIC; + } else { + /* + * Heimdal-specific enhancement to RFC6171: Kerberos wire protocol + * error codes. + */ + code = KRB5KDC_ERR_NONE + r.error_code - kx509_krb5_error_base; + if (code >= KRB5_ERR_RCSID) + code = KRB5KRB_ERR_GENERIC; + if (code == KRB5KDC_ERR_NONE) + code = 0; + } + } + if (r.certificate) + HMAC_Update(&ctx, r.certificate->data, r.certificate->length); + if (r.e_text) + HMAC_Update(&ctx, *r.e_text, strlen(*r.e_text)); + HMAC_Final(&ctx, &digest, 0); + HMAC_CTX_cleanup(&ctx); + + if (r.hash == NULL) { + /* + * No HMAC -> unauthenticated [error] response. + * + * Do not output any certificate. + */ + free_Kx509Response(&r); + return code; + } + + /* + * WARNING: We do not validate that `r.certificate' is a DER-encoded + * Certificate, not here, and we don't use a different HMAC key + * for the response than for the request. + * + * If ever we start sending a Certificate as the Kx509Request + * pk-key field, then we'll have a reflection attack. As the + * Certificate we'd send in that case will be expired, the + * reflection attack would be just a DoS. + */ + if (r.hash->length != sizeof(digest) || + ct_memcmp(r.hash->data, digest, sizeof(digest)) != 0) { + krb5_set_error_message(context, KRB5KDC_ERR_PREAUTH_FAILED, + "kx509 response MAC mismatch"); + free_Kx509Response(&r); + return KRB5KRB_AP_ERR_BAD_INTEGRITY; + } + + if (r.certificate == NULL) { + /* Authenticated response, either an error or probe success */ + free_Kx509Response(&r); + if (code != KRB5KDC_ERR_POLICY && kx509_ctx->priv_key == NULL) + return 0; /* Probe success */ + return code; + } + + /* Import the certificate payload */ + *cert = hx509_cert_init_data(context->hx509ctx, r.certificate->data, + r.certificate->length, &herr); + free_Kx509Response(&r); + if (cert) { + heim_release(herr); + return 0; + } + + hestr = herr ? heim_error_copy_string(herr) : NULL; + estr = hestr ? heim_string_get_utf8(hestr) : "(no error message)"; + krb5_set_error_message(context, ret, "Could not parse certificate " + "produced by kx509 KDC: %s (%ld)", + estr, + herr ? (long)heim_error_get_code(herr) : 0L); + + heim_release(hestr); + heim_release(herr); + return HEIM_PKINIT_CERTIFICATE_INVALID; /* XXX */ +} + +/* + * Make a request, send it, get the response, parse it, and store the + * private key and certificate. + */ +static krb5_error_code +kx509_core(krb5_context context, + struct kx509_ctx_data *kx509_ctx, + krb5_ccache incc, + const char *realm, + const char *hx509_store, + krb5_ccache outcc) +{ + krb5_error_code ret; + hx509_cert cert = NULL; + krb5_data req, resp; + + krb5_data_zero(&req); + krb5_data_zero(&resp); + + /* Make the kx509 request */ + ret = mk_kx509_req(context, kx509_ctx, incc, realm, kx509_ctx->priv_key, + &req); + + /* Send the kx509 request and get the response */ + if (ret == 0) + ret = krb5_sendto_context(context, NULL, &req, + kx509_ctx->send_to_realm, &resp); + if (ret == 0) + ret = rd_kx509_resp(context, kx509_ctx, &resp, &cert); + + /* Store the key and cert! */ + if (ret == 0 && kx509_ctx->priv_key) + ret = store(context, hx509_store, kx509_ctx->send_to_realm, outcc, + kx509_ctx->priv_key, cert); + else if (ret == KRB5KDC_ERR_POLICY || ret == KRB5_KDC_UNREACH) + /* Probe failed -> Record that the realm does not support kx509 */ + store_kx509_disabled(context, kx509_ctx->send_to_realm, outcc); + + hx509_cert_free(cert); + krb5_data_free(&resp); + krb5_data_free(&req); + return ret; +} + +/** + * Use the kx509 v2 protocol to get a certificate for the client principal. + * + * Given a private key this function will get a certificate. If no private key + * is given, one will be generated. + * + * The private key and certificate will be stored in the given hx509 store + * (e.g, "PEM-FILE:/path/to/file.pem") and/or given output ccache. When stored + * in a ccache, the DER-encoded Certificate will be stored as the data payload + * of a "cc config" named "kx509cert", while the key will be stored as a + * DER-encoded PKCS#8 PrivateKeyInfo in a cc config named "kx509key". + * + * @param context The Kerberos library context + * @param incc A credential cache + * @param realm A realm from which to get the certificate (uses the client + * principal's realm if NULL) + * @param use_priv_key_store An hx509 store containing a private key to certify + * (if NULL, a key will be generated) + * @param gen_type The public key algorithm for which to generate a private key + * @param gen_bits The size of the public key to generate, in bits + * @param hx509_store An hx509 store into which to store the private key and + * certificate (e.g, "PEM-FILE:/path/to/file.pem") + * @param outcc A ccache into which to store the private key and certificate + * + * @return A krb5 error code. + */ +KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL +krb5_kx509_ext(krb5_context context, + krb5_ccache incc, + const char *realm, + const char *use_priv_key_store, + const char *gen_type, + int gen_bits, + const char *hx509_store, + krb5_ccache outcc) +{ + struct kx509_ctx_data kx509_ctx; + krb5_error_code ret; + char *freeme = NULL; + + /* TODO: Eventually switch to ECDSA, and eventually to ed25519 or ed448 */ + if (gen_type == NULL) { + gen_type = krb5_config_get_string_default(context, NULL, "rsa", + "libdefaults", + "kx509_gen_key_type", NULL); + } + + if (gen_bits == 0) { + /* + * The key size is really only for non-ECC, of which we'll only support + * RSA. For ECC key sizes will either be implied by the `key_type' or + * will have to be a magic value that allows us to pick from some small + * set of curves (e.g., 255 == Curve25519). + */ + gen_bits = krb5_config_get_int_default(context, NULL, 2048, + "libdefaults", + "kx509_gen_rsa_key_size", NULL); + } + + init_kx509_ctx(&kx509_ctx); + + if (realm == NULL) { + krb5_data data; + + ret = krb5_cc_get_config(context, incc, NULL, "start_realm", &data); + if (ret == 0) { + if ((freeme = strndup(data.data, data.length)) == NULL) + return krb5_enomem(context); + realm = freeme; + } + } + + if (use_priv_key_store) { + /* Get the given private key if it exists, and use it */ + ret = load_priv_key(context, &kx509_ctx, use_priv_key_store); + if (ret == 0) { + ret = kx509_core(context, &kx509_ctx, incc, realm, hx509_store, + outcc); + free_kx509_ctx(context, &kx509_ctx); + free(freeme); + return ret; + } + if (ret != ENOENT) { + free_kx509_ctx(context, &kx509_ctx); + free(freeme); + return ret; + } + /* Key store doesn't exist or has no keys, fall through */ + } + + /* + * No private key given, so we generate one. + * + * However, before taking the hit for generating a keypair we probe to see + * if we're likely to succeeed. + */ + + /* Probe == call kx509_core() w/o a private key */ + ret = kx509_core(context, &kx509_ctx, incc, realm, NULL, outcc); + if (ret == 0) + ret = gen_priv_key(context, gen_type, gen_bits, &kx509_ctx.priv_key); + if (ret == 0) + ret = kx509_core(context, &kx509_ctx, incc, realm, hx509_store, outcc); + free_kx509_ctx(context, &kx509_ctx); + free(freeme); + return ret; +} + +/** + * Generates a public key and uses the kx509 v2 protocol to get a certificate + * for that key and the client principal's subject name. + * + * The private key and certificate will be stored in the given ccache, and also + * in a corresponding hx509 store if one is configured via [libdefaults] + * kx509_store. + * + * XXX NOTE: Dicey feature here... Review carefully! + * + * @param context The Kerberos library context + * @param cc A credential cache + * @param realm A realm from which to get the certificate (uses the client + * principal's realm if NULL) + * + * @return A krb5 error code. + */ +KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL +krb5_kx509(krb5_context context, krb5_ccache cc, const char *realm) +{ + krb5_error_code ret = 0; + const char *defcc; + char *ccache_full_name = NULL; + char *store_exp = NULL; + + /* + * The idea is that IF we are asked to do kx509 w/ creds from a default + * ccache THEN we should store the kx509 certificate (if we get one) and + * private key in the default hx509 store for kx509. + * + * Ideally we could have HTTP user-agents and/or TLS libraries look for + * client certificates and private keys in that default hx509 store. + * + * Of course, those user-agents / libraries should be configured to use + * those credentials with specific hostnames/domainnames, not the entire + * Internet, as the latter leaks the user's identity to the world. + * + * So we check if the full name for `cc' is the same as that of the default + * ccache name, and if so we get the [libdefaults] kx509_store string and + * expand it, then use it. + */ + if ((defcc = krb5_cc_configured_default_name(context)) && + krb5_cc_get_full_name(context, cc, &ccache_full_name) == 0 && + strcmp(defcc, ccache_full_name) == 0) { + + /* Find an hx509 store */ + const char *store = krb5_config_get_string(context, NULL, + "libdefaults", + "kx509_store", NULL); + if (store) + ret = _krb5_expand_path_tokens(context, store, 1, &store_exp); + } + + /* + * If we did settle on a default hx509 store, we'll use it for reading the + * private key from (if it exists) as well as for storing the certificate + * (and private key) into, which may save us some key generation cycles. + */ + ret = krb5_kx509_ext(context, cc, realm, store_exp, NULL, 0, + store_exp, cc); + free(ccache_full_name); + free(store_exp); + return ret; +} diff --git a/lib/krb5/kx509_err.et b/lib/krb5/kx509_err.et new file mode 100644 index 000000000..0be3907f1 --- /dev/null +++ b/lib/krb5/kx509_err.et @@ -0,0 +1,39 @@ +# +# Error messages for the krb5 library +# +# This might look like a com_err file, but is not +# + +# RFC6171 says: +# +# +------------+-----------------------------+------------------------+ +# | error-code | Condition | Example | +# +------------+-----------------------------+------------------------+ +# | 1 | Permanent problem with | Incompatible version | +# | | client request | | +# | 2 | Solvable problem with | Expired Kerberos | +# | | client request | credentials | +# | 3 | Temporary problem with | Packet loss | +# | | client request | | +# | 4 | Permanent problem with the | Internal | +# | | server | misconfiguration | +# | 5 | Temporary problem with the | Server overloaded | +# | | server | | +# +------------+-----------------------------+------------------------+ +# +# Error 3 makes no sense on the wire, and in the library it only makes sense as +# a timeout, so we'll name it KX509_ERR_TIMEOUT. + +# Error table names must be no more than four characters... +error_table kx59 + +prefix KX509_ERR + +error_code NONE, "Kx509 success" +error_code CLNT_FATAL, "Kx509 request error, possibly unsupported version" +error_code CLNT_SOLVABLE, "Kx509 request error such as expired credentials" +error_code TIMEOUT, "Kx509 request timed out" +error_code SRV_FATAL, "Permanent server problem" +error_code SRV_OVERLOADED, "Kx509 server is overloaded" + +end diff --git a/lib/krb5/libkrb5-exports.def.in b/lib/krb5/libkrb5-exports.def.in index cf06021d8..e68bdb8d6 100644 --- a/lib/krb5/libkrb5-exports.def.in +++ b/lib/krb5/libkrb5-exports.def.in @@ -438,6 +438,8 @@ EXPORTS krb5_kt_resolve krb5_kt_start_seq_get krb5_kuserok + krb5_kx509 + krb5_kx509_ext krb5_log krb5_log_msg krb5_make_addrport diff --git a/lib/krb5/version-script.map b/lib/krb5/version-script.map index 11e6ef6da..869d91b6f 100644 --- a/lib/krb5/version-script.map +++ b/lib/krb5/version-script.map @@ -431,6 +431,8 @@ HEIMDAL_KRB5_2.0 { krb5_kt_resolve; krb5_kt_start_seq_get; krb5_kuserok; + krb5_kx509; + krb5_kx509_ext; krb5_log; krb5_log_msg; krb5_make_addrport; diff --git a/tests/gss/krb5.conf.in b/tests/gss/krb5.conf.in index bfdf5aef5..2e469ecd3 100644 --- a/tests/gss/krb5.conf.in +++ b/tests/gss/krb5.conf.in @@ -2,6 +2,8 @@ include @srcdirabs@/include-krb5.conf [libdefaults] default_keytab_name = @objdir@/server.keytab + enable-kx509 = yes + kx509_store = PEM-FILE:/tmp/cert_%{euid}.pem [realms] TEST.H5L.SE = { diff --git a/tests/kdc/Makefile.am b/tests/kdc/Makefile.am index e080527db..95f9d5ea4 100644 --- a/tests/kdc/Makefile.am +++ b/tests/kdc/Makefile.am @@ -232,6 +232,11 @@ krb5-pkinit-win.conf: krb5-pkinit.conf.in Makefile CLEANFILES= \ $(TESTS) \ + *.crt \ + *.der \ + *.log \ + *.pem \ + *.pid \ *.tmp \ acache.krb5 \ barpassword \ @@ -239,7 +244,6 @@ CLEANFILES= \ cache.krb5 \ cdigest-reply \ client-cache \ - current*.log \ current-db* \ digest-reply \ foopassword \ @@ -248,7 +252,6 @@ CLEANFILES= \ iprop.keytab \ ipropd.dumpfile \ kdc-tester4.json \ - kdc.crt \ krb5-authz.conf \ krb5-authz2.conf \ krb5-canon.conf \ @@ -267,18 +270,11 @@ CLEANFILES= \ malloc-log \ malloc-log-master \ malloc-log-slave \ - messages.log \ + notfoopassword \ o2cache.krb5 \ o2digest-reply \ ocache.krb5 \ out-log \ - pkinit.crt \ - pkinit2.crt \ - pkinit3.crt \ - pkinit4.crt \ - req-kdc.der \ - req-pkinit.der \ - req-pkinit2.der \ s2digest-reply \ sdigest-init \ sdigest-reply \ diff --git a/tests/kdc/check-pkinit.in b/tests/kdc/check-pkinit.in index 2e55e34ae..a1affac03 100644 --- a/tests/kdc/check-pkinit.in +++ b/tests/kdc/check-pkinit.in @@ -55,9 +55,14 @@ keyfile="${hx509_data}/key.der" keyfile2="${hx509_data}/key2.der" kinit="${kinit} -c $cache ${afs_no_afslog}" +klist="${klist} --hidden -v -c $cache" kgetcred="${kgetcred} -c $cache" kdestroy="${kdestroy} -c $cache ${afs_no_unlog}" +kextract() { + ${klist} --extract-kx509-cert="$1" +} + KRB5_CONFIG="${objdir}/krb5-pkinit.conf" export KRB5_CONFIG @@ -104,6 +109,13 @@ ${kadmin} add -p kaka --use-defaults ${server}@${R} || exit 1 echo "Doing database check" ${kadmin} check ${R} || exit 1 +# XXX Do not use committed, in-tree private keys or certificates! +# XXX Add hxtool command to generate a private key w/o generating a CSR +# XXX Use hxtool to generate a fresh private key +# XXX Use hxtool to generate self-signed CA certs +# XXX Use PEM-FILE and store private key and certificate in same file +# XXX Update krb5.conf.in to use ${objdir}-relative keys and certificates + echo "Setting up certificates" ${hxtool} request-create \ --subject="CN=kdc,DC=test,DC=h5l,DC=se" \ @@ -165,6 +177,12 @@ ${hxtool} issue-certificate \ --req="PKCS10:req-pkinit2.der" \ --certificate="FILE:pkinit4.crt" || exit 1 +echo "issue self-signed kx509 template cert" +${hxtool} issue-certificate \ + --self-signed \ + --ca-private-key=FILE:${keyfile} \ + --subject='CN=${principal-component0},DC=test,DC=h5l,DC=se' \ + --certificate="FILE:kx509-template.crt" || exit 1 echo foo > ${objdir}/foopassword @@ -181,8 +199,16 @@ base="${objdir}" ${kinit} -C FILE:${base}/pkinit.crt,${keyfile2} bar@${R} || \ { ec=1 ; eval "${testfailed}"; } ${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; } +${klist} + +echo "Check kx509 certificate acquisition" +kextract PEM-FILE:${objdir}/kx509.pem || { ec=1 ; eval "${testfailed}"; } ${kdestroy} +echo "Check PKINIT w/ kx509 certificate" +${kinit} -C PEM-FILE:${objdir}/kx509.pem bar@${R} || \ + { ec=1 ; eval "${testfailed}"; } + echo "Trying pk-init (principal in pki-mapping file) "; > messages.log ${kinit} -C FILE:${base}/pkinit.crt,${keyfile2} foo@${R} || \ { ec=1 ; eval "${testfailed}"; } diff --git a/tests/kdc/krb5-pkinit.conf.in b/tests/kdc/krb5-pkinit.conf.in index 9be7ea400..5f3d0ce80 100644 --- a/tests/kdc/krb5-pkinit.conf.in +++ b/tests/kdc/krb5-pkinit.conf.in @@ -19,6 +19,14 @@ pkinit_anchors = FILE:@objdir@/ca.crt pkinit_mappings_file = @srcdir@/pki-mapping + enable-kx509 = true + kx509_include_email_san = true + kx509_include_pkinit_san = true + kx509_include_dnsname_san = true + require_initial_kca_tickets = false + kx509_template = FILE:@objdir@/kx509-template.crt + kx509_ca = FILE:@objdir@/ca.crt,@srcdir@/../../lib/hx509/data/key.der + database = { dbname = @objdir@/current-db realm = TEST.H5L.SE