Browse Source

Bug 2145: Support for OpenSSH certificates for host verification

https://winscp.net/tracker/2145

Source commit: 9dda484bf38f9a72b61a2c3d8e7dd8892ee96c2b
Martin Prikryl 2 years ago
parent
commit
db87bc1810

+ 3 - 0
source/Putty.cbproj

@@ -367,6 +367,9 @@
 		<CppCompile Include="putty\utils\base64_encode_atom.c">
 			<BuildOrder>80</BuildOrder>
 		</CppCompile>
+		<CppCompile Include="putty\utils\base64_valid.c">
+			<BuildOrder>165</BuildOrder>
+		</CppCompile>
 		<CppCompile Include="putty\utils\bufchain.c">
 			<BuildOrder>81</BuildOrder>
 		</CppCompile>

+ 16 - 0
source/core/Common.cpp

@@ -19,6 +19,7 @@
 #include <psapi.h>
 #include <CoreMain.h>
 #include <SessionInfo.h>
+#include <Soap.EncdDecd.hpp>
 #include <openssl/pkcs12.h>
 #include <openssl/pem.h>
 #include <openssl/err.h>
@@ -484,6 +485,21 @@ bool IsNumber(const UnicodeString Str)
   return Result;
 }
 //---------------------------------------------------------------------------
+UnicodeString EncodeStrToBase64(const RawByteString & Str)
+{
+  UnicodeString Result = EncodeBase64(Str.c_str(), Str.Length());
+  Result = ReplaceStr(Result, sLineBreak, EmptyStr);
+  return Result;
+}
+//---------------------------------------------------------------------------
+RawByteString DecodeBase64ToStr(const UnicodeString & Str)
+{
+  TBytes Bytes = DecodeBase64(Str);
+  // This might be the same as TEncoding::ASCII->GetString.
+  // const_cast: The operator[] const is (badly?) implemented to return by value
+  return RawByteString(reinterpret_cast<const char *>(&const_cast<TBytes &>(Bytes)[0]), Bytes.Length);
+}
+//---------------------------------------------------------------------------
 UnicodeString Base64ToUrlSafe(const UnicodeString & S)
 {
   UnicodeString Result = S;

+ 2 - 0
source/core/Common.h

@@ -71,6 +71,8 @@ UnicodeString RemoveInteractiveMsgTag(UnicodeString S);
 UnicodeString RemoveEmptyLines(const UnicodeString & S);
 bool IsNumber(const UnicodeString Str);
 extern const wchar_t NormalizedFingerprintSeparator;
+UnicodeString EncodeStrToBase64(const RawByteString & Str);
+RawByteString DecodeBase64ToStr(const UnicodeString & Str);
 UnicodeString Base64ToUrlSafe(const UnicodeString & S);
 UnicodeString MD5ToUrlSafe(const UnicodeString & S);
 bool SameChecksum(const UnicodeString & AChecksum1, const UnicodeString & AChecksum2, bool Base64);

+ 152 - 11
source/core/Configuration.cpp

@@ -41,6 +41,7 @@ const UnicodeString FtpsCertificateStorageKey(L"FtpsCertificates");
 const UnicodeString HttpsCertificateStorageKey(L"HttpsCertificates");
 const UnicodeString LastFingerprintsStorageKey(L"LastFingerprints");
 const UnicodeString DirectoryStatisticsCacheKey(L"DirectoryStatisticsCache");
+const UnicodeString SshHostCAsKey(L"SshHostCAs");
 const UnicodeString CDCacheKey(L"CDCache");
 const UnicodeString BannersKey(L"Banners");
 //---------------------------------------------------------------------------
@@ -49,6 +50,120 @@ const UnicodeString OpensshAuthorizedKeysFileName(L"authorized_keys");
 //---------------------------------------------------------------------------
 const int BelowNormalLogLevels = 1;
 //---------------------------------------------------------------------------
+//---------------------------------------------------------------------------
+TSshHostCA::TSshHostCA()
+{
+  PermitRsaSha1 = false;
+  PermitRsaSha256 = true;
+  PermitRsaSha512 = true;
+}
+//---------------------------------------------------------------------------
+void TSshHostCA::Load(THierarchicalStorage * Storage)
+{
+  PublicKey = DecodeBase64ToStr(Storage->ReadString(L"PublicKey", PublicKey));
+  ValidityExpression = Storage->ReadString(L"Validity", ValidityExpression);
+  PermitRsaSha1 = Storage->ReadBool(L"PermitRSASHA1", PermitRsaSha1);
+  PermitRsaSha256 = Storage->ReadBool(L"PermitRSASHA256", PermitRsaSha256);
+  PermitRsaSha512 = Storage->ReadBool(L"PermitRSASHA512", PermitRsaSha512);
+}
+//---------------------------------------------------------------------------
+void TSshHostCA::Save(THierarchicalStorage * Storage) const
+{
+  Storage->WriteString(L"PublicKey", EncodeStrToBase64(PublicKey));
+  Storage->WriteString(L"Validity", ValidityExpression);
+  Storage->WriteBool(L"PermitRSASHA1", PermitRsaSha1);
+  Storage->WriteBool(L"PermitRSASHA256", PermitRsaSha256);
+  Storage->WriteBool(L"PermitRSASHA512", PermitRsaSha512);
+}
+//---------------------------------------------------------------------------
+//---------------------------------------------------------------------------
+TSshHostCAList::TSshHostCAList()
+{
+}
+//---------------------------------------------------------------------------
+TSshHostCAList::TSshHostCAList(const TSshHostCA::TList & List)
+{
+  FList = List;
+}
+//---------------------------------------------------------------------------
+void TSshHostCAList::Default()
+{
+  FList.clear();
+}
+//---------------------------------------------------------------------------
+void TSshHostCAList::Save(THierarchicalStorage * Storage)
+{
+  Storage->ClearSubKeys();
+  TSshHostCA::TList::const_iterator I = FList.begin();
+  while (I != FList.end())
+  {
+    const TSshHostCA & SshHostCA = *I;
+    if (Storage->OpenSubKey(SshHostCA.Name, true))
+    {
+      SshHostCA.Save(Storage);
+      Storage->CloseSubKey();
+    }
+    ++I;
+  }
+}
+//---------------------------------------------------------------------------
+void TSshHostCAList::Load(THierarchicalStorage * Storage)
+{
+  FList.clear();
+  std::unique_ptr<TStrings> SubKeys(new TStringList());
+  Storage->GetSubKeyNames(SubKeys.get());
+
+  for (int Index = 0; Index < SubKeys->Count; Index++)
+  {
+    TSshHostCA SshHostCA;
+    SshHostCA.Name = SubKeys->Strings[Index];
+    if (Storage->OpenSubKey(SshHostCA.Name, false))
+    {
+      SshHostCA.Load(Storage);
+      Storage->CloseSubKey();
+
+      FList.push_back(SshHostCA);
+    }
+  }
+}
+//---------------------------------------------------------------------------
+int TSshHostCAList::GetCount() const
+{
+  return FList.size();
+}
+//---------------------------------------------------------------------------
+const TSshHostCA * TSshHostCAList::Get(int Index) const
+{
+  return &FList[Index];
+}
+//---------------------------------------------------------------------------
+const TSshHostCA * TSshHostCAList::Find(const UnicodeString & Name) const
+{
+  TSshHostCA::TList::const_iterator I = FList.begin();
+  while (I != FList.end())
+  {
+    const TSshHostCA & SshHostCA = *I;
+    if (SameStr(SshHostCA.Name, Name))
+    {
+      return &SshHostCA;
+    }
+    ++I;
+  }
+  return NULL;
+}
+//---------------------------------------------------------------------------
+TSshHostCA::TList TSshHostCAList::GetList() const
+{
+  return FList;
+}
+//---------------------------------------------------------------------------
+TSshHostCAList & TSshHostCAList::operator =(const TSshHostCAList & other)
+{
+  FList = other.FList;
+  return *this;
+}
+//---------------------------------------------------------------------------
+//---------------------------------------------------------------------------
 __fastcall TConfiguration::TConfiguration()
 {
   FCriticalSection = new TCriticalSection();
@@ -60,6 +175,7 @@ __fastcall TConfiguration::TConfiguration()
   FUsage = new TUsage(this);
   FDefaultCollectUsage = false;
   FScripting = false;
+  FSshHostCAList.reset(new TSshHostCAList());
 
   UnicodeString RandomSeedPath;
   if (!GetEnvironmentVariable(L"APPDATA").IsEmpty())
@@ -128,6 +244,7 @@ void __fastcall TConfiguration::Default()
   FQueueTransfersLimit = 2;
   FParallelTransferThreshold = -1; // default (currently off), 0 = explicitly off
   FKeyVersion = 0;
+  FSshHostCAList->Default();
   CollectUsage = FDefaultCollectUsage;
 
   FLogging = false;
@@ -307,6 +424,21 @@ void __fastcall TConfiguration::SaveExplicit()
   DoSave(false, true);
 }
 //---------------------------------------------------------------------------
+void TConfiguration::DoSave(THierarchicalStorage * AStorage, bool All)
+{
+  if (AStorage->OpenSubKey(ConfigurationSubKey, true))
+  {
+    SaveData(AStorage, All);
+    AStorage->CloseSubKey();
+  }
+
+  if (AStorage->OpenSubKey(SshHostCAsKey, true))
+  {
+    FSshHostCAList->Save(AStorage);
+    AStorage->CloseSubKey();
+  }
+}
+//---------------------------------------------------------------------------
 void __fastcall TConfiguration::DoSave(bool All, bool Explicit)
 {
   if (FDontSave) return;
@@ -317,13 +449,10 @@ void __fastcall TConfiguration::DoSave(bool All, bool Explicit)
     AStorage->AccessMode = smReadWrite;
     AStorage->Explicit = Explicit;
     AStorage->ForceSave = FForceSave;
-    if (AStorage->OpenSubKey(ConfigurationSubKey, true))
-    {
-      // if saving to TOptionsStorage, make sure we save everything so that
-      // all configuration is properly transferred to the master storage
-      bool ConfigAll = All || AStorage->Temporary;
-      SaveData(AStorage, ConfigAll);
-    }
+    // if saving to TOptionsStorage, make sure we save everything so that
+    // all configuration is properly transferred to the master storage
+    bool ConfigAll = All || AStorage->Temporary;
+    DoSave(AStorage, ConfigAll);
   }
   __finally
   {
@@ -384,10 +513,7 @@ void __fastcall TConfiguration::Export(const UnicodeString & FileName)
 
     CopyData(Storage, ExportStorage);
 
-    if (ExportStorage->OpenSubKey(ConfigurationSubKey, true))
-    {
-      SaveData(ExportStorage, true);
-    }
+    DoSave(ExportStorage, true);
   }
   __finally
   {
@@ -475,6 +601,11 @@ void __fastcall TConfiguration::LoadFrom(THierarchicalStorage * Storage)
     LoadData(Storage);
     Storage->CloseSubKey();
   }
+  if (Storage->OpenSubKey(SshHostCAsKey, false))
+  {
+    FSshHostCAList->Load(Storage);
+    Storage->CloseSubKey();
+  }
 }
 //---------------------------------------------------------------------------
 UnicodeString __fastcall TConfiguration::GetRegistryStorageOverrideKey()
@@ -2081,6 +2212,16 @@ void TConfiguration::SetQueueTransfersLimit(int value)
   SET_CONFIG_PROPERTY(QueueTransfersLimit);
 }
 //---------------------------------------------------------------------------
