111 def _hostsettings(ui, hostname): |
111 def _hostsettings(ui, hostname): |
112 """Obtain security settings for a hostname. |
112 """Obtain security settings for a hostname. |
113 |
113 |
114 Returns a dict of settings relevant to that hostname. |
114 Returns a dict of settings relevant to that hostname. |
115 """ |
115 """ |
|
116 bhostname = pycompat.bytesurl(hostname) |
116 s = { |
117 s = { |
117 # Whether we should attempt to load default/available CA certs |
118 # Whether we should attempt to load default/available CA certs |
118 # if an explicit ``cafile`` is not defined. |
119 # if an explicit ``cafile`` is not defined. |
119 'allowloaddefaultcerts': True, |
120 'allowloaddefaultcerts': True, |
120 # List of 2-tuple of (hash algorithm, hash). |
121 # List of 2-tuple of (hash algorithm, hash). |
160 # internal config: hostsecurity.disabletls10warning |
161 # internal config: hostsecurity.disabletls10warning |
161 if not ui.configbool('hostsecurity', 'disabletls10warning'): |
162 if not ui.configbool('hostsecurity', 'disabletls10warning'): |
162 ui.warn(_('warning: connecting to %s using legacy security ' |
163 ui.warn(_('warning: connecting to %s using legacy security ' |
163 'technology (TLS 1.0); see ' |
164 'technology (TLS 1.0); see ' |
164 'https://mercurial-scm.org/wiki/SecureConnections for ' |
165 'https://mercurial-scm.org/wiki/SecureConnections for ' |
165 'more info\n') % hostname) |
166 'more info\n') % bhostname) |
166 defaultprotocol = 'tls1.0' |
167 defaultprotocol = 'tls1.0' |
167 |
168 |
168 key = 'minimumprotocol' |
169 key = 'minimumprotocol' |
169 protocol = ui.config('hostsecurity', key, defaultprotocol) |
170 protocol = ui.config('hostsecurity', key, defaultprotocol) |
170 validateprotocol(protocol, key) |
171 validateprotocol(protocol, key) |
171 |
172 |
172 key = '%s:minimumprotocol' % hostname |
173 key = '%s:minimumprotocol' % bhostname |
173 protocol = ui.config('hostsecurity', key, protocol) |
174 protocol = ui.config('hostsecurity', key, protocol) |
174 validateprotocol(protocol, key) |
175 validateprotocol(protocol, key) |
175 |
176 |
176 # If --insecure is used, we allow the use of TLS 1.0 despite config options. |
177 # If --insecure is used, we allow the use of TLS 1.0 despite config options. |
177 # We always print a "connection security to %s is disabled..." message when |
178 # We always print a "connection security to %s is disabled..." message when |
180 protocol = 'tls1.0' |
181 protocol = 'tls1.0' |
181 |
182 |
182 s['protocol'], s['ctxoptions'], s['protocolui'] = protocolsettings(protocol) |
183 s['protocol'], s['ctxoptions'], s['protocolui'] = protocolsettings(protocol) |
183 |
184 |
184 ciphers = ui.config('hostsecurity', 'ciphers') |
185 ciphers = ui.config('hostsecurity', 'ciphers') |
185 ciphers = ui.config('hostsecurity', '%s:ciphers' % hostname, ciphers) |
186 ciphers = ui.config('hostsecurity', '%s:ciphers' % bhostname, ciphers) |
186 s['ciphers'] = ciphers |
187 s['ciphers'] = ciphers |
187 |
188 |
188 # Look for fingerprints in [hostsecurity] section. Value is a list |
189 # Look for fingerprints in [hostsecurity] section. Value is a list |
189 # of <alg>:<fingerprint> strings. |
190 # of <alg>:<fingerprint> strings. |
190 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % hostname) |
191 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % bhostname) |
191 for fingerprint in fingerprints: |
192 for fingerprint in fingerprints: |
192 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))): |
193 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))): |
193 raise error.Abort(_('invalid fingerprint for %s: %s') % ( |
194 raise error.Abort(_('invalid fingerprint for %s: %s') % ( |
194 hostname, fingerprint), |
195 bhostname, fingerprint), |
195 hint=_('must begin with "sha1:", "sha256:", ' |
196 hint=_('must begin with "sha1:", "sha256:", ' |
196 'or "sha512:"')) |
197 'or "sha512:"')) |
197 |
198 |
198 alg, fingerprint = fingerprint.split(':', 1) |
199 alg, fingerprint = fingerprint.split(':', 1) |
199 fingerprint = fingerprint.replace(':', '').lower() |
200 fingerprint = fingerprint.replace(':', '').lower() |
200 s['certfingerprints'].append((alg, fingerprint)) |
201 s['certfingerprints'].append((alg, fingerprint)) |
201 |
202 |
202 # Fingerprints from [hostfingerprints] are always SHA-1. |
203 # Fingerprints from [hostfingerprints] are always SHA-1. |
203 for fingerprint in ui.configlist('hostfingerprints', hostname): |
204 for fingerprint in ui.configlist('hostfingerprints', bhostname): |
204 fingerprint = fingerprint.replace(':', '').lower() |
205 fingerprint = fingerprint.replace(':', '').lower() |
205 s['certfingerprints'].append(('sha1', fingerprint)) |
206 s['certfingerprints'].append(('sha1', fingerprint)) |
206 s['legacyfingerprint'] = True |
207 s['legacyfingerprint'] = True |
207 |
208 |
208 # If a host cert fingerprint is defined, it is the only thing that |
209 # If a host cert fingerprint is defined, it is the only thing that |
221 s['allowloaddefaultcerts'] = False |
222 s['allowloaddefaultcerts'] = False |
222 |
223 |
223 # If both fingerprints and a per-host ca file are specified, issue a warning |
224 # If both fingerprints and a per-host ca file are specified, issue a warning |
224 # because users should not be surprised about what security is or isn't |
225 # because users should not be surprised about what security is or isn't |
225 # being performed. |
226 # being performed. |
226 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % hostname) |
227 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % bhostname) |
227 if s['certfingerprints'] and cafile: |
228 if s['certfingerprints'] and cafile: |
228 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host ' |
229 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host ' |
229 'fingerprints defined; using host fingerprints for ' |
230 'fingerprints defined; using host fingerprints for ' |
230 'verification)\n') % hostname) |
231 'verification)\n') % bhostname) |
231 |
232 |
232 # Try to hook up CA certificate validation unless something above |
233 # Try to hook up CA certificate validation unless something above |
233 # makes it not necessary. |
234 # makes it not necessary. |
234 if s['verifymode'] is None: |
235 if s['verifymode'] is None: |
235 # Look at per-host ca file first. |
236 # Look at per-host ca file first. |
236 if cafile: |
237 if cafile: |
237 cafile = util.expandpath(cafile) |
238 cafile = util.expandpath(cafile) |
238 if not os.path.exists(cafile): |
239 if not os.path.exists(cafile): |
239 raise error.Abort(_('path specified by %s does not exist: %s') % |
240 raise error.Abort(_('path specified by %s does not exist: %s') % |
240 ('hostsecurity.%s:verifycertsfile' % hostname, |
241 ('hostsecurity.%s:verifycertsfile' % ( |
241 cafile)) |
242 bhostname,), cafile)) |
242 s['cafile'] = cafile |
243 s['cafile'] = cafile |
243 else: |
244 else: |
244 # Find global certificates file in config. |
245 # Find global certificates file in config. |
245 cafile = ui.config('web', 'cacerts') |
246 cafile = ui.config('web', 'cacerts') |
246 |
247 |
388 if len(e.args) == 1: # pypy has different SSLError args |
389 if len(e.args) == 1: # pypy has different SSLError args |
389 msg = e.args[0] |
390 msg = e.args[0] |
390 else: |
391 else: |
391 msg = e.args[1] |
392 msg = e.args[1] |
392 raise error.Abort(_('error loading CA file %s: %s') % ( |
393 raise error.Abort(_('error loading CA file %s: %s') % ( |
393 settings['cafile'], msg), |
394 settings['cafile'], util.forcebytestr(msg)), |
394 hint=_('file is empty or malformed?')) |
395 hint=_('file is empty or malformed?')) |
395 caloaded = True |
396 caloaded = True |
396 elif settings['allowloaddefaultcerts']: |
397 elif settings['allowloaddefaultcerts']: |
397 # This is a no-op on old Python. |
398 # This is a no-op on old Python. |
398 sslcontext.load_default_certs() |
399 sslcontext.load_default_certs() |
635 if key == 'DNS': |
638 if key == 'DNS': |
636 try: |
639 try: |
637 if _dnsnamematch(value, hostname): |
640 if _dnsnamematch(value, hostname): |
638 return |
641 return |
639 except wildcarderror as e: |
642 except wildcarderror as e: |
640 return e.args[0] |
643 return util.forcebytestr(e.args[0]) |
641 |
644 |
642 dnsnames.append(value) |
645 dnsnames.append(value) |
643 |
646 |
644 if not dnsnames: |
647 if not dnsnames: |
645 # The subject is only checked when there is no DNS in subjectAltName. |
648 # The subject is only checked when there is no DNS in subjectAltName. |
646 for sub in cert.get('subject', []): |
649 for sub in cert.get(r'subject', []): |
647 for key, value in sub: |
650 for key, value in sub: |
648 # According to RFC 2818 the most specific Common Name must |
651 # According to RFC 2818 the most specific Common Name must |
649 # be used. |
652 # be used. |
650 if key == 'commonName': |
653 if key == r'commonName': |
651 # 'subject' entries are unicode. |
654 # 'subject' entries are unicode. |
652 try: |
655 try: |
653 value = value.encode('ascii') |
656 value = value.encode('ascii') |
654 except UnicodeEncodeError: |
657 except UnicodeEncodeError: |
655 return _('IDN in certificate not supported') |
658 return _('IDN in certificate not supported') |
656 |
659 |
657 try: |
660 try: |
658 if _dnsnamematch(value, hostname): |
661 if _dnsnamematch(value, hostname): |
659 return |
662 return |
660 except wildcarderror as e: |
663 except wildcarderror as e: |
661 return e.args[0] |
664 return util.forcebytestr(e.args[0]) |
662 |
665 |
663 dnsnames.append(value) |
666 dnsnames.append(value) |
664 |
667 |
665 if len(dnsnames) > 1: |
668 if len(dnsnames) > 1: |
666 return _('certificate is for %s') % ', '.join(dnsnames) |
669 return _('certificate is for %s') % ', '.join(dnsnames) |
778 def validatesocket(sock): |
781 def validatesocket(sock): |
779 """Validate a socket meets security requirements. |
782 """Validate a socket meets security requirements. |
780 |
783 |
781 The passed socket must have been created with ``wrapsocket()``. |
784 The passed socket must have been created with ``wrapsocket()``. |
782 """ |
785 """ |
783 host = sock._hgstate['hostname'] |
786 shost = sock._hgstate['hostname'] |
|
787 host = pycompat.bytesurl(shost) |
784 ui = sock._hgstate['ui'] |
788 ui = sock._hgstate['ui'] |
785 settings = sock._hgstate['settings'] |
789 settings = sock._hgstate['settings'] |
786 |
790 |
787 try: |
791 try: |
788 peercert = sock.getpeercert(True) |
792 peercert = sock.getpeercert(True) |
854 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for ' |
858 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for ' |
855 'how to configure Mercurial to avoid this error or set ' |
859 'how to configure Mercurial to avoid this error or set ' |
856 'hostsecurity.%s:fingerprints=%s to trust this server') % |
860 'hostsecurity.%s:fingerprints=%s to trust this server') % |
857 (host, nicefingerprint)) |
861 (host, nicefingerprint)) |
858 |
862 |
859 msg = _verifycert(peercert2, host) |
863 msg = _verifycert(peercert2, shost) |
860 if msg: |
864 if msg: |
861 raise error.Abort(_('%s certificate error: %s') % (host, msg), |
865 raise error.Abort(_('%s certificate error: %s') % (host, msg), |
862 hint=_('set hostsecurity.%s:certfingerprints=%s ' |
866 hint=_('set hostsecurity.%s:certfingerprints=%s ' |
863 'config setting or use --insecure to connect ' |
867 'config setting or use --insecure to connect ' |
864 'insecurely') % |
868 'insecurely') % |