Merge branch 'master' into herman/allow-deny

This commit is contained in:
Herman Slatman 2022-04-15 11:57:05 +02:00
commit d6be9450be
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
11 changed files with 70 additions and 81 deletions

View file

@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased - 0.18.3] - DATE ## [Unreleased - 0.18.3] - DATE
### Added ### Added
- Added support for renew after expiry using the claim `allowRenewAfterExpiry`. - Added support for certificate renewals after expiry using the claim `allowRenewalAfterExpiry`.
- Added support for `extraNames` in X.509 templates. - Added support for `extraNames` in X.509 templates.
### Changed ### Changed
- Made SCEP CA URL paths dynamic - Made SCEP CA URL paths dynamic

View file

@ -54,7 +54,7 @@ Setting up a *public key infrastructure* (PKI) is out of reach for many small te
- [Short-lived certificates](https://smallstep.com/blog/passive-revocation.html) with automated enrollment, renewal, and passive revocation - [Short-lived certificates](https://smallstep.com/blog/passive-revocation.html) with automated enrollment, renewal, and passive revocation
- Capable of high availability (HA) deployment using [root federation](https://smallstep.com/blog/step-v0.8.3-federation-root-rotation.html) and/or multiple intermediaries - Capable of high availability (HA) deployment using [root federation](https://smallstep.com/blog/step-v0.8.3-federation-root-rotation.html) and/or multiple intermediaries
- Can operate as [an online intermediate CA for an existing root CA](https://smallstep.com/docs/tutorials/intermediate-ca-new-ca) - Can operate as [an online intermediate CA for an existing root CA](https://smallstep.com/docs/tutorials/intermediate-ca-new-ca)
- [Badger, BoltDB, and MySQL database backends](https://smallstep.com/docs/step-ca/configuration#databases) - [Badger, BoltDB, Postgres, and MySQL database backends](https://smallstep.com/docs/step-ca/configuration#databases)
### ⚙️ Many ways to automate ### ⚙️ Many ways to automate

View file

@ -29,27 +29,27 @@ var (
DefaultBackdate = time.Minute DefaultBackdate = time.Minute
// DefaultDisableRenewal disables renewals per provisioner. // DefaultDisableRenewal disables renewals per provisioner.
DefaultDisableRenewal = false DefaultDisableRenewal = false
// DefaultAllowRenewAfterExpiry allows renewals even if the certificate is // DefaultAllowRenewalAfterExpiry allows renewals even if the certificate is
// expired. // expired.
DefaultAllowRenewAfterExpiry = false DefaultAllowRenewalAfterExpiry = false
// DefaultEnableSSHCA enable SSH CA features per provisioner or globally // DefaultEnableSSHCA enable SSH CA features per provisioner or globally
// for all provisioners. // for all provisioners.
DefaultEnableSSHCA = false DefaultEnableSSHCA = false
// GlobalProvisionerClaims default claims for the Authority. Can be overridden // GlobalProvisionerClaims default claims for the Authority. Can be overridden
// by provisioner specific claims. // by provisioner specific claims.
GlobalProvisionerClaims = provisioner.Claims{ GlobalProvisionerClaims = provisioner.Claims{
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour}, DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
MinUserSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // User SSH certs MinUserSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &provisioner.Duration{Duration: 24 * time.Hour}, MaxUserSSHDur: &provisioner.Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &provisioner.Duration{Duration: 16 * time.Hour}, DefaultUserSSHDur: &provisioner.Duration{Duration: 16 * time.Hour},
MinHostSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // Host SSH certs MinHostSSHDur: &provisioner.Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, MaxHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour}, DefaultHostSSHDur: &provisioner.Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &DefaultEnableSSHCA, EnableSSHCA: &DefaultEnableSSHCA,
DisableRenewal: &DefaultDisableRenewal, DisableRenewal: &DefaultDisableRenewal,
AllowRenewAfterExpiry: &DefaultAllowRenewAfterExpiry, AllowRenewalAfterExpiry: &DefaultAllowRenewalAfterExpiry,
} }
) )

View file

@ -24,8 +24,8 @@ type Claims struct {
EnableSSHCA *bool `json:"enableSSHCA,omitempty"` EnableSSHCA *bool `json:"enableSSHCA,omitempty"`
// Renewal properties // Renewal properties
DisableRenewal *bool `json:"disableRenewal,omitempty"` DisableRenewal *bool `json:"disableRenewal,omitempty"`
AllowRenewAfterExpiry *bool `json:"allowRenewAfterExpiry,omitempty"` AllowRenewalAfterExpiry *bool `json:"allowRenewalAfterExpiry,omitempty"`
} }
// Claimer is the type that controls claims. It provides an interface around the // Claimer is the type that controls claims. It provides an interface around the
@ -44,22 +44,22 @@ func NewClaimer(claims *Claims, global Claims) (*Claimer, error) {
// Claims returns the merge of the inner and global claims. // Claims returns the merge of the inner and global claims.
func (c *Claimer) Claims() Claims { func (c *Claimer) Claims() Claims {
disableRenewal := c.IsDisableRenewal() disableRenewal := c.IsDisableRenewal()
allowRenewAfterExpiry := c.AllowRenewAfterExpiry() allowRenewalAfterExpiry := c.AllowRenewalAfterExpiry()
enableSSHCA := c.IsSSHCAEnabled() enableSSHCA := c.IsSSHCAEnabled()
return Claims{ return Claims{
MinTLSDur: &Duration{c.MinTLSCertDuration()}, MinTLSDur: &Duration{c.MinTLSCertDuration()},
MaxTLSDur: &Duration{c.MaxTLSCertDuration()}, MaxTLSDur: &Duration{c.MaxTLSCertDuration()},
DefaultTLSDur: &Duration{c.DefaultTLSCertDuration()}, DefaultTLSDur: &Duration{c.DefaultTLSCertDuration()},
MinUserSSHDur: &Duration{c.MinUserSSHCertDuration()}, MinUserSSHDur: &Duration{c.MinUserSSHCertDuration()},
MaxUserSSHDur: &Duration{c.MaxUserSSHCertDuration()}, MaxUserSSHDur: &Duration{c.MaxUserSSHCertDuration()},
DefaultUserSSHDur: &Duration{c.DefaultUserSSHCertDuration()}, DefaultUserSSHDur: &Duration{c.DefaultUserSSHCertDuration()},
MinHostSSHDur: &Duration{c.MinHostSSHCertDuration()}, MinHostSSHDur: &Duration{c.MinHostSSHCertDuration()},
MaxHostSSHDur: &Duration{c.MaxHostSSHCertDuration()}, MaxHostSSHDur: &Duration{c.MaxHostSSHCertDuration()},
DefaultHostSSHDur: &Duration{c.DefaultHostSSHCertDuration()}, DefaultHostSSHDur: &Duration{c.DefaultHostSSHCertDuration()},
EnableSSHCA: &enableSSHCA, EnableSSHCA: &enableSSHCA,
DisableRenewal: &disableRenewal, DisableRenewal: &disableRenewal,
AllowRenewAfterExpiry: &allowRenewAfterExpiry, AllowRenewalAfterExpiry: &allowRenewalAfterExpiry,
} }
} }
@ -109,14 +109,14 @@ func (c *Claimer) IsDisableRenewal() bool {
return *c.claims.DisableRenewal return *c.claims.DisableRenewal
} }
// AllowRenewAfterExpiry returns if the renewal flow is authorized if the // AllowRenewalAfterExpiry returns if the renewal flow is authorized if the
// certificate is expired. If the property is not set within the provisioner // certificate is expired. If the property is not set within the provisioner
// then the global value from the authority configuration will be used. // then the global value from the authority configuration will be used.
func (c *Claimer) AllowRenewAfterExpiry() bool { func (c *Claimer) AllowRenewalAfterExpiry() bool {
if c.claims == nil || c.claims.AllowRenewAfterExpiry == nil { if c.claims == nil || c.claims.AllowRenewalAfterExpiry == nil {
return *c.global.AllowRenewAfterExpiry return *c.global.AllowRenewalAfterExpiry
} }
return *c.claims.AllowRenewAfterExpiry return *c.claims.AllowRenewalAfterExpiry
} }
// DefaultSSHCertDuration returns the default SSH certificate duration for the // DefaultSSHCertDuration returns the default SSH certificate duration for the

View file

@ -124,7 +124,7 @@ func DefaultAuthorizeRenew(ctx context.Context, p *Controller, cert *x509.Certif
if now.Before(cert.NotBefore) { if now.Before(cert.NotBefore) {
return errs.Unauthorized("certificate is not yet valid" + " " + now.UTC().Format(time.RFC3339Nano) + " vs " + cert.NotBefore.Format(time.RFC3339Nano)) return errs.Unauthorized("certificate is not yet valid" + " " + now.UTC().Format(time.RFC3339Nano) + " vs " + cert.NotBefore.Format(time.RFC3339Nano))
} }
if now.After(cert.NotAfter) && !p.Claimer.AllowRenewAfterExpiry() { if now.After(cert.NotAfter) && !p.Claimer.AllowRenewalAfterExpiry() {
return errs.Unauthorized("certificate has expired") return errs.Unauthorized("certificate has expired")
} }
@ -144,7 +144,7 @@ func DefaultAuthorizeSSHRenew(ctx context.Context, p *Controller, cert *ssh.Cert
if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) { if after := int64(cert.ValidAfter); after < 0 || unixNow < int64(cert.ValidAfter) {
return errs.Unauthorized("certificate is not yet valid") return errs.Unauthorized("certificate is not yet valid")
} }
if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) && !p.Claimer.AllowRenewAfterExpiry() { if before := int64(cert.ValidBefore); cert.ValidBefore != uint64(ssh.CertTimeInfinity) && (unixNow >= before || before < 0) && !p.Claimer.AllowRenewalAfterExpiry() {
return errs.Unauthorized("certificate has expired") return errs.Unauthorized("certificate has expired")
} }

View file

@ -160,13 +160,13 @@ func TestController_AuthorizeRenew(t *testing.T) {
NotBefore: now, NotBefore: now,
NotAfter: now.Add(time.Hour), NotAfter: now.Add(time.Hour),
}}, false}, }}, false},
{"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error { {"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *x509.Certificate) error {
return nil return nil
}}, args{ctx, &x509.Certificate{ }}, args{ctx, &x509.Certificate{
NotBefore: now, NotBefore: now,
NotAfter: now.Add(time.Hour), NotAfter: now.Add(time.Hour),
}}, false}, }}, false},
{"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{ {"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &x509.Certificate{
NotBefore: now.Add(-time.Hour), NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(-time.Minute), NotAfter: now.Add(-time.Minute),
}}, false}, }}, false},
@ -231,13 +231,13 @@ func TestController_AuthorizeSSHRenew(t *testing.T) {
ValidAfter: uint64(now.Unix()), ValidAfter: uint64(now.Unix()),
ValidBefore: uint64(now.Add(time.Hour).Unix()), ValidBefore: uint64(now.Add(time.Hour).Unix()),
}}, false}, }}, false},
{"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error { {"ok custom disabled", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), func(ctx context.Context, p *Controller, cert *ssh.Certificate) error {
return nil return nil
}}, args{ctx, &ssh.Certificate{ }}, args{ctx, &ssh.Certificate{
ValidAfter: uint64(now.Unix()), ValidAfter: uint64(now.Unix()),
ValidBefore: uint64(now.Add(time.Hour).Unix()), ValidBefore: uint64(now.Add(time.Hour).Unix()),
}}, false}, }}, false},
{"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{ {"ok renew after expiry", fields{&JWK{}, mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims), nil}, args{ctx, &ssh.Certificate{
ValidAfter: uint64(now.Add(-time.Hour).Unix()), ValidAfter: uint64(now.Add(-time.Hour).Unix()),
ValidBefore: uint64(now.Add(-time.Minute).Unix()), ValidBefore: uint64(now.Add(-time.Minute).Unix()),
}}, false}, }}, false},
@ -296,7 +296,7 @@ func TestDefaultAuthorizeRenew(t *testing.T) {
}}, false}, }}, false},
{"ok renew after expiry", args{ctx, &Controller{ {"ok renew after expiry", args{ctx, &Controller{
Interface: &JWK{}, Interface: &JWK{},
Claimer: mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), Claimer: mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims),
}, &x509.Certificate{ }, &x509.Certificate{
NotBefore: now.Add(-time.Hour), NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(-time.Minute), NotAfter: now.Add(-time.Minute),
@ -354,7 +354,7 @@ func TestDefaultAuthorizeSSHRenew(t *testing.T) {
}}, false}, }}, false},
{"ok renew after expiry", args{ctx, &Controller{ {"ok renew after expiry", args{ctx, &Controller{
Interface: &JWK{}, Interface: &JWK{},
Claimer: mustClaimer(t, &Claims{AllowRenewAfterExpiry: &trueValue}, globalProvisionerClaims), Claimer: mustClaimer(t, &Claims{AllowRenewalAfterExpiry: &trueValue}, globalProvisionerClaims),
}, &ssh.Certificate{ }, &ssh.Certificate{
ValidAfter: uint64(now.Add(-time.Hour).Unix()), ValidAfter: uint64(now.Add(-time.Hour).Unix()),
ValidBefore: uint64(now.Add(-time.Minute).Unix()), ValidBefore: uint64(now.Add(-time.Minute).Unix()),

View file

@ -24,22 +24,22 @@ import (
) )
var ( var (
defaultDisableRenewal = false defaultDisableRenewal = false
defaultAllowRenewAfterExpiry = false defaultAllowRenewalAfterExpiry = false
defaultEnableSSHCA = true defaultEnableSSHCA = true
globalProvisionerClaims = Claims{ globalProvisionerClaims = Claims{
MinTLSDur: &Duration{5 * time.Minute}, MinTLSDur: &Duration{5 * time.Minute},
MaxTLSDur: &Duration{24 * time.Hour}, MaxTLSDur: &Duration{24 * time.Hour},
DefaultTLSDur: &Duration{24 * time.Hour}, DefaultTLSDur: &Duration{24 * time.Hour},
MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs MinUserSSHDur: &Duration{Duration: 5 * time.Minute}, // User SSH certs
MaxUserSSHDur: &Duration{Duration: 24 * time.Hour}, MaxUserSSHDur: &Duration{Duration: 24 * time.Hour},
DefaultUserSSHDur: &Duration{Duration: 16 * time.Hour}, DefaultUserSSHDur: &Duration{Duration: 16 * time.Hour},
MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs MinHostSSHDur: &Duration{Duration: 5 * time.Minute}, // Host SSH certs
MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, MaxHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour}, DefaultHostSSHDur: &Duration{Duration: 30 * 24 * time.Hour},
EnableSSHCA: &defaultEnableSSHCA, EnableSSHCA: &defaultEnableSSHCA,
DisableRenewal: &defaultDisableRenewal, DisableRenewal: &defaultDisableRenewal,
AllowRenewAfterExpiry: &defaultAllowRenewAfterExpiry, AllowRenewalAfterExpiry: &defaultAllowRenewalAfterExpiry,
} }
testAudiences = Audiences{ testAudiences = Audiences{
Sign: []string{"https://ca.smallstep.com/1.0/sign", "https://ca.smallstep.com/sign"}, Sign: []string{"https://ca.smallstep.com/1.0/sign", "https://ca.smallstep.com/sign"},

View file

@ -504,8 +504,8 @@ func claimsToCertificates(c *linkedca.Claims) (*provisioner.Claims, error) {
} }
pc := &provisioner.Claims{ pc := &provisioner.Claims{
DisableRenewal: &c.DisableRenewal, DisableRenewal: &c.DisableRenewal,
AllowRenewAfterExpiry: &c.AllowRenewAfterExpiry, AllowRenewalAfterExpiry: &c.AllowRenewalAfterExpiry,
} }
var err error var err error
@ -543,18 +543,18 @@ func claimsToLinkedca(c *provisioner.Claims) *linkedca.Claims {
} }
disableRenewal := config.DefaultDisableRenewal disableRenewal := config.DefaultDisableRenewal
allowRenewAfterExpiry := config.DefaultAllowRenewAfterExpiry allowRenewalAfterExpiry := config.DefaultAllowRenewalAfterExpiry
if c.DisableRenewal != nil { if c.DisableRenewal != nil {
disableRenewal = *c.DisableRenewal disableRenewal = *c.DisableRenewal
} }
if c.AllowRenewAfterExpiry != nil { if c.AllowRenewalAfterExpiry != nil {
allowRenewAfterExpiry = *c.AllowRenewAfterExpiry allowRenewalAfterExpiry = *c.AllowRenewalAfterExpiry
} }
lc := &linkedca.Claims{ lc := &linkedca.Claims{
DisableRenewal: disableRenewal, DisableRenewal: disableRenewal,
AllowRenewAfterExpiry: allowRenewAfterExpiry, AllowRenewalAfterExpiry: allowRenewalAfterExpiry,
} }
if c.DefaultTLSDur != nil || c.MinTLSDur != nil || c.MaxTLSDur != nil { if c.DefaultTLSDur != nil || c.MinTLSDur != nil || c.MaxTLSDur != nil {

View file

@ -1,11 +0,0 @@
package status
// Type is the type for status.
type Type string
var (
// Active active
Active = Type("active")
// Deleted deleted
Deleted = Type("deleted")
)

2
go.mod
View file

@ -38,7 +38,7 @@ require (
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
go.step.sm/cli-utils v0.7.0 go.step.sm/cli-utils v0.7.0
go.step.sm/crypto v0.16.1 go.step.sm/crypto v0.16.1
go.step.sm/linkedca v0.12.1-0.20220405095509-878e3e5f78a3 go.step.sm/linkedca v0.15.0
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b golang.org/x/net v0.0.0-20220403103023-749bd193bc2b
golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64 // indirect

4
go.sum
View file

@ -711,8 +711,8 @@ go.step.sm/cli-utils v0.7.0/go.mod h1:Ur6bqA/yl636kCUJbp30J7Unv5JJ226eW2KqXPDwF/
go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0= go.step.sm/crypto v0.9.0/go.mod h1:+CYG05Mek1YDqi5WK0ERc6cOpKly2i/a5aZmU1sfGj0=
go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk= go.step.sm/crypto v0.16.1 h1:4mnZk21cSxyMGxsEpJwZKKvJvDu1PN09UVrWWFNUBdk=
go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g= go.step.sm/crypto v0.16.1/go.mod h1:3G0yQr5lQqfEG0CMYz8apC/qMtjLRQlzflL2AxkcN+g=
go.step.sm/linkedca v0.12.1-0.20220405095509-878e3e5f78a3 h1:CIq0rMhfcV3oDRT0h4de2GVpRQnBnLJTTVIdc0eFjUg= go.step.sm/linkedca v0.15.0 h1:lEkGRDY+u7FudGKt8yEo7nBy5OzceO9s3rl+/sZVL5M=
go.step.sm/linkedca v0.12.1-0.20220405095509-878e3e5f78a3/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM= go.step.sm/linkedca v0.15.0/go.mod h1:W59ucS4vFpuR0g4PtkGbbtXAwxbDEnNCg+ovkej1ANM=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=