Example #1
0
        /// <inheritdoc/>
        public AcmeResponse PostAccount(AcmeRequest request)
        {
            return(WrapAction((response) =>
            {
                var @params = (UpdateAccount)request.GetContent(ConverterService.GetType <UpdateAccount>());

                var account = GetAccount(request);

                if (@params.Status != null)
                {
                    // Deactivate
                    if (@params.Status != AccountStatus.Deactivated)
                    {
                        throw new MalformedException("Request parameter status must be 'deactivated'");
                    }

                    account = AccountService.Deactivate(account.Id);
                }
                else if (@params.Contacts != null)
                {
                    // Update
                    account = AccountService.Update(account.Id, @params);
                }

                response.Headers.Location = $"{Options.BaseAddress}acct/{account.Id}";
                response.Content = ConverterService.ToAccount(account);
            }, request));
        }
Example #2
0
        /// <inheritdoc/>
        public AcmeResponse GetCertificate(AcmeRequest request, string thumbprint)
        {
            return(WrapAction((response) =>
            {
                var account = GetAccount(request);
                var certs = OrderService.GetCertificate(account.Id, thumbprint);

                switch (Options.DownloadCertificateFormat)
                {
                case DownloadCertificateFormat.PemCertificateChain:
                    {
                        var pem = PemConverter.Encode(certs.Select(o => o.RawData).ToArray(), "certificate");
                        response.Content = new MediaTypeContent("application/pem-certificate-chain", pem);
                    }
                    break;

                case DownloadCertificateFormat.PkixCert:
                    {
                        var cert = new X509Certificate2(certs[0].RawData);
                        response.Content = new MediaTypeContent("application/pkix-cert", cert.RawData);
                    }
                    break;

                case DownloadCertificateFormat.Pkcs7Mime:
                    {
                        var x509Certs = certs.Select(o => new X509Certificate2(o.RawData)).ToArray();
                        var x509Collection = new X509Certificate2Collection(x509Certs);
                        response.Content = new MediaTypeContent("application/pkcs7-mime", x509Collection.Export(X509ContentType.Pkcs7));
                    }
                    break;
                }
            }, request));
        }
Example #3
0
        /// <inheritdoc/>
        public AcmeResponse CreateOrder(AcmeRequest request)
        {
            return(WrapAction((response) =>
            {
                // get account
                var account = GetAccount(request);

                // get params
                var @params = (NewOrder)request.GetContent(ConverterService.GetType <NewOrder>());

                // get order
                var order = OrderService.GetActual(account.Id, @params);
                if (order == null)
                {
                    // create order
                    order = OrderService.Create(account.Id, @params);
                    response.StatusCode = 201; // Created
                }

                // add headers
                response.Headers.Location = new Uri(new Uri(Options.BaseAddress), $"order/{order.Id}").ToString();

                // convert to JSON
                response.Content = ConverterService.ToOrder(order);
            }, request));
        }
        public virtual HttpResponseMessage NewNonce()
        {
            AcmeRequest acmeRequest = GetAcmeRequest();
            var         response    = Controller.GetNonce(acmeRequest);

            return(CreateHttpResponseMessage(response));
        }
Example #5
0
 /// <inheritdoc/>
 public AcmeResponse GetDirectory(AcmeRequest request)
 {
     return(WrapAction((response) =>
     {
         response.Content = DirectoryService.GetDirectory();
         response.StatusCode = 200; // Ok
     }, request));
 }
Example #6
0
        /// <summary>
        /// Gets account by kid
        /// </summary>
        /// <param name="kid">Key identifier link</param>
        /// <returns>Account</returns>
        /// <exception cref="MalformedException"/>
        /// <exception cref="AccountDoesNotExistException"/>
        protected IAccount GetAccount(AcmeRequest request)
        {
            var header = request.Token.GetProtected();

            var account = AccountService.GetById(GetIdFromLink(header.KeyID));

            AssertAccountStatus(account);

            return(account);
        }
Example #7
0
        /// <inheritdoc/>
        public AcmeResponse PostAuthorization(AcmeRequest request, int authzId)
        {
            return(WrapAction((response) =>
            {
                var account = GetAccount(request);

                var authz = AuthorizationService.GetById(account.Id, authzId);

                response.Content = ConverterService.ToAuthorization(authz);
            }, request));
        }
Example #8
0
 /// <inheritdoc/>
 public AcmeResponse GetNonce(AcmeRequest request)
 {
     return(WrapAction((response) =>
     {
         response.Headers.ReplayNonce = NonceService.Create();
         if (request.Method == null ||
             !request.Method.Equals("head", StringComparison.CurrentCultureIgnoreCase))
         {
             response.StatusCode = 204; // No content
         }
     }, request));
 }
