1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394
|
package provisioner
import (
"context"
"crypto/x509"
"encoding/pem"
"fmt"
"net"
"strings"
"time"
"github.com/pkg/errors"
"github.com/smallstep/certificates/acme/wire"
"github.com/smallstep/linkedca"
)
// ACMEChallenge represents the supported acme challenges.
type ACMEChallenge string
//nolint:staticcheck,revive // better names
const (
// HTTP_01 is the http-01 ACME challenge.
HTTP_01 ACMEChallenge = "http-01"
// DNS_01 is the dns-01 ACME challenge.
DNS_01 ACMEChallenge = "dns-01"
// TLS_ALPN_01 is the tls-alpn-01 ACME challenge.
TLS_ALPN_01 ACMEChallenge = "tls-alpn-01"
// DEVICE_ATTEST_01 is the device-attest-01 ACME challenge.
DEVICE_ATTEST_01 ACMEChallenge = "device-attest-01"
// WIREOIDC_01 is the Wire OIDC challenge.
WIREOIDC_01 ACMEChallenge = "wire-oidc-01"
// WIREDPOP_01 is the Wire DPoP challenge.
WIREDPOP_01 ACMEChallenge = "wire-dpop-01"
)
// String returns a normalized version of the challenge.
func (c ACMEChallenge) String() string {
return strings.ToLower(string(c))
}
// Validate returns an error if the acme challenge is not a valid one.
func (c ACMEChallenge) Validate() error {
switch ACMEChallenge(c.String()) {
case HTTP_01, DNS_01, TLS_ALPN_01, DEVICE_ATTEST_01, WIREOIDC_01, WIREDPOP_01:
return nil
default:
return fmt.Errorf("acme challenge %q is not supported", c)
}
}
// ACMEAttestationFormat represents the format used on a device-attest-01
// challenge.
type ACMEAttestationFormat string
const (
// APPLE is the format used to enable device-attest-01 on Apple devices.
APPLE ACMEAttestationFormat = "apple"
// STEP is the format used to enable device-attest-01 on devices that
// provide attestation certificates like the PIV interface on YubiKeys.
//
// TODO(mariano): should we rename this to something else.
STEP ACMEAttestationFormat = "step"
// TPM is the format used to enable device-attest-01 with TPMs.
TPM ACMEAttestationFormat = "tpm"
)
// String returns a normalized version of the attestation format.
func (f ACMEAttestationFormat) String() string {
return strings.ToLower(string(f))
}
// Validate returns an error if the attestation format is not a valid one.
func (f ACMEAttestationFormat) Validate() error {
switch ACMEAttestationFormat(f.String()) {
case APPLE, STEP, TPM:
return nil
default:
return fmt.Errorf("acme attestation format %q is not supported", f)
}
}
// ACME is the acme provisioner type, an entity that can authorize the ACME
// provisioning flow.
type ACME struct {
*base
ID string `json:"-"`
Type string `json:"type"`
Name string `json:"name"`
ForceCN bool `json:"forceCN,omitempty"`
// TermsOfService contains a URL pointing to the ACME server's
// terms of service. Defaults to empty.
TermsOfService string `json:"termsOfService,omitempty"`
// Website contains an URL pointing to more information about
// the ACME server. Defaults to empty.
Website string `json:"website,omitempty"`
// CaaIdentities is an array of hostnames that the ACME server
// identifies itself with. These hostnames can be used by ACME
// clients to determine the correct issuer domain name to use
// when configuring CAA records. Defaults to empty array.
CaaIdentities []string `json:"caaIdentities,omitempty"`
// RequireEAB makes the provisioner require ACME EAB to be provided
// by clients when creating a new Account. If set to true, the provided
// EAB will be verified. If set to false and an EAB is provided, it is
// not verified. Defaults to false.
RequireEAB bool `json:"requireEAB,omitempty"`
// Challenges contains the enabled challenges for this provisioner. If this
// value is not set the default http-01, dns-01 and tls-alpn-01 challenges
// will be enabled, device-attest-01, wire-oidc-01 and wire-dpop-01 will be
// disabled.
Challenges []ACMEChallenge `json:"challenges,omitempty"`
// AttestationFormats contains the enabled attestation formats for this
// provisioner. If this value is not set the default apple, step and tpm
// will be used.
AttestationFormats []ACMEAttestationFormat `json:"attestationFormats,omitempty"`
// AttestationRoots contains a bundle of root certificates in PEM format
// that will be used to verify the attestation certificates. If provided,
// this bundle will be used even for well-known CAs like Apple and Yubico.
AttestationRoots []byte `json:"attestationRoots,omitempty"`
Claims *Claims `json:"claims,omitempty"`
Options *Options `json:"options,omitempty"`
attestationRootPool *x509.CertPool
ctl *Controller
}
// GetID returns the provisioner unique identifier.
func (p ACME) GetID() string {
if p.ID != "" {
return p.ID
}
return p.GetIDForToken()
}
// GetIDForToken returns an identifier that will be used to load the provisioner
// from a token.
func (p *ACME) GetIDForToken() string {
return "acme/" + p.Name
}
// GetTokenID returns the identifier of the token.
func (p *ACME) GetTokenID(string) (string, error) {
return "", errors.New("acme provisioner does not implement GetTokenID")
}
// GetName returns the name of the provisioner.
func (p *ACME) GetName() string {
return p.Name
}
// GetType returns the type of provisioner.
func (p *ACME) GetType() Type {
return TypeACME
}
// GetEncryptedKey returns the base provisioner encrypted key if it's defined.
func (p *ACME) GetEncryptedKey() (string, string, bool) {
return "", "", false
}
// GetOptions returns the configured provisioner options.
func (p *ACME) GetOptions() *Options {
return p.Options
}
// DefaultTLSCertDuration returns the default TLS cert duration enforced by
// the provisioner.
func (p *ACME) DefaultTLSCertDuration() time.Duration {
return p.ctl.Claimer.DefaultTLSCertDuration()
}
// Init initializes and validates the fields of an ACME type.
func (p *ACME) Init(config Config) (err error) {
switch {
case p.Type == "":
return errors.New("provisioner type cannot be empty")
case p.Name == "":
return errors.New("provisioner name cannot be empty")
}
for _, c := range p.Challenges {
if err := c.Validate(); err != nil {
return err
}
}
for _, f := range p.AttestationFormats {
if err := f.Validate(); err != nil {
return err
}
}
// Parse attestation roots.
// The pool will be nil if there are no roots.
if rest := p.AttestationRoots; len(rest) > 0 {
var block *pem.Block
var hasCert bool
p.attestationRootPool = x509.NewCertPool()
for rest != nil {
block, rest = pem.Decode(rest)
if block == nil {
break
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return errors.New("error parsing attestationRoots: malformed certificate")
}
p.attestationRootPool.AddCert(cert)
hasCert = true
}
if !hasCert {
return errors.New("error parsing attestationRoots: no certificates found")
}
}
if err := p.initializeWireOptions(); err != nil {
return fmt.Errorf("failed initializing Wire options: %w", err)
}
p.ctl, err = NewController(p, p.Claims, config, p.Options)
return
}
// initializeWireOptions initializes the options for the ACME Wire
// integration. It'll return early if no Wire challenge types are
// enabled.
func (p *ACME) initializeWireOptions() error {
hasWireChallenges := false
for _, c := range p.Challenges {
if c == WIREOIDC_01 || c == WIREDPOP_01 {
hasWireChallenges = true
break
}
}
if !hasWireChallenges {
return nil
}
w, err := p.GetOptions().GetWireOptions()
if err != nil {
return fmt.Errorf("failed getting Wire options: %w", err)
}
if err := w.Validate(); err != nil {
return fmt.Errorf("failed validating Wire options: %w", err)
}
// at this point the Wire options have been validated, and (mostly)
// initialized. Remote keys will be loaded upon the first verification,
// currently.
// TODO(hs): can/should we "prime" the underlying remote keyset, to verify
// auto discovery works as expected? Because of the current way provisioners
// are initialized, doing that as part of the initialization isn't the best
// time to do it, because it could result in operations not resulting in the
// expected result in all cases.
return nil
}
// ACMEIdentifierType encodes ACME Identifier types
type ACMEIdentifierType string
const (
// IP is the ACME ip identifier type
IP ACMEIdentifierType = "ip"
// DNS is the ACME dns identifier type
DNS ACMEIdentifierType = "dns"
// WireUser is the Wire user identifier type
WireUser ACMEIdentifierType = "wireapp-user"
// WireDevice is the Wire device identifier type
WireDevice ACMEIdentifierType = "wireapp-device"
)
// ACMEIdentifier encodes ACME Order Identifiers
type ACMEIdentifier struct {
Type ACMEIdentifierType
Value string
}
// AuthorizeOrderIdentifier verifies the provisioner is allowed to issue a
// certificate for an ACME Order Identifier.
func (p *ACME) AuthorizeOrderIdentifier(_ context.Context, identifier ACMEIdentifier) error {
x509Policy := p.ctl.getPolicy().getX509()
// identifier is allowed if no policy is configured
if x509Policy == nil {
return nil
}
// assuming only valid identifiers (IP or DNS) are provided
var err error
switch identifier.Type {
case IP:
err = x509Policy.IsIPAllowed(net.ParseIP(identifier.Value))
case DNS:
err = x509Policy.IsDNSAllowed(identifier.Value)
case WireUser:
var wireID wire.UserID
if wireID, err = wire.ParseUserID(identifier.Value); err != nil {
return fmt.Errorf("failed parsing Wire SANs: %w", err)
}
err = x509Policy.AreSANsAllowed([]string{wireID.Handle})
case WireDevice:
var wireID wire.DeviceID
if wireID, err = wire.ParseDeviceID(identifier.Value); err != nil {
return fmt.Errorf("failed parsing Wire SANs: %w", err)
}
err = x509Policy.AreSANsAllowed([]string{wireID.ClientID})
default:
err = fmt.Errorf("invalid ACME identifier type '%s' provided", identifier.Type)
}
return err
}
// AuthorizeSign does not do any validation, because all validation is handled
// in the ACME protocol. This method returns a list of modifiers / constraints
// on the resulting certificate.
func (p *ACME) AuthorizeSign(context.Context, string) ([]SignOption, error) {
opts := []SignOption{
p,
// modifiers / withOptions
newProvisionerExtensionOption(TypeACME, p.Name, "").WithControllerOptions(p.ctl),
newForceCNOption(p.ForceCN),
profileDefaultDuration(p.ctl.Claimer.DefaultTLSCertDuration()),
// validators
defaultPublicKeyValidator{},
newValidityValidator(p.ctl.Claimer.MinTLSCertDuration(), p.ctl.Claimer.MaxTLSCertDuration()),
newX509NamePolicyValidator(p.ctl.getPolicy().getX509()),
p.ctl.newWebhookController(nil, linkedca.Webhook_X509),
}
return opts, nil
}
// AuthorizeRevoke is called just before the certificate is to be revoked by
// the CA. It can be used to authorize revocation of a certificate. With the
// ACME protocol, revocation authorization is specified and performed as part
// of the client/server interaction, so this is a no-op.
func (p *ACME) AuthorizeRevoke(context.Context, string) error {
return nil
}
// AuthorizeRenew returns an error if the renewal is disabled.
// NOTE: This method does not actually validate the certificate or check its
// revocation status. Just confirms that the provisioner that created the
// certificate was configured to allow renewals.
func (p *ACME) AuthorizeRenew(ctx context.Context, cert *x509.Certificate) error {
return p.ctl.AuthorizeRenew(ctx, cert)
}
// IsChallengeEnabled checks if the given challenge is enabled. By default
// http-01, dns-01 and tls-alpn-01 are enabled, to disable any of them the
// Challenge provisioner property should have at least one element.
func (p *ACME) IsChallengeEnabled(_ context.Context, challenge ACMEChallenge) bool {
enabledChallenges := []ACMEChallenge{
HTTP_01, DNS_01, TLS_ALPN_01,
}
if len(p.Challenges) > 0 {
enabledChallenges = p.Challenges
}
for _, ch := range enabledChallenges {
if strings.EqualFold(string(ch), string(challenge)) {
return true
}
}
return false
}
// IsAttestationFormatEnabled checks if the given attestation format is enabled.
// By default apple, step and tpm are enabled, to disable any of them the
// AttestationFormat provisioner property should have at least one element.
func (p *ACME) IsAttestationFormatEnabled(_ context.Context, format ACMEAttestationFormat) bool {
enabledFormats := []ACMEAttestationFormat{
APPLE, STEP, TPM,
}
if len(p.AttestationFormats) > 0 {
enabledFormats = p.AttestationFormats
}
for _, f := range enabledFormats {
if strings.EqualFold(string(f), string(format)) {
return true
}
}
return false
}
// GetAttestationRoots returns certificate pool with the configured attestation
// roots and reports if the pool contains at least one certificate.
//
// TODO(hs): we may not want to expose the root pool like this; call into an
// interface function instead to authorize?
func (p *ACME) GetAttestationRoots() (*x509.CertPool, bool) {
return p.attestationRootPool, p.attestationRootPool != nil
}
|