+const TSshHostCAList * TConfiguration::GetSshHostCAList()
+{
+  return FSshHostCAList.get();
+}
+//---------------------------------------------------------------------------
+void TConfiguration::SetSshHostCAList(const TSshHostCAList * value)
+{
+  *FSshHostCAList = *value;
+}
+//---------------------------------------------------------------------------
 bool __fastcall TConfiguration::GetPersistent()
 {
   return (Storage != stNul) && !FDontSave;

+ 42 - 0
source/core/Configuration.h

@@ -3,6 +3,7 @@
 #define ConfigurationH
 
 #include <set>
+#include <list>
 #include "RemoteFiles.h"
 #include "FileBuffer.h"
 #include "HierarchicalStorage.h"
@@ -20,6 +21,42 @@ enum TAutoSwitch { asOn, asOff, asAuto }; // Has to match PuTTY FORCE_ON, FORCE_
 class TStoredSessionList;
 class TCopyParamType;
 //---------------------------------------------------------------------------
+class TSshHostCA
+{
+public:
+  TSshHostCA();
+  void Save(THierarchicalStorage * Storage) const;
+  void Load(THierarchicalStorage * Storage);
+
+  UnicodeString Name;
+  RawByteString PublicKey;
+  UnicodeString ValidityExpression;
+  bool PermitRsaSha1;
+  bool PermitRsaSha256;
+  bool PermitRsaSha512;
+
+  typedef std::vector<TSshHostCA> TList;
+};
+//---------------------------------------------------------------------------
+class TSshHostCAList
+{
+public:
+  TSshHostCAList();
+  TSshHostCAList(const TSshHostCA::TList & List);
+  TSshHostCAList & operator =(const TSshHostCAList & other);
+  void Default();
+  TSshHostCA::TList GetList() const;
+  int GetCount() const;
+  const TSshHostCA * Get(int Index) const;
+  const TSshHostCA * Find(const UnicodeString & Name) const;
+
+  void Save(THierarchicalStorage * Storage);
+  void Load(THierarchicalStorage * Storage);
+
+private:
+  TSshHostCA::TList FList;
+};
+//---------------------------------------------------------------------------
 class TConfiguration : public TObject
 {
 private:
@@ -85,6 +122,7 @@ private:
   UnicodeString FCertificateStorage;
   UnicodeString FAWSMetadataService;
   UnicodeString FChecksumCommands;
+  std::unique_ptr<TSshHostCAList> FSshHostCAList;
 
   bool FDisablePasswordStoring;
   bool FForceBanners;
@@ -159,6 +197,8 @@ private:
   void SetLocalPortNumberMin(int value);
   void SetLocalPortNumberMax(int value);
   void SetQueueTransfersLimit(int value);
+  const TSshHostCAList * GetSshHostCAList();
+  void SetSshHostCAList(const TSshHostCAList * value);
 
 protected:
   TStorage FStorage;
@@ -181,6 +221,7 @@ protected:
   void __fastcall SetBannerData(const UnicodeString & SessionKey, const UnicodeString & BannerHash, unsigned int Params);
   void __fastcall GetBannerData(const UnicodeString & SessionKey, UnicodeString & BannerHash, unsigned int & Params);
   static UnicodeString __fastcall PropertyToKey(const UnicodeString & Property);
+  void DoSave(THierarchicalStorage * AStorage, bool All);
   virtual void __fastcall DoSave(bool All, bool Explicit);
   UnicodeString __fastcall FormatFingerprintKey(const UnicodeString & SiteKey, const UnicodeString & FingerprintType);
   THierarchicalStorage * OpenDirectoryStatisticsCache(bool CanCreate);
@@ -349,6 +390,7 @@ public:
   __property int QueueTransfersLimit = { read = FQueueTransfersLimit, write = SetQueueTransfersLimit };
   __property int ParallelTransferThreshold = { read = FParallelTransferThreshold, write = FParallelTransferThreshold };
   __property int KeyVersion = { read = FKeyVersion, write = FKeyVersion };
+  __property TSshHostCAList * SshHostCAList = { read = GetSshHostCAList, write = SetSshHostCAList };
 
   __property UnicodeString TimeFormat = { read = GetTimeFormat };
   __property TStorage Storage  = { read=GetStorage };

+ 1 - 2
source/core/Cryptography.cpp

@@ -849,8 +849,7 @@ UnicodeString TEncryption::DecryptFileName(const UnicodeString & FileName)
   {
     Base64 += UnicodeString::StringOfChar(L'=', Padding);
   }
-  DynamicArray<Byte> BufferBytes = DecodeBase64(Base64);
-  RawByteString Buffer(reinterpret_cast<const char *>(&BufferBytes[0]), BufferBytes.Length);
+  RawByteString Buffer = DecodeBase64ToStr(Base64);
   FSalt = Buffer.SubString(1, SALT_LENGTH(PASSWORD_MANAGER_AES_MODE));
   SetSalt();
   Buffer.Delete(1, FSalt.Length());

+ 126 - 34
source/core/PuttyIntf.cpp

@@ -265,12 +265,8 @@ static void connection_fatal(Seat * seat, const char * message)
 SeatPromptResult confirm_ssh_host_key(Seat * seat, const char * host, int port, const char * keytype,
   char * keystr, SeatDialogText *, HelpCtx,
   void (*DebugUsedArg(callback))(void *ctx, SeatPromptResult result), void * DebugUsedArg(ctx),
-  char **key_fingerprints, bool is_certificate)
+  char **key_fingerprints, bool is_certificate, int ca_count, bool already_verified)
 {
-  if (DebugAlwaysFalse(is_certificate))
-  {
-    NotImplemented();
-  }
   UnicodeString FingerprintSHA256, FingerprintMD5;
   if (key_fingerprints[SSH_FPTYPE_SHA256] != NULL)
   {
@@ -281,7 +277,8 @@ SeatPromptResult confirm_ssh_host_key(Seat * seat, const char * host, int port,
     FingerprintMD5 = key_fingerprints[SSH_FPTYPE_MD5];
   }
   TSecureShell * SecureShell = static_cast<ScpSeat *>(seat)->SecureShell;
-  SecureShell->VerifyHostKey(host, port, keytype, keystr, FingerprintSHA256, FingerprintMD5);
+  SecureShell->VerifyHostKey(
+    host, port, keytype, keystr, FingerprintSHA256, FingerprintMD5, is_certificate, ca_count, already_verified);
 
   // We should return 0 when key was not confirmed, we throw exception instead.
   return SPR_OK;
@@ -978,6 +975,11 @@ void FreeKey(TPrivateKey * PrivateKey)
   sfree(Ssh2Key);
 }
 //---------------------------------------------------------------------------
+RawByteString StrBufToString(strbuf * StrBuf)
+{
+  return RawByteString(reinterpret_cast<char *>(StrBuf->s), StrBuf->len);
+}
+//---------------------------------------------------------------------------
 RawByteString LoadPublicKey(
   const UnicodeString & FileName, UnicodeString & Algorithm, UnicodeString & Comment, bool & HasCertificate)
 {
@@ -1001,7 +1003,7 @@ RawByteString LoadPublicKey(
     sfree(AlgorithmStr);
     Comment = UnicodeString(AnsiString(CommentStr));
     sfree(CommentStr);
-    Result = RawByteString(reinterpret_cast<char *>(PublicKeyBuf->s), PublicKeyBuf->len);
+    Result = StrBufToString(PublicKeyBuf);
     strbuf_free(PublicKeyBuf);
   }
   __finally
@@ -1161,7 +1163,7 @@ UnicodeString __fastcall ParseOpenSshPubLine(const UnicodeString & Line, const s
   {
     try
     {
-      Algorithm = find_pubkey_alg_winscp_host(AlgorithmName);
+      Algorithm = find_pubkey_alg(AlgorithmName);
       if (Algorithm == NULL)
       {
         throw Exception(FMTLOAD(PUB_KEY_UNKNOWN, (AlgorithmName)));
@@ -1188,38 +1190,81 @@ UnicodeString __fastcall ParseOpenSshPubLine(const UnicodeString & Line, const s
   return Result;
 }
 //---------------------------------------------------------------------------
-UnicodeString __fastcall GetKeyTypeHuman(const UnicodeString & KeyType)
+// Based on ca_refresh_pubkey_info
+void ParseCertificatePublicKey(const UnicodeString & Str, RawByteString & PublicKey, UnicodeString & Fingerprint)
 {
-  UnicodeString Result;
-  if (KeyType == ssh_dsa.cache_id)
-  {
-    Result = L"DSA";
-  }
-  else if ((KeyType == ssh_rsa.cache_id) ||
-           (KeyType == L"rsa")) // SSH1
-  {
-    Result = L"RSA";
-  }
-  else if (KeyType == ssh_ecdsa_ed25519.cache_id)
-  {
-    Result = L"Ed25519";
-  }
-  else if (KeyType == ssh_ecdsa_nistp256.cache_id)
-  {
-    Result = L"ECDSA/nistp256";
-  }
-  else if (KeyType == ssh_ecdsa_nistp384.cache_id)
+  AnsiString AnsiStr = AnsiString(Str);
+  ptrlen Data = ptrlen_from_asciz(AnsiStr.c_str());
+  strbuf * Blob = strbuf_new();
+  try
   {
-    Result = L"ECDSA/nistp384";
+    // See if we have a plain base64-encoded public key blob.
+    if (base64_valid(Data))
+    {
+      base64_decode_bs(BinarySink_UPCAST(Blob), Data);
+    }
+    else
+    {
+      // Otherwise, try to decode as if it was a public key _file_.
+      BinarySource Src[1];
+      BinarySource_BARE_INIT_PL(Src, Data);
+      const char * Error;
+      if (!ppk_loadpub_s(Src, NULL, BinarySink_UPCAST(Blob), NULL, &Error))
+      {
+        throw Exception(FMTLOAD(SSH_HOST_CA_DECODE_ERROR, (Error)));
+      }
+    }
+
+    ptrlen AlgNamePtrLen = pubkey_blob_to_alg_name(ptrlen_from_strbuf(Blob));
+    if (!AlgNamePtrLen.len)
+    {
+      throw Exception(LoadStr(SSH_HOST_CA_NO_KEY_TYPE));
+    }
+
+    UnicodeString AlgName = UnicodeString(AnsiString(static_cast<const char *>(AlgNamePtrLen.ptr), AlgNamePtrLen.len));
+    const ssh_keyalg * Alg = find_pubkey_alg_len(AlgNamePtrLen);
+    if (Alg == NULL)
+    {
+      throw Exception(FMTLOAD(PUB_KEY_UNKNOWN, (AlgName)));
+    }
+    if (Alg->is_certificate)
+    {
+      throw Exception(FMTLOAD(SSH_HOST_CA_CERTIFICATE, (AlgName)));
+    }
+
+    ssh_key * Key = ssh_key_new_pub(Alg, ptrlen_from_strbuf(Blob));
+    if (Key == NULL)
+    {
+      throw Exception(FMTLOAD(SSH_HOST_CA_INVALID, (AlgName)));
+    }
+
+    char * FingerprintPtr = ssh2_fingerprint(Key, SSH_FPTYPE_DEFAULT);
+    Fingerprint = UnicodeString(FingerprintPtr);
+    sfree(FingerprintPtr);
+    ssh_key_free(Key);
+
+    PublicKey = StrBufToString(Blob);
   }
-  else if (KeyType == ssh_ecdsa_nistp521.cache_id)
+  __finally
   {
-    Result = L"ECDSA/nistp521";
+    strbuf_free(Blob);
   }
-  else
+}
+//---------------------------------------------------------------------------
+bool IsCertificateValidityExpressionValid(
+  const UnicodeString & Str, UnicodeString & Error, int & ErrorStart, int & ErrorLen)
+{
+  char * ErrorMsg;
+  ptrlen ErrorLoc;
+  AnsiString StrAnsi(Str);
+  const char * StrPtr = StrAnsi.c_str();
+  bool Result = cert_expr_valid(StrPtr, &ErrorMsg, &ErrorLoc);
+  if (!Result)
   {
-    DebugFail();
-    Result = KeyType;
+    Error = UnicodeString(ErrorMsg);
+    sfree(ErrorMsg);
+    ErrorStart = static_cast<const char *>(ErrorLoc.ptr) - StrPtr;
+    ErrorLen = ErrorLoc.len;
   }
   return Result;
 }
@@ -1466,4 +1511,51 @@ void SavePuttyDefaults(const UnicodeString & Name)
   }
 }
 //---------------------------------------------------------------------------
+struct host_ca_enum
+{
+  int Index;
+};
+//---------------------------------------------------------------------------
+host_ca_enum * enum_host_ca_start()
+{
+  host_ca_enum * Result = new host_ca_enum();
+  Result->Index = 0;
+  return Result;
+}
+//---------------------------------------------------------------------------
+bool enum_host_ca_next(host_ca_enum * Enum, strbuf * StrBuf)
+{
+  const TSshHostCAList * SshHostCAList = Configuration->SshHostCAList;
+  bool Result = (Enum->Index < SshHostCAList->GetCount());
+  if (Result)
+  {
+    put_asciz(StrBuf, UTF8String(SshHostCAList->Get(Enum->Index)->Name).c_str());
+    Enum->Index++;
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+void enum_host_ca_finish(host_ca_enum * Enum)
+{
+  delete Enum;
+}
+//---------------------------------------------------------------------------
+host_ca * host_ca_load(const char * NameStr)
+{
+  host_ca * Result = NULL;
+  UnicodeString Name = UTF8String(NameStr);
+  const TSshHostCA * SshHostCA = Configuration->SshHostCAList->Find(Name);
+  if (DebugAlwaysTrue(SshHostCA != NULL))
+  {
+    Result = host_ca_new();
+    Result->name = dupstr(NameStr);
+    Result->ca_public_key = strbuf_dup(make_ptrlen(SshHostCA->PublicKey.c_str(), SshHostCA->PublicKey.Length()));
+    Result->validity_expression = dupstr(UTF8String(SshHostCA->ValidityExpression).c_str());
+    Result->opts.permit_rsa_sha1 = SshHostCA->PermitRsaSha1;
+    Result->opts.permit_rsa_sha256 = SshHostCA->PermitRsaSha256;
+    Result->opts.permit_rsa_sha512 = SshHostCA->PermitRsaSha512;
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
 //---------------------------------------------------------------------------

+ 5 - 2
source/core/PuttyTools.h

@@ -21,6 +21,8 @@ void AddCertificateToKey(TPrivateKey * PrivateKey, const UnicodeString & Certifi
 void SaveKey(TKeyType KeyType, const UnicodeString & FileName,
   const UnicodeString & Passphrase, TPrivateKey * PrivateKey);
 void FreeKey(TPrivateKey * PrivateKey);
+RawByteString LoadPublicKey(
+  const UnicodeString & FileName, UnicodeString & Algorithm, UnicodeString & Comment, bool & HasCertificate);
 UnicodeString GetPublicKeyLine(const UnicodeString & FileName, UnicodeString & Comment, bool & HasCertificate);
 extern const UnicodeString PuttyKeyExt;
 //---------------------------------------------------------------------------
@@ -39,8 +41,9 @@ UnicodeString __fastcall Sha256(const char * Data, size_t Size);
 void __fastcall DllHijackingProtection();
 //---------------------------------------------------------------------------
 UnicodeString __fastcall ParseOpenSshPubLine(const UnicodeString & Line, const struct ssh_keyalg *& Algorithm);
-//---------------------------------------------------------------------------
-UnicodeString __fastcall GetKeyTypeHuman(const UnicodeString & KeyType);
+void ParseCertificatePublicKey(const UnicodeString & Str, RawByteString & PublicKey, UnicodeString & Fingerprint);
+bool IsCertificateValidityExpressionValid(
+  const UnicodeString & Str, UnicodeString & Error, int & ErrorStart, int & ErrorLen);
 //---------------------------------------------------------------------------
 bool IsOpenSSH(const UnicodeString & SshImplementation);
 //---------------------------------------------------------------------------

+ 260 - 216
source/core/SecureShell.cpp

@@ -2468,11 +2468,13 @@ void TSecureShell::ParseFingerprint(const UnicodeString & Fingerprint, UnicodeSt
 //---------------------------------------------------------------------------
 void __fastcall TSecureShell::VerifyHostKey(
   const UnicodeString & AHost, int Port, const UnicodeString & KeyType, const UnicodeString & KeyStr,
-  const UnicodeString & AFingerprintSHA256, const UnicodeString & AFingerprintMD5)
+  const UnicodeString & AFingerprintSHA256, const UnicodeString & AFingerprintMD5,
+  bool IsCertificate, int CACount, bool AlreadyVerified)
 {
   if (Configuration->ActualLogProtocol >= 1)
   {
-    LogEvent(FORMAT(L"Verifying host key %s %s with fingerprints %s, %s", (KeyType, FormatKeyStr(KeyStr), AFingerprintSHA256, AFingerprintMD5)));
+    UnicodeString HostKeyAction = AlreadyVerified ? L"Got" : L"Verifying";
+    LogEvent(FORMAT(L"%s host key %s %s with fingerprints %s, %s", (HostKeyAction, KeyType, FormatKeyStr(KeyStr), AFingerprintSHA256, AFingerprintMD5)));
   }
 
   GotHostKey();
@@ -2507,257 +2509,299 @@ void __fastcall TSecureShell::VerifyHostKey(
     Abort();
   }
 
-  bool AcceptNew = HaveAcceptNewHostKeyPolicy();
-  UnicodeString ConfigHostKey;
-  if (!AcceptNew)
+  if (!AlreadyVerified)
   {
-    ConfigHostKey = FSessionData->HostKey;
-  }
-
-  UnicodeString StoredKeys = RetrieveHostKey(Host, Port, KeyType);
-  bool Result = VerifyCachedHostKey(StoredKeys, KeyStr, FingerprintMD5, FingerprintSHA256);
-  if (!Result && AcceptNew)
-  {
-    if (!StoredKeys.IsEmpty()) // optimization + avoiding the log message
+    bool AcceptNew = HaveAcceptNewHostKeyPolicy();
+    UnicodeString ConfigHostKey;
+    if (!AcceptNew)
     {
-      AcceptNew = false;
+      ConfigHostKey = FSessionData->HostKey;
     }
-    else if (have_any_ssh2_hostkey(FSeat, AnsiString(Host).c_str(), Port))
-    {
-      LogEvent(L"Host key not found in the cache, but other key types found, cannot accept new key");
-      AcceptNew = false;
-    }
-  }
-
-  bool ConfiguredKeyNotMatch = false;
 
-  if (!Result && !ConfigHostKey.IsEmpty() &&
-      // Should test have_any_ssh2_hostkey + No need to bother with AcceptNew, as we never get here
-      (StoredKeys.IsEmpty() || FSessionData->OverrideCachedHostKey))
-  {
-    UnicodeString Buf = ConfigHostKey;
-    while (!Result && !Buf.IsEmpty())
+    UnicodeString StoredKeys = RetrieveHostKey(Host, Port, KeyType);
+    bool Result = VerifyCachedHostKey(StoredKeys, KeyStr, FingerprintMD5, FingerprintSHA256);
+    if (!Result && AcceptNew)
     {
-      UnicodeString ExpectedKey = CutToChar(Buf, HostKeyDelimiter, false);
-      if (ExpectedKey == L"*")
+      if (!StoredKeys.IsEmpty()) // optimization + avoiding the log message
       {
-        UnicodeString Message = LoadStr(ANY_HOSTKEY);
-        FUI->Information(Message, true);
-        FLog->Add(llException, Message);
-        Result = true;
+        AcceptNew = false;
       }
-      else if (VerifyFingerprint(ExpectedKey, FingerprintMD5, FingerprintSHA256))
+      else if (have_any_ssh2_hostkey(FSeat, AnsiString(Host).c_str(), Port))
       {
-        LogEvent(L"Host key matches configured key fingerprint");
-        Result = true;
-      }
-      else
-      {
-        LogEvent(FORMAT(L"Host key does not match configured key fingerprint %s", (ExpectedKey)));
+        LogEvent(L"Host key not found in the cache, but other key types found, cannot accept new key");
+        AcceptNew = false;
       }
     }
 
-    if (!Result)
-    {
-      ConfiguredKeyNotMatch = true;
-    }
-  }
+    bool ConfiguredKeyNotMatch = false;
 
-  if (!Result && AcceptNew && DebugAlwaysTrue(ConfigHostKey.IsEmpty()))
-  {
-    try
+    if (!Result && !ConfigHostKey.IsEmpty() &&
+        // Should test have_any_ssh2_hostkey + No need to bother with AcceptNew, as we never get here
+        (StoredKeys.IsEmpty() || FSessionData->OverrideCachedHostKey))
     {
-      UnicodeString StorageSource = StoreHostKey(Host, Port, KeyType, KeyStr);
-      UnicodeString StoredKeys = RetrieveHostKey(Host, Port, KeyType);
-      if (StoredKeys != KeyStr)
+      UnicodeString Buf = ConfigHostKey;
+      while (!Result && !Buf.IsEmpty())
+      {
+        UnicodeString ExpectedKey = CutToChar(Buf, HostKeyDelimiter, false);
+        if (ExpectedKey == L"*")
+        {
+          UnicodeString Message = LoadStr(ANY_HOSTKEY);
+          FUI->Information(Message, true);
+          FLog->Add(llException, Message);
+          Result = true;
+        }
+        else if (VerifyFingerprint(ExpectedKey, FingerprintMD5, FingerprintSHA256))
+        {
+          LogEvent(L"Host key matches configured key fingerprint");
+          Result = true;
+        }
+        else
+        {
+          LogEvent(FORMAT(L"Host key does not match configured key fingerprint %s", (ExpectedKey)));
+        }
+      }
+
+      if (!Result)
       {
-        throw Exception(UnicodeString());
+        ConfiguredKeyNotMatch = true;
       }
-      Configuration->Usage->Inc(L"HostKeyNewAccepted");
-      LogEvent(FORMAT(L"Warning: Stored new host key to %s - This should occur only on the first connection", (StorageSource)));
-      Result = true;
-    }
-    catch (Exception & E)
-    {
-      FUI->FatalError(&E, LoadStr(STORE_NEW_HOSTKEY_ERROR));
     }
-  }
 
-  if (!Result)
-  {
-    bool Verified;
-    if (ConfiguredKeyNotMatch || Configuration->DisableAcceptingHostKeys)
+    if (!Result && AcceptNew && DebugAlwaysTrue(ConfigHostKey.IsEmpty()))
     {
-      Verified = false;
-    }
-    // no point offering manual verification, if we cannot persist the verified key
-    else if (!Configuration->Persistent && Configuration->Scripting)
-    {
-      Verified = false;
-    }
-    else
-    {
-      // We should not offer caching if !Configuration->Persistent,
-      // but as scripting mode is handled earlier and in GUI it hardly happens,
-      // it's a small issue.
-      TClipboardHandler ClipboardHandler;
-      ClipboardHandler.Text = FingerprintSHA256 + L"\n" + FingerprintMD5;
-      TPasteKeyHandler PasteKeyHandler;
-      PasteKeyHandler.KeyStr = KeyStr;
-      PasteKeyHandler.FingerprintMD5 = FingerprintMD5;
-      PasteKeyHandler.FingerprintSHA256 = FingerprintSHA256;
-      PasteKeyHandler.UI = FUI;
-
-      bool Unknown = StoredKeys.IsEmpty();
-
-      UnicodeString AcceptButton = LoadStr(HOSTKEY_ACCEPT_BUTTON);
-      UnicodeString OnceButton = LoadStr(HOSTKEY_ONCE_BUTTON);
-      UnicodeString CancelButton = Vcl_Consts_SMsgDlgCancel;
-      UnicodeString UpdateButton = LoadStr(UPDATE_KEY_BUTTON);
-      UnicodeString AddButton = LoadStr(ADD_KEY_BUTTON);
-      int Answers;
-      std::vector<TQueryButtonAlias> Aliases;
-
-      TQueryButtonAlias CopyAlias;
-      CopyAlias.Button = qaRetry;
-      CopyAlias.Alias = LoadStr(COPY_KEY_BUTTON);
-      CopyAlias.ActionAlias = LoadStr(COPY_KEY_ACTION);
-      CopyAlias.OnSubmit = &ClipboardHandler.Copy;
-      Aliases.push_back(CopyAlias);
-
-      TQueryButtonAlias PasteAlias;
-      PasteAlias.Button = qaIgnore;
-      PasteAlias.Alias = LoadStr(PASTE_KEY_BUTTON);
-      PasteAlias.OnSubmit = &PasteKeyHandler.Paste;
-      PasteAlias.GroupWith = qaYes;
-      Aliases.push_back(PasteAlias);
-
-      TQueryButtonAlias OnceAlias;
-      OnceAlias.Button = qaOK;
-      OnceAlias.Alias = OnceButton;
-      OnceAlias.GroupWith = qaYes;
-      Aliases.push_back(OnceAlias);
-
-      Answers = qaYes | qaOK | qaCancel | qaRetry | qaIgnore;
-      if (!Unknown)
+      try
       {
-        TQueryButtonAlias UpdateAlias;
-        UpdateAlias.Button = qaYes;
-        UpdateAlias.Alias = UpdateButton;
-        Aliases.push_back(UpdateAlias);
-
-        TQueryButtonAlias AddAlias;
-        AddAlias.Button = qaNo;
-        AddAlias.Alias = AddButton;
-        AddAlias.GroupWith = qaYes;
-        Aliases.push_back(AddAlias);
-
-        Answers |= qaNo;
+        UnicodeString StorageSource = StoreHostKey(Host, Port, KeyType, KeyStr);
+        UnicodeString StoredKeys = RetrieveHostKey(Host, Port, KeyType);
+        if (StoredKeys != KeyStr)
+        {
+          throw Exception(UnicodeString());
+        }
+        Configuration->Usage->Inc(L"HostKeyNewAccepted");
+        LogEvent(FORMAT(L"Warning: Stored new host key to %s - This should occur only on the first connection", (StorageSource)));
+        Result = true;
       }
-      else
+      catch (Exception & E)
       {
-        TQueryButtonAlias AcceptAlias;
-        AcceptAlias.Button = qaYes;
-        AcceptAlias.Alias = AcceptButton;
-        Aliases.push_back(AcceptAlias);
+        FUI->FatalError(&E, LoadStr(STORE_NEW_HOSTKEY_ERROR));
       }
+    }
 
-      TQueryParams Params(qpWaitInBatch);
-      Params.NoBatchAnswers = qaYes | qaNo | qaRetry | qaIgnore | qaOK;
-      Params.HelpKeyword = (Unknown ? HELP_UNKNOWN_KEY : HELP_DIFFERENT_KEY);
-      Params.Aliases = &Aliases[0];
-      Params.AliasesCount = Aliases.size();
-
-      UnicodeString NewLine = L"\n";
-      UnicodeString Para = NewLine + NewLine;
-      UnicodeString Message;
-      UnicodeString ServerPara = FMTLOAD(HOSTKEY_SERVER, (Host, Port)) + Para;
-      UnicodeString KeyTypeHuman = GetKeyTypeHuman(KeyType);
-      UnicodeString Nbsp = L"\xA0";
-      UnicodeString Indent = Nbsp + Nbsp + Nbsp + Nbsp;
-      UnicodeString FingerprintPara =
-        Indent + FMTLOAD(HOSTKEY_FINGERPRINT, (KeyTypeHuman)) + NewLine +
-        Indent + ReplaceStr(FingerprintSHA256, L" ", Nbsp) + Para;
-      if (Unknown)
+    if (!Result)
+    {
+      bool Verified;
+      if (ConfiguredKeyNotMatch || Configuration->DisableAcceptingHostKeys)
       {
-        Message =
-          MainInstructions(LoadStr(HOSTKEY_UNKNOWN)) + Para +
-          LoadStr(HOSTKEY_NOT_CACHED) + NewLine +
-          ServerPara +
-          LoadStr(HOSTKEY_NO_GUARANTEE) + Para +
-          FingerprintPara +
-          FMTLOAD(HOSTKEY_ACCEPT_NEW, (StripHotkey(AcceptButton))) + NewLine +
-          FMTLOAD(HOSTKEY_ONCE_NEW, (StripHotkey(OnceButton))) + NewLine +
-          FMTLOAD(HOSTKEY_CANCEL_NEW, (StripHotkey(CancelButton)));
+        Verified = false;
       }
-      else
+      // no point offering manual verification, if we cannot persist the verified key
+      else if (!Configuration->Persistent && Configuration->Scripting)
       {
-        Message =
-          MainInstructions(LoadStr(HOSTKEY_SECURITY_BREACH)) + Para +
-          LoadStr(HOSTKEY_DOESNT_MATCH) + NewLine +
-          ServerPara +
-          FMTLOAD(HOSTKEY_TWO_EXPLANATIONS, (LoadStr(HOSTKEY_ADMINISTRATOR_CHANGED), LoadStr(HOSTKEY_ANOTHER_COMPUTER))) + Para +
-          FingerprintPara +
-          FMTLOAD(HOSTKEY_ACCEPT_CHANGE, (StripHotkey(UpdateButton), StripHotkey(AddButton))) + NewLine +
-          FMTLOAD(HOSTKEY_ONCE_CHANGE, (StripHotkey(OnceButton))) + NewLine +
-          FMTLOAD(HOSTKEY_CANCEL_CHANGE, (StripHotkey(CancelButton), StripHotkey(CancelButton)));
+        Verified = false;
       }
-
-      if (Configuration->Scripting)
+      else
       {
-        AddToList(Message, LoadStr(SCRIPTING_USE_HOSTKEY), Para);
-      }
+        // We should not offer caching if !Configuration->Persistent,
+        // but as scripting mode is handled earlier and in GUI it hardly happens,
+        // it's a small issue.
+        TClipboardHandler ClipboardHandler;
+        ClipboardHandler.Text = FingerprintSHA256 + L"\n" + FingerprintMD5;
+        TPasteKeyHandler PasteKeyHandler;
+        PasteKeyHandler.KeyStr = KeyStr;
+        PasteKeyHandler.FingerprintMD5 = FingerprintMD5;
+        PasteKeyHandler.FingerprintSHA256 = FingerprintSHA256;
+        PasteKeyHandler.UI = FUI;
+
+        bool Unknown = StoredKeys.IsEmpty();
+
+        UnicodeString AcceptButton = LoadStr(HOSTKEY_ACCEPT_BUTTON);
+        UnicodeString OnceButton = LoadStr(HOSTKEY_ONCE_BUTTON);
+        UnicodeString CancelButton = Vcl_Consts_SMsgDlgCancel;
+        UnicodeString UpdateButton = LoadStr(UPDATE_KEY_BUTTON);
+        UnicodeString AddButton = LoadStr(ADD_KEY_BUTTON);
+        int Answers;
+        std::vector<TQueryButtonAlias> Aliases;
+
+        TQueryButtonAlias CopyAlias;
+        CopyAlias.Button = qaRetry;
+        CopyAlias.Alias = LoadStr(COPY_KEY_BUTTON);
+        CopyAlias.ActionAlias = LoadStr(COPY_KEY_ACTION);
+        CopyAlias.OnSubmit = &ClipboardHandler.Copy;
+        Aliases.push_back(CopyAlias);
+
+        TQueryButtonAlias PasteAlias;
+        PasteAlias.Button = qaIgnore;
+        PasteAlias.Alias = LoadStr(PASTE_KEY_BUTTON);
+        PasteAlias.OnSubmit = &PasteKeyHandler.Paste;
+        PasteAlias.GroupWith = qaYes;
+        Aliases.push_back(PasteAlias);
+
+        TQueryButtonAlias OnceAlias;
+        OnceAlias.Button = qaOK;
+        OnceAlias.Alias = OnceButton;
+        OnceAlias.GroupWith = qaYes;
+        Aliases.push_back(OnceAlias);
+
+        Answers = qaYes | qaOK | qaCancel | qaRetry | qaIgnore;
+        if (!Unknown)
+        {
+          TQueryButtonAlias UpdateAlias;
+          UpdateAlias.Button = qaYes;
+          UpdateAlias.Alias = UpdateButton;
+          Aliases.push_back(UpdateAlias);
+
+          TQueryButtonAlias AddAlias;
+          AddAlias.Button = qaNo;
+          AddAlias.Alias = AddButton;
+          AddAlias.GroupWith = qaYes;
+          Aliases.push_back(AddAlias);
+
+          Answers |= qaNo;
+        }
+        else
+        {
+          TQueryButtonAlias AcceptAlias;
+          AcceptAlias.Button = qaYes;
+          AcceptAlias.Alias = AcceptButton;
+          Aliases.push_back(AcceptAlias);
+        }
 
-      unsigned int R =
-        FUI->QueryUser(Message, NULL, Answers, &Params, qtWarning);
-      UnicodeString StoreKeyStr = KeyStr;
+        TQueryParams Params(qpWaitInBatch);
+        Params.NoBatchAnswers = qaYes | qaNo | qaRetry | qaIgnore | qaOK;
+        Params.HelpKeyword = (Unknown ? HELP_UNKNOWN_KEY : HELP_DIFFERENT_KEY);
+        Params.Aliases = &Aliases[0];
+        Params.AliasesCount = Aliases.size();
+
+        UnicodeString NewLine = L"\n";
+        UnicodeString Para = NewLine + NewLine;
+        UnicodeString Message;
+        UnicodeString ServerPara = FMTLOAD(HOSTKEY_SERVER, (Host, Port)) + Para;
+        UnicodeString Nbsp = L"\xA0";
+        UnicodeString Indent = Nbsp + Nbsp + Nbsp + Nbsp;
+        UnicodeString FingerprintPara =
+          Indent + FMTLOAD(HOSTKEY_FINGERPRINT, (KeyType)) + NewLine +
+          Indent + ReplaceStr(FingerprintSHA256, L" ", Nbsp) + Para;
+        UnicodeString AdministratorChangerOrAnotherComputerExplanationPara =
+          FMTLOAD(HOSTKEY_TWO_EXPLANATIONS, (LoadStr(HOSTKEY_ADMINISTRATOR_CHANGED), LoadStr(HOSTKEY_ANOTHER_COMPUTER))) + Para;
+        UnicodeString CertifiedTrustLine;
+        if (IsCertificate)
+        {
+          Message =
+            MainInstructions(LoadStr(HOSTKEY_SECURITY_BREACH)) + Para +
+            LoadStr(HOSTKEY_CERTIFIED1) + NewLine +
+            ServerPara;
+          if (CACount > 0)
+          {
+            Message +=
+              LoadStr(HOSTKEY_CERTIFIED2) + Para;
+            if (!Unknown)
+            {
+              Message += LoadStr(HOSTKEY_CERTIFIED_DOESNT_MATCH_ALSO) + Para;
+            }
+            Message +=
+              FMTLOAD(HOSTKEY_TWO_EXPLANATIONS, (LoadStr(HOSTKEY_CERTIFIED_ANOTHER), LoadStr(HOSTKEY_ANOTHER_COMPUTER))) + Para;
+          }
+          else
+          {
+            Message +=
+              LoadStr(HOSTKEY_CERTIFIED_DOESNT_MATCH) + Para +
+              AdministratorChangerOrAnotherComputerExplanationPara;
+          }
 
-      switch (R) {
-        case qaNo:
-          DebugAssert(!Unknown);
-          StoreKeyStr = (StoredKeys + HostKeyDelimiter + StoreKeyStr);
-          // fall thru
-        case qaYes:
-          StoreHostKey(Host, Port, KeyType, StoreKeyStr);
-          Verified = true;
-          break;
+          CertifiedTrustLine = LoadStr(HOSTKEY_CERTIFIED_TRUST) + NewLine;
+        }
+        else if (Unknown)
+        {
+          Message =
+            MainInstructions(LoadStr(HOSTKEY_UNKNOWN)) + Para +
+            LoadStr(HOSTKEY_NOT_CACHED) + NewLine +
+            ServerPara +
+            LoadStr(HOSTKEY_NO_GUARANTEE) + Para;
+        }
+        else
+        {
+          Message =
+            MainInstructions(LoadStr(HOSTKEY_SECURITY_BREACH)) + Para +
+            LoadStr(HOSTKEY_DOESNT_MATCH) + NewLine +
+            ServerPara +
+            AdministratorChangerOrAnotherComputerExplanationPara;
+        }
 
-        case qaCancel:
-          Verified = false;
-          break;
+        Message += FingerprintPara;
 
-        default:
-          Verified = true;
-          break;
-      }
-    }
+        if (Unknown)
+        {
+          Message +=
+            FMTLOAD(HOSTKEY_ACCEPT_NEW, (StripHotkey(AcceptButton))) + NewLine +
+            CertifiedTrustLine +
+            FMTLOAD(HOSTKEY_ONCE_NEW, (StripHotkey(OnceButton))) + NewLine +
+            FMTLOAD(HOSTKEY_CANCEL_NEW, (StripHotkey(CancelButton)));
+        }
+        else
+        {
+          Message +=
+            FMTLOAD(HOSTKEY_ACCEPT_CHANGE, (StripHotkey(UpdateButton), StripHotkey(AddButton))) + NewLine +
+            CertifiedTrustLine +
+            FMTLOAD(HOSTKEY_ONCE_CHANGE, (StripHotkey(OnceButton))) + NewLine +
+            FMTLOAD(HOSTKEY_CANCEL_CHANGE, (StripHotkey(CancelButton), StripHotkey(CancelButton)));
+        }
 
-    if (!Verified)
-    {
-      Configuration->Usage->Inc(L"HostNotVerified");
+        if (Configuration->Scripting)
+        {
+          AddToList(Message, LoadStr(SCRIPTING_USE_HOSTKEY), Para);
+        }
 
-      UnicodeString Message;
-      if (ConfiguredKeyNotMatch)
-      {
-        Message = FMTLOAD(CONFIGURED_KEY_NOT_MATCH, (ConfigHostKey));
-      }
-      else if (!Configuration->Persistent && Configuration->Scripting)
-      {
-        Message = LoadStr(HOSTKEY_NOT_CONFIGURED);
-      }
-      else
-      {
-        Message = LoadStr(KEY_NOT_VERIFIED);
+        unsigned int R =
+          FUI->QueryUser(Message, NULL, Answers, &Params, qtWarning);
+        UnicodeString StoreKeyStr = KeyStr;
+
+        switch (R) {
+          case qaNo:
+            DebugAssert(!Unknown);
+            StoreKeyStr = (StoredKeys + HostKeyDelimiter + StoreKeyStr);
+            // fall thru
+          case qaYes:
+            StoreHostKey(Host, Port, KeyType, StoreKeyStr);
+            Verified = true;
+            break;
+
+          case qaCancel:
+            Verified = false;
+            break;
+
+          default:
+            Verified = true;
+            break;
+        }
       }
 
-      Exception * E = new Exception(MainInstructions(Message));
-      try
+      if (!Verified)
       {
-        FUI->FatalError(E, FMTLOAD(HOSTKEY, (FingerprintSHA256)));
-      }
-      __finally
-      {
-        delete E;
+        Configuration->Usage->Inc(L"HostNotVerified");
+
+        UnicodeString Message;
+        if (ConfiguredKeyNotMatch)
+        {
+          Message = FMTLOAD(CONFIGURED_KEY_NOT_MATCH, (ConfigHostKey));
+        }
+        else if (!Configuration->Persistent && Configuration->Scripting)
+        {
+          Message = LoadStr(HOSTKEY_NOT_CONFIGURED);
+        }
+        else
+        {
+          Message = LoadStr(KEY_NOT_VERIFIED);
+        }
+
+        Exception * E = new Exception(MainInstructions(Message));
+        try
+        {
+          FUI->FatalError(E, FMTLOAD(HOSTKEY, (FingerprintSHA256)));
+        }
+        __finally
+        {
+          delete E;
+        }
       }
     }
   }

+ 2 - 1
source/core/SecureShell.h

@@ -172,7 +172,8 @@ public:
   const UnicodeString & __fastcall GetStdError();
   void __fastcall VerifyHostKey(
     const UnicodeString & Host, int Port, const UnicodeString & KeyType, const UnicodeString & KeyStr,
-    const UnicodeString & FingerprintSHA256, const UnicodeString & FingerprintMD5);
+    const UnicodeString & FingerprintSHA256, const UnicodeString & FingerprintMD5,
+    bool IsCertificate, int CACount, bool AlreadyVerified);
   bool __fastcall HaveHostKey(UnicodeString Host, int Port, const UnicodeString KeyType);
   void __fastcall AskAlg(UnicodeString AlgType, UnicodeString AlgName);
   void __fastcall DisplayBanner(const UnicodeString & Banner);

+ 215 - 7
source/forms/Custom.cpp

@@ -15,6 +15,7 @@
 #include <ProgParams.h>
 #include <Tools.h>
 #include <GUITools.h>
+#include <PuttyTools.h>
 #include <HistoryComboBox.hpp>
 #include <Math.hpp>
 
@@ -313,6 +314,17 @@ void __fastcall TCustomDialog::AddButtonControl(TButtonControl * Control)
   }
 }
 //---------------------------------------------------------------------------
+void TCustomDialog::AddButtonNextToEdit(TButton * Button, TWinControl * Edit)
+{
+  Button->Parent = GetDefaultParent();
+  Button->Width = HelpButton->Width;
+  Button->Left = GetDefaultParent()->ClientWidth - Button->Width - HorizontalMargin;
+  Edit->Width = Button->Left - Edit->Left - ScaleByTextHeight(this, 6);
+  Button->Top = Edit->Top - ScaleByTextHeight(this, 2);
+  ScaleButtonControl(Button);
+  AddWinControl(Button);
+}
+//---------------------------------------------------------------------------
 void __fastcall TCustomDialog::AddText(TLabel * Label)
 {
   Label->Parent = GetDefaultParent();
@@ -1016,16 +1028,10 @@ __fastcall TCustomCommandOptionsDialog::TCustomCommandOptionsDialog(
       {
         THistoryComboBox * ComboBox = CreateHistoryComboBox(Option, Value);
         TButton * Button = new TButton(this);
-        Button->Parent = GetDefaultParent();
-        Button->Width = HelpButton->Width;
-        Button->Left = GetDefaultParent()->ClientWidth - Button->Width - HorizontalMargin;
-        ComboBox->Width = Button->Left - ComboBox->Left - ScaleByTextHeight(this, 6);
-        Button->Top = ComboBox->Top - ScaleByTextHeight(this, 2);
+        AddButtonNextToEdit(Button, ComboBox);
         Button->Tag = Tag;
         Button->Caption = LoadStr(EXTENSION_OPTIONS_BROWSE);
         Button->OnClick = BrowseButtonClick;
-        ScaleButtonControl(Button);
-        AddWinControl(Button);
         Control = ComboBox;
       }
       else if (Option.Kind == TCustomCommandType::okDropDownList)
@@ -1595,3 +1601,205 @@ void __fastcall DoSiteRawDialog(TSessionData * Data)
   std::unique_ptr<TSiteRawDialog> Dialog(new TSiteRawDialog());
   Dialog->Execute(Data);
 }
+//---------------------------------------------------------------------------
+//---------------------------------------------------------------------------
+class TSshHostCADialog : public TCustomDialog
+{
+public:
+  TSshHostCADialog(bool Add);
+  bool Execute(TSshHostCA & SshHostCA);
+
+protected:
+  virtual void __fastcall DoChange(bool & CanSubmit);
+  virtual void __fastcall DoValidate();
+
+private:
+  TEdit * NameEdit;
+  TEdit * PublicKeyEdit;
+  TEdit * PublicKeyLabel;
+  TEdit * ValidityExpressionEdit;
+  TCheckBox * PermitRsaSha1Check;
+  TCheckBox * PermitRsaSha256Check;
+  TCheckBox * PermitRsaSha512Check;
+
+  void __fastcall BrowseButtonClick(TObject * Sender);
+  TCheckBox * AddValidityCheckBox(int CaptionStrPart);
+  bool ValidatePublicKey(UnicodeString & Status);
+};
+//---------------------------------------------------------------------------
+TSshHostCADialog::TSshHostCADialog(bool Add) :
+  TCustomDialog(HELP_SSH_HOST_CA)
+{
+  ClientWidth = ScaleByTextHeight(this, 520);
+  Caption = LoadStr(Add ? SSH_HOST_CA_ADD : SSH_HOST_CA_EDIT);
+
+  NameEdit = new TEdit(this);
+  AddEdit(NameEdit, CreateLabel(LoadStr(SSH_HOST_CA_NAME)));
+
+  PublicKeyEdit = new TEdit(this);
+  AddEdit(PublicKeyEdit, CreateLabel(LoadStr(SSH_HOST_CA_PUBLIC_KEY)));
+
+  TButton * BrowseButton = new TButton(this);
+  BrowseButton->Caption = LoadStr(SSH_HOST_CA_BROWSE);
+  BrowseButton->OnClick = BrowseButtonClick;
+  AddButtonNextToEdit(BrowseButton, PublicKeyEdit);
+  NameEdit->Width = PublicKeyEdit->Width;
+
+  PublicKeyLabel = new TEdit(this);
+  ReadOnlyControl(PublicKeyLabel);
+  PublicKeyLabel->BorderStyle = bsNone;
+  AddEditLikeControl(PublicKeyLabel, NULL);
+
+  ValidityExpressionEdit = new TEdit(this);
+  AddEdit(ValidityExpressionEdit, CreateLabel(LoadStr(SSH_HOST_CA_PUBLIC_HOSTS)));
+
+  TLabel * Label = CreateLabel(LoadStr(SSH_HOST_CA_SIGNATURE_TYPES));
+  AddText(Label);
+
+  PermitRsaSha1Check = AddValidityCheckBox(1);
+  int PermitCheckBoxTop = Label->Top - (PermitRsaSha1Check->Height - Label->Height) / 2;
+  PermitRsaSha1Check->Left = OKButton->Left + 1;
+  PermitRsaSha1Check->Top = PermitCheckBoxTop;
+  PermitRsaSha256Check = AddValidityCheckBox(2);
+  PermitRsaSha256Check->Left = CancelButton->Left + 1;
+  PermitRsaSha256Check->Top = PermitCheckBoxTop;
+  PermitRsaSha512Check = AddValidityCheckBox(3);
+  PermitRsaSha512Check->Left = HelpButton->Left + 1;
+  PermitRsaSha512Check->Top = PermitCheckBoxTop;
+}
+//---------------------------------------------------------------------------
+TCheckBox * TSshHostCADialog::AddValidityCheckBox(int CaptionStrPart)
+{
+  TCheckBox * Result = new TCheckBox(this);
+  Result->Parent = this;
+  Result->Caption = LoadStrPart(SSH_HOST_CA_SIGNATURES, CaptionStrPart);
+  ScaleButtonControl(Result);
+  Result->Left = Left;
+  Result->OnClick = Change;
+  AddWinControl(Result);
+  return Result;
+}
+//---------------------------------------------------------------------------
+bool TSshHostCADialog::ValidatePublicKey(UnicodeString & Status)
+{
+  bool Result = false;
+  UnicodeString PublicKeyText = PublicKeyEdit->Text.Trim();
+  if (PublicKeyText.IsEmpty())
+  {
+    Status = LoadStr(SSH_HOST_CA_NO_KEY);
+  }
+  else
+  {
+    RawByteString PublicKeyDummy;
+    try
+    {
+      ParseCertificatePublicKey(PublicKeyText, PublicKeyDummy, Status);
+      Result = true;
+    }
+    catch (Exception & E)
+    {
+      Status = E.Message;
+    }
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+void __fastcall TSshHostCADialog::DoChange(bool & CanSubmit)
+{
+  TCustomDialog::DoChange(CanSubmit);
+
+  UnicodeString PublicKeyStatus;
+  ValidatePublicKey(PublicKeyStatus);
+  // Should drop "SHA256:" prefix, the way we do in TSecureShell::VerifyHostKey
+  PublicKeyLabel->Text = PublicKeyStatus;
+
+  CanSubmit = !NameEdit->Text.Trim().IsEmpty() && !PublicKeyEdit->Text.Trim().IsEmpty();
+}
+//---------------------------------------------------------------------
+void __fastcall TSshHostCADialog::DoValidate()
+{
+  TCustomDialog::DoValidate();
+
+  UnicodeString PublicKeyStatus;
+  if (!ValidatePublicKey(PublicKeyStatus))
+  {
+    PublicKeyEdit->SetFocus();
+    throw Exception(MainInstructions(PublicKeyStatus));
+  }
+
+  if (ValidityExpressionEdit->Text.Trim().IsEmpty())
+  {
+    ValidityExpressionEdit->SetFocus();
+    throw Exception(MainInstructions(LoadStr(SSH_HOST_CA_NO_HOSTS)));
+  }
+
+  UnicodeString Error;
+  int ErrorStart, ErrorLen;
+  if (!IsCertificateValidityExpressionValid(ValidityExpressionEdit->Text, Error, ErrorStart, ErrorLen))
+  {
+    std::unique_ptr<TStrings> MoreMessages(TextToStringList(Error));
+    MoreMessageDialog(MainInstructions(LoadStr(SSH_HOST_CA_HOSTS_INVALID)), MoreMessages.get(), qtError, qaOK, HelpKeyword);
+
+    ValidityExpressionEdit->SetFocus();
+    ValidityExpressionEdit->SelStart = ErrorStart;
+    ValidityExpressionEdit->SelLength = ErrorLen;
+    Abort();
+  }
+}
+//---------------------------------------------------------------------------
+bool TSshHostCADialog::Execute(TSshHostCA & SshHostCA)
+{
+  NameEdit->Text = SshHostCA.Name;
+  RawByteString PublicKey = SshHostCA.PublicKey;
+  PublicKeyEdit->Text = EncodeStrToBase64(PublicKey);
+  ValidityExpressionEdit->Text = SshHostCA.ValidityExpression;
+  PermitRsaSha1Check->Checked = SshHostCA.PermitRsaSha1;
+  PermitRsaSha256Check->Checked = SshHostCA.PermitRsaSha256;
+  PermitRsaSha512Check->Checked = SshHostCA.PermitRsaSha512;
+  bool Result = TCustomDialog::Execute();
+  if (Result)
+  {
+    SshHostCA.Name = NameEdit->Text;
+    SshHostCA.PublicKey = DecodeBase64ToStr(PublicKeyEdit->Text);
+    SshHostCA.ValidityExpression = ValidityExpressionEdit->Text;
+    SshHostCA.PermitRsaSha1 = PermitRsaSha1Check->Checked;
+    SshHostCA.PermitRsaSha256 = PermitRsaSha256Check->Checked;
+    SshHostCA.PermitRsaSha512 = PermitRsaSha512Check->Checked;
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+void __fastcall TSshHostCADialog::BrowseButtonClick(TObject *)
+{
+  std::unique_ptr<TOpenDialog> OpenDialog(new TOpenDialog(Application));
+  OpenDialog->Title = LoadStr(SSH_HOST_CA_BROWSE_TITLE);
+  OpenDialog->Filter = LoadStr(SSH_HOST_CA_BROWSE_FILTER);
+  bool Result = OpenDialog->Execute();
+  if (Result)
+  {
+    UnicodeString FileName = OpenDialog->FileName;
+    UnicodeString Algorithm, Comment;
+    bool HasCertificate;
+    RawByteString PublicKey;
+    try
+    {
+      PublicKey = LoadPublicKey(FileName, Algorithm, Comment, HasCertificate);
+    }
+    catch (Exception & E)
+    {
+      throw ExtException(&E, MainInstructions(FMTLOAD(SSH_HOST_CA_LOAD_ERROR, (FileName))));
+    }
+
+    if (NameEdit->Text.IsEmpty())
+    {
+      NameEdit->Text = Comment;
+    }
+    PublicKeyEdit->Text = EncodeStrToBase64(PublicKey);
+  }
+}
+//---------------------------------------------------------------------------
+bool DoSshHostCADialog(bool Add, TSshHostCA & SshHostCA)
+{
+  std::unique_ptr<TSshHostCADialog> Dialog(new TSshHostCADialog(Add));
+  return Dialog->Execute(SshHostCA);
+}

+ 2 - 1
source/forms/Custom.h

@@ -25,7 +25,6 @@ private:
   short FCount;
   TGroupBox * FGroupBox;
 
-  void __fastcall Change(TObject * Sender);
   void __fastcall Changed();
   int __fastcall GetMaxControlWidth(TControl * Control);
   int __fastcall GetParentClientWidth();
@@ -39,6 +38,7 @@ protected:
   virtual void __fastcall DoChange(bool & CanSubmit);
   virtual void __fastcall DoValidate();
   virtual void __fastcall DoHelp();
+  void __fastcall Change(TObject * Sender);
 
 public:
   __fastcall TCustomDialog(UnicodeString HelpKeyword);
@@ -51,6 +51,7 @@ public:
   void __fastcall AddComboBox(TCustomCombo * Combo, TLabel * Label, TStrings * Items = NULL, bool OneLine = false);
   void __fastcall AddShortCutComboBox(TComboBox * Combo, TLabel * Label, const TShortCuts & ShortCuts);
   void __fastcall AddButtonControl(TButtonControl * Control);
+  void AddButtonNextToEdit(TButton * Control, TWinControl * Edit);
   void __fastcall AddImage(const UnicodeString & ImageName);
   void __fastcall AddWinControl(TWinControl * Control);
   void __fastcall AddText(TLabel * Label);

+ 72 - 0
source/forms/Preferences.cpp

@@ -640,6 +640,7 @@ void __fastcall TPreferencesDialog::LoadConfiguration()
     // security
     UseMasterPasswordCheck->Checked = WinConfiguration->UseMasterPassword;
     SessionRememberPasswordCheck->Checked = GUIConfiguration->SessionRememberPassword;
+    FSshHostCAPlainList = Configuration->SshHostCAList->GetList();
 
     // network
     RetrieveExternalIpAddressButton->Checked = Configuration->ExternalIpAddress.IsEmpty();
@@ -1000,6 +1001,8 @@ void __fastcall TPreferencesDialog::SaveConfiguration()
 
     // security
     GUIConfiguration->SessionRememberPassword = SessionRememberPasswordCheck->Checked;
+    std::unique_ptr<TSshHostCAList> SshHostCAList(new TSshHostCAList(FSshHostCAPlainList));
+    Configuration->SshHostCAList = SshHostCAList.get();
 
     // logging
     Configuration->Logging = EnableLoggingCheck->Checked && !LogFileNameEdit3->Text.IsEmpty();
@@ -1426,7 +1429,12 @@ void __fastcall TPreferencesDialog::UpdateControls()
       AnyPuttyPath && !IsSiteCommand && !IsUWP());
     EnableControl(PuttyRegistryStorageKeyLabel, PuttyRegistryStorageKeyEdit->Enabled);
 
+    // security
     EnableControl(SetMasterPasswordButton, WinConfiguration->UseMasterPassword);
+    UpdateSshHostCAsViewView();
+    bool SshHostCASelected = (SshHostCAsView->Selected != NULL);
+    EnableControl(EditSshHostCAButton, SshHostCASelected);
+    EnableControl(RemoveSshHostCAButton, SshHostCASelected);
 
     // network
     EnableControl(CustomExternalIpAddressEdit, CustomExternalIpAddressButton->Checked);
@@ -3281,3 +3289,67 @@ UnicodeString TPreferencesDialog::Bullet(const UnicodeString & S)
   return Result;
 }
 //---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::SshHostCAsViewDblClick(TObject *)
+{
+  if (EditSshHostCAButton->Enabled)
+  {
+    EditSshHostCAButtonClick(NULL);
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::SshHostCAsViewKeyDown(TObject *, WORD & Key, TShiftState)
+{
+  if (RemoveSshHostCAButton->Enabled && (Key == VK_DELETE))
+  {
+    RemoveSshHostCAButtonClick(NULL);
+  }
+
+  if (AddSshHostCAButton->Enabled && (Key == VK_INSERT))
+  {
+    AddSshHostCAButtonClick(NULL);
+  }
+}
+//---------------------------------------------------------------------------
+void TPreferencesDialog::UpdateSshHostCAsViewView()
+{
+  SshHostCAsView->Items->Count = FSshHostCAPlainList.size();
+  AutoSizeListColumnsWidth(SshHostCAsView, 1);
+  SshHostCAsView->Invalidate();
+}
+//---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::AddSshHostCAButtonClick(TObject *)
+{
+  TSshHostCA SshHostCA;
+  if (DoSshHostCADialog(true, SshHostCA))
+  {
+    FSshHostCAPlainList.push_back(SshHostCA);
+    UpdateSshHostCAsViewView();
+    SshHostCAsView->ItemIndex = FSshHostCAPlainList.size() - 1;
+    SshHostCAsView->ItemFocused->MakeVisible(false);
+    UpdateControls();
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::SshHostCAsViewData(TObject *, TListItem * Item)
+{
+  TSshHostCA & SshHostCA = FSshHostCAPlainList[Item->Index];
+  Item->Caption = SshHostCA.Name;
+  Item->SubItems->Add(SshHostCA.ValidityExpression);
+}
+//---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::EditSshHostCAButtonClick(TObject *)
+{
+  if (DoSshHostCADialog(true, FSshHostCAPlainList[SshHostCAsView->ItemIndex]))
+  {
+    UpdateSshHostCAsViewView();
+    UpdateControls();
+  }
+}
+//---------------------------------------------------------------------------
+void __fastcall TPreferencesDialog::RemoveSshHostCAButtonClick(TObject *)
+{
+  FSshHostCAPlainList.erase(FSshHostCAPlainList.begin() + SshHostCAsView->ItemIndex);
+  UpdateSshHostCAsViewView();
+  UpdateControls();
+}
+//---------------------------------------------------------------------------

+ 71 - 0
source/forms/Preferences.dfm

@@ -2737,6 +2737,77 @@ object PreferencesDialog: TPreferencesDialog
             TabOrder = 0
           end
         end
+        object SshHostCAsGroup: TGroupBox
+          Left = 8
+          Top = 164
+          Width = 389
+          Height = 205
+          Anchors = [akLeft, akTop, akRight, akBottom]
+          Caption = 'Trusted host certification authorities'
+          TabOrder = 2
+          DesignSize = (
+            389
+            205)
+          object SshHostCAsView: TListView
+            Left = 16
+            Top = 24
+            Width = 356
+            Height = 139
+            Anchors = [akLeft, akTop, akRight, akBottom]
+            Columns = <
+              item
+                Caption = 'Name'
+                Width = 100
+              end
+              item
+                Caption = 'Hosts'
+                Width = 100
+              end>
+            ColumnClick = False
+            DoubleBuffered = True
+            HideSelection = False
+            OwnerData = True
+            ReadOnly = True
+            RowSelect = True
+            ParentDoubleBuffered = False
+            TabOrder = 0
+            ViewStyle = vsReport
+            OnData = SshHostCAsViewData
+            OnDblClick = SshHostCAsViewDblClick
+            OnKeyDown = SshHostCAsViewKeyDown
+            OnSelectItem = ListViewSelectItem
+          end
+          object AddSshHostCAButton: TButton
+            Left = 16
+            Top = 169
+            Width = 83
+            Height = 25
+            Anchors = [akLeft, akBottom]
+            Caption = '&Add...'
+            TabOrder = 1
+            OnClick = AddSshHostCAButtonClick
+          end
+          object RemoveSshHostCAButton: TButton
+            Left = 208
+            Top = 169
+            Width = 83
+            Height = 25
+            Anchors = [akLeft, akBottom]
+            Caption = '&Remove'
+            TabOrder = 3
+            OnClick = RemoveSshHostCAButtonClick
+          end
+          object EditSshHostCAButton: TButton
+            Left = 112
+            Top = 169
+            Width = 83
+            Height = 25
+            Anchors = [akLeft, akBottom]
+            Caption = '&Edit...'
+            TabOrder = 2
+            OnClick = EditSshHostCAButtonClick
+          end
+        end
       end
       object IntegrationAppSheet: TTabSheet
         Tag = 18

+ 13 - 0
source/forms/Preferences.h

@@ -347,6 +347,11 @@ __published:
   TCheckBox *ParallelTransferCheck;
   TComboBox *ParallelTransferThresholdCombo;
   TLabel *ParallelTransferThresholdUnitLabel;
+  TGroupBox *SshHostCAsGroup;
+  TListView *SshHostCAsView;
+  TButton *AddSshHostCAButton;
+  TButton *RemoveSshHostCAButton;
+  TButton *EditSshHostCAButton;
   void __fastcall FormShow(TObject *Sender);
   void __fastcall ControlChange(TObject *Sender);
   void __fastcall EditorFontButtonClick(TObject *Sender);
@@ -453,6 +458,12 @@ __published:
   void __fastcall CopyParamListViewDragOver(TObject *Sender, TObject *Source, int X, int Y, TDragState State, bool &Accept);
   void __fastcall LocalPortNumberMinEditExit(TObject *Sender);
   void __fastcall LocalPortNumberMaxEditExit(TObject *Sender);
+  void __fastcall SshHostCAsViewDblClick(TObject *Sender);
+  void __fastcall SshHostCAsViewKeyDown(TObject *Sender, WORD &Key, TShiftState Shift);
+  void __fastcall AddSshHostCAButtonClick(TObject *Sender);
+  void __fastcall SshHostCAsViewData(TObject *Sender, TListItem *Item);
+  void __fastcall EditSshHostCAButtonClick(TObject *Sender);
+  void __fastcall RemoveSshHostCAButtonClick(TObject *Sender);
 
 private:
   TPreferencesMode FPreferencesMode;
@@ -485,6 +496,7 @@ private:
   std::unique_ptr<TStringList> FCustomCommandOptions;
   UnicodeString FCustomIniFileStorageName;
   TFileColorData::TList FFileColors;
+  TSshHostCA::TList FSshHostCAPlainList;
   void __fastcall CMDialogKey(TWMKeyDown & Message);
   void __fastcall WMHelp(TWMHelp & Message);
   void __fastcall CMDpiChanged(TMessage & Message);
@@ -537,6 +549,7 @@ protected:
   void __fastcall UpdateFileColorsView();
   void __fastcall AddEditFileColor(bool Edit);
   UnicodeString Bullet(const UnicodeString & S);
+  void UpdateSshHostCAsViewView();
 
   INTERFACE_HOOK;
 };

+ 4 - 4
source/putty/putty.h

@@ -1285,7 +1285,7 @@ struct SeatVtable {
         Seat *seat, const char *host, int port, const char *keytype,
         char *keystr, SeatDialogText *text, HelpCtx helpctx,
         void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
-        char **fingerprints, bool is_certificate); // WINSCP
+        char **fingerprints, bool is_certificate, int ca_count, bool already_verified); // WINSCP
 
     /*
      * Check with the seat whether it's OK to use a cryptographic
@@ -1442,10 +1442,10 @@ static inline SeatPromptResult seat_confirm_ssh_host_key(
     InteractionReadySeat iseat, const char *h, int p, const char *ktyp,
     char *kstr, SeatDialogText *text, HelpCtx helpctx,
     void (*cb)(void *ctx, SeatPromptResult result), void *ctx,
-    char **fingerprints, bool is_certificate) // WINSCP
+    char **fingerprints, bool is_certificate, int ca_count, bool already_verified) // WINSCP
 { return iseat.seat->vt->confirm_ssh_host_key(
         iseat.seat, h, p, ktyp, kstr, text, helpctx, cb, ctx,
-        fingerprints, is_certificate); } // WINSCP
+        fingerprints, is_certificate, ca_count, already_verified); } // WINSCP
 static inline SeatPromptResult seat_confirm_weak_crypto_primitive(
     InteractionReadySeat iseat, const char *atyp, const char *aname,
     void (*cb)(void *ctx, SeatPromptResult result), void *ctx)
@@ -1539,7 +1539,7 @@ SeatPromptResult nullseat_confirm_ssh_host_key(
     Seat *seat, const char *host, int port, const char *keytype,
     char *keystr, SeatDialogText *text, HelpCtx helpctx,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
-    char **fingerprints, bool is_certificate); // WINSCP
+    char **fingerprints, bool is_certificate, int ca_count, bool already_verified); // WINSCP
 SeatPromptResult nullseat_confirm_weak_crypto_primitive(
     Seat *seat, const char *algtype, const char *algname,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);

+ 0 - 1
source/putty/ssh.h

@@ -1508,7 +1508,6 @@ int rsa1_loadpub_f(const Filename *filename, BinarySink *bs,
 extern const ssh_keyalg *const all_keyalgs[];
 extern const size_t n_keyalgs;
 const ssh_keyalg *find_pubkey_alg(const char *name);
-const ssh_keyalg *find_pubkey_alg_winscp_host(const char *name);
 const ssh_keyalg *find_pubkey_alg_len(ptrlen name);
 
 ptrlen pubkey_blob_to_alg_name(ptrlen blob);

+ 2 - 2
source/putty/ssh/common_p.c

@@ -1010,7 +1010,7 @@ SeatPromptResult verify_ssh_host_key(
                     "pretending to be the server.");
             }
         } else {
-            assert(storage_status == 2);
+            // assert(storage_status == 2); WINSCP
             seat_dialog_text_append(
                 text, SDT_PARA, "which does not match the certified key %s "
                 "had previously cached for this server.", appname);
@@ -1121,7 +1121,7 @@ SeatPromptResult verify_ssh_host_key(
     { // WINSCP
     SeatPromptResult toret = seat_confirm_ssh_host_key(
         iseat, host, port, keytype, keystr, text, helpctx, callback, ctx,
-        fingerprints, key && ssh_key_alg(key)->is_certificate); // WINSCP
+        fingerprints, key && ssh_key_alg(key)->is_certificate, ca_count, false); // WINSCP
     seat_dialog_text_free(text);
     return toret;
     } // WINSCP

+ 4 - 0
source/putty/ssh/kex2-client.c

@@ -985,6 +985,10 @@ void ssh2kex_coroutine(struct ssh2_transport_state *s, bool *aborted)
                 }
                 if (cert_ok) {
                     strbuf_free(error);
+                    // WINSCP (does not "verify", only informs about the hostkeys)
+                    seat_confirm_ssh_host_key(
+                        ppl_get_iseat(&s->ppl), s->savedhost, s->savedport, ssh_key_cache_id(s->hkey), s->keystr, NULL, NULL, NULL, NULL,
+                        fingerprints, true, 0, true);
                     ssh2_free_all_fingerprints(fingerprints);
                     ppl_logevent("Accepted certificate");
                     goto host_key_ok;

+ 0 - 2
source/putty/ssh/transport2.c

@@ -727,7 +727,6 @@ static void ssh2_write_kexinit_lists(
          */
 
         bool accept_certs = false;
-        #ifndef WINSCP
         {
             host_ca_enum *handle = enum_host_ca_start();
             if (handle) {
@@ -750,7 +749,6 @@ static void ssh2_write_kexinit_lists(
                 strbuf_free(name);
             }
         }
-        #endif
 
         if (accept_certs) {
             /* Add all the certificate algorithms first, in preference order */

+ 8 - 8
source/putty/ssh/transport2.h

@@ -60,15 +60,15 @@ struct kexinit_algorithm_list {
     X(HK_RSA, ssh_rsa_sha256)                                   \
     X(HK_RSA, ssh_rsa)                                          \
     /* WINSCP */ \
-    /*X(HK_ED25519, opensshcert_ssh_ecdsa_ed25519)*/                \
+    X(HK_ED25519, opensshcert_ssh_ecdsa_ed25519)                \
     /* OpenSSH defines no certified version of Ed448 */         \
-    /*X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp256)*/                 \
-    /*X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp384)*/                 \
-    /*X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp521)*/                 \
-    /*X(HK_DSA, opensshcert_ssh_dsa)*/                              \
-    /*X(HK_RSA, opensshcert_ssh_rsa_sha512)*/                       \
-    /*X(HK_RSA, opensshcert_ssh_rsa_sha256)*/                       \
-    /*X(HK_RSA, opensshcert_ssh_rsa)*/                              \
+    X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp256)                 \
+    X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp384)                 \
+    X(HK_ECDSA, opensshcert_ssh_ecdsa_nistp521)                 \
+    X(HK_DSA, opensshcert_ssh_dsa)                              \
+    X(HK_RSA, opensshcert_ssh_rsa_sha512)                       \
+    X(HK_RSA, opensshcert_ssh_rsa_sha256)                       \
+    X(HK_RSA, opensshcert_ssh_rsa)                              \
     /* end of list */
 #define COUNT_HOSTKEY_ALGORITHM(type, alg) +1
 #define N_HOSTKEY_ALGORITHMS (0 HOSTKEY_ALGORITHMS(COUNT_HOSTKEY_ALGORITHM))

+ 1 - 17
source/putty/sshpubk.c

@@ -631,27 +631,11 @@ const ssh_keyalg *find_pubkey_alg_len(ptrlen name)
     return NULL;
 }
 
-static const ssh_keyalg *find_pubkey_alg_len_winscp_host(ptrlen name)
-{
-    size_t i;
-    for (i = 0; i < n_keyalgs; i++)
-        if (!all_keyalgs[i]->is_certificate &&
-            ptrlen_eq_string(name, all_keyalgs[i]->ssh_id))
-            return all_keyalgs[i];
-
-    return NULL;
-}
-
 const ssh_keyalg *find_pubkey_alg(const char *name)
 {
     return find_pubkey_alg_len(ptrlen_from_asciz(name));
 }
 
-const ssh_keyalg *find_pubkey_alg_winscp_host(const char *name)
-{
-    return find_pubkey_alg_len_winscp_host(ptrlen_from_asciz(name));
-}
-
 ptrlen pubkey_blob_to_alg_name(ptrlen blob)
 {
     BinarySource src[1];
@@ -1859,7 +1843,7 @@ char *ssh2_fingerprint_blob(ptrlen blob, FingerprintType fptype)
     { // WINSCP
     ptrlen algname = get_string(src);
     if (!get_err(src)) {
-        const ssh_keyalg *alg = find_pubkey_alg_len_winscp_host(algname);
+        const ssh_keyalg *alg = find_pubkey_alg_len(algname);
         if (alg) {
             int bits = ssh_key_public_bits(alg, blob);
             put_fmt(sb, "%.*s %d ", PTRLEN_PRINTF(algname), bits);

+ 1 - 1
source/putty/stubs/null-seat.c

@@ -24,7 +24,7 @@ SeatPromptResult nullseat_confirm_ssh_host_key(
     Seat *seat, const char *host, int port, const char *keytype,
     char *keystr, SeatDialogText *text, HelpCtx helpctx,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
-    char **fingerprints, bool is_certificate) // WINSCP
+    char **fingerprints, bool is_certificate, int ca_count, bool already_verified) // WINSCP
 { return SPR_SW_ABORT("this seat can't handle interactive prompts"); }
 SeatPromptResult nullseat_confirm_weak_crypto_primitive(
     Seat *seat, const char *algtype, const char *algname,

+ 55 - 0
source/putty/utils/base64_valid.c

@@ -0,0 +1,55 @@
+/*
+ * Determine whether a string looks like valid base64-encoded data.
+ */
+
+#include "misc.h"
+
+static inline bool valid_char_main(char c)
+{
+    return ((c >= 'A' && c <= 'Z') ||
+            (c >= 'a' && c <= 'z') ||
+            (c >= '0' && c <= '9') ||
+            c == '+' || c == '/');
+}
+
+bool base64_valid(ptrlen data)
+{
+    size_t blocklen = 0, nequals = 0;
+
+    size_t i; // WINSCP
+    for (i = 0; i < data.len; i++) {
+        char c = ((const char *)data.ptr)[i];
+
+        if (c == '\n' || c == '\r')
+            continue;
+
+        if (valid_char_main(c)) {
+            if (nequals)               /* can't go back to data after = */
+                return false;
+            blocklen++;
+            if (blocklen == 4)
+                blocklen = 0;
+            continue;
+        }
+
+        if (c == '=') {
+            if (blocklen == 0 && nequals) /* started a fresh block */
+                return false;
+
+            nequals++;
+            blocklen++;
+            if (blocklen == 4) {
+                if (nequals > 2)
+                    return false;      /* nonsensical final block */
+                blocklen = 0;
+            }
+            continue;
+        }
+
+        return false;                  /* bad character */
+    }
+
+    if (blocklen == 0 || blocklen == 2 || blocklen == 3)
+        return true;                   /* permit eliding the trailing = */
+    return false;
+}

+ 1 - 1
source/putty/utils/dupprintf.c

@@ -65,7 +65,7 @@ char *dupvprintf_inner(char *buf, size_t oldlen, size_t *sizeptr,
     // CodeGuard breaks execution when vsnprintf function returns -1.
     // Prevent that by making the buffer large enough not to ever return -1.
     // (particularly when called from verify_ssh_host_key for keydisp)
-    sgrowarrayn_nm(buf, size, oldlen, 2048);
+    sgrowarrayn_nm(buf, size, oldlen, 4096);
 #endif
 
     while (1) {

+ 1 - 1
source/putty/utils/tempseat.c

@@ -250,7 +250,7 @@ static SeatPromptResult tempseat_confirm_ssh_host_key(
     Seat *seat, const char *host, int port, const char *keytype,
     char *keystr, SeatDialogText *text, HelpCtx helpctx,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
-    char **fingerprints, bool is_certificate) // WINSCP
+    char **fingerprints, bool is_certificate, int ca_count, bool already_verified) // WINSCP
 {
     unreachable("confirm_ssh_host_key should never be called on TempSeat");
 }

+ 1 - 1
source/putty/windows/platform.h

@@ -252,7 +252,7 @@ SeatPromptResult win_seat_confirm_ssh_host_key(
     Seat *seat, const char *host, int port, const char *keytype,
     char *keystr, SeatDialogText *text, HelpCtx helpctx,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx,
-    char **fingerprints, bool is_certificate); // WINSCP
+    char **fingerprints, bool is_certificate, int ca_count, bool already_verified); // WINSCP
 SeatPromptResult win_seat_confirm_weak_crypto_primitive(
     Seat *seat, const char *algtype, const char *algname,
     void (*callback)(void *ctx, SeatPromptResult result), void *ctx);

+ 1 - 3
source/putty/windows/storage.c

@@ -418,6 +418,7 @@ void store_host_key(const char *hostname, int port,
     } // WINSCP
 }
 
+#ifndef WINSCP
 struct host_ca_enum {
     HKEY key;
     int i;
@@ -456,7 +457,6 @@ void enum_host_ca_finish(host_ca_enum *e)
     sfree(e);
 }
 
-#ifndef WINSCP
 host_ca *host_ca_load(const char *name)
 {
     strbuf *sb;
@@ -512,7 +512,6 @@ host_ca *host_ca_load(const char *name)
     } // WINSCP
     } // WINSCP
 }
-#endif
 
 char *host_ca_save(host_ca *hca)
 {
@@ -556,7 +555,6 @@ char *host_ca_save(host_ca *hca)
     } // WINSCP
 }
 
-#ifndef WINSCP
 char *host_ca_delete(const char *name)
 {
     HKEY rkey = open_regkey(false, HKEY_CURRENT_USER, host_ca_key);

+ 1 - 0
source/resource/HelpWin.h

@@ -71,5 +71,6 @@
 #define HELP_ADD_EXTENSION           "ui_pref_commands#extensions"
 #define HELP_TOO_MANY_SESSIONS       "ui_tabs"
 #define HELP_STORE_TRANSITION        "microsoft_store#transitioning"
+#define HELP_SSH_HOST_CA             "ui_ssh_host_ca"
 
 #endif // TextsWin

+ 10 - 0
source/resource/TextsCore.h

@@ -284,6 +284,10 @@
 #define KEYGEN_NOT_PUBLIC       764
 #define INCONSISTENT_SIZE       765
 #define CREDENTIALS_NOT_SPECIFIED 766
+#define SSH_HOST_CA_DECODE_ERROR 767
+#define SSH_HOST_CA_NO_KEY_TYPE 768
+#define SSH_HOST_CA_CERTIFICATE 769
+#define SSH_HOST_CA_INVALID     770
 
 #define CORE_CONFIRMATION_STRINGS 300
 #define CONFIRM_PROLONG_TIMEOUT3 301
@@ -362,6 +366,12 @@
 #define HOSTKEY_ACCEPT_CHANGE   382
 #define HOSTKEY_ONCE_CHANGE     383
 #define HOSTKEY_CANCEL_CHANGE   384
+#define HOSTKEY_CERTIFIED1      385
+#define HOSTKEY_CERTIFIED2      386
+#define HOSTKEY_CERTIFIED_DOESNT_MATCH_ALSO 387
+#define HOSTKEY_CERTIFIED_ANOTHER 388
+#define HOSTKEY_CERTIFIED_DOESNT_MATCH 389
+#define HOSTKEY_CERTIFIED_TRUST 390
 
 #define CORE_INFORMATION_STRINGS 400
 #define YES_STR                 401

+ 10 - 0
source/resource/TextsCore1.rc

@@ -260,6 +260,10 @@ BEGIN
   KEYGEN_NOT_PUBLIC, "File \"%s\" is not a public key in a known format."
   INCONSISTENT_SIZE, "File part \"%s\" size is %s, but %s was expected."
   CREDENTIALS_NOT_SPECIFIED, "Credentials were not specified."
+  SSH_HOST_CA_DECODE_ERROR, "Cannot decode key: %s"
+  SSH_HOST_CA_NO_KEY_TYPE, "Invalid key (no key type)."
+  SSH_HOST_CA_CERTIFICATE, "CA key may not be a certificate (type is '%s')."
+  SSH_HOST_CA_INVALID, "Invalid '%s' key data."
 
   CORE_CONFIRMATION_STRINGS, "CORE_CONFIRMATION"
   CONFIRM_PROLONG_TIMEOUT3, "Host is not communicating for %d seconds.\n\nWait for another %0:d seconds?"
@@ -335,6 +339,12 @@ BEGIN
   HOSTKEY_ACCEPT_CHANGE, "If you were expecting this change, trust the new key and want to continue connecting to the server, either select %s to update cache, or select %s to add the new key to the cache while keeping the old one(s)."
   HOSTKEY_ONCE_CHANGE, "If you want to carry on connecting but without updating the cache, select %s."
   HOSTKEY_CANCEL_CHANGE, "If you want to abandon the connection completely, select %s to cancel. Selecting %s is the ONLY guaranteed safe choice."
+  HOSTKEY_CERTIFIED1, "This server presented a certified host key:"
+  HOSTKEY_CERTIFIED2, "which was signed by a different certification authority from the one(s) WinSCP is configured to trust for this server."
+  HOSTKEY_CERTIFIED_DOESNT_MATCH_ALSO, "Also, that key does not match the key WinSCP had previously cached for this server."
+  HOSTKEY_CERTIFIED_ANOTHER, "another certification authority is operating in this realm"
+  HOSTKEY_CERTIFIED_DOESNT_MATCH, "which does not match the certified key WinSCP had previously cached for this server."
+  HOSTKEY_CERTIFIED_TRUST, "(Storing this certified key in the cache will NOT cause its certification authority to be trusted for any other key or host.)"
 
   CORE_INFORMATION_STRINGS, "CORE_INFORMATION"
   YES_STR, "Yes"

+ 14 - 0
source/resource/TextsWin.h

@@ -675,6 +675,20 @@
 #define KEX_NAME_NTRU_HYBRID    6080
 #define LOGIN_KEY_WITH_CERTIFICATE 6090
 #define CERTIFICATE_ADDED       6091
+#define SSH_HOST_CA_EDIT        6092
+#define SSH_HOST_CA_ADD         6093
+#define SSH_HOST_CA_NAME        6094
+#define SSH_HOST_CA_PUBLIC_KEY  6095
+#define SSH_HOST_CA_PUBLIC_HOSTS 6096
+#define SSH_HOST_CA_BROWSE      6097
+#define SSH_HOST_CA_NO_KEY      6098
+#define SSH_HOST_CA_BROWSE_TITLE 6099
+#define SSH_HOST_CA_BROWSE_FILTER 6100
+#define SSH_HOST_CA_LOAD_ERROR  6201
+#define SSH_HOST_CA_SIGNATURE_TYPES 6202
+#define SSH_HOST_CA_SIGNATURES  6203
+#define SSH_HOST_CA_NO_HOSTS    6204
+#define SSH_HOST_CA_HOSTS_INVALID 6205
 
 // 2xxx is reserved for TextsFileZilla.h
 

+ 14 - 0
source/resource/TextsWin1.rc

@@ -680,6 +680,20 @@ BEGIN
         KEX_NAME_NTRU_HYBRID, "NTRU Prime / Curve25519 hybrid kex"
         LOGIN_KEY_WITH_CERTIFICATE, "**This key contains an OpenSSH certificate.**\nIt is not supposed to be added to OpenSSH authorized_keys file."
         CERTIFICATE_ADDED, "Matching certificate was detected in '%s' and added to the converted key file."
+        SSH_HOST_CA_EDIT, "Edit trusted host certificate authority"
+        SSH_HOST_CA_ADD, "Add trusted host certificate authority"
+        SSH_HOST_CA_NAME, "&Name:"
+        SSH_HOST_CA_PUBLIC_KEY, "Public &key:"
+        SSH_HOST_CA_PUBLIC_HOSTS, "Valid &hosts this key is trusted to certify:"
+        SSH_HOST_CA_BROWSE, "B&rowse..."
+        SSH_HOST_CA_NO_KEY, "No public key specified."
+        SSH_HOST_CA_BROWSE_TITLE, "Select public key file of certification authority"
+        SSH_HOST_CA_BROWSE_FILTER, "Public key files (*.pub)|*.pub|All Files (*.*)|*.*"
+        SSH_HOST_CA_LOAD_ERROR, "Unable to load public key from '%s'"
+        SSH_HOST_CA_SIGNATURE_TYPES, "Signature types (RSA keys only):"
+        SSH_HOST_CA_SIGNATURES, "SHA-&1|SHA-&256|SHA-&512"
+        SSH_HOST_CA_NO_HOSTS, "No validity expression configured."
+        SSH_HOST_CA_HOSTS_INVALID, "Error in validity expression."
 
         WIN_VARIABLE_STRINGS, "WIN_VARIABLE"
         WINSCP_COPYRIGHT, "Copyright © 2000–2023 Martin Prikryl"

+ 1 - 0
source/windows/WinInterface.h

@@ -162,6 +162,7 @@ bool __fastcall DoCustomCommandOptionsDialog(
   TCustomCommand * CustomCommandForOptions, const UnicodeString & Site, const TShortCuts * ShortCuts);
 void __fastcall DoUsageStatisticsDialog();
 void __fastcall DoSiteRawDialog(TSessionData * Data);
+bool DoSshHostCADialog(bool Add, TSshHostCA & SshHostCA);
 
 // windows\UserInterface.cpp
 bool __fastcall DoMasterPasswordDialog();