|
|
|
@ -1,7 +1,6 @@
|
|
|
|
|
package acme
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"crypto"
|
|
|
|
|
"crypto/ecdsa"
|
|
|
|
@ -21,8 +20,6 @@ import (
|
|
|
|
|
"io"
|
|
|
|
|
"net"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"reflect"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
@ -354,13 +351,11 @@ func dns01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebK
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type WireChallengePayload struct {
|
|
|
|
|
// IDToken
|
|
|
|
|
type wireOidcPayload struct {
|
|
|
|
|
// IDToken contains the OIDC identity token
|
|
|
|
|
IDToken string `json:"id_token,omitempty"`
|
|
|
|
|
// KeyAuth ({challenge-token}.{jwk-thumbprint})
|
|
|
|
|
KeyAuth string `json:"keyauth,omitempty"`
|
|
|
|
|
// AccessToken is the token generated by wire-server
|
|
|
|
|
AccessToken string `json:"access_token,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error {
|
|
|
|
@ -369,15 +364,15 @@ func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSO
|
|
|
|
|
return NewErrorISE("no provisioner provided")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var wireChallengePayload WireChallengePayload
|
|
|
|
|
err := json.Unmarshal(payload, &wireChallengePayload)
|
|
|
|
|
var oidcPayload wireOidcPayload
|
|
|
|
|
err := json.Unmarshal(payload, &oidcPayload)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return storeError(ctx, db, ch, false, WrapError(ErrorRejectedIdentifierType, err,
|
|
|
|
|
"error unmarshalling Wire challenge payload"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
oidcOptions := prov.GetOptions().GetOIDCOptions()
|
|
|
|
|
idToken, err := oidcOptions.GetProvider(ctx).Verifier(oidcOptions.GetConfig()).Verify(ctx, wireChallengePayload.IDToken)
|
|
|
|
|
idToken, err := oidcOptions.GetProvider(ctx).Verifier(oidcOptions.GetConfig()).Verify(ctx, oidcPayload.IDToken)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return storeError(ctx, db, ch, false, WrapError(ErrorRejectedIdentifierType, err,
|
|
|
|
|
"error verifying ID token signature"))
|
|
|
|
@ -404,9 +399,9 @@ func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSO
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if expectedKeyAuth != wireChallengePayload.KeyAuth {
|
|
|
|
|
if expectedKeyAuth != oidcPayload.KeyAuth {
|
|
|
|
|
return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType,
|
|
|
|
|
"keyAuthorization does not match; expected %s, but got %s", expectedKeyAuth, wireChallengePayload.KeyAuth))
|
|
|
|
|
"keyAuthorization does not match; expected %s, but got %s", expectedKeyAuth, oidcPayload.KeyAuth))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if challengeValues.Name != claims.Name || challengeValues.Handle != claims.Handle {
|
|
|
|
@ -422,33 +417,36 @@ func wireOIDC01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSO
|
|
|
|
|
return WrapErrorISE(err, "error updating challenge")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parsedIDToken, err := jwt.ParseSigned(wireChallengePayload.IDToken)
|
|
|
|
|
parsedIDToken, err := jwt.ParseSigned(oidcPayload.IDToken)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Invalid OIDC id token")
|
|
|
|
|
return WrapErrorISE(err, "invalid OIDC id token")
|
|
|
|
|
}
|
|
|
|
|
oidcToken := make(map[string]interface{})
|
|
|
|
|
if err := parsedIDToken.UnsafeClaimsWithoutVerification(&oidcToken); err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Failed parsing OIDC id token")
|
|
|
|
|
return WrapErrorISE(err, "failed parsing OIDC id token")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
orders, err := db.GetAllOrdersByAccountID(ctx, ch.AccountID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Could not find current order by account id")
|
|
|
|
|
return WrapErrorISE(err, "could not find current order by account id")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(orders) == 0 {
|
|
|
|
|
return WrapErrorISE(err, "There are not enough orders for this account for this custom OIDC challenge")
|
|
|
|
|
return WrapErrorISE(err, "there are not enough orders for this account for this custom OIDC challenge")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
order := orders[len(orders)-1]
|
|
|
|
|
|
|
|
|
|
if err := db.CreateOidcToken(ctx, order, oidcToken); err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Failed storing OIDC id token")
|
|
|
|
|
return WrapErrorISE(err, "failed storing OIDC id token")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type wireDpopPayload struct {
|
|
|
|
|
// AccessToken is the token generated by wire-server
|
|
|
|
|
AccessToken string `json:"access_token,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func wireDPOP01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSONWebKey, payload []byte) error {
|
|
|
|
|
prov, ok := ProvisionerFromContext(ctx)
|
|
|
|
|
if !ok {
|
|
|
|
@ -459,150 +457,152 @@ func wireDPOP01Validate(ctx context.Context, ch *Challenge, db DB, jwk *jose.JSO
|
|
|
|
|
if thumbprintErr != nil {
|
|
|
|
|
return storeError(ctx, db, ch, false, WrapError(ErrorServerInternalType, thumbprintErr, "failed to compute JWK thumbprint"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
kid := base64.RawURLEncoding.EncodeToString(rawKid)
|
|
|
|
|
|
|
|
|
|
dpopOptions := prov.GetOptions().GetDPOPOptions()
|
|
|
|
|
key := dpopOptions.GetSigningKey()
|
|
|
|
|
|
|
|
|
|
var wireChallengePayload WireChallengePayload
|
|
|
|
|
err := json.Unmarshal(payload, &wireChallengePayload)
|
|
|
|
|
var dpopPayload wireDpopPayload
|
|
|
|
|
err := json.Unmarshal(payload, &dpopPayload)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return storeError(ctx, db, ch, false, WrapError(ErrorRejectedIdentifierType, err,
|
|
|
|
|
"error unmarshalling Wire challenge payload"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file, err := os.CreateTemp(os.TempDir(), "acme-validate-challenge-pubkey-")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "temporary file could not be created")
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
defer os.Remove(file.Name())
|
|
|
|
|
|
|
|
|
|
buf := bytes.NewBuffer(nil)
|
|
|
|
|
buf.WriteString(key)
|
|
|
|
|
|
|
|
|
|
n, err := file.Write(buf.Bytes())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Failed writing signature key to temp file")
|
|
|
|
|
}
|
|
|
|
|
if n != buf.Len() {
|
|
|
|
|
return WrapErrorISE(err, "expected to write %d characters to the key file, got %d", buf.Len(), n)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
challengeValues, err := wire.ParseID([]byte(ch.Value))
|
|
|
|
|
wireID, err := wire.ParseID([]byte(ch.Value))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "error unmarshalling challenge data")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clientID, err := wire.ParseClientID(challengeValues.ClientID)
|
|
|
|
|
clientID, err := wire.ParseClientID(wireID.ClientID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "error parsing device id")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dpopOptions := prov.GetOptions().GetDPOPOptions()
|
|
|
|
|
|
|
|
|
|
issuer, err := dpopOptions.GetTarget(clientID.DeviceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Invalid Go template registered for 'target'")
|
|
|
|
|
return WrapErrorISE(err, "invalid Go template registered for 'target'")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
key := dpopOptions.GetSigningKey()
|
|
|
|
|
expiry := strconv.FormatInt(time.Now().Add(time.Hour*24*365).Unix(), 10)
|
|
|
|
|
cmd := exec.CommandContext( //nolint:gosec // TODO(hs): replace this with Go implementation
|
|
|
|
|
ctx,
|
|
|
|
|
dpopOptions.GetValidationExecPath(),
|
|
|
|
|
"verify-access",
|
|
|
|
|
"--client-id",
|
|
|
|
|
challengeValues.ClientID,
|
|
|
|
|
"--handle",
|
|
|
|
|
challengeValues.Handle,
|
|
|
|
|
"--challenge",
|
|
|
|
|
ch.Token,
|
|
|
|
|
"--leeway",
|
|
|
|
|
"360",
|
|
|
|
|
"--max-expiry",
|
|
|
|
|
expiry,
|
|
|
|
|
"--issuer",
|
|
|
|
|
issuer,
|
|
|
|
|
"--hash-algorithm",
|
|
|
|
|
`SHA-256`,
|
|
|
|
|
"--kid",
|
|
|
|
|
kid,
|
|
|
|
|
"--key",
|
|
|
|
|
file.Name(),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
|
|
|
_, dpop, err := parseAndVerifyAccess(dpopPayload.AccessToken, key, issuer, kid, wireID, expiry, ch) // TODO: right format for key in config?
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "error getting process stdin")
|
|
|
|
|
return WrapErrorISE(err, "failed validating token")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = cmd.Start()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "error starting validation process")
|
|
|
|
|
// Update and store the challenge.
|
|
|
|
|
ch.Status = StatusValid
|
|
|
|
|
ch.Error = nil
|
|
|
|
|
ch.ValidatedAt = clock.Now().Format(time.RFC3339)
|
|
|
|
|
|
|
|
|
|
if err = db.UpdateChallenge(ctx, ch); err != nil {
|
|
|
|
|
return WrapErrorISE(err, "error updating challenge")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_, err = stdin.Write([]byte(wireChallengePayload.AccessToken))
|
|
|
|
|
orders, err := db.GetAllOrdersByAccountID(ctx, ch.AccountID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "error writing to stdin")
|
|
|
|
|
return WrapErrorISE(err, "could not find current order by account id")
|
|
|
|
|
}
|
|
|
|
|
if len(orders) == 0 {
|
|
|
|
|
return NewErrorISE("there are not enough orders for this account for this custom OIDC challenge")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = stdin.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "error closing stdin")
|
|
|
|
|
order := orders[len(orders)-1]
|
|
|
|
|
if err := db.CreateDpopToken(ctx, order, map[string]any(*dpop)); err != nil {
|
|
|
|
|
return WrapErrorISE(err, "failed storing DPoP token")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = cmd.Wait()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type cnf struct {
|
|
|
|
|
Kid string `json:"kid,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type wireAccessToken struct {
|
|
|
|
|
jose.Claims
|
|
|
|
|
Challenge string `json:"chal,omitempty"`
|
|
|
|
|
Cnf *cnf `json:"cnf,omitempty"`
|
|
|
|
|
Proof string `json:"proof,omitempty"`
|
|
|
|
|
ClientID string `json:"client_id,omitempty"`
|
|
|
|
|
APIVersion int `json:"api_version,omitempty"`
|
|
|
|
|
Scope string `json:"scope,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type wireDpopToken map[string]any
|
|
|
|
|
|
|
|
|
|
func parseAndVerifyAccess(token string, key string, issuer string, kid string, wireID wire.ID, maxExpiry string, ch *Challenge) (*wireAccessToken, *wireDpopToken, error) {
|
|
|
|
|
k, err := pemutil.Parse([]byte(key)) // TODO(hs): move this to earlier in the configuration process? Do it once?
|
|
|
|
|
if err != nil {
|
|
|
|
|
return storeError(ctx, db, ch, true, NewError(ErrorRejectedIdentifierType, "error finishing validation: %s", err))
|
|
|
|
|
return nil, nil, fmt.Errorf("failed parsing public key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update and store the challenge.
|
|
|
|
|
ch.Status = StatusValid
|
|
|
|
|
ch.Error = nil
|
|
|
|
|
ch.ValidatedAt = clock.Now().Format(time.RFC3339)
|
|
|
|
|
|
|
|
|
|
if err = db.UpdateChallenge(ctx, ch); err != nil {
|
|
|
|
|
return WrapErrorISE(err, "error updating challenge")
|
|
|
|
|
pk, ok := k.(ed25519.PublicKey) // TODO(hs): allow more key types
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, nil, fmt.Errorf("unexpected type: %T", k)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parsedAccessToken, err := jwt.ParseSigned(wireChallengePayload.AccessToken)
|
|
|
|
|
jwt, err := jose.ParseSigned(token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Invalid access token")
|
|
|
|
|
return nil, nil, fmt.Errorf("failed parsing token: %w", err)
|
|
|
|
|
}
|
|
|
|
|
access := make(map[string]interface{})
|
|
|
|
|
if err := parsedAccessToken.UnsafeClaimsWithoutVerification(&access); err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Failed parsing access token")
|
|
|
|
|
|
|
|
|
|
var accessToken wireAccessToken
|
|
|
|
|
if err = jwt.Claims(pk, &accessToken); err != nil {
|
|
|
|
|
return nil, nil, fmt.Errorf("failed getting token claims: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rawDpop, ok := access["proof"].(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return WrapErrorISE(err, "Invalid dpop proof format in access token")
|
|
|
|
|
fmt.Println(fmt.Sprintf("%#+v", jwt))
|
|
|
|
|
fmt.Println(fmt.Sprintf("%#+v", accessToken))
|
|
|
|
|
|
|
|
|
|
if err := accessToken.ValidateWithLeeway(jose.Expected{
|
|
|
|
|
Time: time.Now().UTC(),
|
|
|
|
|
Issuer: issuer,
|
|
|
|
|
}, 360*time.Second); err != nil {
|
|
|
|
|
return nil, nil, fmt.Errorf("failed validation: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
parsedDpopToken, err := jwt.ParseSigned(rawDpop)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Invalid DPoP token")
|
|
|
|
|
if accessToken.Cnf == nil {
|
|
|
|
|
return nil, nil, errors.New("'cnf' is nil")
|
|
|
|
|
}
|
|
|
|
|
dpop := make(map[string]interface{})
|
|
|
|
|
if err := parsedDpopToken.UnsafeClaimsWithoutVerification(&dpop); err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Failed parsing dpop token")
|
|
|
|
|
|
|
|
|
|
if accessToken.Cnf.Kid != kid {
|
|
|
|
|
return nil, nil, fmt.Errorf("expected kid %q; got %q", kid, accessToken.Cnf.Kid)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
orders, err := db.GetAllOrdersByAccountID(ctx, ch.AccountID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Could not find current order by account id")
|
|
|
|
|
if accessToken.ClientID != wireID.ClientID {
|
|
|
|
|
return nil, nil, fmt.Errorf("invalid client ID %q", accessToken.ClientID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(orders) == 0 {
|
|
|
|
|
return WrapErrorISE(err, "There are not enough orders for this account for this custom OIDC challenge")
|
|
|
|
|
parsedDpopToken, err := jose.ParseSigned(accessToken.Proof)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, fmt.Errorf("invalid Wire DPoP token: %w", err)
|
|
|
|
|
}
|
|
|
|
|
var dpopToken wireDpopToken
|
|
|
|
|
if err := parsedDpopToken.UnsafeClaimsWithoutVerification(&dpopToken); err != nil {
|
|
|
|
|
return nil, nil, fmt.Errorf("failed parsing dpop token: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
order := orders[len(orders)-1]
|
|
|
|
|
// TODO: validate DPoP too? Which key?
|
|
|
|
|
|
|
|
|
|
if err := db.CreateDpopToken(ctx, order, dpop); err != nil {
|
|
|
|
|
return WrapErrorISE(err, "Failed storing DPoP token")
|
|
|
|
|
handle, ok := dpopToken["handle"].(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, nil, fmt.Errorf("invalid handle in dpop token")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
_ = handle
|
|
|
|
|
|
|
|
|
|
// TODO: compare handle?
|
|
|
|
|
// TODO: compare challenge token / value?
|
|
|
|
|
// TODO: max expiry?
|
|
|
|
|
// "--handle",
|
|
|
|
|
// challengeValues.Handle,
|
|
|
|
|
// "--challenge",
|
|
|
|
|
// ch.Token,
|
|
|
|
|
// "--max-expiry",
|
|
|
|
|
// expiry,
|
|
|
|
|
|
|
|
|
|
return &accessToken, &dpopToken, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type payloadType struct {
|
|
|
|
|