T ExtractPayload <T>(JwsSignedPayload signedPayload) { var payloadBytes = CryptoHelper.Base64.UrlDecode(signedPayload.Payload); var payloadJson = CryptoHelper.Base64.UrlDecodeToString(signedPayload.Payload); return(JsonConvert.DeserializeObject <T>(payloadJson)); }
public ActionResult <Account> NewAccount([FromBody] JwsSignedPayload signedPayload) { var ph = ExtractProtectedHeader(signedPayload); var jwkSer = JsonConvert.SerializeObject(ph.Jwk); ValidateNonce(ph); var requ = ExtractPayload <CreateAccountRequest>(signedPayload); // We start by saving an empty acct in order to compute the next ID var dbAcct = new DbAccount(); _repo.SaveAccount(dbAcct); // Then compute the acct-specific URL based on the assigned ID // Sample Kid: https://acme-staging-v02.api.letsencrypt.org/acme/acct/6484231 var acctId = dbAcct.Id.ToString(); //var kid = ComputeRelativeUrl($"acct/{acctId}").ToString(); var kid = Url.Action(nameof(GetAccount), new { acctId }); // Then we actually fill out the details dbAcct.Details = new AccountDetails { Kid = kid, Payload = new Account { Id = acctId, Key = ph.Jwk, Contact = requ.Contact?.ToArray(), Status = "testing", TermsOfServiceAgreed = true, InitialIp = HttpContext.Connection.RemoteIpAddress?.ToString(), CreatedAt = DateTime.Now.ToString(), } }; dbAcct.Jwk = jwkSer; _repo.SaveAccount(dbAcct); GenerateNonce(); Response.Headers.Add( "Location", dbAcct.Details.Kid); return(dbAcct.Details.Payload); }
public ActionResult <Authorization> GetAuthorizationPost(string authzKey, [FromBody] JwsSignedPayload signedPayload) { var ph = ExtractProtectedHeader(signedPayload); ValidateNonce(ph); var acct = _repo.GetAccountByKid(ph.Kid); if (acct == null) { throw new Exception("could not resolve account"); } ValidateAccount(acct, signedPayload); var requ = ExtractPayload <string>(signedPayload); if (requ != null) { return(NotFound()); } GenerateNonce(); return(GetAuthorization(authzKey)); }
public ActionResult <bool> Revoke(string acctId, [FromBody] JwsSignedPayload signedPayload) { var ph = ExtractProtectedHeader(signedPayload); ValidateNonce(ph); var requ = ExtractPayload <RevokeCertificateRequest>(signedPayload); var acct = _repo.GetAccountByKid(ph.Kid); if (acct == null) { throw new Exception("could not resolve account"); } ValidateAccount(acct, signedPayload); var derEncodedCertificate = CryptoHelper.Base64.UrlDecode(requ.Certificate); var xcrt = new X509Certificate2(derEncodedCertificate); var dbCert = _repo.GetCertificateByNative(derEncodedCertificate); if (dbCert == null) { return(NotFound()); } if (dbCert.RevokedReason != null) { throw new Exception("certificate already revoked"); } dbCert.RevokedReason = requ.Reason; _repo.SaveCertificate(dbCert); GenerateNonce(); return(true); }
void ValidateAccount(DbAccount acct, JwsSignedPayload signedPayload) { var ph = ExtractProtectedHeader(signedPayload); var jwk = JsonConvert.DeserializeObject <Dictionary <string, string> >(acct.Jwk); if (string.IsNullOrEmpty(ph.Alg)) { throw new Exception("invalid JWS header, missing 'alg'"); } if (string.IsNullOrEmpty(ph.Url)) { throw new Exception("invalid JWS header, missing 'url'"); } if (string.IsNullOrEmpty(ph.Nonce)) { throw new Exception("invalid JWS header, missing 'nonce'"); } IJwsTool tool = null; switch (ph.Alg) { case "RS256": tool = new RSJwsTool { HashSize = 256 }; ((RSJwsTool)tool).ImportJwk(acct.Jwk); break; case "RS384": tool = new RSJwsTool { HashSize = 384 }; ((RSJwsTool)tool).ImportJwk(acct.Jwk); break; case "RS512": tool = new RSJwsTool { HashSize = 512 }; ((RSJwsTool)tool).ImportJwk(acct.Jwk); break; case "ES256": tool = new ESJwsTool { HashSize = 256 }; break; case "ES384": tool = new ESJwsTool { HashSize = 384 }; break; case "ES512": tool = new ESJwsTool { HashSize = 512 }; break; default: throw new Exception("unknown or unsupported signature algorithm"); } var sig = CryptoHelper.Base64.UrlDecode(signedPayload.Signature); var pld = CryptoHelper.Base64.UrlDecode(signedPayload.Payload); var prt = CryptoHelper.Base64.UrlDecode(signedPayload.Protected); var sigInput = $"{signedPayload.Protected}.{signedPayload.Payload}"; var sigInputBytes = Encoding.ASCII.GetBytes(sigInput); if (!tool.Verify(sigInputBytes, sig)) { throw new Exception("account signature failure"); } }
void ValidateNonce(JwsSignedPayload signedPayload) { var protectedHeader = ExtractProtectedHeader(signedPayload); ValidateNonce(protectedHeader); }
ProtectedHeader ExtractProtectedHeader(JwsSignedPayload signedPayload) { var protectedJson = CryptoHelper.Base64.UrlDecodeToString(signedPayload.Protected); return(JsonConvert.DeserializeObject <ProtectedHeader>(protectedJson)); }
public ActionResult <Challenge> AnswerChallenge(string authzKey, string challengeId, [FromBody] JwsSignedPayload signedPayload) { var ph = ExtractProtectedHeader(signedPayload); ValidateNonce(ph); var acct = _repo.GetAccountByKid(ph.Kid); if (acct == null) { throw new Exception("could not resolve account"); } ValidateAccount(acct, signedPayload); var chlngUrl = Request.GetEncodedUrl(); var dbChlng = _repo.GetChallengeByUrl(chlngUrl); if (dbChlng == null) { return(NotFound()); } var dbAuthz = _repo.GetAuthorization(dbChlng.AuthorizationId); if (dbAuthz == null) { return(NotFound()); } var dbOrder = _repo.GetOrder(dbAuthz.OrderId); if (dbOrder == null) { return(NotFound()); } if (acct.Id != dbOrder.AccountId) { throw new Exception("inconsistent state -- " + "Challenge Order does not belong to resolved Account"); } if (dbChlng.Payload.Status != "pending") { throw new Exception("Challenge no longer pending"); } string answer; if (dbChlng.Payload.Type == "dns-01") { // dns-01 Challenge type takes no answer input var requ = ExtractPayload <object>(signedPayload); answer = "dns-01"; } else if (dbChlng.Payload.Type == "http-01") { // http-01 Challenge type takes no answer input var requ = ExtractPayload <object>(signedPayload); answer = "http-01"; } else { throw new Exception("unsupported Challenge type: " + dbChlng.Payload.Type); } dbChlng.Payload.Status = "valid"; dbChlng.Payload.Validated = DateTime.Now.ToUniversalTime().ToString(); dbChlng.Payload.ValidationRecord = new object[] { new { iTakeYourWordForIt = answer } }; _repo.SaveChallenge(dbChlng); if (dbAuthz.Payload.Status == "pending") { dbAuthz.Payload.Status = "valid"; _repo.SaveAuthorization(dbAuthz); } GenerateNonce(); return(dbChlng.Payload); }
public ActionResult <Order> FinalizeOrder(string acctId, string orderId, [FromBody] JwsSignedPayload signedPayload) { if (!int.TryParse(acctId, out var acctIdNum)) { return(NotFound()); } if (!int.TryParse(orderId, out var orderIdNum)) { return(NotFound()); } var ph = ExtractProtectedHeader(signedPayload); ValidateNonce(ph); var acct = _repo.GetAccountByKid(ph.Kid); if (acct == null) { throw new Exception("could not resolve account"); } ValidateAccount(acct, signedPayload); var dbOrder = _repo.GetOrder(orderIdNum); if (dbOrder == null || dbOrder.AccountId != acctIdNum) { return(NotFound()); } if (acct.Id != dbOrder.AccountId) { throw new Exception("inconsistent state -- " + "Challenge Order does not belong to resolved Account"); } if (dbOrder.Details.Payload.Status != "pending") { throw new Exception("Order no longer pending"); } var requ = ExtractPayload <FinalizeOrderRequest>(signedPayload); var encodedCsr = CryptoHelper.Base64.UrlDecode(requ.Csr); var crt = _ca.Sign(PkiEncodingFormat.Der, encodedCsr, PkiHashAlgorithm.Sha256); byte[] crtBytes; using (var ms = new MemoryStream()) { crt.Save(ms); ms.Flush(); ms.Position = 0; crtBytes = ms.ToArray(); } var certKey = Guid.NewGuid().ToString(); var certPem = Encoding.UTF8.GetString(crt.Export(PkiEncodingFormat.Pem)) + ResolveCaCertPem(); var dbCert = new DbCertificate { OrderId = dbOrder.Id, CertKey = certKey, Native = crtBytes, Pem = certPem, }; _repo.SaveCertificate(dbCert); dbOrder.Details.Payload.Status = "valid"; dbOrder.Details.Payload.Certificate = Url.Action(nameof(GetCertificate), controller: null, values: new { certKey }, protocol: Request.Scheme); _repo.SaveOrder(dbOrder); GenerateNonce(); return(dbOrder.Details.Payload); }
public ActionResult <Order> NewOrder([FromBody] JwsSignedPayload signedPayload) { var ph = ExtractProtectedHeader(signedPayload); ValidateNonce(ph); var requ = ExtractPayload <CreateOrderRequest>(signedPayload); var acct = _repo.GetAccountByKid(ph.Kid); if (acct == null) { throw new Exception("could not resolve account"); } var acctId = acct.Id.ToString(); ValidateAccount(acct, signedPayload); if (requ.Identifiers.Length == 0) { throw new Exception("at least one identifier is required"); } if (requ.Identifiers.Length > 100) { throw new Exception("too many identifiers"); } if (requ.Identifiers.Count(x => x.Type != "dns") > 0) { throw new Exception("unsupported identifier type"); } // We start by saving an empty order so we can compute the next ID var dbOrder = new DbOrder(); _repo.SaveOrder(dbOrder); var orderId = dbOrder.Id.ToString(); var orderIds = new { acctId, orderId }; var orderUrl = Url.Action(nameof(GetOrder), controller: null, values: orderIds, protocol: Request.Scheme); var finalizeUrl = Url.Action(nameof(FinalizeOrder), controller: null, values: orderIds, protocol: Request.Scheme); var authzs = new List <DbAuthorization>(); foreach (var dnsId in requ.Identifiers) { var authzKey = Guid.NewGuid().ToString(); var chlngs = new List <DbChallenge>(); var chlngTypes = ChallengeTypes; var isWildcard = dnsId.Value.StartsWith("*."); if (isWildcard) { chlngTypes = ChallengeTypesForWildcard; } foreach (var chlngType in chlngTypes) { var chlngToken = Guid.NewGuid().ToString(); // We start by saving an empty challenge so we can compute the next ID var chlng = new DbChallenge { Payload = new Challenge { Token = chlngToken, // We temporarily assign the token to the URL in order // to satisfy the unique constraint on the URL index Url = chlngToken, } }; _repo.SaveChallenge(chlng); chlng.Payload = new Challenge { Type = chlngType, Token = chlngToken, Status = "pending", Url = Url.Action(nameof(GetChallenge), controller: null, values: new { authzKey, challengeId = chlng.Id.ToString() }, protocol: Request.Scheme), }; _repo.SaveChallenge(chlng); chlngs.Add(chlng); } var dbAuthz = new DbAuthorization { OrderId = dbOrder.Id, Url = Url.Action(nameof(GetAuthorization), controller: null, values: new { authzKey }, protocol: Request.Scheme), Payload = new Authorization { Identifier = dnsId, Status = "pending", Expires = DateTime.Now.AddHours(24).ToUniversalTime().ToString(), Challenges = chlngs.Select(x => x.Payload).ToArray(), Wildcard = isWildcard ? (bool?)true : null, } }; _repo.SaveAuthorization(dbAuthz); authzs.Add(dbAuthz); foreach (var chlng in chlngs) { chlng.AuthorizationId = dbAuthz.Id; _repo.SaveChallenge(chlng); } } dbOrder.Url = orderUrl; dbOrder.AccountId = acct.Id; dbOrder.Details = new OrderDetails { OrderUrl = orderUrl, Payload = new Order { Expires = DateTime.Now.AddHours(24).ToUniversalTime().ToString(), NotBefore = null, // requ.NotBefore, NotAfter = null, //requ.NotAfter, Identifiers = requ.Identifiers, Authorizations = authzs.Select(x => x.Url).ToArray(), Finalize = finalizeUrl, Status = "pending", Error = null, Certificate = null, } }; _repo.SaveOrder(dbOrder); GenerateNonce(); return(Created(orderUrl, dbOrder.Details.Payload)); }