• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

randombit / botan / 13215274653

08 Feb 2025 11:38AM UTC coverage: 91.655% (-0.009%) from 91.664%
13215274653

Pull #4650

github

web-flow
Merge 107f31833 into bc555cd3c
Pull Request #4650: Reorganize code and reduce header dependencies

94836 of 103471 relevant lines covered (91.65%)

11230958.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.67
/src/lib/x509/pkix_types.h
1
/*
2
* (C) 1999-2010,2012,2018,2020 Jack Lloyd
3
* (C) 2007 Yves Jerschow
4
* (C) 2015 Kai Michaelis
5
* (C) 2016 René Korthaus, Rohde & Schwarz Cybersecurity
6
* (C) 2017 Fabian Weissberg, Rohde & Schwarz Cybersecurity
7
*
8
* Botan is released under the Simplified BSD License (see license.txt)
9
*/
10

11
#ifndef BOTAN_PKIX_TYPES_H_
12
#define BOTAN_PKIX_TYPES_H_
13

14
#include <botan/asn1_obj.h>
15

16
#include <botan/assert.h>
17
#include <botan/pkix_enums.h>
18
#include <iosfwd>
19
#include <map>
20
#include <memory>
21
#include <set>
22
#include <string>
23
#include <string_view>
24
#include <variant>
25
#include <vector>
26

