package nns01 import ( "crypto/sha256" "encoding/base64" "fmt" "time" "github.com/go-acme/lego/v4/acme" "github.com/go-acme/lego/v4/acme/api" "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/log" ) type ValidateFunc func(core *api.Core, domain string, chlng acme.Challenge) error // Challenge implements the nns-01 challenge. type Challenge struct { core *api.Core validate ValidateFunc provider challenge.Provider } func NewChallenge(core *api.Core, validate ValidateFunc, provider challenge.Provider) *Challenge { chlg := &Challenge{ core: core, validate: validate, provider: provider, } return chlg } // PreSolve submits the txt record to the nns provider. func (c *Challenge) PreSolve(authz acme.Authorization) error { domain := challenge.GetTargetedDomain(authz) log.Infof("[%s] acme: Preparing to solve NNS-01", domain) chlng, keyAuth, err := c.getChallengeInfo(authz) if err != nil { return err } err = c.provider.Present(authz.Identifier.Value, chlng.Token, keyAuth) if err != nil { return fmt.Errorf("[%s] acme: error presenting token: %w", domain, err) } return nil } func (c *Challenge) Solve(authz acme.Authorization) error { domain := challenge.GetTargetedDomain(authz) log.Infof("[%s] acme: Trying to solve NNS-01", domain) chlng, keyAuth, err := c.getChallengeInfo(authz) if err != nil { return err } chlng.KeyAuthorization = keyAuth return c.validate(c.core, domain, chlng) } // CleanUp cleans the challenge. func (c *Challenge) CleanUp(authz acme.Authorization) error { log.Infof("[%s] acme: Cleaning NNS-01 challenge", challenge.GetTargetedDomain(authz)) chlng, keyAuth, err := c.getChallengeInfo(authz) if err != nil { return err } return c.provider.CleanUp(authz.Identifier.Value, chlng.Token, keyAuth) } func (c *Challenge) Sequential() (bool, time.Duration) { if p, ok := c.provider.(sequential); ok { return ok, p.Sequential() } return false, 0 } type sequential interface { Sequential() time.Duration } // RecordInfo contains the information use to create the TXT record. type RecordInfo struct { // FQDN is the full-qualified challenge domain (`acme-challenge.[domain]`) FQDN string // Value contains the value for the TXT record Value string } // GetRecordInfo returns information used to create a TXT record which will fulfill the `nns-01` challenge. func GetRecordInfo(domain, keyAuth string) RecordInfo { keyAuthShaBytes := sha256.Sum256([]byte(keyAuth)) // base64URL encoding without padding value := base64.RawURLEncoding.EncodeToString(keyAuthShaBytes[:sha256.Size]) return RecordInfo{ Value: value, FQDN: getRecordFQDN(domain), } } func getRecordFQDN(domain string) string { return fmt.Sprintf("acme-challenge.%s", domain) } func (c *Challenge) getChallengeInfo(authz acme.Authorization) (acme.Challenge, string, error) { chlng, err := challenge.FindChallenge(challenge.NNS01, authz) if err != nil { return acme.Challenge{}, "", err } // Generate the Key Authorization for the challenge keyAuth, err := c.core.GetKeyAuthorization(chlng.Token) if err != nil { return acme.Challenge{}, "", err } return chlng, keyAuth, nil }