Example #9
0
        public static string GenerateScriptArgs(string scriptArgs, AcmeRequest req)
        {
            var fullRecord        = "_acme-challenge" + "." + req.Domain;
            var registrableDomain = DomainParser.RegistrableDomain(req.Domain);
            var subdomain         = "_acme-challenge" + "." + DomainParser.Subdomain(req.Domain);

            var transformedArgs = scriptArgs?
                                  .Replace("{{FullRecord}}", fullRecord)
                                  .Replace("{{Subject}}", req.Domain)
                                  .Replace("{{Domain}}", registrableDomain)
                                  .Replace("{{Record}}", subdomain)
                                  .Replace("{{Value}}", req.DnsTxtValue);

            return(transformedArgs);
        }
Example #10
0
        /// <inheritdoc/>
        public AcmeResponse FinalizeOrder(AcmeRequest request, int orderId)
        {
            return(WrapAction((response) =>
            {
                // get account
                var account = GetAccount(request);

                // enroll certificate
                var @params = (FinalizeOrder)request.GetContent(ConverterService.GetType <FinalizeOrder>());
                var order = OrderService.EnrollCertificate(account.Id, orderId, @params);

                // add headers
                response.Headers.Location = new Uri(new Uri(Options.BaseAddress), $"order/{order.Id}").ToString();
                response.Content = ConverterService.ToOrder(order);
            }, request));
        }
Example #11
0
        /// <inheritdoc/>
        public AcmeResponse PostOrders(AcmeRequest request)
        {
            return(WrapAction((response) =>
            {
                // get account
                var account = GetAccount(request);

                // get orders
                var @params = request.Query;
                var orderList = OrderService.GetList(account.Id, @params);

                // Create query link
                string addingString = null;
                if (@params.Count > 0)
                {
                    foreach (var item in @params)
                    {
                        if (item.Key != "cursor")
                        {
                            foreach (var value in item.Value)
                            {
                                addingString += $"&{item.Key}={value}";
                            }
                        }
                    }
                }

                // Add links
                var link = $"{Options.BaseAddress}orders";
                int page = 0;
                if (@params.ContainsKey("cursor"))
                {
                    page = int.Parse(@params["cursor"].FirstOrDefault());
                }
                if (page > 0)
                {
                    response.Headers.Link.Add(new LinkHeader($"{link}?cursor={page - 1}{addingString}", new Web.Http.LinkHeaderItem("rel", "previous", true)));
                }
                if (orderList.NextPage)
                {
                    response.Headers.Link.Add(new LinkHeader($"{link}?cursor={page + 1}{addingString}", new Web.Http.LinkHeaderItem("rel", "next", true)));
                }

                response.Content = ConverterService.ToOrderList(orderList.Orders);
            }, request));
        }
Example #12
0
        /// <inheritdoc/>
        public AcmeResponse PostOrder(AcmeRequest request, int orderId)
        {
            return(WrapAction((response) =>
            {
                // get account
                var account = GetAccount(request);

                // get order
                var order = OrderService.GetById(account.Id, orderId);

                // add headers
                response.Headers.Location = new Uri(new Uri(Options.BaseAddress), $"order/{order.Id}").ToString();

                // convert to JSON
                response.Content = ConverterService.ToOrder(order);
            }, request));
        }
Example #13
0
 /// <inheritdoc/>
 public AcmeResponse RevokeCertificate(AcmeRequest request)
 {
     return(WrapAction((response) =>
     {
         var header = request.Token.GetProtected();
         var @params = (RevokeCertificate)request.GetContent(ConverterService.GetType <RevokeCertificate>());
         if (header.KeyID != null)
         {
             var account = GetAccount(request);
             OrderService.RevokeCertificate(account.Id, @params);
         }
         else
         {
             OrderService.RevokeCertificate(header.Key, @params);
         }
     }, request));
 }
Example #14
0
        /// <inheritdoc/>
        public AcmeResponse PostChallenge(AcmeRequest request, int challengeId)
        {
            return(WrapAction((response) =>
            {
                var account = GetAccount(request);

                // get challenge
                var challenge = ChallengeService.GetById(challengeId);
                _ = AuthorizationService.GetById(account.Id, challenge.AuthorizationId);

                if (request.Token.IsPayloadEmptyObject)
                {
                    ChallengeService.Validate(challenge);

                    var authzLocation = new Uri(new Uri(Options.BaseAddress), $"authz/{challenge.AuthorizationId}").ToString();
                    response.Headers.Link.Add(new LinkHeader(authzLocation, new LinkHeaderItem("rel", "up", true)));
                }

                response.Content = ConverterService.ToChallenge(challenge);
            }, request));
        }
Example #15
0
        /// <inheritdoc/>
        public AcmeResponse CreateAccount(AcmeRequest request)
        {
            return(WrapAction(response =>
            {
                var header = request.Token.GetProtected();
                var @params = (NewAccount)request.GetContent(ConverterService.GetType <NewAccount>());
                var account = AccountService.FindByPublicKey(header.Key);

                if (@params.OnlyReturnExisting == true)
                {
                    if (account == null)
                    {
                        throw new AccountDoesNotExistException();
                    }
                    response.Content = ConverterService.ToAccount(account);
                    response.StatusCode = 200; // Ok
                }
                else
                {
                    if (account == null)
                    {
                        // Create new account
                        account = AccountService.Create(header.Key, @params);
                        response.Content = ConverterService.ToAccount(account);
                        response.StatusCode = 201; // Created
                    }
                    else
                    {
                        // Existing account
                        response.Content = ConverterService.ToAccount(account);
                        response.StatusCode = 200; // Ok
                    }
                }

                response.Headers.Location = $"{Options.BaseAddress}acct/{account.Id}";
            }, request, true));
        }
Example #16
0
        public async Task <AcmeOrder> BeginOrder()
        {
            var domains = new List <string> {
                _acmeCertificate.Subject
            };

            if (!string.IsNullOrWhiteSpace(_acmeCertificate.SANs))
            {
                var sans = _acmeCertificate.SANs
                           .Split(new[] { "\r\n", "\r", "\n", ",", ";" }, StringSplitOptions.RemoveEmptyEntries)
                           .Select(x => x.Trim())
                           .ToList();

                domains.AddRange(sans);
            }

            _acmeOrder = new AcmeOrder
            {
                AcmeCertificate = _acmeCertificate,
                DateCreated     = DateTime.UtcNow,
                Status          = AcmeOrderStatus.Created
            };

            _acmeCertificate.AcmeOrders.Add(_acmeOrder);

            try
            {
                _order = await _acmeContext.NewOrder(domains);

                _logger.LogDebug($"Order created: {_order.Location}");

                // Get authorizations for the new order which we'll then place
                var authz = await _order.Authorizations();

                // Track all auth requests to the corresponding validation and
                // subsequent completion and certificate response
                _authChallengeContainers = authz.Select(x => new AuthChallengeContainer
                {
                    AuthorizationContext = x,

                    // TODO: Once dns-01 is implemented, check and specify the type below
                    ChallengeContextTask = x.Http()
                }).ToList();

                await Task.WhenAll(_authChallengeContainers.Select(x => x.ChallengeContextTask).ToList());

                _acmeOrder.Status = AcmeOrderStatus.Challenging;

                foreach (var cc in _authChallengeContainers)
                {
                    cc.ChallengeContext = cc.ChallengeContextTask.Result;
                    var acmeReq = new AcmeRequest
                    {
                        KeyAuthorization = cc.ChallengeContext.KeyAuthz,
                        Token            = cc.ChallengeContext.Token,
                        DateCreated      = DateTime.UtcNow,
                        AcmeOrder        = _acmeOrder
                    };
                    _acmeOrder.AcmeRequests.Add(acmeReq);
                    cc.AcmeRequest = acmeReq;
                }
            }
            catch (AcmeRequestException e)
            {
                _logger.LogError(e, "Error requesting order");
                _acmeOrder.Status = AcmeOrderStatus.Error;
                _acmeOrder.Errors = e.Message;
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Error creating order");
                _acmeOrder.Status = AcmeOrderStatus.Error;
            }

            return(_acmeOrder);
        }