27
namespace Botan {
28

29
class X509_Certificate;
30
class Public_Key;
31

32
BOTAN_DEPRECATED("Use Key_Constraints::to_string")
33

34
inline std::string key_constraints_to_string(Key_Constraints c) {
35
   return c.to_string();
36
}
37

38
/**
39
* Distinguished Name
40
*/
41
class BOTAN_PUBLIC_API(2, 0) X509_DN final : public ASN1_Object {
19,614✔
42
   public:
43
      X509_DN() = default;
3,785✔
44

45
      explicit X509_DN(const std::multimap<OID, std::string>& args) {
46
         for(const auto& i : args) {
47
            add_attribute(i.first, i.second);
48
         }
49
      }
50

51
      explicit X509_DN(const std::multimap<std::string, std::string>& args) {
1✔
52
         for(const auto& i : args) {
2✔
53
            add_attribute(i.first, i.second);
1✔
54
         }
55
      }
1✔
56

57
      void encode_into(DER_Encoder&) const override;
58
      void decode_from(BER_Decoder&) override;
59

60
      bool has_field(const OID& oid) const;
61
      ASN1_String get_first_attribute(const OID& oid) const;
62

63
      /*
64
      * Return the BER encoded data, if any
65
      */
66
      const std::vector<uint8_t>& get_bits() const { return m_dn_bits; }
38,532✔
67

68
      std::vector<uint8_t> DER_encode() const;
69

70
      bool empty() const { return m_rdn.empty(); }
133✔
71

72
      size_t count() const { return m_rdn.size(); }
53✔
73

74
      std::string to_string() const;
75

76
      const std::vector<std::pair<OID, ASN1_String>>& dn_info() const { return m_rdn; }
3,901✔
77

78
      std::multimap<OID, std::string> get_attributes() const;
79
      std::multimap<std::string, std::string> contents() const;
80

81
      bool has_field(std::string_view attr) const;
82
      std::vector<std::string> get_attribute(std::string_view attr) const;
83
      std::string get_first_attribute(std::string_view attr) const;
84

85
      void add_attribute(std::string_view key, std::string_view val);
86

87
      void add_attribute(const OID& oid, std::string_view val) { add_attribute(oid, ASN1_String(val)); }
5,002✔
88

89
      void add_attribute(const OID& oid, const ASN1_String& val);
90

91
      static std::string deref_info_field(std::string_view key);
92

93
      /**
94
      * Lookup upper bounds in characters for the length of distinguished name fields
95
      * as given in RFC 5280, Appendix A.
96
      *
97
      * @param oid the oid of the DN to lookup
98
      * @return the upper bound, or zero if no ub is known to Botan
99
      */
100
      static size_t lookup_ub(const OID& oid);
101

102
   private:
103
      std::vector<std::pair<OID, ASN1_String>> m_rdn;
104
      std::vector<uint8_t> m_dn_bits;
105
};
106

107
BOTAN_PUBLIC_API(2, 0) bool operator==(const X509_DN& dn1, const X509_DN& dn2);
108
BOTAN_PUBLIC_API(2, 0) bool operator!=(const X509_DN& dn1, const X509_DN& dn2);
109

110
/*
111
The ordering here is arbitrary and may change from release to release.
112
It is intended for allowing DNs as keys in std::map and similiar containers
113
*/
114
BOTAN_PUBLIC_API(2, 0) bool operator<(const X509_DN& dn1, const X509_DN& dn2);
115

116
BOTAN_PUBLIC_API(2, 0) std::ostream& operator<<(std::ostream& out, const X509_DN& dn);
117
BOTAN_PUBLIC_API(2, 0) std::istream& operator>>(std::istream& in, X509_DN& dn);
118

119
/**
120
* Alternative Name
121
*/
122
class BOTAN_PUBLIC_API(2, 0) AlternativeName final : public ASN1_Object {
123
   public:
124
      void encode_into(DER_Encoder&) const override;
125
      void decode_from(BER_Decoder&) override;
126

127
      /// Create an empty name
128
      AlternativeName() {}
11,085✔
129

130
      /// Add a URI to this AlternativeName
131
      void add_uri(std::string_view uri);
132

133
      /// Add a URI to this AlternativeName
134
      void add_email(std::string_view addr);
135

136
      /// Add a DNS name to this AlternativeName
137
      void add_dns(std::string_view dns);
138

139
      /// Add an "OtherName" identified by object identifier to this AlternativeName
140
      void add_other_name(const OID& oid, const ASN1_String& value);
141

142
      /// Add a directory name to this AlternativeName
143
      void add_dn(const X509_DN& dn);
144

145
      /// Add an IP address to this alternative name
146
      void add_ipv4_address(uint32_t ipv4);
147

148
      /// Return the set of URIs included in this alternative name
149
      const std::set<std::string>& uris() const { return m_uri; }
1,775✔
150

151
      /// Return the set of email addresses included in this alternative name
152
      const std::set<std::string>& email() const { return m_email; }
1,705✔
153

154
      /// Return the set of DNS names included in this alternative name
155
      const std::set<std::string>& dns() const { return m_dns; }
1,956✔
156

157
      /// Return the set of IPv4 addresses included in this alternative name
158
      const std::set<uint32_t>& ipv4_address() const { return m_ipv4_addr; }
1,735✔
159

160
      /// Return the set of "other names" included in this alternative name
161
      BOTAN_DEPRECATED("Support for other names is deprecated")
162
      const std::set<std::pair<OID, ASN1_String>>& other_names() const {
163
         return m_othernames;
1,691✔
164
      }
165

166
      /// Return the set of directory names included in this alternative name
167
      const std::set<X509_DN>& directory_names() const { return m_dn_names; }
1,764✔
168

169
      /// Return the total number of names in this AlternativeName
170
      ///
171
      /// This only counts names which were parsed, ignoring names which
172
      /// were of some unknown type
173
      size_t count() const;
174

175
      /// Return true if this has any names set
176
      bool has_items() const;
177

178
      // Old, now deprecated interface follows:
179
      BOTAN_DEPRECATED("Use AlternativeName::{uris, email, dns, othernames, directory_names}")
180
      std::multimap<std::string, std::string> contents() const;
181

182
      BOTAN_DEPRECATED("Use AlternativeName::{uris, email, dns, othernames, directory_names}.empty()")
183
      bool has_field(std::string_view attr) const;
184

185
      BOTAN_DEPRECATED("Use AlternativeName::{uris, email, dns, othernames, directory_names}")
186
      std::vector<std::string> get_attribute(std::string_view attr) const;
187

188
      BOTAN_DEPRECATED("Use AlternativeName::{uris, email, dns, othernames, directory_names}")
189
      std::multimap<std::string, std::string, std::less<>> get_attributes() const;
190

191
      BOTAN_DEPRECATED("Use AlternativeName::{uris, email, dns, othernames, directory_names}")
192
      std::string get_first_attribute(std::string_view attr) const;
193

194
      BOTAN_DEPRECATED("Use AlternativeName::add_{uri, dns, email, ...}")
195
      void add_attribute(std::string_view type, std::string_view value);
196

197
      BOTAN_DEPRECATED("Use AlternativeName::add_other_name")
198
      void add_othername(const OID& oid, std::string_view value, ASN1_Type type);
199

200
      BOTAN_DEPRECATED("Use AlternativeName::othernames") std::multimap<OID, ASN1_String> get_othernames() const;
201

202
      BOTAN_DEPRECATED("Use AlternativeName::directory_names") X509_DN dn() const;
203

204
      BOTAN_DEPRECATED("Use plain constructor plus add_{uri,dns,email,ipv4_address}")
205
      AlternativeName(std::string_view email_addr,
206
                      std::string_view uri = "",
207
                      std::string_view dns = "",
208
                      std::string_view ip_address = "");
209

210
   private:
211
      std::set<std::string> m_dns;
212
      std::set<std::string> m_uri;
213
      std::set<std::string> m_email;
214
      std::set<uint32_t> m_ipv4_addr;
215
      std::set<X509_DN> m_dn_names;
216
      std::set<std::pair<OID, ASN1_String>> m_othernames;
217
};
218

219
/**
220
* Attribute
221
*/
222
class BOTAN_PUBLIC_API(2, 0) Attribute final : public ASN1_Object {
452✔
223
   public:
224
      void encode_into(DER_Encoder& to) const override;
225
      void decode_from(BER_Decoder& from) override;
226

227
      Attribute() = default;
228
      Attribute(const OID& oid, const std::vector<uint8_t>& params);
229
      Attribute(std::string_view oid_str, const std::vector<uint8_t>& params);
230

231
      const OID& oid() const { return m_oid; }
232

233
      const std::vector<uint8_t>& parameters() const { return m_parameters; }
234

235
      const OID& object_identifier() const { return m_oid; }
236

237
      const std::vector<uint8_t>& get_parameters() const { return m_parameters; }
235✔
238

239
   private:
240
      OID m_oid;
241
      std::vector<uint8_t> m_parameters;
242
};
243

244
/**
245
* @brief X.509 GeneralName Type
246
*
247
* Handles parsing GeneralName types in their BER and canonical string
248
* encoding. Allows matching GeneralNames against each other using
249
* the rules laid out in the RFC 5280, sec. 4.2.1.10 (Name Contraints).
250
*
251
* This entire class is deprecated and will be removed in a future
252
* major release
253
*/
254
class BOTAN_PUBLIC_API(2, 0) GeneralName final : public ASN1_Object {
5,299✔
255
   public:
256
      enum MatchResult : int {
257
         All,
258
         Some,
259
         None,
260
         NotFound,
261
         UnknownType,
262
      };
263

264
      enum class NameType : uint8_t {
265
         Unknown = 0,
266
         RFC822 = 1,
267
         DNS = 2,
268
         URI = 3,
269
         DN = 4,
270
         IPv4 = 5,
271
         Other = 6,
272
      };
273

274
      BOTAN_DEPRECATED("Deprecated use NameConstraints") GeneralName() = default;
1,073✔
275

276
      // Encoding is not implemented
277
      void encode_into(DER_Encoder&) const override;
278

279
      void decode_from(BER_Decoder&) override;
280

281
      /**
282
      * @return Type of the name expressed in this restriction
283
      */
284
      NameType type_code() const { return m_type; }
768✔
285

286
      /**
287
      * @return Type of the name. Can be DN, DNS, IP, RFC822 or URI.
288
      */
289
      BOTAN_DEPRECATED("Deprecated use type_code") std::string type() const;
290

291
      /**
292
      * @return The name as string. Format depends on type.
293
      */
294
      BOTAN_DEPRECATED("Deprecated no replacement") std::string name() const;
295

296
      /**
297
      * Checks whether a given certificate (partially) matches this name.
298
      * @param cert certificate to be matched
299
      * @return the match result
300
      */
301
      BOTAN_DEPRECATED("Deprecated use NameConstraints type") MatchResult matches(const X509_Certificate& cert) const;
302

303
      bool matches_dns(const std::string& dns_name) const;
304
      bool matches_ipv4(uint32_t ip) const;
305
      bool matches_dn(const X509_DN& dn) const;
306

307
   private:
308
      static constexpr size_t RFC822_IDX = 0;
309
      static constexpr size_t DNS_IDX = 1;
310
      static constexpr size_t URI_IDX = 2;
311
      static constexpr size_t DN_IDX = 3;
312
      static constexpr size_t IPV4_IDX = 4;
313

314
      NameType m_type;
315
      std::variant<std::string, std::string, std::string, X509_DN, std::pair<uint32_t, uint32_t>> m_name;
316

317
      static bool matches_dns(std::string_view name, std::string_view constraint);
318

319
      static bool matches_dn(const X509_DN& name, const X509_DN& constraint);
320
};
321

322
BOTAN_DEPRECATED("Deprecated no replacement") std::ostream& operator<<(std::ostream& os, const GeneralName& gn);
323

324
/**
325
* @brief A single Name Constraint
326
*
327
* The Name Constraint extension adds a minimum and maximum path
328
* length to a GeneralName to form a constraint. The length limits
329
* are not used in PKIX.
330
*
331
* This entire class is deprecated and will be removed in a future
332
* major release
333
*/
334
class BOTAN_PUBLIC_API(2, 0) GeneralSubtree final : public ASN1_Object {
8,954✔
335
   public:
336
      /**
337
      * Creates an empty name constraint.
338
      */
339
      BOTAN_DEPRECATED("Deprecated use NameConstraints") GeneralSubtree();
340

341
      void encode_into(DER_Encoder&) const override;
342

343
      void decode_from(BER_Decoder&) override;
344

345
      /**
346
      * @return name
347
      */
348
      const GeneralName& base() const { return m_base; }
1,131✔
349

350
   private:
351
      GeneralName m_base;
352
};
353

354
BOTAN_DEPRECATED("Deprecated no replacement") std::ostream& operator<<(std::ostream& os, const GeneralSubtree& gs);
355

356
/**
357
* @brief Name Constraints
358
*
359
* Wraps the Name Constraints associated with a certificate.
360
*/
361
class BOTAN_PUBLIC_API(2, 0) NameConstraints final {
362
   public:
363
      /**
364
      * Creates an empty name NameConstraints.
365
      */
366
      NameConstraints() : m_permitted_subtrees(), m_excluded_subtrees() {}
423✔
367

368
      /**
369
      * Creates NameConstraints from a list of permitted and excluded subtrees.
370
      * @param permitted_subtrees names for which the certificate is permitted
371
      * @param excluded_subtrees names for which the certificate is not permitted
372
      */
373
      NameConstraints(std::vector<GeneralSubtree>&& permitted_subtrees,
374
                      std::vector<GeneralSubtree>&& excluded_subtrees);
375

376
      /**
377
      * @return permitted names
378
      */
379
      BOTAN_DEPRECATED("Deprecated no replacement") const std::vector<GeneralSubtree>& permitted() const {
380
         return m_permitted_subtrees;
132✔
381
      }
382

383
      /**
384
      * @return excluded names
385
      */
386
      BOTAN_DEPRECATED("Deprecated no replacement") const std::vector<GeneralSubtree>& excluded() const {
387
         return m_excluded_subtrees;
115✔
388
      }
389

390
      /**
391
      * Return true if all of the names in the certificate are permitted
392
      */
393
      bool is_permitted(const X509_Certificate& cert, bool reject_unknown) const;
394

395
      /**
396
      * Return true if any of the names in the certificate are excluded
397
      */
398
      bool is_excluded(const X509_Certificate& cert, bool reject_unknown) const;
399

400
   private:
401
      std::vector<GeneralSubtree> m_permitted_subtrees;
402
      std::vector<GeneralSubtree> m_excluded_subtrees;
403

404
      std::set<GeneralName::NameType> m_permitted_name_types;
405
      std::set<GeneralName::NameType> m_excluded_name_types;
406
};
407

408
/**
409
* X.509 Certificate Extension
410
*/
411
class BOTAN_PUBLIC_API(2, 0) Certificate_Extension {
54,475✔
412
   public:
413
      /**
414
      * @return OID representing this extension
415
      */
416
      virtual OID oid_of() const = 0;
417

418
      /*
419
      * @return specific OID name
420
      * If possible OIDS table should match oid_name to OIDS, ie
421
      * OID::from_string(ext->oid_name()) == ext->oid_of()
422
      * Should return empty string if OID is not known
423
      */
424
      virtual std::string oid_name() const = 0;
425

426
      /**
427
      * Make a copy of this extension
428
      * @return copy of this
429
      */
430

431
      virtual std::unique_ptr<Certificate_Extension> copy() const = 0;
432

433
      /*
434
      * Callback visited during path validation.
435
      *
436
      * An extension can implement this callback to inspect
437
      * the path during path validation.
438
      *
439
      * If an error occurs during validation of this extension,
440
      * an appropriate status code shall be added to cert_status.
441
      *
442
      * @param subject Subject certificate that contains this extension
443
      * @param issuer Issuer certificate
444
      * @param status Certificate validation status codes for subject certificate
445
      * @param cert_path Certificate path which is currently validated
446
      * @param pos Position of subject certificate in cert_path
447
      */
448
      virtual void validate(const X509_Certificate& subject,
449
                            const X509_Certificate& issuer,
450
                            const std::vector<X509_Certificate>& cert_path,
451
                            std::vector<std::set<Certificate_Status_Code>>& cert_status,
452
                            size_t pos);
453

454
      virtual ~Certificate_Extension() = default;
214✔
455

456
   protected:
457
      friend class Extensions;
458

459
      virtual bool should_encode() const { return true; }
554✔
460

461
      virtual std::vector<uint8_t> encode_inner() const = 0;
462
      virtual void decode_inner(const std::vector<uint8_t>&) = 0;
463
};
464

465
/**
466
* X.509 Certificate Extension List
467
*/
468
class BOTAN_PUBLIC_API(2, 0) Extensions final : public ASN1_Object {
1,826✔
469
   public:
470
      /**
471
      * Look up an object in the extensions, based on OID Returns
472
      * nullptr if not set, if the extension was either absent or not
473
      * handled. The pointer returned is owned by the Extensions
474
      * object.
475
      * This would be better with an optional<T> return value
476
      */
477
      const Certificate_Extension* get_extension_object(const OID& oid) const;
478

479
      template <typename T>
480
      const T* get_extension_object_as(const OID& oid = T::static_oid()) const {
207,158✔
481
         if(const Certificate_Extension* extn = get_extension_object(oid)) {
207,158✔
482
            // Unknown_Extension oid_name is empty
483
            if(extn->oid_name().empty()) {
60,259✔
484
               return nullptr;
485
            } else if(const T* extn_as_T = dynamic_cast<const T*>(extn)) {
59,875✔
486
               return extn_as_T;
487
            } else {
488
               throw Decoding_Error("Exception::get_extension_object_as dynamic_cast failed");
×
489
            }
490
         }
491

492
         return nullptr;
493
      }
494

495
      /**
496
      * Return the set of extensions in the order they appeared in the certificate
497
      * (or as they were added, if constructed)
498
      */
499
      const std::vector<OID>& get_extension_oids() const { return m_extension_oids; }
3,683✔
500

501
      /**
502
      * Return true if an extension was set
503
      */
504
      bool extension_set(const OID& oid) const;
505

506
      /**
507
      * Return true if an extesion was set and marked critical
508
      */
509
      bool critical_extension_set(const OID& oid) const;
510

511
      /**
512
      * Return the raw bytes of the extension
513
      * Will throw if OID was not set as an extension.
514
      */
515
      std::vector<uint8_t> get_extension_bits(const OID& oid) const;
516

517
      void encode_into(DER_Encoder&) const override;
518
      void decode_from(BER_Decoder&) override;
519

520
      /**
521
      * Adds a new extension to the list.
522
      * @param extn pointer to the certificate extension (Extensions takes ownership)
523
      * @param critical whether this extension should be marked as critical
524
      * @throw Invalid_Argument if the extension is already present in the list
525
      */
526
      void add(std::unique_ptr<Certificate_Extension> extn, bool critical = false);
527

528
      /**
529
      * Adds a new extension to the list unless it already exists. If the extension
530
      * already exists within the Extensions object, the extn pointer will be deleted.
531
      *
532
      * @param extn pointer to the certificate extension (Extensions takes ownership)
533
      * @param critical whether this extension should be marked as critical
534
      * @return true if the object was added false if the extension was already used
535
      */
536
      bool add_new(std::unique_ptr<Certificate_Extension> extn, bool critical = false);
537

538
      /**
539
      * Adds an extension to the list or replaces it.
540
      * @param extn the certificate extension
541
      * @param critical whether this extension should be marked as critical
542
      */
543
      void replace(std::unique_ptr<Certificate_Extension> extn, bool critical = false);
544

545
      /**
546
      * Remove an extension from the list. Returns true if the
547
      * extension had been set, false otherwise.
548
      */
549
      bool remove(const OID& oid);
550

551
      /**
552
      * Searches for an extension by OID and returns the result.
553
      * Only the known extensions types declared in this header
554
      * are searched for by this function.
555
      * @return Copy of extension with oid, nullptr if not found.
556
      * Can avoid creating a copy by using get_extension_object function
557
      */
558
      std::unique_ptr<Certificate_Extension> get(const OID& oid) const;
559

560
      /**
561
      * Searches for an extension by OID and returns the result decoding
562
      * it to some arbitrary extension type chosen by the application.
563
      *
564
      * Only the unknown extensions, that is, extensions types that
565
      * are not declared in this header, are searched for by this
566
      * function.
567
      *
568
      * @return Pointer to new extension with oid, nullptr if not found.
569
      */
570
      template <typename T>
571
      std::unique_ptr<T> get_raw(const OID& oid) const {
24✔
572
         auto extn_info = m_extension_info.find(oid);
24✔
573

574
         if(extn_info != m_extension_info.end()) {
24✔
575
            // Unknown_Extension oid_name is empty
576
            if(extn_info->second.obj().oid_name().empty()) {
24✔
577
               auto ext = std::make_unique<T>();
24✔
578
               ext->decode_inner(extn_info->second.bits());
24✔
579
               return ext;
24✔
580
            }
24✔
581
         }
582
         return nullptr;
×
583
      }
584

585
      /**
586
      * Returns a copy of the list of extensions together with the corresponding
587
      * criticality flag. All extensions are encoded as some object, falling back
588
      * to Unknown_Extension class which simply allows reading the bytes as well
589
      * as the criticality flag.
590
      */
591
      std::vector<std::pair<std::unique_ptr<Certificate_Extension>, bool>> extensions() const;
592

593
      /**
594
      * Returns the list of extensions as raw, encoded bytes
595
      * together with the corresponding criticality flag.
596
      * Contains all extensions, including any extensions encoded as Unknown_Extension
597
      */
598
      std::map<OID, std::pair<std::vector<uint8_t>, bool>> extensions_raw() const;
599

600
      Extensions() = default;
5,010✔
601

602
      Extensions(const Extensions&) = default;
554✔
603
      Extensions& operator=(const Extensions&) = default;
206✔
604

605
      Extensions(Extensions&&) = default;
606
      Extensions& operator=(Extensions&&) = default;
607

608
   private:
609
      static std::unique_ptr<Certificate_Extension> create_extn_obj(const OID& oid,
610
                                                                    bool critical,
611
                                                                    const std::vector<uint8_t>& body);
612

613
      class Extensions_Info {
614
         public:
615
            Extensions_Info(bool critical, std::unique_ptr<Certificate_Extension> ext) :
2,640✔
616
                  m_obj(std::move(ext)), m_bits(m_obj->encode_inner()), m_critical(critical) {}
2,640✔
617

618
            Extensions_Info(bool critical,
67,271✔
619
                            const std::vector<uint8_t>& encoding,
620
                            std::unique_ptr<Certificate_Extension> ext) :
67,271✔
621
                  m_obj(std::move(ext)), m_bits(encoding), m_critical(critical) {}
67,271✔
622

623
            bool is_critical() const { return m_critical; }
2,026✔
624

625
            const std::vector<uint8_t>& bits() const { return m_bits; }
1,996✔
626

627
            const Certificate_Extension& obj() const {
80,904✔
628
               BOTAN_ASSERT_NONNULL(m_obj.get());
80,904✔
629
               return *m_obj;
19,993✔
630
            }
631

632
         private:
633
            std::shared_ptr<Certificate_Extension> m_obj;
634
            std::vector<uint8_t> m_bits;
635
            bool m_critical = false;
636
      };
637

638
      std::vector<OID> m_extension_oids;
639
      std::map<OID, Extensions_Info> m_extension_info;
640
};
641

642
}  // namespace Botan
643

644
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc