Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions acme/api/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,18 +282,21 @@ func newAuthorization(ctx context.Context, az *acme.Authorization) error {
if err != nil {
return acme.WrapError(acme.ErrorMalformedType, err, "failed parsing ClientID")
}

var targetProvider interface{ GetTarget(string) (string, error) }
wireOptions, err := prov.GetOptions().GetWireOptions()
if err != nil {
return acme.WrapErrorISE(err, "failed getting Wire options")
}
var targetProvider interface{ EvaluateTarget(string) (string, error) }
switch typ {
case acme.WIREOIDC01:
targetProvider = prov.GetOptions().GetWireOptions().GetOIDCOptions()
targetProvider = wireOptions.GetOIDCOptions()
case acme.WIREDPOP01:
targetProvider = prov.GetOptions().GetWireOptions().GetDPOPOptions()
targetProvider = wireOptions.GetDPOPOptions()
default:
return acme.NewError(acme.ErrorMalformedType, "unsupported type %q", typ)
}

target, err = targetProvider.GetTarget(clientID.DeviceID)
target, err = targetProvider.EvaluateTarget(clientID.DeviceID)
if err != nil {
return acme.WrapError(acme.ErrorMalformedType, err, "invalid Go template registered for 'target'")
}
Expand Down
16 changes: 11 additions & 5 deletions acme/api/order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,10 @@ func TestHandler_NewOrder(t *testing.T) {
u := fmt.Sprintf("%s/acme/%s/order/ordID",
baseURL.String(), escProvName)

fakeWireSigningKey := `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
-----END PUBLIC KEY-----`

type test struct {
ca acme.CertificateAuthority
db acme.DB
Expand Down Expand Up @@ -1719,25 +1723,27 @@ func TestHandler_NewOrder(t *testing.T) {
acmeWireProv := newWireProvisionerWithOptions(t, &provisioner.Options{
Wire: &wire.Options{
OIDC: &wire.OIDCOptions{
Provider: wire.ProviderJSON{
IssuerURL: "",
Provider: &wire.Provider{
IssuerURL: "https://issuer.example.com",
AuthURL: "",
TokenURL: "",
JWKSURL: "",
UserInfoURL: "",
Algorithms: []string{},
},
Config: wire.ConfigJSON{
Config: &wire.Config{
ClientID: "integration test",
SupportedSigningAlgs: []string{},
SignatureAlgorithms: []string{},
SkipClientIDCheck: true,
SkipExpiryCheck: true,
SkipIssuerCheck: true,
InsecureSkipSignatureCheck: true,
Now: time.Now,
},
},
DPOP: &wire.DPOPOptions{},
DPOP: &wire.DPOPOptions{
SigningKey: []byte(fakeWireSigningKey),
},
},
})
acc := &acme.Account{ID: "accID"}
Expand Down
15 changes: 10 additions & 5 deletions acme/api/wire_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,28 +51,33 @@ func newWireProvisionerWithOptions(t *testing.T, options *provisioner.Options) *
}

func TestWireIntegration(t *testing.T) {
fakeKey := `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA5c+4NKZSNQcR1T8qN6SjwgdPZQ0Ge12Ylx/YeGAJ35k=
-----END PUBLIC KEY-----`
prov := newWireProvisionerWithOptions(t, &provisioner.Options{
Wire: &wire.Options{
OIDC: &wire.OIDCOptions{
Provider: wire.ProviderJSON{
IssuerURL: "",
Provider: &wire.Provider{
IssuerURL: "https://issuer.example.com",
AuthURL: "",
TokenURL: "",
JWKSURL: "",
UserInfoURL: "",
Algorithms: []string{},
},
Config: wire.ConfigJSON{
Config: &wire.Config{
ClientID: "integration test",
SupportedSigningAlgs: []string{},
SignatureAlgorithms: []string{},
SkipClientIDCheck: true,
SkipExpiryCheck: true,
SkipIssuerCheck: true,
InsecureSkipSignatureCheck: true,
Now: time.Now,
},
},
DPOP: &wire.DPOPOptions{},
DPOP: &wire.DPOPOptions{
SigningKey: []byte(fakeKey),
},
},
})

Expand Down
61 changes: 30 additions & 31 deletions acme/challenge.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,12 @@ func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSO
"error unmarshalling Wire challenge payload"))
}

oidcOptions := prov.GetOptions().GetWireOptions().GetOIDCOptions()
wireOptions, err := prov.GetOptions().GetWireOptions()
if err != nil {
return WrapErrorISE(err, "failed getting Wire options")
}

oidcOptions := wireOptions.GetOIDCOptions()
idToken, err := oidcOptions.GetProvider(ctx).Verifier(oidcOptions.GetConfig()).Verify(ctx, oidcPayload.IDToken)
if err != nil {
return storeError(ctx, db, ch, false, WrapError(ErrorRejectedIdentifierType, err,
Expand Down Expand Up @@ -468,20 +473,25 @@ func wireDPOP01Validate(ctx context.Context, ch *Challenge, db DB, accountJWK *j
return WrapErrorISE(err, "error parsing device id")
}

dpopOptions := prov.GetOptions().GetWireOptions().GetDPOPOptions()
issuer, err := dpopOptions.GetTarget(clientID.DeviceID)
wireOptions, err := prov.GetOptions().GetWireOptions()
if err != nil {
return WrapErrorISE(err, "failed getting Wire options")
}

dpopOptions := wireOptions.GetDPOPOptions()
issuer, err := dpopOptions.EvaluateTarget(clientID.DeviceID)
if err != nil {
return WrapErrorISE(err, "invalid Go template registered for 'target'")
}

params := verifyParams{
token: dpopPayload.AccessToken,
key: dpopOptions.GetSigningKey(),
accountJWK: accountJWK,
issuer: issuer,
wireID: wireID,
challenge: ch,
t: clock.Now().UTC(),
token: dpopPayload.AccessToken,
tokenKey: dpopOptions.GetSigningKey(),
dpopKey: accountJWK,
issuer: issuer,
wireID: wireID,
challenge: ch,
t: clock.Now().UTC(),
}
_, dpop, err := parseAndVerifyWireAccessToken(params)
if err != nil {
Expand Down Expand Up @@ -530,33 +540,23 @@ type wireAccessToken struct {
type wireDpopToken map[string]any

type verifyParams struct {
token string
key string
issuer string
accountJWK *jose.JSONWebKey
wireID wire.ID
challenge *Challenge
t time.Time
token string
tokenKey crypto.PublicKey
dpopKey *jose.JSONWebKey
issuer string
wireID wire.ID
challenge *Challenge
t time.Time
}

func parseAndVerifyWireAccessToken(v verifyParams) (*wireAccessToken, *wireDpopToken, error) {
k, err := pemutil.Parse([]byte(v.key)) // TODO(hs): move this to earlier in the configuration process? Do it once?
if err != nil {
return nil, nil, fmt.Errorf("failed parsing public key: %w", err)
}

pk, ok := k.(ed25519.PublicKey) // TODO(hs): allow more key types
if !ok {
return nil, nil, fmt.Errorf("unexpected type: %T", k)
}

jwt, err := jose.ParseSigned(v.token)
if err != nil {
return nil, nil, fmt.Errorf("failed parsing token: %w", err)
}

var accessToken wireAccessToken
if err = jwt.Claims(pk, &accessToken); err != nil {
if err = jwt.Claims(v.tokenKey, &accessToken); err != nil {
return nil, nil, fmt.Errorf("failed validating Wire DPoP token claims: %w", err)
}

Expand All @@ -567,7 +567,7 @@ func parseAndVerifyWireAccessToken(v verifyParams) (*wireAccessToken, *wireDpopT
return nil, nil, fmt.Errorf("failed validation: %w", err)
}

rawKid, err := v.accountJWK.Thumbprint(crypto.SHA256)
rawKid, err := v.dpopKey.Thumbprint(crypto.SHA256)
if err != nil {
return nil, nil, fmt.Errorf("failed to compute JWK thumbprint")
}
Expand All @@ -590,9 +590,8 @@ func parseAndVerifyWireAccessToken(v verifyParams) (*wireAccessToken, *wireDpopT
if err != nil {
return nil, nil, fmt.Errorf("invalid Wire DPoP token: %w", err)
}

var dpopToken wireDpopToken
if err := dpopJWT.Claims(v.accountJWK.Key, &dpopToken); err != nil {
if err := dpopJWT.Claims(v.dpopKey.Key, &dpopToken); err != nil {
return nil, nil, fmt.Errorf("failed validating Wire DPoP token claims: %w", err)
}

Expand Down
19 changes: 11 additions & 8 deletions acme/challenge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"go.step.sm/crypto/jose"
"go.step.sm/crypto/keyutil"
"go.step.sm/crypto/minica"
"go.step.sm/crypto/pemutil"
"go.step.sm/crypto/x509util"

"github.com/smallstep/certificates/acme/wire"
Expand Down Expand Up @@ -4308,7 +4309,9 @@ func Test_parseAndVerifyWireAccessToken(t *testing.T) {
key := `
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAB2IYqBWXAouDt3WcCZgCM3t9gumMEKMlgMsGenSu+fA=
-----END PUBLIC KEY-----` // TODO(hs): different format?
-----END PUBLIC KEY-----`
publicKey, err := pemutil.Parse([]byte(key))
require.NoError(t, err)
issuer := "http://wire.com:19983/clients/7a41cf5b79683410/access-token"
wireID := wire.ID{
ClientID: "wireapp://guVX5xeFS3eTatmXBIyA4A!7a41cf5b79683410@wire.com",
Expand All @@ -4330,13 +4333,13 @@ MCowBQYDK2VwAyEAB2IYqBWXAouDt3WcCZgCM3t9gumMEKMlgMsGenSu+fA=
json.Unmarshal(jwkBytes, &accountJWK)

at, dpop, err := parseAndVerifyWireAccessToken(verifyParams{
token: token,
key: key,
accountJWK: &accountJWK,
issuer: issuer,
wireID: wireID,
challenge: ch,
t: issuedAt.Add(1 * time.Minute), // set validation time to be one minute after issuance
token: token,
tokenKey: publicKey,
dpopKey: &accountJWK,
issuer: issuer,
wireID: wireID,
challenge: ch,
t: issuedAt.Add(1 * time.Minute), // set validation time to be one minute after issuance
})
if assert.NoError(t, err) {
// token assertions
Expand Down
10 changes: 7 additions & 3 deletions authority/provisioner/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package provisioner

import (
"encoding/json"
"fmt"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -54,11 +55,14 @@ func (o *Options) GetSSHOptions() *SSHOptions {
}

// GetWireOptions returns the SSH options.
func (o *Options) GetWireOptions() *wire.Options {
func (o *Options) GetWireOptions() (*wire.Options, error) {
if o == nil {
return nil
return nil, errors.New("no Wire options available")
}
if err := o.Wire.Validate(); err != nil {
return nil, fmt.Errorf("failed validating Wire options: %w", err)
}
return o.Wire
return o.Wire, nil
}

// GetWebhooks returns the webhooks options.
Expand Down
50 changes: 25 additions & 25 deletions authority/provisioner/wire/dpop_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,44 @@ package wire

import (
"bytes"
"errors"
"crypto"
"fmt"
"text/template"

"go.step.sm/crypto/pemutil"
)

type DPOPOptions struct {
// Backend signing key for DPoP access token
SigningKey string `json:"key"`
// URI template acme client must call to fetch the DPoP challenge proof (an access token from wire-server)
DpopTarget string `json:"dpop-target"`
// Public part of the signing key for DPoP access token
SigningKey []byte `json:"key"`
// URI template for the URI the ACME client must call to fetch the DPoP challenge proof (an access token from wire-server)
Target string `json:"target"`

signingKey crypto.PublicKey
target *template.Template
}

func (o *DPOPOptions) GetSigningKey() string {
if o == nil {
return ""
}
return o.SigningKey
func (o *DPOPOptions) GetSigningKey() crypto.PublicKey {
return o.signingKey
}

func (o *DPOPOptions) GetDPOPTarget() string {
if o == nil {
return ""
func (o *DPOPOptions) EvaluateTarget(deviceID string) (string, error) {
buf := new(bytes.Buffer)
if err := o.target.Execute(buf, struct{ DeviceID string }{DeviceID: deviceID}); err != nil {
return "", fmt.Errorf("failed executing dpop template: %w", err)
}
return o.DpopTarget
return buf.String(), nil
}

func (o *DPOPOptions) GetTarget(deviceID string) (string, error) {
if o == nil {
return "", errors.New("misconfigured target template configuration")
}
targetTemplate := o.GetDPOPTarget()
tmpl, err := template.New("DeviceId").Parse(targetTemplate)
func (o *DPOPOptions) validateAndInitialize() (err error) {
o.signingKey, err = pemutil.Parse(o.SigningKey)
if err != nil {
return "", fmt.Errorf("failed parsing dpop template: %w", err)
return fmt.Errorf("failed parsing key: %w", err)
}
buf := new(bytes.Buffer)
if err = tmpl.Execute(buf, struct{ DeviceId string }{DeviceId: deviceID}); err != nil { //nolint:revive,stylecheck // TODO(hs): this requires changes in configuration
return "", fmt.Errorf("failed executing dpop template: %w", err)
o.target, err = template.New("DeviceID").Parse(o.Target)
if err != nil {
return fmt.Errorf("failed parsing DPoP template: %w", err)
}
return buf.String(), nil

return nil
}
Loading