Example #17
0
        public async Task <AcmeOrder> BeginOrder()
        {
            var domains = new List <string> {
                _acmeCertificate.Subject
            };

            if (!string.IsNullOrWhiteSpace(_acmeCertificate.SANs))
            {
                var sans = _acmeCertificate.SANs
                           .Split(new[] { "\r\n", "\r", "\n", ",", ";" }, StringSplitOptions.RemoveEmptyEntries)
                           .Select(x => x.Trim())
                           .ToList();

                domains.AddRange(sans);
            }

            domains = domains.Distinct().ToList();

            _acmeOrder = new AcmeOrder
            {
                AcmeCertificate = _acmeCertificate,
                DateCreated     = DateTime.UtcNow,
                Status          = AcmeOrderStatus.Created
            };

            _acmeCertificate.AcmeOrders.Add(_acmeOrder);

            try
            {
                _order = await _acmeContext.NewOrder(domains);

                _logger.LogDebug($"Order created: {_order.Location}");

                // Get authorizations for the new order which we'll then place
                var authz = await _order.Authorizations();

                // Track all auth requests to the corresponding validation and
                // subsequent completion and certificate response
                _authChallengeContainers = new List <AuthChallengeContainer>();

                foreach (var auth in authz)
                {
                    var resource = await auth.Resource();

                    var domain = resource.Identifier.Value;

                    _authChallengeContainers.Add(new AuthChallengeContainer
                    {
                        AuthorizationContext = auth,
                        Domain = domain,
                        ChallengeContextTask = _acmeCertificate.IsDnsChallengeType() ? auth.Dns() : auth.Http(),
                    });
                }

                await Task.WhenAll(_authChallengeContainers.Select(x => x.ChallengeContextTask).ToList());

                _acmeOrder.Status = AcmeOrderStatus.Challenging;

                var newRequests = new List <AcmeRequest>();

                foreach (var cc in _authChallengeContainers)
                {
                    cc.ChallengeContext = cc.ChallengeContextTask.Result;

                    var acmeReq = new AcmeRequest
                    {
                        KeyAuthorization = cc.ChallengeContext.KeyAuthz,
                        Token            = cc.ChallengeContext.Token,
                        DateCreated      = DateTime.UtcNow,
                        Domain           = cc.Domain,
                        DnsTxtValue      = _acmeContext.AccountKey.DnsTxt(cc.ChallengeContext.Token),
                        AcmeOrder        = _acmeOrder
                    };
                    _acmeOrder.AcmeRequests.Add(acmeReq);
                    newRequests.Add(acmeReq);

                    cc.AcmeRequest = acmeReq;
                }
            }
            catch (AcmeRequestException e)
            {
                _logger.LogError(e, "Error requesting order");
                _acmeOrder.Status = AcmeOrderStatus.Error;
                _acmeOrder.Errors = e.Message;
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Error creating order");
                _acmeOrder.Status = AcmeOrderStatus.Error;
            }

            return(_acmeOrder);
        }
Example #18
0
        /// <summary>
        /// Wraps controller action
        /// </summary>
        /// <param name="action">Action</param>
        /// <param name="request">ACME request. If presents wrapper validates JWS</param>
        /// <returns></returns>
        public AcmeResponse WrapAction(Action <AcmeResponse> action, AcmeRequest request, bool UseJwk = false)
        {
            var response = CreateResponse();

            try
            {
                Logger.Info("Request {method} {path} {token}", request.Method, request.Path, request.Token);

                if (request.Method == "POST")
                {
                    #region Check JWS
                    IAccount account = null;

                    // Parse JWT
                    var token  = request.Token;
                    var header = token.GetProtected();
                    try
                    {
                        if (token == null)
                        {
                            throw new Exception("JSON Web Token is empty");
                        }
                    }
                    catch (Exception e)
                    {
                        throw new AcmeException(ErrorType.Unauthorized, "Cannot parse JSON Web Token", System.Net.HttpStatusCode.Unauthorized, e);
                    }

                    if (header.Url == null)
                    {
                        throw new MalformedException("The JWS header MUST have 'url' field");
                    }

                    /// The "jwk" and "kid" fields are mutually exclusive. Servers MUST
                    /// reject requests that contain both.
                    if (header.Key != null && header.KeyID != null)
                    {
                        throw new MalformedException("The JWS header contains both mutually exclusive fields 'jwk' and 'kid'");
                    }

                    if (UseJwk)
                    {
                        if (header.Key == null)
                        {
                            throw new AcmeException(ErrorType.IncorrectResponse, "JWS MUST contain 'jwk' field", System.Net.HttpStatusCode.BadRequest);
                        }
                        if (!token.Verify())
                        {
                            throw new AcmeException(ErrorType.Unauthorized, "JWS signature is invalid", System.Net.HttpStatusCode.Unauthorized);
                        }

                        account = AccountService.FindByPublicKey(header.Key);
                        // If a server receives a POST or POST-as-GET from a deactivated account, it MUST return an error response with status
                        // code 401(Unauthorized) and type "urn:ietf:params:acme:error:unauthorized"
                    }
                    else
                    {
                        if (header.KeyID == null)
                        {
                            throw new AcmeException(ErrorType.IncorrectResponse, "JWS MUST contain 'kid' field", System.Net.HttpStatusCode.BadRequest);
                        }

                        account = AccountService.GetById(GetIdFromLink(header.KeyID));

                        if (!token.Verify(account.Key.GetPublicKey()))
                        {
                            throw new AcmeException(ErrorType.Unauthorized, "JWS signature is invalid", System.Net.HttpStatusCode.Unauthorized);
                        }

                        // Once an account is deactivated, the server MUST NOT accept further
                        // requests authorized by that account's key
                        // https://tools.ietf.org/html/rfc8555#section-7.3.6
                    }
                    if (account != null)
                    {
                        AssertAccountStatus(account);
                    }
                    #endregion

                    #region Check Nonce

                    // The "nonce" header parameter MUST be carried in the protected header of the JWS.
                    var nonce = request.Token.GetProtected().Nonce
                                ?? throw new MalformedException("The 'nonce' header parameter doesn't present in the protected header of the JWS");
                    NonceService.Validate(nonce);

                    #endregion
                }

                // Invoke action
                action.Invoke(response);
            }
            catch (AcmeException e)
            {
                response.StatusCode = (int)e.StatusCode;
                Error error = e;
                response.Content = error;
                Logger.Error(e);
            }
            catch (Exception e)
            {
                response.StatusCode = 500; // Internal Server Error
                Error error = e;
                response.Content = error;
                Logger.Error(e);
            }

            Logger.Info("Response {@response}", response);

            return(response);
        }
Example #19
0
        /// <inheritdoc/>
        public AcmeResponse ChangeKey(AcmeRequest request)
        {
            return(WrapAction((response) =>
            {
                var reqProtected = request.Token.GetProtected();

                // Validate the POST request belongs to a currently active account, as described in Section 6.
                var account = GetAccount(request);
                var jws = request.Token;
                if (!jws.Verify(account.Key.GetPublicKey()))
                {
                    throw new MalformedException();
                }

                // Check that the payload of the JWS is a well - formed JWS object(the "inner JWS").
                var innerJWS = request.Token.GetPayload <JsonWebSignature>();
                var innerProtected = innerJWS.GetProtected();

                // Check that the JWS protected header of the inner JWS has a "jwk" field.
                var jwk = innerProtected.Key;
                if (jwk == null)
                {
                    throw new MalformedException("The inner JWS hasn't got a 'jwk' field");
                }

                // Check that the inner JWS verifies using the key in its "jwk" field.
                if (!innerJWS.Verify(jwk.GetPublicKey()))
                {
                    throw new MalformedException("The inner JWT not verified");
                }

                // Check that the payload of the inner JWS is a well-formed keyChange object (as described above).
                if (!innerJWS.TryGetPayload(out ChangeKey param))
                {
                    throw new MalformedException("The payload of the inner JWS is not a well-formed keyChange object");
                }

                // Check that the "url" parameters of the inner and outer JWS are the same.
                if (reqProtected.Url != innerProtected.Url)
                {
                    throw new MalformedException("The 'url' parameters of the inner and outer JWS are not the same");
                }

                // Check that the "account" field of the keyChange object contains the URL for the account matching the old key (i.e., the "kid" field in the outer JWS).
                if (reqProtected.KeyID != param.Account)
                {
                    throw new MalformedException("The 'account' field not contains the URL for the account matching the old key");
                }

                // Check that the "oldKey" field of the keyChange object is the same as the account key for the account in question.
                var testAccount = AccountService.GetByPublicKey(param.Key);
                if (testAccount.Id != account.Id)
                {
                    throw new MalformedException("The 'oldKey' is the not same as the account key");
                }

                // TODO Check that no account exists whose account key is the same as the key in the "jwk" header parameter of the inner JWS.
                // in repository

                try
                {
                    var updatedAccount = AccountService.ChangeKey(account.Id, jwk);
                    response.Content = ConverterService.ToAccount(updatedAccount);
                    response.Headers.Location = $"{Options.BaseAddress}acct/{updatedAccount.Id}";
                    response.StatusCode = 200; // Ok
                }
                catch (AcmeException e)
                {
                    if (e.StatusCode == System.Net.HttpStatusCode.Conflict)
                    {
                        var conflictAccount = AccountService.GetByPublicKey(jwk);
                        response.Headers.Location = $"{Options.BaseAddress}acct/{conflictAccount.Id}";
                    }
                    throw e;
                }
            }, request));
        }