github.com/letsencrypt/boulder@v0.20251208.0/wfe2/wfe_test.go (about) 1 package wfe2 2 3 import ( 4 "bytes" 5 "context" 6 "crypto" 7 "crypto/ecdsa" 8 "crypto/elliptic" 9 "crypto/rand" 10 "crypto/rsa" 11 "crypto/x509" 12 "encoding/asn1" 13 "encoding/base64" 14 "encoding/json" 15 "encoding/pem" 16 "errors" 17 "fmt" 18 "io" 19 "math/big" 20 "net/http" 21 "net/http/httptest" 22 "net/url" 23 "os" 24 "reflect" 25 "slices" 26 "sort" 27 "strconv" 28 "strings" 29 "testing" 30 "time" 31 32 "github.com/go-jose/go-jose/v4" 33 "github.com/jmhodges/clock" 34 "github.com/prometheus/client_golang/prometheus" 35 "google.golang.org/grpc" 36 "google.golang.org/protobuf/types/known/emptypb" 37 "google.golang.org/protobuf/types/known/timestamppb" 38 39 "github.com/letsencrypt/boulder/cmd" 40 "github.com/letsencrypt/boulder/config" 41 "github.com/letsencrypt/boulder/core" 42 corepb "github.com/letsencrypt/boulder/core/proto" 43 berrors "github.com/letsencrypt/boulder/errors" 44 "github.com/letsencrypt/boulder/features" 45 "github.com/letsencrypt/boulder/goodkey" 46 "github.com/letsencrypt/boulder/identifier" 47 "github.com/letsencrypt/boulder/issuance" 48 blog "github.com/letsencrypt/boulder/log" 49 "github.com/letsencrypt/boulder/metrics" 50 "github.com/letsencrypt/boulder/mocks" 51 "github.com/letsencrypt/boulder/must" 52 "github.com/letsencrypt/boulder/nonce" 53 noncepb "github.com/letsencrypt/boulder/nonce/proto" 54 "github.com/letsencrypt/boulder/probs" 55 rapb "github.com/letsencrypt/boulder/ra/proto" 56 "github.com/letsencrypt/boulder/ratelimits" 57 "github.com/letsencrypt/boulder/revocation" 58 sapb "github.com/letsencrypt/boulder/sa/proto" 59 "github.com/letsencrypt/boulder/test" 60 inmemnonce "github.com/letsencrypt/boulder/test/inmem/nonce" 61 "github.com/letsencrypt/boulder/unpause" 62 "github.com/letsencrypt/boulder/web" 63 ) 64 65 const ( 66 agreementURL = "http://example.invalid/terms" 67 68 test1KeyPublicJSON = ` 69 { 70 "kty":"RSA", 71 "n":"yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ", 72 "e":"AQAB" 73 }` 74 75 test1KeyPrivatePEM = ` 76 -----BEGIN RSA PRIVATE KEY----- 77 MIIEowIBAAKCAQEAyNWVhtYEKJR21y9xsHV+PD/bYwbXSeNuFal46xYxVfRL5mqh 78 a7vttvjB/vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K/klBYN8oYvTwwmeSkAz 79 6ut7ZxPv+nZaT5TJhGk0NT2kh/zSpdriEJ/3vW+mqxYbbBmpvHqsa1/zx9fSuHYc 80 tAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV+mzfMyboQjujPh7aNJxAWS 81 q4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF+w8hOTI3XXohUdu 82 29Se26k2B0PolDSuj0GIQU6+W9TdLXSjBb2SpQIDAQABAoIBAHw58SXYV/Yp72Cn 83 jjFSW+U0sqWMY7rmnP91NsBjl9zNIe3C41pagm39bTIjB2vkBNR8ZRG7pDEB/QAc 84 Cn9Keo094+lmTArjL407ien7Ld+koW7YS8TyKADYikZo0vAK3qOy14JfQNiFAF9r 85 Bw61hG5/E58cK5YwQZe+YcyBK6/erM8fLrJEyw4CV49wWdq/QqmNYU1dx4OExAkl 86 KMfvYXpjzpvyyTnZuS4RONfHsO8+JTyJVm+lUv2x+bTce6R4W++UhQY38HakJ0x3 87 XRfXooRv1Bletu5OFlpXfTSGz/5gqsfemLSr5UHncsCcFMgoFBsk2t/5BVukBgC7 88 PnHrAjkCgYEA887PRr7zu3OnaXKxylW5U5t4LzdMQLpslVW7cLPD4Y08Rye6fF5s 89 O/jK1DNFXIoUB7iS30qR7HtaOnveW6H8/kTmMv/YAhLO7PAbRPCKxxcKtniEmP1x 90 ADH0tF2g5uHB/zeZhCo9qJiF0QaJynvSyvSyJFmY6lLvYZsAW+C+PesCgYEA0uCi 91 Q8rXLzLpfH2NKlLwlJTi5JjE+xjbabgja0YySwsKzSlmvYJqdnE2Xk+FHj7TCnSK 92 KUzQKR7+rEk5flwEAf+aCCNh3W4+Hp9MmrdAcCn8ZsKmEW/o7oDzwiAkRCmLw/ck 93 RSFJZpvFoxEg15riT37EjOJ4LBZ6SwedsoGA/a8CgYEA2Ve4sdGSR73/NOKZGc23 94 q4/B4R2DrYRDPhEySnMGoPCeFrSU6z/lbsUIU4jtQWSaHJPu4n2AfncsZUx9WeSb 95 OzTCnh4zOw33R4N4W8mvfXHODAJ9+kCc1tax1YRN5uTEYzb2dLqPQtfNGxygA1DF 96 BkaC9CKnTeTnH3TlKgK8tUcCgYB7J1lcgh+9ntwhKinBKAL8ox8HJfkUM+YgDbwR 97 sEM69E3wl1c7IekPFvsLhSFXEpWpq3nsuMFw4nsVHwaGtzJYAHByhEdpTDLXK21P 98 heoKF1sioFbgJB1C/Ohe3OqRLDpFzhXOkawOUrbPjvdBM2Erz/r11GUeSlpNazs7 99 vsoYXQKBgFwFM1IHmqOf8a2wEFa/a++2y/WT7ZG9nNw1W36S3P04K4lGRNRS2Y/S 100 snYiqxD9nL7pVqQP2Qbqbn0yD6d3G5/7r86F7Wu2pihM8g6oyMZ3qZvvRIBvKfWo 101 eROL1ve1vmQF3kjrMPhhK2kr6qdWnTE5XlPllVSZFQenSTzj98AO 102 -----END RSA PRIVATE KEY----- 103 ` 104 105 test2KeyPublicJSON = `{ 106 "kty":"RSA", 107 "n":"qnARLrT7Xz4gRcKyLdydmCr-ey9OuPImX4X40thk3on26FkMznR3fRjs66eLK7mmPcBZ6uOJseURU6wAaZNmemoYx1dMvqvWWIyiQleHSD7Q8vBrhR6uIoO4jAzJZR-ChzZuSDt7iHN-3xUVspu5XGwXU_MVJZshTwp4TaFx5elHIT_ObnTvTOU3Xhish07AbgZKmWsVbXh5s-CrIicU4OexJPgunWZ_YJJueOKmTvnLlTV4MzKR2oZlBKZ27S0-SfdV_QDx_ydle5oMAyKVtlAV35cyPMIsYNwgUGBCdY_2Uzi5eX0lTc7MPRwz6qR1kip-i59VcGcUQgqHV6Fyqw", 108 "e":"AQAB" 109 }` 110 111 test2KeyPrivatePEM = ` 112 -----BEGIN RSA PRIVATE KEY----- 113 MIIEpAIBAAKCAQEAqnARLrT7Xz4gRcKyLdydmCr+ey9OuPImX4X40thk3on26FkM 114 znR3fRjs66eLK7mmPcBZ6uOJseURU6wAaZNmemoYx1dMvqvWWIyiQleHSD7Q8vBr 115 hR6uIoO4jAzJZR+ChzZuSDt7iHN+3xUVspu5XGwXU/MVJZshTwp4TaFx5elHIT/O 116 bnTvTOU3Xhish07AbgZKmWsVbXh5s+CrIicU4OexJPgunWZ/YJJueOKmTvnLlTV4 117 MzKR2oZlBKZ27S0+SfdV/QDx/ydle5oMAyKVtlAV35cyPMIsYNwgUGBCdY/2Uzi5 118 eX0lTc7MPRwz6qR1kip+i59VcGcUQgqHV6FyqwIDAQABAoIBAG5m8Xpj2YC0aYtG 119 tsxmX9812mpJFqFOmfS+f5N0gMJ2c+3F4TnKz6vE/ZMYkFnehAT0GErC4WrOiw68 120 F/hLdtJM74gQ0LGh9dKeJmz67bKqngcAHWW5nerVkDGIBtzuMEsNwxofDcIxrjkr 121 G0b7AHMRwXqrt0MI3eapTYxby7+08Yxm40mxpSsW87FSaI61LDxUDpeVkn7kolSN 122 WifVat7CpZb/D2BfGAQDxiU79YzgztpKhbynPdGc/OyyU+CNgk9S5MgUX2m9Elh3 123 aXrWh2bT2xzF+3KgZdNkJQcdIYVoGq/YRBxlGXPYcG4Do3xKhBmH79Io2BizevZv 124 nHkbUGECgYEAydjb4rl7wYrElDqAYpoVwKDCZAgC6o3AKSGXfPX1Jd2CXgGR5Hkl 125 ywP0jdSLbn2v/jgKQSAdRbYuEiP7VdroMb5M6BkBhSY619cH8etoRoLzFo1GxcE8 126 Y7B598VXMq8TT+TQqw/XRvM18aL3YDZ3LSsR7Gl2jF/sl6VwQAaZToUCgYEA2Cn4 127 fG58ME+M4IzlZLgAIJ83PlLb9ip6MeHEhUq2Dd0In89nss7Acu0IVg8ES88glJZy 128 4SjDLGSiuQuoQVo9UBq/E5YghdMJFp5ovwVfEaJ+ruWqOeujvWzzzPVyIWSLXRQa 129 N4kedtfrlqldMIXywxVru66Q1NOGvhDHm/Q8+28CgYEAkhLCbn3VNed7A9qidrkT 130 7OdqRoIVujEDU8DfpKtK0jBP3EA+mJ2j4Bvoq4uZrEiBSPS9VwwqovyIstAfX66g 131 Qv95IK6YDwfvpawUL9sxB3ZU/YkYIp0JWwun+Mtzo1ZYH4V0DZfVL59q9of9hj9k 132 V+fHfNOF22jAC67KYUtlPxECgYEAwF6hj4L3rDqvQYrB/p8tJdrrW+B7dhgZRNkJ 133 fiGd4LqLGUWHoH4UkHJXT9bvWNPMx88YDz6qapBoq8svAnHfTLFwyGp7KP1FAkcZ 134 Kp4KG/SDTvx+QCtvPX1/fjAUUJlc2QmxxyiU3uiK9Tpl/2/FOk2O4aiZpX1VVUIz 135 kZuKxasCgYBiVRkEBk2W4Ia0B7dDkr2VBrz4m23Y7B9cQLpNAapiijz/0uHrrCl8 136 TkLlEeVOuQfxTadw05gzKX0jKkMC4igGxvEeilYc6NR6a4nvRulG84Q8VV9Sy9Ie 137 wk6Oiadty3eQqSBJv0HnpmiEdQVffIK5Pg4M8Dd+aOBnEkbopAJOuA== 138 -----END RSA PRIVATE KEY----- 139 ` 140 test3KeyPrivatePEM = ` 141 -----BEGIN RSA PRIVATE KEY----- 142 MIIEpAIBAAKCAQEAuTQER6vUA1RDixS8xsfCRiKUNGRzzyIK0MhbS2biClShbb0h 143 Sx2mPP7gBvis2lizZ9r+y9hL57kNQoYCKndOBg0FYsHzrQ3O9AcoV1z2Mq+XhHZb 144 FrVYaXI0M3oY9BJCWog0dyi3XC0x8AxC1npd1U61cToHx+3uSvgZOuQA5ffEn5L3 145 8Dz1Ti7OV3E4XahnRJvejadUmTkki7phLBUXm5MnnyFm0CPpf6ApV7zhLjN5W+nV 146 0WL17o7v8aDgV/t9nIdi1Y26c3PlCEtiVHZcebDH5F1Deta3oLLg9+g6rWnTqPbY 147 3knffhp4m0scLD6e33k8MtzxDX/D7vHsg0/X1wIDAQABAoIBAQCnFJpX3lhiuH5G 148 1uqHmmdVxpRVv9oKn/eJ63cRSzvZfgg0bE/A6Hq0xGtvXqDySttvck4zsGqqHnQr 149 86G4lfE53D1jnv4qvS5bUKnARwmFKIxU4EHE9s1QM8uMNTaV2nMqIX7TkVP6QHuw 150 yB70R2inq15dS7EBWVGFKNX6HwAAdj8pFuF6o2vIwmAfee20aFzpWWf81jOH9Ai6 151 hyJyV3NqrU1JzIwlXaeX67R1VroFdhN/lapp+2b0ZEcJJtFlcYFl99NjkQeVZyik 152 izNv0GZZNWizc57wU0/8cv+jQ2f26ltvyrPz3QNK61bFfzy+/tfMvLq7sdCmztKJ 153 tMxCBJOBAoGBAPKnIVQIS2nTvC/qZ8ajw1FP1rkvYblIiixegjgfFhM32HehQ+nu 154 3TELi3I3LngLYi9o6YSqtNBmdBJB+DUAzIXp0TdOihOweGiv5dAEWwY9rjCzMT5S 155 GP7dCWiJwoMUHrOs1Po3dwcjj/YsoAW+FC0jSvach2Ln2CvPgr5FP0ARAoGBAMNj 156 64qUCzgeXiSyPKK69bCCGtHlTYUndwHQAZmABjbmxAXZNYgp/kBezFpKOwmICE8R 157 kK8YALRrL0VWXl/yj85b0HAZGkquNFHPUDd1e6iiP5TrY+Hy4oqtlYApjH6f85CE 158 lWjQ1iyUL7aT6fcSgzq65ZWD2hUzvNtWbTt6zQFnAoGAWS/EuDY0QblpOdNWQVR/ 159 vasyqO4ZZRiccKJsCmSioH2uOoozhBAfjJ9JqblOgyDr/bD546E6xD5j+zH0IMci 160 ZTYDh+h+J659Ez1Topl3O1wAYjX6q4VRWpuzkZDQxYznm/KydSVdwmn3x+uvBW1P 161 zSdjrjDqMhg1BCVJUNXy4YECgYEAjX1z+dwO68qB3gz7/9NnSzRL+6cTJdNYSIW6 162 QtAEsAkX9iw+qaXPKgn77X5HljVd3vQXU9QL3pqnloxetxhNrt+p5yMmeOIBnSSF 163 MEPxEkK7zDlRETPzfP0Kf86WoLNviz2XfFmOXqXIj2w5RuOvB/6DdmwOpr/aiPLj 164 EulwPw0CgYAMSzsWOt6vU+y/G5NyhUCHvY50TdnGOj2btBk9rYVwWGWxCpg2QF0R 165 pcKXgGzXEVZKFAqB8V1c/mmCo8ojPgmqGM+GzX2Bj4seVBW7PsTeZUjrHpADshjV 166 F7o5b7y92NlxO5kwQzRKEAhwS5PbKJdx90iCuG+JlI1YgWlA1VcJMw== 167 -----END RSA PRIVATE KEY----- 168 ` 169 170 testE1KeyPrivatePEM = ` 171 -----BEGIN EC PRIVATE KEY----- 172 MHcCAQEEIH+p32RUnqT/iICBEGKrLIWFcyButv0S0lU/BLPOyHn2oAoGCCqGSM49 173 AwEHoUQDQgAEFwvSZpu06i3frSk/mz9HcD9nETn4wf3mQ+zDtG21GapLytH7R1Zr 174 ycBzDV9u6cX9qNLc9Bn5DAumz7Zp2AuA+Q== 175 -----END EC PRIVATE KEY----- 176 ` 177 178 testE2KeyPrivatePEM = ` 179 -----BEGIN EC PRIVATE KEY----- 180 MHcCAQEEIFRcPxQ989AY6se2RyIoF1ll9O6gHev4oY15SWJ+Jf5eoAoGCCqGSM49 181 AwEHoUQDQgAES8FOmrZ3ywj4yyFqt0etAD90U+EnkNaOBSLfQmf7pNi8y+kPKoUN 182 EeMZ9nWyIM6bktLrE11HnFOnKhAYsM5fZA== 183 -----END EC PRIVATE KEY-----` 184 ) 185 186 type MockRegistrationAuthority struct { 187 rapb.RegistrationAuthorityClient 188 clk clock.Clock 189 lastRevocationReason revocation.Reason 190 } 191 192 func (ra *MockRegistrationAuthority) NewRegistration(ctx context.Context, in *corepb.Registration, _ ...grpc.CallOption) (*corepb.Registration, error) { 193 in.Id = 1 194 created := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) 195 in.CreatedAt = timestamppb.New(created) 196 return in, nil 197 } 198 199 func (ra *MockRegistrationAuthority) UpdateRegistrationKey(ctx context.Context, in *rapb.UpdateRegistrationKeyRequest, _ ...grpc.CallOption) (*corepb.Registration, error) { 200 return &corepb.Registration{ 201 Status: string(core.StatusValid), 202 Key: in.Jwk, 203 }, nil 204 } 205 206 func (ra *MockRegistrationAuthority) DeactivateRegistration(context.Context, *rapb.DeactivateRegistrationRequest, ...grpc.CallOption) (*corepb.Registration, error) { 207 return &corepb.Registration{ 208 Status: string(core.StatusDeactivated), 209 Key: []byte(test1KeyPublicJSON), 210 }, nil 211 } 212 213 func (ra *MockRegistrationAuthority) PerformValidation(context.Context, *rapb.PerformValidationRequest, ...grpc.CallOption) (*corepb.Authorization, error) { 214 return &corepb.Authorization{}, nil 215 } 216 217 func (ra *MockRegistrationAuthority) RevokeCertByApplicant(ctx context.Context, in *rapb.RevokeCertByApplicantRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { 218 ra.lastRevocationReason = revocation.Reason(in.Code) 219 return &emptypb.Empty{}, nil 220 } 221 222 func (ra *MockRegistrationAuthority) RevokeCertByKey(ctx context.Context, in *rapb.RevokeCertByKeyRequest, _ ...grpc.CallOption) (*emptypb.Empty, error) { 223 ra.lastRevocationReason = revocation.KeyCompromise 224 return &emptypb.Empty{}, nil 225 } 226 227 // GetAuthorization returns a different authorization depending on the requested 228 // ID. All authorizations are associated with RegID 1, except for the one that isn't. 229 func (ra *MockRegistrationAuthority) GetAuthorization(_ context.Context, in *rapb.GetAuthorizationRequest, _ ...grpc.CallOption) (*corepb.Authorization, error) { 230 switch in.Id { 231 case 1: // Return a valid authorization with a single valid challenge. 232 return &corepb.Authorization{ 233 Id: "1", 234 RegistrationID: 1, 235 Identifier: identifier.NewDNS("not-an-example.com").ToProto(), 236 Status: string(core.StatusValid), 237 Expires: timestamppb.New(ra.clk.Now().AddDate(100, 0, 0)), 238 Challenges: []*corepb.Challenge{ 239 {Id: 1, Type: "http-01", Status: string(core.StatusValid), Token: "token"}, 240 }, 241 }, nil 242 case 2: // Return a pending authorization with three pending challenges. 243 return &corepb.Authorization{ 244 Id: "2", 245 RegistrationID: 1, 246 Identifier: identifier.NewDNS("not-an-example.com").ToProto(), 247 Status: string(core.StatusPending), 248 Expires: timestamppb.New(ra.clk.Now().AddDate(100, 0, 0)), 249 Challenges: []*corepb.Challenge{ 250 {Id: 1, Type: "http-01", Status: string(core.StatusPending), Token: "token"}, 251 {Id: 2, Type: "dns-01", Status: string(core.StatusPending), Token: "token"}, 252 {Id: 3, Type: "tls-alpn-01", Status: string(core.StatusPending), Token: "token"}, 253 }, 254 }, nil 255 case 3: // Return an expired authorization with three pending (but expired) challenges. 256 return &corepb.Authorization{ 257 Id: "3", 258 RegistrationID: 1, 259 Identifier: identifier.NewDNS("not-an-example.com").ToProto(), 260 Status: string(core.StatusPending), 261 Expires: timestamppb.New(ra.clk.Now().AddDate(-1, 0, 0)), 262 Challenges: []*corepb.Challenge{ 263 {Id: 1, Type: "http-01", Status: string(core.StatusPending), Token: "token"}, 264 {Id: 2, Type: "dns-01", Status: string(core.StatusPending), Token: "token"}, 265 {Id: 3, Type: "tls-alpn-01", Status: string(core.StatusPending), Token: "token"}, 266 }, 267 }, nil 268 case 4: // Return an internal server error. 269 return nil, fmt.Errorf("unspecified error") 270 case 5: // Return a pending authorization as above, but associated with RegID 2. 271 return &corepb.Authorization{ 272 Id: "5", 273 RegistrationID: 2, 274 Identifier: identifier.NewDNS("not-an-example.com").ToProto(), 275 Status: string(core.StatusPending), 276 Expires: timestamppb.New(ra.clk.Now().AddDate(100, 0, 0)), 277 Challenges: []*corepb.Challenge{ 278 {Id: 1, Type: "http-01", Status: string(core.StatusPending), Token: "token"}, 279 {Id: 2, Type: "dns-01", Status: string(core.StatusPending), Token: "token"}, 280 {Id: 3, Type: "tls-alpn-01", Status: string(core.StatusPending), Token: "token"}, 281 }, 282 }, nil 283 } 284 285 return nil, berrors.NotFoundError("no authorization found with id %q", in.Id) 286 } 287 288 func (ra *MockRegistrationAuthority) DeactivateAuthorization(context.Context, *corepb.Authorization, ...grpc.CallOption) (*emptypb.Empty, error) { 289 return &emptypb.Empty{}, nil 290 } 291 292 func (ra *MockRegistrationAuthority) NewOrder(ctx context.Context, in *rapb.NewOrderRequest, _ ...grpc.CallOption) (*corepb.Order, error) { 293 created := time.Date(2021, 1, 1, 1, 1, 1, 0, time.UTC) 294 expires := time.Date(2021, 2, 1, 1, 1, 1, 0, time.UTC) 295 296 return &corepb.Order{ 297 Id: 1, 298 RegistrationID: in.RegistrationID, 299 Created: timestamppb.New(created), 300 Expires: timestamppb.New(expires), 301 Identifiers: in.Identifiers, 302 Status: string(core.StatusPending), 303 V2Authorizations: []int64{1}, 304 }, nil 305 } 306 307 func (ra *MockRegistrationAuthority) FinalizeOrder(ctx context.Context, in *rapb.FinalizeOrderRequest, _ ...grpc.CallOption) (*corepb.Order, error) { 308 in.Order.Status = string(core.StatusProcessing) 309 return in.Order, nil 310 } 311 312 func makeBody(s string) io.ReadCloser { 313 return io.NopCloser(strings.NewReader(s)) 314 } 315 316 // loadKey loads a private key from PEM/DER-encoded data and returns 317 // a `crypto.Signer`. 318 func loadKey(t *testing.T, keyBytes []byte) crypto.Signer { 319 // pem.Decode does not return an error as its 2nd arg, but instead the "rest" 320 // that was leftover from parsing the PEM block. We only care if the decoded 321 // PEM block was empty for this test function. 322 block, _ := pem.Decode(keyBytes) 323 if block == nil { 324 t.Fatal("Unable to decode private key PEM bytes") 325 } 326 327 // Try decoding as an RSA private key 328 if rsaKey, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil { 329 return rsaKey 330 } 331 332 // Try decoding as a PKCS8 private key 333 if key, err := x509.ParsePKCS8PrivateKey(block.Bytes); err == nil { 334 // Determine the key's true type and return it as a crypto.Signer 335 switch k := key.(type) { 336 case *rsa.PrivateKey: 337 return k 338 case *ecdsa.PrivateKey: 339 return k 340 } 341 } 342 343 // Try as an ECDSA private key 344 if ecdsaKey, err := x509.ParseECPrivateKey(block.Bytes); err == nil { 345 return ecdsaKey 346 } 347 348 // Nothing worked! Fail hard. 349 t.Fatalf("Unable to decode private key PEM bytes") 350 // NOOP - the t.Fatal() call will abort before this return 351 return nil 352 } 353 354 var ctx = context.Background() 355 356 func setupWFE(t *testing.T) (WebFrontEndImpl, clock.FakeClock, requestSigner) { 357 features.Reset() 358 359 fc := clock.NewFake() 360 stats := metrics.NoopRegisterer 361 logger := blog.NewMock() 362 363 testKeyPolicy, err := goodkey.NewPolicy(nil, nil) 364 test.AssertNotError(t, err, "creating test keypolicy") 365 366 certChains := map[issuance.NameID][][]byte{} 367 issuerCertificates := map[issuance.NameID]*issuance.Certificate{} 368 for _, files := range [][]string{ 369 { 370 "../test/hierarchy/int-r3.cert.pem", 371 "../test/hierarchy/root-x1.cert.pem", 372 }, 373 { 374 "../test/hierarchy/int-r3-cross.cert.pem", 375 "../test/hierarchy/root-dst.cert.pem", 376 }, 377 { 378 "../test/hierarchy/int-e1.cert.pem", 379 "../test/hierarchy/root-x2.cert.pem", 380 }, 381 { 382 "../test/hierarchy/int-e1.cert.pem", 383 "../test/hierarchy/root-x2-cross.cert.pem", 384 "../test/hierarchy/root-x1-cross.cert.pem", 385 "../test/hierarchy/root-dst.cert.pem", 386 }, 387 } { 388 certs, err := issuance.LoadChain(files) 389 test.AssertNotError(t, err, "Unable to load chain") 390 var buf bytes.Buffer 391 for _, cert := range certs { 392 buf.Write([]byte("\n")) 393 buf.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})) 394 } 395 id := certs[0].NameID() 396 certChains[id] = append(certChains[id], buf.Bytes()) 397 issuerCertificates[id] = certs[0] 398 } 399 400 mockSA := mocks.NewStorageAuthorityReadOnly(fc) 401 402 // Use derived nonces. 403 rncKey := []byte("b8c758dd85e113ea340ce0b3a99f389d40a308548af94d1730a7692c1874f1f") 404 noncePrefix := nonce.DerivePrefix("192.168.1.1:8080", rncKey) 405 nonceService, err := nonce.NewNonceService(metrics.NoopRegisterer, 100, noncePrefix) 406 test.AssertNotError(t, err, "making nonceService") 407 408 inmemNonceService := &inmemnonce.NonceService{Impl: nonceService} 409 gnc := inmemNonceService 410 rnc := inmemNonceService 411 412 // Setup rate limiting. 413 limiter, err := ratelimits.NewLimiter(fc, ratelimits.NewInmemSource(), stats) 414 test.AssertNotError(t, err, "making limiter") 415 txnBuilder, err := ratelimits.NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "", stats, logger) 416 test.AssertNotError(t, err, "making transaction composer") 417 418 unpauseSigner, err := unpause.NewJWTSigner(cmd.HMACKeyConfig{KeyFile: "../test/secrets/sfe_unpause_key"}) 419 test.AssertNotError(t, err, "making unpause signer") 420 unpauseLifetime := time.Hour * 24 * 14 421 unpauseURL := "https://boulder.service.consul:4003" 422 wfe, err := NewWebFrontEndImpl( 423 stats, 424 fc, 425 testKeyPolicy, 426 certChains, 427 issuerCertificates, 428 logger, 429 10*time.Second, 430 10*time.Second, 431 2, 432 &MockRegistrationAuthority{clk: fc}, 433 mockSA, 434 nil, 435 gnc, 436 rnc, 437 rncKey, 438 mockSA, 439 limiter, 440 txnBuilder, 441 map[string]string{"default": "a test profile"}, 442 unpauseSigner, 443 unpauseLifetime, 444 unpauseURL, 445 ) 446 test.AssertNotError(t, err, "Unable to create WFE") 447 448 wfe.SubscriberAgreementURL = agreementURL 449 450 return wfe, fc, requestSigner{t, inmemNonceService.AsSource()} 451 } 452 453 // makePostRequestWithPath creates an http.Request for localhost with method 454 // POST, the provided body, and the correct Content-Length. The path provided 455 // will be parsed as a URL and used to populate the request URL and RequestURI 456 func makePostRequestWithPath(path string, body string) *http.Request { 457 request := &http.Request{ 458 Method: "POST", 459 RemoteAddr: "1.1.1.1:7882", 460 Header: map[string][]string{ 461 "Content-Length": {strconv.Itoa(len(body))}, 462 "Content-Type": {expectedJWSContentType}, 463 }, 464 Body: makeBody(body), 465 Host: "localhost", 466 } 467 url := mustParseURL(path) 468 request.URL = url 469 request.RequestURI = url.Path 470 return request 471 } 472 473 // makeChunkedPostRequestWithPath is the same as makePostRequestWithPath, but 474 // with a chunked encoded request body instead of a fixed content length. 475 func makeChunkedPostRequestWithPath(path string, body string) *http.Request { 476 request := &http.Request{ 477 Method: "POST", 478 RemoteAddr: "1.1.1.1:7882", 479 Header: map[string][]string{ 480 "Transfer-Encoding": {"chunked"}, 481 "Content-Type": {expectedJWSContentType}, 482 }, 483 Body: makeBody(body), 484 Host: "localhost", 485 } 486 url := mustParseURL(path) 487 request.URL = url 488 request.RequestURI = url.Path 489 return request 490 } 491 492 // signAndPost constructs a JWS signed by the account with ID 1, over the given 493 // payload, with the protected URL set to the provided signedURL. An HTTP 494 // request constructed to the provided path with the encoded JWS body as the 495 // POST body is returned. 496 func signAndPost(signer requestSigner, path, signedURL, payload string) *http.Request { 497 _, _, body := signer.byKeyID(1, nil, signedURL, payload) 498 return makePostRequestWithPath(path, body) 499 } 500 501 func mustParseURL(s string) *url.URL { 502 return must.Do(url.Parse(s)) 503 } 504 505 func sortHeader(s string) string { 506 a := strings.Split(s, ", ") 507 sort.Strings(a) 508 return strings.Join(a, ", ") 509 } 510 511 func addHeadIfGet(s []string) []string { 512 if slices.Contains(s, "GET") { 513 return append(s, "HEAD") 514 } 515 return s 516 } 517 518 func TestHandleFunc(t *testing.T) { 519 wfe, _, _ := setupWFE(t) 520 var mux *http.ServeMux 521 var rw *httptest.ResponseRecorder 522 var stubCalled bool 523 runWrappedHandler := func(req *http.Request, pattern string, allowed ...string) { 524 mux = http.NewServeMux() 525 rw = httptest.NewRecorder() 526 stubCalled = false 527 wfe.HandleFunc(mux, pattern, func(context.Context, *web.RequestEvent, http.ResponseWriter, *http.Request) { 528 stubCalled = true 529 }, allowed...) 530 req.URL = mustParseURL(pattern) 531 mux.ServeHTTP(rw, req) 532 } 533 534 // Plain requests (no CORS) 535 type testCase struct { 536 allowed []string 537 reqMethod string 538 shouldCallStub bool 539 shouldSucceed bool 540 pattern string 541 } 542 var lastNonce string 543 for _, c := range []testCase{ 544 {[]string{"GET", "POST"}, "GET", true, true, "/test"}, 545 {[]string{"GET", "POST"}, "GET", true, true, newNoncePath}, 546 {[]string{"GET", "POST"}, "POST", true, true, "/test"}, 547 {[]string{"GET"}, "", false, false, "/test"}, 548 {[]string{"GET"}, "POST", false, false, "/test"}, 549 {[]string{"GET"}, "OPTIONS", false, true, "/test"}, 550 {[]string{"GET"}, "MAKE-COFFEE", false, false, "/test"}, // 405, or 418? 551 {[]string{"GET"}, "GET", true, true, directoryPath}, 552 } { 553 runWrappedHandler(&http.Request{Method: c.reqMethod}, c.pattern, c.allowed...) 554 test.AssertEquals(t, stubCalled, c.shouldCallStub) 555 if c.shouldSucceed { 556 test.AssertEquals(t, rw.Code, http.StatusOK) 557 } else { 558 test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed) 559 test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), sortHeader(strings.Join(addHeadIfGet(c.allowed), ", "))) 560 test.AssertUnmarshaledEquals(t, 561 rw.Body.String(), 562 `{"type":"`+probs.ErrorNS+`malformed","detail":"Method not allowed","status":405}`) 563 } 564 if c.reqMethod == "GET" && c.pattern != newNoncePath { 565 nonce := rw.Header().Get("Replay-Nonce") 566 test.AssertEquals(t, nonce, "") 567 } else { 568 nonce := rw.Header().Get("Replay-Nonce") 569 test.AssertNotEquals(t, nonce, lastNonce) 570 test.AssertNotEquals(t, nonce, "") 571 lastNonce = nonce 572 } 573 linkHeader := rw.Header().Get("Link") 574 if c.pattern != directoryPath { 575 // If the pattern wasn't the directory there should be a Link header for the index 576 test.AssertEquals(t, linkHeader, `<http://localhost/directory>;rel="index"`) 577 } else { 578 // The directory resource shouldn't get a link header 579 test.AssertEquals(t, linkHeader, "") 580 } 581 } 582 583 // Disallowed method returns error JSON in body 584 runWrappedHandler(&http.Request{Method: "PUT"}, "/test", "GET", "POST") 585 test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json") 586 test.AssertUnmarshaledEquals(t, rw.Body.String(), `{"type":"`+probs.ErrorNS+`malformed","detail":"Method not allowed","status":405}`) 587 test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, HEAD, POST") 588 589 // Disallowed method special case: response to HEAD has got no body 590 runWrappedHandler(&http.Request{Method: "HEAD"}, "/test", "GET", "POST") 591 test.AssertEquals(t, stubCalled, true) 592 test.AssertEquals(t, rw.Body.String(), "") 593 594 // HEAD doesn't work with POST-only endpoints 595 runWrappedHandler(&http.Request{Method: "HEAD"}, "/test", "POST") 596 test.AssertEquals(t, stubCalled, false) 597 test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed) 598 test.AssertEquals(t, rw.Header().Get("Content-Type"), "application/problem+json") 599 test.AssertEquals(t, rw.Header().Get("Allow"), "POST") 600 test.AssertUnmarshaledEquals(t, rw.Body.String(), `{"type":"`+probs.ErrorNS+`malformed","detail":"Method not allowed","status":405}`) 601 602 wfe.AllowOrigins = []string{"*"} 603 testOrigin := "https://example.com" 604 605 // CORS "actual" request for disallowed method 606 runWrappedHandler(&http.Request{ 607 Method: "POST", 608 Header: map[string][]string{ 609 "Origin": {testOrigin}, 610 }, 611 }, "/test", "GET") 612 test.AssertEquals(t, stubCalled, false) 613 test.AssertEquals(t, rw.Code, http.StatusMethodNotAllowed) 614 615 // CORS "actual" request for allowed method 616 runWrappedHandler(&http.Request{ 617 Method: "GET", 618 Header: map[string][]string{ 619 "Origin": {testOrigin}, 620 }, 621 }, "/test", "GET", "POST") 622 test.AssertEquals(t, stubCalled, true) 623 test.AssertEquals(t, rw.Code, http.StatusOK) 624 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Methods"), "") 625 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "*") 626 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Headers"), "Content-Type") 627 test.AssertEquals(t, sortHeader(rw.Header().Get("Access-Control-Expose-Headers")), "Link, Location, Replay-Nonce") 628 629 // CORS preflight request for disallowed method 630 runWrappedHandler(&http.Request{ 631 Method: "OPTIONS", 632 Header: map[string][]string{ 633 "Origin": {testOrigin}, 634 "Access-Control-Request-Method": {"POST"}, 635 }, 636 }, "/test", "GET") 637 test.AssertEquals(t, stubCalled, false) 638 test.AssertEquals(t, rw.Code, http.StatusOK) 639 test.AssertEquals(t, rw.Header().Get("Allow"), "GET, HEAD") 640 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "") 641 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Headers"), "") 642 643 // CORS preflight request for allowed method 644 runWrappedHandler(&http.Request{ 645 Method: "OPTIONS", 646 Header: map[string][]string{ 647 "Origin": {testOrigin}, 648 "Access-Control-Request-Method": {"POST"}, 649 "Access-Control-Request-Headers": {"X-Accept-Header1, X-Accept-Header2", "X-Accept-Header3"}, 650 }, 651 }, "/test", "GET", "POST") 652 test.AssertEquals(t, rw.Code, http.StatusOK) 653 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "*") 654 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Headers"), "Content-Type") 655 test.AssertEquals(t, rw.Header().Get("Access-Control-Max-Age"), "86400") 656 test.AssertEquals(t, sortHeader(rw.Header().Get("Access-Control-Allow-Methods")), "GET, HEAD, POST") 657 test.AssertEquals(t, sortHeader(rw.Header().Get("Access-Control-Expose-Headers")), "Link, Location, Replay-Nonce") 658 659 // OPTIONS request without an Origin header (i.e., not a CORS 660 // preflight request) 661 runWrappedHandler(&http.Request{ 662 Method: "OPTIONS", 663 Header: map[string][]string{ 664 "Access-Control-Request-Method": {"POST"}, 665 }, 666 }, "/test", "GET", "POST") 667 test.AssertEquals(t, rw.Code, http.StatusOK) 668 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "") 669 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Headers"), "") 670 test.AssertEquals(t, sortHeader(rw.Header().Get("Allow")), "GET, HEAD, POST") 671 672 // CORS preflight request missing optional Request-Method 673 // header. The "actual" request will be GET. 674 for _, allowedMethod := range []string{"GET", "POST"} { 675 runWrappedHandler(&http.Request{ 676 Method: "OPTIONS", 677 Header: map[string][]string{ 678 "Origin": {testOrigin}, 679 }, 680 }, "/test", allowedMethod) 681 test.AssertEquals(t, rw.Code, http.StatusOK) 682 if allowedMethod == "GET" { 683 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "*") 684 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Headers"), "Content-Type") 685 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Methods"), "GET, HEAD") 686 } else { 687 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), "") 688 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Headers"), "") 689 } 690 } 691 692 // No CORS headers are given when configuration does not list 693 // "*" or the client-provided origin. 694 for _, wfe.AllowOrigins = range [][]string{ 695 {}, 696 {"http://example.com", "https://other.example"}, 697 {""}, // Invalid origin is never matched 698 } { 699 runWrappedHandler(&http.Request{ 700 Method: "OPTIONS", 701 Header: map[string][]string{ 702 "Origin": {testOrigin}, 703 "Access-Control-Request-Method": {"POST"}, 704 }, 705 }, "/test", "POST") 706 test.AssertEquals(t, rw.Code, http.StatusOK) 707 for _, h := range []string{ 708 "Access-Control-Allow-Methods", 709 "Access-Control-Allow-Origin", 710 "Access-Control-Allow-Headers", 711 "Access-Control-Expose-Headers", 712 "Access-Control-Request-Headers", 713 } { 714 test.AssertEquals(t, rw.Header().Get(h), "") 715 } 716 } 717 718 // CORS headers are offered when configuration lists "*" or 719 // the client-provided origin. 720 for _, wfe.AllowOrigins = range [][]string{ 721 {testOrigin, "http://example.org", "*"}, 722 {"", "http://example.org", testOrigin}, // Invalid origin is harmless 723 } { 724 runWrappedHandler(&http.Request{ 725 Method: "OPTIONS", 726 Header: map[string][]string{ 727 "Origin": {testOrigin}, 728 "Access-Control-Request-Method": {"POST"}, 729 }, 730 }, "/test", "POST") 731 test.AssertEquals(t, rw.Code, http.StatusOK) 732 test.AssertEquals(t, rw.Header().Get("Access-Control-Allow-Origin"), testOrigin) 733 // http://www.w3.org/TR/cors/ section 6.4: 734 test.AssertEquals(t, rw.Header().Get("Vary"), "Origin") 735 } 736 } 737 738 func TestPOST404(t *testing.T) { 739 wfe, _, _ := setupWFE(t) 740 responseWriter := httptest.NewRecorder() 741 url, _ := url.Parse("/foobar") 742 wfe.Index(ctx, newRequestEvent(), responseWriter, &http.Request{ 743 Method: "POST", 744 URL: url, 745 }) 746 test.AssertEquals(t, responseWriter.Code, http.StatusNotFound) 747 } 748 749 func TestIndex(t *testing.T) { 750 wfe, _, _ := setupWFE(t) 751 752 responseWriter := httptest.NewRecorder() 753 754 url, _ := url.Parse("/") 755 wfe.Index(ctx, newRequestEvent(), responseWriter, &http.Request{ 756 Method: "GET", 757 URL: url, 758 }) 759 test.AssertEquals(t, responseWriter.Code, http.StatusOK) 760 test.AssertNotEquals(t, responseWriter.Body.String(), "404 page not found\n") 761 test.Assert(t, strings.Contains(responseWriter.Body.String(), directoryPath), 762 "directory path not found") 763 test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "public, max-age=0, no-cache") 764 765 responseWriter.Body.Reset() 766 responseWriter.Header().Del("Cache-Control") 767 url, _ = url.Parse("/foo") 768 wfe.Index(ctx, newRequestEvent(), responseWriter, &http.Request{ 769 URL: url, 770 }) 771 //test.AssertEquals(t, responseWriter.Code, http.StatusNotFound) 772 test.AssertEquals(t, responseWriter.Body.String(), "404 page not found\n") 773 test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), "") 774 } 775 776 // randomDirectoryKeyPresent unmarshals the given buf of JSON and returns true 777 // if `randomDirKeyExplanationLink` appears as the value of a key in the directory 778 // object. 779 func randomDirectoryKeyPresent(t *testing.T, buf []byte) bool { 780 var dir map[string]any 781 err := json.Unmarshal(buf, &dir) 782 if err != nil { 783 t.Errorf("Failed to unmarshal directory: %s", err) 784 } 785 for _, v := range dir { 786 if v == randomDirKeyExplanationLink { 787 return true 788 } 789 } 790 return false 791 } 792 793 type fakeRand struct{} 794 795 func (fr fakeRand) Read(p []byte) (int, error) { 796 return len(p), nil 797 } 798 799 func TestDirectory(t *testing.T) { 800 wfe, _, signer := setupWFE(t) 801 mux := wfe.Handler(metrics.NoopRegisterer) 802 core.RandReader = fakeRand{} 803 defer func() { core.RandReader = rand.Reader }() 804 805 dirURL, _ := url.Parse("/directory") 806 807 getReq := &http.Request{ 808 Method: http.MethodGet, 809 URL: dirURL, 810 Host: "localhost:4300", 811 } 812 813 _, _, jwsBody := signer.byKeyID(1, nil, "http://localhost/directory", "") 814 postAsGetReq := makePostRequestWithPath("/directory", jwsBody) 815 816 testCases := []struct { 817 name string 818 caaIdent string 819 website string 820 expectedJSON string 821 request *http.Request 822 }{ 823 { 824 name: "standard GET, no CAA ident/website meta", 825 request: getReq, 826 expectedJSON: `{ 827 "keyChange": "http://localhost:4300/acme/key-change", 828 "meta": { 829 "termsOfService": "http://example.invalid/terms", 830 "profiles": { 831 "default": "a test profile" 832 } 833 }, 834 "newNonce": "http://localhost:4300/acme/new-nonce", 835 "newAccount": "http://localhost:4300/acme/new-acct", 836 "newOrder": "http://localhost:4300/acme/new-order", 837 "revokeCert": "http://localhost:4300/acme/revoke-cert", 838 "AAAAAAAAAAA": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417" 839 }`, 840 }, 841 { 842 name: "standard GET, CAA ident/website meta", 843 caaIdent: "Radiant Lock", 844 website: "zombo.com", 845 request: getReq, 846 expectedJSON: `{ 847 "AAAAAAAAAAA": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417", 848 "keyChange": "http://localhost:4300/acme/key-change", 849 "meta": { 850 "caaIdentities": [ 851 "Radiant Lock" 852 ], 853 "termsOfService": "http://example.invalid/terms", 854 "website": "zombo.com", 855 "profiles": { 856 "default": "a test profile" 857 } 858 }, 859 "newAccount": "http://localhost:4300/acme/new-acct", 860 "newNonce": "http://localhost:4300/acme/new-nonce", 861 "newOrder": "http://localhost:4300/acme/new-order", 862 "revokeCert": "http://localhost:4300/acme/revoke-cert" 863 }`, 864 }, 865 { 866 name: "POST-as-GET, CAA ident/website meta", 867 caaIdent: "Radiant Lock", 868 website: "zombo.com", 869 request: postAsGetReq, 870 expectedJSON: `{ 871 "AAAAAAAAAAA": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417", 872 "keyChange": "http://localhost/acme/key-change", 873 "meta": { 874 "caaIdentities": [ 875 "Radiant Lock" 876 ], 877 "termsOfService": "http://example.invalid/terms", 878 "website": "zombo.com", 879 "profiles": { 880 "default": "a test profile" 881 } 882 }, 883 "newAccount": "http://localhost/acme/new-acct", 884 "newNonce": "http://localhost/acme/new-nonce", 885 "newOrder": "http://localhost/acme/new-order", 886 "revokeCert": "http://localhost/acme/revoke-cert" 887 }`, 888 }, 889 } 890 891 for _, tc := range testCases { 892 t.Run(tc.name, func(t *testing.T) { 893 // Configure a caaIdentity and website for the /directory meta based on the tc 894 wfe.DirectoryCAAIdentity = tc.caaIdent // "Radiant Lock" 895 wfe.DirectoryWebsite = tc.website //"zombo.com" 896 responseWriter := httptest.NewRecorder() 897 // Serve the /directory response for this request into a recorder 898 mux.ServeHTTP(responseWriter, tc.request) 899 // We expect all directory requests to return a json object with a good HTTP status 900 test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/json") 901 // We expect all requests to return status OK 902 test.AssertEquals(t, responseWriter.Code, http.StatusOK) 903 // The response should match expected 904 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.expectedJSON) 905 // Check that the random directory key is present 906 test.AssertEquals(t, 907 randomDirectoryKeyPresent(t, responseWriter.Body.Bytes()), 908 true) 909 }) 910 } 911 } 912 913 func TestRelativeDirectory(t *testing.T) { 914 wfe, _, _ := setupWFE(t) 915 mux := wfe.Handler(metrics.NoopRegisterer) 916 core.RandReader = fakeRand{} 917 defer func() { core.RandReader = rand.Reader }() 918 919 expectedDirectory := func(hostname string) string { 920 expected := new(bytes.Buffer) 921 922 fmt.Fprintf(expected, "{") 923 fmt.Fprintf(expected, `"keyChange":"%s/acme/key-change",`, hostname) 924 fmt.Fprintf(expected, `"newNonce":"%s/acme/new-nonce",`, hostname) 925 fmt.Fprintf(expected, `"newAccount":"%s/acme/new-acct",`, hostname) 926 fmt.Fprintf(expected, `"newOrder":"%s/acme/new-order",`, hostname) 927 fmt.Fprintf(expected, `"revokeCert":"%s/acme/revoke-cert",`, hostname) 928 fmt.Fprintf(expected, `"AAAAAAAAAAA":"https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",`) 929 fmt.Fprintf(expected, `"meta":{`) 930 fmt.Fprintf(expected, `"termsOfService":"http://example.invalid/terms",`) 931 fmt.Fprintf(expected, `"profiles":{"default":"a test profile"}`) 932 fmt.Fprintf(expected, "}") 933 fmt.Fprintf(expected, "}") 934 return expected.String() 935 } 936 937 dirTests := []struct { 938 host string 939 protoHeader string 940 result string 941 }{ 942 // Test '' (No host header) with no proto header 943 {"", "", expectedDirectory("http://localhost")}, 944 // Test localhost:4300 with no proto header 945 {"localhost:4300", "", expectedDirectory("http://localhost:4300")}, 946 // Test 127.0.0.1:4300 with no proto header 947 {"127.0.0.1:4300", "", expectedDirectory("http://127.0.0.1:4300")}, 948 // Test localhost:4300 with HTTP proto header 949 {"localhost:4300", "http", expectedDirectory("http://localhost:4300")}, 950 // Test localhost:4300 with HTTPS proto header 951 {"localhost:4300", "https", expectedDirectory("https://localhost:4300")}, 952 } 953 954 for _, tt := range dirTests { 955 var headers map[string][]string 956 responseWriter := httptest.NewRecorder() 957 958 if tt.protoHeader != "" { 959 headers = map[string][]string{ 960 "X-Forwarded-Proto": {tt.protoHeader}, 961 } 962 } 963 964 mux.ServeHTTP(responseWriter, &http.Request{ 965 Method: "GET", 966 Host: tt.host, 967 URL: mustParseURL(directoryPath), 968 Header: headers, 969 }) 970 test.AssertEquals(t, responseWriter.Header().Get("Content-Type"), "application/json") 971 test.AssertEquals(t, responseWriter.Code, http.StatusOK) 972 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tt.result) 973 } 974 } 975 976 // TestNonceEndpoint tests requests to the WFE2's new-nonce endpoint 977 func TestNonceEndpoint(t *testing.T) { 978 wfe, _, signer := setupWFE(t) 979 mux := wfe.Handler(metrics.NoopRegisterer) 980 981 getReq := &http.Request{ 982 Method: http.MethodGet, 983 URL: mustParseURL(newNoncePath), 984 } 985 headReq := &http.Request{ 986 Method: http.MethodHead, 987 URL: mustParseURL(newNoncePath), 988 } 989 990 _, _, jwsBody := signer.byKeyID(1, nil, fmt.Sprintf("http://localhost%s", newNoncePath), "") 991 postAsGetReq := makePostRequestWithPath(newNoncePath, jwsBody) 992 993 testCases := []struct { 994 name string 995 request *http.Request 996 expectedStatus int 997 }{ 998 { 999 name: "GET new-nonce request", 1000 request: getReq, 1001 expectedStatus: http.StatusNoContent, 1002 }, 1003 { 1004 name: "HEAD new-nonce request", 1005 request: headReq, 1006 expectedStatus: http.StatusOK, 1007 }, 1008 { 1009 name: "POST-as-GET new-nonce request", 1010 request: postAsGetReq, 1011 expectedStatus: http.StatusOK, 1012 }, 1013 } 1014 1015 for _, tc := range testCases { 1016 t.Run(tc.name, func(t *testing.T) { 1017 responseWriter := httptest.NewRecorder() 1018 mux.ServeHTTP(responseWriter, tc.request) 1019 // The response should have the expected HTTP status code 1020 test.AssertEquals(t, responseWriter.Code, tc.expectedStatus) 1021 // And the response should contain a valid nonce in the Replay-Nonce header 1022 nonce := responseWriter.Header().Get("Replay-Nonce") 1023 redeemResp, err := wfe.rnc.Redeem(context.Background(), &noncepb.NonceMessage{Nonce: nonce}) 1024 test.AssertNotError(t, err, "redeeming nonce") 1025 test.AssertEquals(t, redeemResp.Valid, true) 1026 // The server MUST include a Cache-Control header field with the "no-store" 1027 // directive in responses for the newNonce resource, in order to prevent 1028 // caching of this resource. 1029 cacheControl := responseWriter.Header().Get("Cache-Control") 1030 test.AssertEquals(t, cacheControl, "no-store") 1031 }) 1032 } 1033 } 1034 1035 func TestHTTPMethods(t *testing.T) { 1036 wfe, _, _ := setupWFE(t) 1037 mux := wfe.Handler(metrics.NoopRegisterer) 1038 1039 // NOTE: Boulder's muxer treats HEAD as implicitly allowed if GET is specified 1040 // so we include both here in `getOnly` 1041 getOnly := map[string]bool{http.MethodGet: true, http.MethodHead: true} 1042 postOnly := map[string]bool{http.MethodPost: true} 1043 getOrPost := map[string]bool{http.MethodGet: true, http.MethodHead: true, http.MethodPost: true} 1044 1045 testCases := []struct { 1046 Name string 1047 Path string 1048 Allowed map[string]bool 1049 }{ 1050 { 1051 Name: "Index path should be GET only", 1052 Path: "/", 1053 Allowed: getOnly, 1054 }, 1055 { 1056 Name: "Directory path should be GET or POST only", 1057 Path: directoryPath, 1058 Allowed: getOrPost, 1059 }, 1060 { 1061 Name: "NewAcct path should be POST only", 1062 Path: newAcctPath, 1063 Allowed: postOnly, 1064 }, 1065 { 1066 Name: "Acct path should be POST only", 1067 Path: acctPath, 1068 Allowed: postOnly, 1069 }, 1070 // TODO(@cpu): Remove GET authz support, support only POST-as-GET 1071 { 1072 Name: "Authz path should be GET or POST only", 1073 Path: authzPath, 1074 Allowed: getOrPost, 1075 }, 1076 // TODO(@cpu): Remove GET challenge support, support only POST-as-GET 1077 { 1078 Name: "Challenge path should be GET or POST only", 1079 Path: challengePath, 1080 Allowed: getOrPost, 1081 }, 1082 // TODO(@cpu): Remove GET certificate support, support only POST-as-GET 1083 { 1084 Name: "Certificate path should be GET or POST only", 1085 Path: certPath, 1086 Allowed: getOrPost, 1087 }, 1088 { 1089 Name: "RevokeCert path should be POST only", 1090 Path: revokeCertPath, 1091 Allowed: postOnly, 1092 }, 1093 { 1094 Name: "Build ID path should be GET only", 1095 Path: buildIDPath, 1096 Allowed: getOnly, 1097 }, 1098 { 1099 Name: "Health path should be GET only", 1100 Path: healthzPath, 1101 Allowed: getOnly, 1102 }, 1103 { 1104 Name: "Rollover path should be POST only", 1105 Path: rolloverPath, 1106 Allowed: postOnly, 1107 }, 1108 { 1109 Name: "New order path should be POST only", 1110 Path: newOrderPath, 1111 Allowed: postOnly, 1112 }, 1113 // TODO(@cpu): Remove GET order support, support only POST-as-GET 1114 { 1115 Name: "Order path should be GET or POST only", 1116 Path: orderPath, 1117 Allowed: getOrPost, 1118 }, 1119 { 1120 Name: "Nonce path should be GET or POST only", 1121 Path: newNoncePath, 1122 Allowed: getOrPost, 1123 }, 1124 } 1125 1126 // NOTE: We omit http.MethodOptions because all requests with this method are 1127 // redirected to a special endpoint for CORS headers 1128 allMethods := []string{ 1129 http.MethodGet, 1130 http.MethodHead, 1131 http.MethodPost, 1132 http.MethodPut, 1133 http.MethodPatch, 1134 http.MethodDelete, 1135 http.MethodConnect, 1136 http.MethodTrace, 1137 } 1138 1139 responseWriter := httptest.NewRecorder() 1140 1141 for _, tc := range testCases { 1142 t.Run(tc.Name, func(t *testing.T) { 1143 // For every possible HTTP method check what the mux serves for the test 1144 // case path 1145 for _, method := range allMethods { 1146 responseWriter.Body.Reset() 1147 mux.ServeHTTP(responseWriter, &http.Request{ 1148 Method: method, 1149 URL: mustParseURL(tc.Path), 1150 }) 1151 // If the method isn't one that is intended to be allowed by the path, 1152 // check that the response was the not allowed response 1153 if _, ok := tc.Allowed[method]; !ok { 1154 var prob probs.ProblemDetails 1155 // Unmarshal the body into a problem 1156 body := responseWriter.Body.String() 1157 err := json.Unmarshal([]byte(body), &prob) 1158 test.AssertNotError(t, err, fmt.Sprintf("Error unmarshalling resp body: %q", body)) 1159 // TODO(@cpu): It seems like the mux should be returning 1160 // http.StatusMethodNotAllowed here, but instead it returns StatusOK 1161 // with a problem that has a StatusMethodNotAllowed HTTPStatus. Is 1162 // this a bug? 1163 test.AssertEquals(t, responseWriter.Code, http.StatusOK) 1164 test.AssertEquals(t, prob.HTTPStatus, http.StatusMethodNotAllowed) 1165 test.AssertEquals(t, prob.Detail, "Method not allowed") 1166 } else { 1167 // Otherwise if it was an allowed method, ensure that the response was 1168 // *not* StatusMethodNotAllowed 1169 test.AssertNotEquals(t, responseWriter.Code, http.StatusMethodNotAllowed) 1170 } 1171 } 1172 }) 1173 } 1174 } 1175 1176 func TestGetChallengeHandler(t *testing.T) { 1177 wfe, _, _ := setupWFE(t) 1178 1179 // The slug "7TyhFQ" is the StringID of a challenge with type "http-01" and 1180 // token "token". 1181 challSlug := "7TyhFQ" 1182 1183 for _, method := range []string{"GET", "HEAD"} { 1184 resp := httptest.NewRecorder() 1185 1186 // We set req.URL.Path separately to emulate the path-stripping that 1187 // Boulder's request handler does. 1188 challengeURL := fmt.Sprintf("http://localhost/acme/chall/1/1/%s", challSlug) 1189 req, err := http.NewRequest(method, challengeURL, nil) 1190 test.AssertNotError(t, err, "Could not make NewRequest") 1191 req.URL.Path = fmt.Sprintf("1/1/%s", challSlug) 1192 1193 wfe.ChallengeHandler(ctx, newRequestEvent(), resp, req) 1194 test.AssertEquals(t, resp.Code, http.StatusOK) 1195 test.AssertEquals(t, resp.Header().Get("Location"), challengeURL) 1196 test.AssertEquals(t, resp.Header().Get("Content-Type"), "application/json") 1197 test.AssertEquals(t, resp.Header().Get("Link"), `<http://localhost/acme/authz/1/1>;rel="up"`) 1198 1199 // Body is only relevant for GET. For HEAD, body will 1200 // be discarded by HandleFunc() anyway, so it doesn't 1201 // matter what Challenge() writes to it. 1202 if method == "GET" { 1203 test.AssertUnmarshaledEquals( 1204 t, resp.Body.String(), 1205 `{"status": "valid", "type":"http-01","token":"token","url":"http://localhost/acme/chall/1/1/7TyhFQ"}`) 1206 } 1207 } 1208 } 1209 1210 func TestChallengeHandler(t *testing.T) { 1211 wfe, _, signer := setupWFE(t) 1212 1213 post := func(path string) *http.Request { 1214 signedURL := fmt.Sprintf("http://localhost/%s", path) 1215 _, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`) 1216 return makePostRequestWithPath(path, jwsBody) 1217 } 1218 postAsGet := func(keyID int64, path, body string) *http.Request { 1219 _, _, jwsBody := signer.byKeyID(keyID, nil, fmt.Sprintf("http://localhost/%s", path), body) 1220 return makePostRequestWithPath(path, jwsBody) 1221 } 1222 1223 testCases := []struct { 1224 Name string 1225 Request *http.Request 1226 ExpectedStatus int 1227 ExpectedHeaders map[string]string 1228 ExpectedBody string 1229 }{ 1230 { 1231 Name: "Valid challenge", 1232 Request: post("1/1/7TyhFQ"), 1233 ExpectedStatus: http.StatusOK, 1234 ExpectedHeaders: map[string]string{ 1235 "Content-Type": "application/json", 1236 "Location": "http://localhost/acme/chall/1/1/7TyhFQ", 1237 "Link": `<http://localhost/acme/authz/1/1>;rel="up"`, 1238 }, 1239 ExpectedBody: `{"status": "valid", "type":"http-01","token":"token","url":"http://localhost/acme/chall/1/1/7TyhFQ"}`, 1240 }, 1241 { 1242 Name: "Expired challenge", 1243 Request: post("1/3/7TyhFQ"), 1244 ExpectedStatus: http.StatusNotFound, 1245 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Expired authorization","status":404}`, 1246 }, 1247 { 1248 Name: "Missing challenge", 1249 Request: post("1/1/"), 1250 ExpectedStatus: http.StatusNotFound, 1251 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"No such challenge","status":404}`, 1252 }, 1253 { 1254 Name: "Unspecified database error", 1255 Request: post("1/4/7TyhFQ"), 1256 ExpectedStatus: http.StatusInternalServerError, 1257 ExpectedBody: `{"type":"` + probs.ErrorNS + `serverInternal","detail":"Problem getting authorization","status":500}`, 1258 }, 1259 { 1260 Name: "POST-as-GET, wrong owner", 1261 Request: postAsGet(1, "1/5/7TyhFQ", ""), 1262 ExpectedStatus: http.StatusForbidden, 1263 ExpectedBody: `{"type":"` + probs.ErrorNS + `unauthorized","detail":"User account ID doesn't match account ID in authorization","status":403}`, 1264 }, 1265 { 1266 Name: "Valid POST-as-GET", 1267 Request: postAsGet(1, "1/1/7TyhFQ", ""), 1268 ExpectedStatus: http.StatusOK, 1269 ExpectedBody: `{"status": "valid", "type":"http-01", "token":"token", "url": "http://localhost/acme/chall/1/1/7TyhFQ"}`, 1270 }, 1271 } 1272 1273 for _, tc := range testCases { 1274 t.Run(tc.Name, func(t *testing.T) { 1275 responseWriter := httptest.NewRecorder() 1276 wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, tc.Request) 1277 // Check the response code, headers and body match expected 1278 headers := responseWriter.Header() 1279 body := responseWriter.Body.String() 1280 test.AssertEquals(t, responseWriter.Code, tc.ExpectedStatus) 1281 for h, v := range tc.ExpectedHeaders { 1282 test.AssertEquals(t, headers.Get(h), v) 1283 } 1284 test.AssertUnmarshaledEquals(t, body, tc.ExpectedBody) 1285 }) 1286 } 1287 } 1288 1289 // MockRAPerformValidationError is a mock RA that just returns an error on 1290 // PerformValidation. 1291 type MockRAPerformValidationError struct { 1292 MockRegistrationAuthority 1293 } 1294 1295 func (ra *MockRAPerformValidationError) PerformValidation(context.Context, *rapb.PerformValidationRequest, ...grpc.CallOption) (*corepb.Authorization, error) { 1296 return nil, errors.New("broken on purpose") 1297 } 1298 1299 // TestUpdateChallengeHandlerFinalizedAuthz tests that POSTing a challenge associated 1300 // with an already valid authorization just returns the challenge without calling 1301 // the RA. 1302 func TestUpdateChallengeHandlerFinalizedAuthz(t *testing.T) { 1303 wfe, fc, signer := setupWFE(t) 1304 wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}} 1305 responseWriter := httptest.NewRecorder() 1306 1307 signedURL := "http://localhost/1/1/7TyhFQ" 1308 _, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`) 1309 request := makePostRequestWithPath("1/1/7TyhFQ", jwsBody) 1310 wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, request) 1311 1312 body := responseWriter.Body.String() 1313 test.AssertUnmarshaledEquals(t, body, `{ 1314 "status": "valid", 1315 "type": "http-01", 1316 "token": "token", 1317 "url": "http://localhost/acme/chall/1/1/7TyhFQ" 1318 }`) 1319 } 1320 1321 // TestUpdateChallengeHandlerRAError tests that when the RA returns an error from 1322 // PerformValidation that the WFE returns an internal server error as expected 1323 // and does not panic or otherwise bug out. 1324 func TestUpdateChallengeHandlerRAError(t *testing.T) { 1325 wfe, fc, signer := setupWFE(t) 1326 // Mock the RA to always fail PerformValidation 1327 wfe.ra = &MockRAPerformValidationError{MockRegistrationAuthority{clk: fc}} 1328 1329 // Update a pending challenge 1330 signedURL := "http://localhost/1/2/7TyhFQ" 1331 _, _, jwsBody := signer.byKeyID(1, nil, signedURL, `{}`) 1332 responseWriter := httptest.NewRecorder() 1333 request := makePostRequestWithPath("1/2/7TyhFQ", jwsBody) 1334 1335 wfe.ChallengeHandler(ctx, newRequestEvent(), responseWriter, request) 1336 1337 // The result should be an internal server error problem. 1338 body := responseWriter.Body.String() 1339 test.AssertUnmarshaledEquals(t, body, `{ 1340 "type": "urn:ietf:params:acme:error:serverInternal", 1341 "detail": "Unable to update challenge", 1342 "status": 500 1343 }`) 1344 } 1345 1346 func TestBadNonce(t *testing.T) { 1347 wfe, _, _ := setupWFE(t) 1348 1349 key := loadKey(t, []byte(test2KeyPrivatePEM)) 1350 rsaKey, ok := key.(*rsa.PrivateKey) 1351 test.Assert(t, ok, "Couldn't load RSA key") 1352 // NOTE: We deliberately do not set the NonceSource in the jose.SignerOptions 1353 // for this test in order to provoke a bad nonce error 1354 noNonceSigner, err := jose.NewSigner(jose.SigningKey{ 1355 Key: rsaKey, 1356 Algorithm: jose.RS256, 1357 }, &jose.SignerOptions{ 1358 EmbedJWK: true, 1359 }) 1360 test.AssertNotError(t, err, "Failed to make signer") 1361 1362 responseWriter := httptest.NewRecorder() 1363 result, err := noNonceSigner.Sign([]byte(`{"contact":["mailto:person@mail.com"]}`)) 1364 test.AssertNotError(t, err, "Failed to sign body") 1365 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, 1366 makePostRequestWithPath("nonce", result.FullSerialize())) 1367 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{"type":"`+probs.ErrorNS+`badNonce","detail":"JWS has an invalid anti-replay nonce","status":400}`) 1368 } 1369 1370 func TestNewECDSAAccount(t *testing.T) { 1371 wfe, _, signer := setupWFE(t) 1372 1373 // E1 always exists; E2 never exists 1374 key := loadKey(t, []byte(testE2KeyPrivatePEM)) 1375 _, ok := key.(*ecdsa.PrivateKey) 1376 test.Assert(t, ok, "Couldn't load ECDSA key") 1377 1378 payload := `{"contact":["mailto:person@mail.com"],"termsOfServiceAgreed":true}` 1379 path := newAcctPath 1380 signedURL := fmt.Sprintf("http://localhost%s", path) 1381 _, _, body := signer.embeddedJWK(key, signedURL, payload) 1382 request := makePostRequestWithPath(path, body) 1383 1384 responseWriter := httptest.NewRecorder() 1385 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request) 1386 1387 var acct core.Registration 1388 responseBody := responseWriter.Body.String() 1389 err := json.Unmarshal([]byte(responseBody), &acct) 1390 test.AssertNotError(t, err, "Couldn't unmarshal returned account object") 1391 test.AssertEquals(t, acct.Agreement, "") 1392 1393 test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/acct/1") 1394 1395 key = loadKey(t, []byte(testE1KeyPrivatePEM)) 1396 _, ok = key.(*ecdsa.PrivateKey) 1397 test.Assert(t, ok, "Couldn't load ECDSA key") 1398 1399 _, _, body = signer.embeddedJWK(key, signedURL, payload) 1400 request = makePostRequestWithPath(path, body) 1401 1402 // Reset the body and status code 1403 responseWriter = httptest.NewRecorder() 1404 // POST, Valid JSON, Key already in use 1405 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request) 1406 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), 1407 `{ 1408 "key": { 1409 "kty": "EC", 1410 "crv": "P-256", 1411 "x": "FwvSZpu06i3frSk_mz9HcD9nETn4wf3mQ-zDtG21Gao", 1412 "y": "S8rR-0dWa8nAcw1fbunF_ajS3PQZ-QwLps-2adgLgPk" 1413 }, 1414 "status": "" 1415 }`) 1416 test.AssertEquals(t, responseWriter.Header().Get("Location"), "http://localhost/acme/acct/3") 1417 test.AssertEquals(t, responseWriter.Code, 200) 1418 1419 // test3KeyPrivatePEM is a private key corresponding to a deactivated account in the mock SA's GetRegistration test data. 1420 key = loadKey(t, []byte(test3KeyPrivatePEM)) 1421 _, ok = key.(*rsa.PrivateKey) 1422 test.Assert(t, ok, "Couldn't load test3 key") 1423 1424 // Reset the body and status code 1425 responseWriter = httptest.NewRecorder() 1426 1427 // Test POST valid JSON with deactivated account 1428 payload = `{}` 1429 path = "1" 1430 signedURL = "http://localhost/1" 1431 _, _, body = signer.embeddedJWK(key, signedURL, payload) 1432 request = makePostRequestWithPath(path, body) 1433 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request) 1434 test.AssertEquals(t, responseWriter.Code, http.StatusForbidden) 1435 } 1436 1437 // Test that the WFE handling of the "empty update" POST is correct. The ACME 1438 // spec describes how when clients wish to query the server for information 1439 // about an account an empty account update should be sent, and 1440 // a populated acct object will be returned. 1441 func TestEmptyAccount(t *testing.T) { 1442 wfe, _, signer := setupWFE(t) 1443 1444 // Test Key 1 is mocked in the mock StorageAuthority used in setupWFE to 1445 // return a populated account for GetRegistrationByKey when test key 1 is 1446 // used. 1447 key := loadKey(t, []byte(test1KeyPrivatePEM)) 1448 _, ok := key.(*rsa.PrivateKey) 1449 test.Assert(t, ok, "Couldn't load RSA key") 1450 1451 path := "1" 1452 signedURL := "http://localhost/1" 1453 1454 testCases := []struct { 1455 Name string 1456 Payload string 1457 ExpectedStatus int 1458 }{ 1459 { 1460 Name: "POST empty string to acct", 1461 Payload: "", 1462 ExpectedStatus: http.StatusOK, 1463 }, 1464 { 1465 Name: "POST empty JSON object to acct", 1466 Payload: "{}", 1467 ExpectedStatus: http.StatusOK, 1468 }, 1469 { 1470 Name: "POST invalid empty JSON string to acct", 1471 Payload: "\"\"", 1472 ExpectedStatus: http.StatusBadRequest, 1473 }, 1474 { 1475 Name: "POST invalid empty JSON array to acct", 1476 Payload: "[]", 1477 ExpectedStatus: http.StatusBadRequest, 1478 }, 1479 } 1480 1481 for _, tc := range testCases { 1482 t.Run(tc.Name, func(t *testing.T) { 1483 responseWriter := httptest.NewRecorder() 1484 1485 _, _, body := signer.byKeyID(1, key, signedURL, tc.Payload) 1486 request := makePostRequestWithPath(path, body) 1487 1488 // Send an account update with the trivial body 1489 wfe.Account( 1490 ctx, 1491 newRequestEvent(), 1492 responseWriter, 1493 request) 1494 1495 responseBody := responseWriter.Body.String() 1496 test.AssertEquals(t, responseWriter.Code, tc.ExpectedStatus) 1497 1498 // If success is expected, we should get back a populated Account 1499 if tc.ExpectedStatus == http.StatusOK { 1500 var acct core.Registration 1501 err := json.Unmarshal([]byte(responseBody), &acct) 1502 test.AssertNotError(t, err, "Couldn't unmarshal returned account object") 1503 test.AssertEquals(t, acct.Agreement, "") 1504 } 1505 1506 responseWriter.Body.Reset() 1507 }) 1508 } 1509 } 1510 1511 func TestNewAccount(t *testing.T) { 1512 wfe, _, signer := setupWFE(t) 1513 mux := wfe.Handler(metrics.NoopRegisterer) 1514 key := loadKey(t, []byte(test2KeyPrivatePEM)) 1515 _, ok := key.(*rsa.PrivateKey) 1516 test.Assert(t, ok, "Couldn't load test2 key") 1517 1518 path := newAcctPath 1519 signedURL := fmt.Sprintf("http://localhost%s", path) 1520 1521 wrongAgreementAcct := `{"contact":["mailto:person@mail.com"],"termsOfServiceAgreed":false}` 1522 // An acct with the terms not agreed to 1523 _, _, wrongAgreementBody := signer.embeddedJWK(key, signedURL, wrongAgreementAcct) 1524 1525 // A non-JSON payload 1526 _, _, fooBody := signer.embeddedJWK(key, signedURL, `foo`) 1527 1528 type newAcctErrorTest struct { 1529 r *http.Request 1530 respBody string 1531 } 1532 1533 acctErrTests := []newAcctErrorTest{ 1534 // POST, but no body. 1535 { 1536 &http.Request{ 1537 Method: "POST", 1538 URL: mustParseURL(newAcctPath), 1539 Header: map[string][]string{ 1540 "Content-Length": {"0"}, 1541 "Content-Type": {expectedJWSContentType}, 1542 }, 1543 }, 1544 `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: No body on POST","status":400}`, 1545 }, 1546 1547 // POST, but body that isn't valid JWS 1548 { 1549 makePostRequestWithPath(newAcctPath, "hi"), 1550 `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Parse error reading JWS","status":400}`, 1551 }, 1552 1553 // POST, Properly JWS-signed, but payload is "foo", not base64-encoded JSON. 1554 { 1555 makePostRequestWithPath(newAcctPath, fooBody), 1556 `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Request payload did not parse as JSON","status":400}`, 1557 }, 1558 1559 // Same signed body, but payload modified by one byte, breaking signature. 1560 // should fail JWS verification. 1561 { 1562 makePostRequestWithPath(newAcctPath, 1563 `{"payload":"Zm9x","protected":"eyJhbGciOiJSUzI1NiIsImp3ayI6eyJrdHkiOiJSU0EiLCJuIjoicW5BUkxyVDdYejRnUmNLeUxkeWRtQ3ItZXk5T3VQSW1YNFg0MHRoazNvbjI2RmtNem5SM2ZSanM2NmVMSzdtbVBjQlo2dU9Kc2VVUlU2d0FhWk5tZW1vWXgxZE12cXZXV0l5aVFsZUhTRDdROHZCcmhSNnVJb080akF6SlpSLUNoelp1U0R0N2lITi0zeFVWc3B1NVhHd1hVX01WSlpzaFR3cDRUYUZ4NWVsSElUX09iblR2VE9VM1hoaXNoMDdBYmdaS21Xc1ZiWGg1cy1DcklpY1U0T2V4SlBndW5XWl9ZSkp1ZU9LbVR2bkxsVFY0TXpLUjJvWmxCS1oyN1MwLVNmZFZfUUR4X3lkbGU1b01BeUtWdGxBVjM1Y3lQTUlzWU53Z1VHQkNkWV8yVXppNWVYMGxUYzdNUFJ3ejZxUjFraXAtaTU5VmNHY1VRZ3FIVjZGeXF3IiwiZSI6IkFRQUIifSwia2lkIjoiIiwibm9uY2UiOiJyNHpuenZQQUVwMDlDN1JwZUtYVHhvNkx3SGwxZVBVdmpGeXhOSE1hQnVvIiwidXJsIjoiaHR0cDovL2xvY2FsaG9zdC9hY21lL25ldy1yZWcifQ","signature":"jcTdxSygm_cvD7KbXqsxgnoPApCTSkV4jolToSOd2ciRkg5W7Yl0ZKEEKwOc-dYIbQiwGiDzisyPCicwWsOUA1WSqHylKvZ3nxSMc6KtwJCW2DaOqcf0EEjy5VjiZJUrOt2c-r6b07tbn8sfOJKwlF2lsOeGi4s-rtvvkeQpAU-AWauzl9G4bv2nDUeCviAZjHx_PoUC-f9GmZhYrbDzAvXZ859ktM6RmMeD0OqPN7bhAeju2j9Gl0lnryZMtq2m0J2m1ucenQBL1g4ZkP1JiJvzd2cAz5G7Ftl2YeJJyWhqNd3qq0GVOt1P11s8PTGNaSoM0iR9QfUxT9A6jxARtg"}`), 1564 `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: JWS verification error","status":400}`, 1565 }, 1566 { 1567 makePostRequestWithPath(newAcctPath, wrongAgreementBody), 1568 `{"type":"` + probs.ErrorNS + `malformed","detail":"must agree to terms of service","status":400}`, 1569 }, 1570 } 1571 for _, rt := range acctErrTests { 1572 responseWriter := httptest.NewRecorder() 1573 mux.ServeHTTP(responseWriter, rt.r) 1574 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), rt.respBody) 1575 } 1576 1577 responseWriter := httptest.NewRecorder() 1578 1579 payload := `{"contact":["mailto:person@mail.com"],"termsOfServiceAgreed":true}` 1580 _, _, body := signer.embeddedJWK(key, signedURL, payload) 1581 request := makePostRequestWithPath(path, body) 1582 1583 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request) 1584 1585 var acct core.Registration 1586 responseBody := responseWriter.Body.String() 1587 err := json.Unmarshal([]byte(responseBody), &acct) 1588 test.AssertNotError(t, err, "Couldn't unmarshal returned account object") 1589 // Agreement is an ACMEv1 field and should not be present 1590 test.AssertEquals(t, acct.Agreement, "") 1591 1592 test.AssertEquals( 1593 t, responseWriter.Header().Get("Location"), 1594 "http://localhost/acme/acct/1") 1595 1596 // Load an existing key 1597 key = loadKey(t, []byte(test1KeyPrivatePEM)) 1598 _, ok = key.(*rsa.PrivateKey) 1599 test.Assert(t, ok, "Couldn't load test1 key") 1600 1601 // Reset the body and status code 1602 responseWriter = httptest.NewRecorder() 1603 // POST, Valid JSON, Key already in use 1604 _, _, body = signer.embeddedJWK(key, signedURL, payload) 1605 request = makePostRequestWithPath(path, body) 1606 // POST the NewAccount request 1607 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request) 1608 // We expect a Location header and a 200 response with an empty body 1609 test.AssertEquals( 1610 t, responseWriter.Header().Get("Location"), 1611 "http://localhost/acme/acct/1") 1612 test.AssertEquals(t, responseWriter.Code, 200) 1613 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), 1614 `{ 1615 "key": { 1616 "kty": "RSA", 1617 "n": "yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ", 1618 "e": "AQAB" 1619 }, 1620 "status": "valid" 1621 }`) 1622 } 1623 1624 func TestNewAccountWhenAccountHasBeenDeactivated(t *testing.T) { 1625 wfe, _, signer := setupWFE(t) 1626 signedURL := fmt.Sprintf("http://localhost%s", newAcctPath) 1627 // test3KeyPrivatePEM is a private key corresponding to a deactivated account in the mock SA's GetRegistration test data. 1628 k := loadKey(t, []byte(test3KeyPrivatePEM)) 1629 _, ok := k.(*rsa.PrivateKey) 1630 test.Assert(t, ok, "Couldn't load test3 key") 1631 1632 payload := `{"contact":["mailto:person@mail.com"],"termsOfServiceAgreed":true}` 1633 _, _, body := signer.embeddedJWK(k, signedURL, payload) 1634 request := makePostRequestWithPath(newAcctPath, body) 1635 1636 responseWriter := httptest.NewRecorder() 1637 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request) 1638 1639 test.AssertEquals(t, responseWriter.Code, http.StatusForbidden) 1640 } 1641 1642 func TestNewAccountNoID(t *testing.T) { 1643 wfe, _, signer := setupWFE(t) 1644 key := loadKey(t, []byte(test2KeyPrivatePEM)) 1645 _, ok := key.(*rsa.PrivateKey) 1646 test.Assert(t, ok, "Couldn't load test2 key") 1647 path := newAcctPath 1648 signedURL := fmt.Sprintf("http://localhost%s", path) 1649 1650 payload := `{"contact":["mailto:person@mail.com"],"termsOfServiceAgreed":true}` 1651 _, _, body := signer.embeddedJWK(key, signedURL, payload) 1652 request := makePostRequestWithPath(path, body) 1653 1654 responseWriter := httptest.NewRecorder() 1655 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, request) 1656 1657 responseBody := responseWriter.Body.String() 1658 test.AssertUnmarshaledEquals(t, responseBody, `{ 1659 "key": { 1660 "kty": "RSA", 1661 "n": "qnARLrT7Xz4gRcKyLdydmCr-ey9OuPImX4X40thk3on26FkMznR3fRjs66eLK7mmPcBZ6uOJseURU6wAaZNmemoYx1dMvqvWWIyiQleHSD7Q8vBrhR6uIoO4jAzJZR-ChzZuSDt7iHN-3xUVspu5XGwXU_MVJZshTwp4TaFx5elHIT_ObnTvTOU3Xhish07AbgZKmWsVbXh5s-CrIicU4OexJPgunWZ_YJJueOKmTvnLlTV4MzKR2oZlBKZ27S0-SfdV_QDx_ydle5oMAyKVtlAV35cyPMIsYNwgUGBCdY_2Uzi5eX0lTc7MPRwz6qR1kip-i59VcGcUQgqHV6Fyqw", 1662 "e": "AQAB" 1663 }, 1664 "createdAt": "2021-01-01T00:00:00Z", 1665 "status": "" 1666 }`) 1667 } 1668 1669 func TestContactsToEmails(t *testing.T) { 1670 t.Parallel() 1671 wfe, _, _ := setupWFE(t) 1672 1673 for _, tc := range []struct { 1674 name string 1675 contacts []string 1676 want []string 1677 wantErr string 1678 }{ 1679 { 1680 name: "no contacts", 1681 contacts: []string{}, 1682 want: []string{}, 1683 }, 1684 { 1685 name: "happy path", 1686 contacts: []string{"mailto:one@mail.com", "mailto:two@mail.com"}, 1687 want: []string{"one@mail.com", "two@mail.com"}, 1688 }, 1689 { 1690 name: "empty url", 1691 contacts: []string{""}, 1692 wantErr: "empty contact", 1693 }, 1694 { 1695 name: "too many contacts", 1696 contacts: []string{"mailto:one@mail.com", "mailto:two@mail.com", "mailto:three@mail.com"}, 1697 wantErr: "too many contacts", 1698 }, 1699 { 1700 name: "unknown scheme", 1701 contacts: []string{"ansible:earth.sol.milkyway.laniakea/letsencrypt"}, 1702 wantErr: "contact scheme", 1703 }, 1704 { 1705 name: "malformed email", 1706 contacts: []string{"mailto:admin.com"}, 1707 wantErr: "unable to parse email address", 1708 }, 1709 { 1710 name: "non-ascii email", 1711 contacts: []string{"mailto:señor@email.com"}, 1712 wantErr: "contains non-ASCII characters", 1713 }, 1714 { 1715 name: "unarseable email", 1716 contacts: []string{"mailto:a@mail.com, b@mail.com"}, 1717 wantErr: "unable to parse email address", 1718 }, 1719 { 1720 name: "forbidden example domain", 1721 contacts: []string{"mailto:a@example.org"}, 1722 wantErr: "forbidden", 1723 }, 1724 { 1725 name: "forbidden non-public domain", 1726 contacts: []string{"mailto:admin@localhost"}, 1727 wantErr: "needs at least one dot", 1728 }, 1729 { 1730 name: "forbidden non-iana domain", 1731 contacts: []string{"mailto:admin@non.iana.suffix"}, 1732 wantErr: "does not end with a valid public suffix", 1733 }, 1734 { 1735 name: "forbidden ip domain", 1736 contacts: []string{"mailto:admin@1.2.3.4"}, 1737 wantErr: "value is an IP address", 1738 }, 1739 { 1740 name: "forbidden bracketed ip domain", 1741 contacts: []string{"mailto:admin@[1.2.3.4]"}, 1742 wantErr: "contains an invalid character", 1743 }, 1744 { 1745 name: "query parameter", 1746 contacts: []string{"mailto:admin@a.com?no-reminder-emails"}, 1747 wantErr: "contains a question mark", 1748 }, 1749 { 1750 name: "empty query parameter", 1751 contacts: []string{"mailto:admin@a.com?"}, 1752 wantErr: "contains a question mark", 1753 }, 1754 { 1755 name: "fragment url", 1756 contacts: []string{"mailto:admin@a.com#optional"}, 1757 wantErr: "contains a '#'", 1758 }, 1759 { 1760 name: "empty fragment url", 1761 contacts: []string{"mailto:admin@a.com#"}, 1762 wantErr: "contains a '#'", 1763 }, 1764 } { 1765 t.Run(tc.name, func(t *testing.T) { 1766 t.Parallel() 1767 got, err := wfe.contactsToEmails(tc.contacts) 1768 if tc.wantErr != "" { 1769 if err == nil { 1770 t.Fatalf("contactsToEmails(%#v) = nil, but want %q", tc.contacts, tc.wantErr) 1771 } 1772 if !strings.Contains(err.Error(), tc.wantErr) { 1773 t.Errorf("contactsToEmails(%#v) = %q, but want %q", tc.contacts, err.Error(), tc.wantErr) 1774 } 1775 } else { 1776 if err != nil { 1777 t.Fatalf("contactsToEmails(%#v) = %q, but want %#v", tc.contacts, err.Error(), tc.want) 1778 } 1779 if !slices.Equal(got, tc.want) { 1780 t.Errorf("contactsToEmails(%#v) = %#v, but want %#v", tc.contacts, got, tc.want) 1781 } 1782 } 1783 }) 1784 } 1785 } 1786 1787 func TestGetAuthorizationHandler(t *testing.T) { 1788 wfe, _, signer := setupWFE(t) 1789 1790 // Expired authorizations should be inaccessible 1791 authzURL := "1/3" 1792 responseWriter := httptest.NewRecorder() 1793 wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ 1794 Method: "GET", 1795 URL: mustParseURL(authzURL), 1796 }) 1797 test.AssertEquals(t, responseWriter.Code, http.StatusNotFound) 1798 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), 1799 `{"type":"`+probs.ErrorNS+`malformed","detail":"Expired authorization","status":404}`) 1800 responseWriter.Body.Reset() 1801 1802 // Ensure that a valid authorization can't be reached with an invalid URL 1803 wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ 1804 URL: mustParseURL("1/1d"), 1805 Method: "GET", 1806 }) 1807 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), 1808 `{"type":"`+probs.ErrorNS+`malformed","detail":"Invalid authorization ID","status":400}`) 1809 1810 _, _, jwsBody := signer.byKeyID(1, nil, "http://localhost/1/1", "") 1811 postAsGet := makePostRequestWithPath("1/1", jwsBody) 1812 1813 responseWriter = httptest.NewRecorder() 1814 // Ensure that a POST-as-GET to an authorization works 1815 wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, postAsGet) 1816 test.AssertEquals(t, responseWriter.Code, http.StatusOK) 1817 body := responseWriter.Body.String() 1818 test.AssertUnmarshaledEquals(t, body, ` 1819 { 1820 "identifier": { 1821 "type": "dns", 1822 "value": "not-an-example.com" 1823 }, 1824 "status": "valid", 1825 "expires": "2070-01-01T00:00:00Z", 1826 "challenges": [ 1827 { 1828 "status": "valid", 1829 "type": "http-01", 1830 "token":"token", 1831 "url": "http://localhost/acme/chall/1/1/7TyhFQ" 1832 } 1833 ] 1834 }`) 1835 } 1836 1837 // TestAuthorizationHandler500 tests that internal errors on GetAuthorization result in 1838 // a 500. 1839 func TestAuthorizationHandler500(t *testing.T) { 1840 wfe, _, _ := setupWFE(t) 1841 1842 responseWriter := httptest.NewRecorder() 1843 wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ 1844 Method: "GET", 1845 URL: mustParseURL("1/4"), 1846 }) 1847 expected := `{ 1848 "type": "urn:ietf:params:acme:error:serverInternal", 1849 "detail": "Problem getting authorization", 1850 "status": 500 1851 }` 1852 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), expected) 1853 } 1854 1855 // RAWithFailedChallenges is a fake RA whose GetAuthorization method returns 1856 // an authz with a failed challenge. 1857 type RAWithFailedChallenge struct { 1858 rapb.RegistrationAuthorityClient 1859 clk clock.Clock 1860 } 1861 1862 func (ra *RAWithFailedChallenge) GetAuthorization(ctx context.Context, id *rapb.GetAuthorizationRequest, _ ...grpc.CallOption) (*corepb.Authorization, error) { 1863 return &corepb.Authorization{ 1864 Id: "6", 1865 RegistrationID: 1, 1866 Identifier: identifier.NewDNS("not-an-example.com").ToProto(), 1867 Status: string(core.StatusInvalid), 1868 Expires: timestamppb.New(ra.clk.Now().AddDate(100, 0, 0)), 1869 Challenges: []*corepb.Challenge{ 1870 { 1871 Id: 1, 1872 Type: "http-01", 1873 Status: string(core.StatusInvalid), 1874 Token: "token", 1875 Error: &corepb.ProblemDetails{ 1876 ProblemType: "things:are:whack", 1877 Detail: "whack attack", 1878 HttpStatus: 555, 1879 }, 1880 }, 1881 }, 1882 }, nil 1883 } 1884 1885 // TestAuthorizationChallengeHandlerNamespace tests that the runtime prefixing of 1886 // Challenge Problem Types works as expected 1887 func TestAuthorizationChallengeHandlerNamespace(t *testing.T) { 1888 wfe, clk, _ := setupWFE(t) 1889 wfe.ra = &RAWithFailedChallenge{clk: clk} 1890 1891 responseWriter := httptest.NewRecorder() 1892 wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, &http.Request{ 1893 Method: "GET", 1894 URL: mustParseURL("1/6"), 1895 }) 1896 1897 var authz core.Authorization 1898 err := json.Unmarshal(responseWriter.Body.Bytes(), &authz) 1899 test.AssertNotError(t, err, "Couldn't unmarshal returned authorization object") 1900 test.AssertEquals(t, len(authz.Challenges), 1) 1901 // The Challenge Error Type should have had the probs.ErrorNS prefix added 1902 test.AssertEquals(t, string(authz.Challenges[0].Error.Type), probs.ErrorNS+"things:are:whack") 1903 responseWriter.Body.Reset() 1904 } 1905 1906 func TestAccount(t *testing.T) { 1907 wfe, _, signer := setupWFE(t) 1908 mux := wfe.Handler(metrics.NoopRegisterer) 1909 responseWriter := httptest.NewRecorder() 1910 1911 // Test GET proper entry returns 405 1912 mux.ServeHTTP(responseWriter, &http.Request{ 1913 Method: "GET", 1914 URL: mustParseURL(acctPath), 1915 }) 1916 test.AssertUnmarshaledEquals(t, 1917 responseWriter.Body.String(), 1918 `{"type":"`+probs.ErrorNS+`malformed","detail":"Method not allowed","status":405}`) 1919 responseWriter.Body.Reset() 1920 1921 // Test POST invalid JSON 1922 wfe.Account(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("2", "invalid")) 1923 test.AssertUnmarshaledEquals(t, 1924 responseWriter.Body.String(), 1925 `{"type":"`+probs.ErrorNS+`malformed","detail":"Unable to validate JWS :: Parse error reading JWS","status":400}`) 1926 responseWriter.Body.Reset() 1927 1928 key := loadKey(t, []byte(test2KeyPrivatePEM)) 1929 _, ok := key.(*rsa.PrivateKey) 1930 test.Assert(t, ok, "Couldn't load RSA key") 1931 1932 signedURL := fmt.Sprintf("http://localhost%s%d", acctPath, 102) 1933 path := fmt.Sprintf("%s%d", acctPath, 102) 1934 payload := `{}` 1935 // ID 102 is used by the mock for missing acct 1936 _, _, body := signer.byKeyID(102, nil, signedURL, payload) 1937 request := makePostRequestWithPath(path, body) 1938 1939 // Test POST valid JSON but key is not registered 1940 wfe.Account(ctx, newRequestEvent(), responseWriter, request) 1941 test.AssertUnmarshaledEquals(t, 1942 responseWriter.Body.String(), 1943 `{"type":"`+probs.ErrorNS+`accountDoesNotExist","detail":"Unable to validate JWS :: Account \"http://localhost/acme/acct/102\" not found","status":400}`) 1944 responseWriter.Body.Reset() 1945 1946 key = loadKey(t, []byte(test1KeyPrivatePEM)) 1947 _, ok = key.(*rsa.PrivateKey) 1948 test.Assert(t, ok, "Couldn't load RSA key") 1949 1950 // Test POST valid JSON with account up in the mock 1951 payload = `{}` 1952 path = "1" 1953 signedURL = "http://localhost/1" 1954 _, _, body = signer.byKeyID(1, nil, signedURL, payload) 1955 request = makePostRequestWithPath(path, body) 1956 1957 wfe.Account(ctx, newRequestEvent(), responseWriter, request) 1958 test.AssertNotContains(t, responseWriter.Body.String(), probs.ErrorNS) 1959 links := responseWriter.Header()["Link"] 1960 test.AssertEquals(t, slices.Contains(links, "<"+agreementURL+">;rel=\"terms-of-service\""), true) 1961 responseWriter.Body.Reset() 1962 1963 // Test POST valid JSON with garbage in URL but valid account ID 1964 payload = `{}` 1965 signedURL = "http://localhost/a/bunch/of/garbage/1" 1966 _, _, body = signer.byKeyID(1, nil, signedURL, payload) 1967 request = makePostRequestWithPath("/a/bunch/of/garbage/1", body) 1968 1969 wfe.Account(ctx, newRequestEvent(), responseWriter, request) 1970 test.AssertContains(t, responseWriter.Body.String(), "400") 1971 test.AssertContains(t, responseWriter.Body.String(), probs.ErrorNS+"malformed") 1972 responseWriter.Body.Reset() 1973 1974 // Test valid POST-as-GET request 1975 responseWriter = httptest.NewRecorder() 1976 _, _, body = signer.byKeyID(1, nil, "http://localhost/1", "") 1977 request = makePostRequestWithPath("1", body) 1978 wfe.Account(ctx, newRequestEvent(), responseWriter, request) 1979 // It should not error 1980 test.AssertNotContains(t, responseWriter.Body.String(), probs.ErrorNS) 1981 test.AssertEquals(t, responseWriter.Code, http.StatusOK) 1982 1983 altKey := loadKey(t, []byte(test2KeyPrivatePEM)) 1984 _, ok = altKey.(*rsa.PrivateKey) 1985 test.Assert(t, ok, "Couldn't load altKey RSA key") 1986 1987 // Test POST-as-GET request signed with wrong account key 1988 responseWriter = httptest.NewRecorder() 1989 _, _, body = signer.byKeyID(2, altKey, "http://localhost/1", "") 1990 request = makePostRequestWithPath("1", body) 1991 wfe.Account(ctx, newRequestEvent(), responseWriter, request) 1992 // It should error 1993 test.AssertEquals(t, responseWriter.Code, http.StatusForbidden) 1994 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), `{ 1995 "type": "urn:ietf:params:acme:error:unauthorized", 1996 "detail": "Request signing key did not match account key", 1997 "status": 403 1998 }`) 1999 } 2000 2001 func TestUpdateAccount(t *testing.T) { 2002 t.Parallel() 2003 wfe, _, _ := setupWFE(t) 2004 2005 for _, tc := range []struct { 2006 name string 2007 req string 2008 wantAcct *core.Registration 2009 wantErr string 2010 }{ 2011 { 2012 name: "empty status", 2013 req: `{}`, 2014 wantAcct: &core.Registration{Status: core.StatusValid}, 2015 }, 2016 { 2017 name: "empty status with contact", 2018 req: `{"contact": ["mailto:admin@example.com"]}`, 2019 wantAcct: &core.Registration{Status: core.StatusValid}, 2020 }, 2021 { 2022 name: "valid", 2023 req: `{"status": "valid"}`, 2024 wantAcct: &core.Registration{Status: core.StatusValid}, 2025 }, 2026 { 2027 name: "valid with contact", 2028 req: `{"status": "valid", "contact": ["mailto:admin@example.com"]}`, 2029 wantAcct: &core.Registration{Status: core.StatusValid}, 2030 }, 2031 { 2032 name: "deactivate", 2033 req: `{"status": "deactivated"}`, 2034 wantAcct: &core.Registration{Status: core.StatusDeactivated}, 2035 }, 2036 { 2037 name: "deactivate with contact", 2038 req: `{"status": "deactivated", "contact": ["mailto:admin@example.com"]}`, 2039 wantAcct: &core.Registration{Status: core.StatusDeactivated}, 2040 }, 2041 { 2042 name: "unrecognized status", 2043 req: `{"status": "foo"}`, 2044 wantErr: "invalid status", 2045 }, 2046 { 2047 // We're happy to ignore fields we don't recognize; they might be useful 2048 // for other CAs. 2049 name: "unrecognized request field", 2050 req: `{"foo": "bar"}`, 2051 wantAcct: &core.Registration{Status: core.StatusValid}, 2052 }, 2053 } { 2054 t.Run(tc.name, func(t *testing.T) { 2055 t.Parallel() 2056 2057 currAcct := core.Registration{Status: core.StatusValid} 2058 2059 gotAcct, gotProb := wfe.updateAccount(context.Background(), []byte(tc.req), &currAcct) 2060 if tc.wantAcct != nil { 2061 if gotAcct.Status != tc.wantAcct.Status { 2062 t.Errorf("want status %s, got %s", tc.wantAcct.Status, gotAcct.Status) 2063 } 2064 if !reflect.DeepEqual(gotAcct.Contact, tc.wantAcct.Contact) { 2065 t.Errorf("want contact %v, got %v", tc.wantAcct.Contact, gotAcct.Contact) 2066 } 2067 } 2068 if tc.wantErr != "" { 2069 if gotProb == nil { 2070 t.Fatalf("want error %q, got nil", tc.wantErr) 2071 } 2072 if !strings.Contains(gotProb.Error(), tc.wantErr) { 2073 t.Errorf("want error %q, got %q", tc.wantErr, gotProb.Error()) 2074 } 2075 } 2076 }) 2077 } 2078 } 2079 2080 type mockSAWithCert struct { 2081 sapb.StorageAuthorityReadOnlyClient 2082 cert *x509.Certificate 2083 status core.OCSPStatus 2084 } 2085 2086 func newMockSAWithCert(t *testing.T, sa sapb.StorageAuthorityReadOnlyClient) *mockSAWithCert { 2087 cert, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem") 2088 test.AssertNotError(t, err, "Failed to load test cert") 2089 return &mockSAWithCert{sa, cert, core.OCSPStatusGood} 2090 } 2091 2092 // GetCertificate returns the mock SA's hard-coded certificate, issued by the 2093 // account with regID 1, if the given serial matches. Otherwise, returns not found. 2094 func (sa *mockSAWithCert) GetCertificate(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { 2095 if req.Serial != core.SerialToString(sa.cert.SerialNumber) { 2096 return nil, berrors.NotFoundError("Certificate with serial %q not found", req.Serial) 2097 } 2098 2099 return &corepb.Certificate{ 2100 RegistrationID: 1, 2101 Serial: core.SerialToString(sa.cert.SerialNumber), 2102 Issued: timestamppb.New(sa.cert.NotBefore), 2103 Expires: timestamppb.New(sa.cert.NotAfter), 2104 Der: sa.cert.Raw, 2105 }, nil 2106 } 2107 2108 // GetCertificateStatus returns the mock SA's status, if the given serial matches. 2109 // Otherwise, returns not found. 2110 func (sa *mockSAWithCert) GetCertificateStatus(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.CertificateStatus, error) { 2111 if req.Serial != core.SerialToString(sa.cert.SerialNumber) { 2112 return nil, berrors.NotFoundError("Status for certificate with serial %q not found", req.Serial) 2113 } 2114 2115 return &corepb.CertificateStatus{ 2116 Serial: core.SerialToString(sa.cert.SerialNumber), 2117 Status: string(sa.status), 2118 }, nil 2119 } 2120 2121 type mockSAWithIncident struct { 2122 sapb.StorageAuthorityReadOnlyClient 2123 incidents map[string]*sapb.Incidents 2124 } 2125 2126 // newMockSAWithIncident returns a mock SA with an enabled (ongoing) incident 2127 // for each of the provided serials. 2128 func newMockSAWithIncident(sa sapb.StorageAuthorityReadOnlyClient, serial []string) *mockSAWithIncident { 2129 incidents := make(map[string]*sapb.Incidents) 2130 for _, s := range serial { 2131 incidents[s] = &sapb.Incidents{ 2132 Incidents: []*sapb.Incident{ 2133 { 2134 Id: 0, 2135 SerialTable: "incident_foo", 2136 Url: "http://big.bad/incident", 2137 RenewBy: nil, 2138 Enabled: true, 2139 }, 2140 }, 2141 } 2142 } 2143 return &mockSAWithIncident{sa, incidents} 2144 } 2145 2146 func (sa *mockSAWithIncident) IncidentsForSerial(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*sapb.Incidents, error) { 2147 incidents, ok := sa.incidents[req.Serial] 2148 if ok { 2149 return incidents, nil 2150 } 2151 return &sapb.Incidents{}, nil 2152 } 2153 2154 type mockSAWithSerialMetadata struct { 2155 sapb.StorageAuthorityReadOnlyClient 2156 } 2157 2158 func (sa *mockSAWithSerialMetadata) GetSerialMetadata(ctx context.Context, serial *sapb.Serial, _ ...grpc.CallOption) (*sapb.SerialMetadata, error) { 2159 return &sapb.SerialMetadata{ 2160 Expires: timestamppb.New(time.Date(2025, 7, 29, 0, 0, 0, 0, time.UTC)), 2161 }, nil 2162 } 2163 2164 func TestGetCertInfo(t *testing.T) { 2165 wfe, _, _ := setupWFE(t) 2166 wfe.sa = &mockSAWithSerialMetadata{} 2167 responseWriter := httptest.NewRecorder() 2168 2169 wfe.CertificateInfo(context.Background(), newRequestEvent(), responseWriter, &http.Request{ 2170 Method: "GET", 2171 URL: &url.URL{Path: "aabbccddeeffaabbccddeeff000102030405"}, 2172 }) 2173 2174 if responseWriter.Code != http.StatusOK { 2175 t.Errorf("got HTTP status code %d, want %d", responseWriter.Code, http.StatusOK) 2176 } 2177 expected := `{ 2178 "notAfter": "2025-07-29T00:00:00Z" 2179 }` 2180 if responseWriter.Body.String() != expected { 2181 t.Errorf("got response body %q, want %q", responseWriter.Body.String(), expected) 2182 } 2183 } 2184 2185 func TestGetCertificate(t *testing.T) { 2186 wfe, _, signer := setupWFE(t) 2187 wfe.sa = newMockSAWithCert(t, wfe.sa) 2188 mux := wfe.Handler(metrics.NoopRegisterer) 2189 2190 makeGet := func(path string) *http.Request { 2191 return &http.Request{URL: &url.URL{Path: path}, Method: "GET"} 2192 } 2193 2194 makePost := func(keyID int64, key any, path, body string) *http.Request { 2195 _, _, jwsBody := signer.byKeyID(keyID, key, fmt.Sprintf("http://localhost%s", path), body) 2196 return makePostRequestWithPath(path, jwsBody) 2197 } 2198 2199 altKey := loadKey(t, []byte(test2KeyPrivatePEM)) 2200 _, ok := altKey.(*rsa.PrivateKey) 2201 test.Assert(t, ok, "Couldn't load RSA key") 2202 2203 certPemBytes, _ := os.ReadFile("../test/hierarchy/ee-r3.cert.pem") 2204 cert, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem") 2205 test.AssertNotError(t, err, "failed to load test certificate") 2206 2207 chainPemBytes, err := os.ReadFile("../test/hierarchy/int-r3.cert.pem") 2208 test.AssertNotError(t, err, "Error reading ../test/hierarchy/int-r3.cert.pem") 2209 2210 chainCrossPemBytes, err := os.ReadFile("../test/hierarchy/int-r3-cross.cert.pem") 2211 test.AssertNotError(t, err, "Error reading ../test/hierarchy/int-r3-cross.cert.pem") 2212 2213 reqPath := fmt.Sprintf("/acme/cert/%s", core.SerialToString(cert.SerialNumber)) 2214 pkixContent := "application/pem-certificate-chain" 2215 noCache := "public, max-age=0, no-cache" 2216 notFound := `{"type":"` + probs.ErrorNS + `malformed","detail":"Certificate not found","status":404}` 2217 2218 testCases := []struct { 2219 Name string 2220 Request *http.Request 2221 ExpectedStatus int 2222 ExpectedHeaders map[string]string 2223 ExpectedLink string 2224 ExpectedBody string 2225 ExpectedCert []byte 2226 AnyCert bool 2227 }{ 2228 { 2229 Name: "Valid serial", 2230 Request: makeGet(reqPath), 2231 ExpectedStatus: http.StatusOK, 2232 ExpectedHeaders: map[string]string{ 2233 "Content-Type": pkixContent, 2234 }, 2235 ExpectedCert: append(certPemBytes, append([]byte("\n"), chainPemBytes...)...), 2236 ExpectedLink: fmt.Sprintf(`<http://localhost%s/1>;rel="alternate"`, reqPath), 2237 }, 2238 { 2239 Name: "Valid serial, POST-as-GET", 2240 Request: makePost(1, nil, reqPath, ""), 2241 ExpectedStatus: http.StatusOK, 2242 ExpectedHeaders: map[string]string{ 2243 "Content-Type": pkixContent, 2244 }, 2245 ExpectedCert: append(certPemBytes, append([]byte("\n"), chainPemBytes...)...), 2246 }, 2247 { 2248 Name: "Valid serial, bad POST-as-GET", 2249 Request: makePost(1, nil, reqPath, "{}"), 2250 ExpectedStatus: http.StatusBadRequest, 2251 ExpectedBody: `{ 2252 "type": "urn:ietf:params:acme:error:malformed", 2253 "status": 400, 2254 "detail": "Unable to validate JWS :: POST-as-GET requests must have an empty payload" 2255 }`, 2256 }, 2257 { 2258 Name: "Valid serial, POST-as-GET from wrong account", 2259 Request: makePost(2, altKey, reqPath, ""), 2260 ExpectedStatus: http.StatusForbidden, 2261 ExpectedBody: `{ 2262 "type": "urn:ietf:params:acme:error:unauthorized", 2263 "status": 403, 2264 "detail": "Account in use did not issue specified certificate" 2265 }`, 2266 }, 2267 { 2268 Name: "Unused serial, no cache", 2269 Request: makeGet("/acme/cert/000000000000000000000000000000000001"), 2270 ExpectedStatus: http.StatusNotFound, 2271 ExpectedBody: notFound, 2272 }, 2273 { 2274 Name: "Invalid serial, no cache", 2275 Request: makeGet("/acme/cert/nothex"), 2276 ExpectedStatus: http.StatusNotFound, 2277 ExpectedBody: notFound, 2278 }, 2279 { 2280 Name: "Another invalid serial, no cache", 2281 Request: makeGet("/acme/cert/00000000000000"), 2282 ExpectedStatus: http.StatusNotFound, 2283 ExpectedBody: notFound, 2284 }, 2285 { 2286 Name: "Valid serial (explicit default chain)", 2287 Request: makeGet(reqPath + "/0"), 2288 ExpectedStatus: http.StatusOK, 2289 ExpectedHeaders: map[string]string{ 2290 "Content-Type": pkixContent, 2291 }, 2292 ExpectedLink: fmt.Sprintf(`<http://localhost%s/1>;rel="alternate"`, reqPath), 2293 ExpectedCert: append(certPemBytes, append([]byte("\n"), chainPemBytes...)...), 2294 }, 2295 { 2296 Name: "Valid serial (explicit alternate chain)", 2297 Request: makeGet(reqPath + "/1"), 2298 ExpectedStatus: http.StatusOK, 2299 ExpectedHeaders: map[string]string{ 2300 "Content-Type": pkixContent, 2301 }, 2302 ExpectedLink: fmt.Sprintf(`<http://localhost%s/0>;rel="alternate"`, reqPath), 2303 ExpectedCert: append(certPemBytes, append([]byte("\n"), chainCrossPemBytes...)...), 2304 }, 2305 { 2306 Name: "Valid serial (explicit non-existent alternate chain)", 2307 Request: makeGet(reqPath + "/2"), 2308 ExpectedStatus: http.StatusNotFound, 2309 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unknown issuance chain","status":404}`, 2310 }, 2311 { 2312 Name: "Valid serial (explicit negative alternate chain)", 2313 Request: makeGet(reqPath + "/-1"), 2314 ExpectedStatus: http.StatusBadRequest, 2315 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Chain ID must be a non-negative integer","status":400}`, 2316 }, 2317 } 2318 2319 for _, tc := range testCases { 2320 t.Run(tc.Name, func(t *testing.T) { 2321 responseWriter := httptest.NewRecorder() 2322 mockLog := wfe.log.(*blog.Mock) 2323 mockLog.Clear() 2324 2325 // Mux a request for a certificate 2326 mux.ServeHTTP(responseWriter, tc.Request) 2327 headers := responseWriter.Header() 2328 2329 // Assert that the status code written is as expected 2330 test.AssertEquals(t, responseWriter.Code, tc.ExpectedStatus) 2331 2332 // All of the responses should have the correct cache control header 2333 test.AssertEquals(t, headers.Get("Cache-Control"), noCache) 2334 2335 // If the test cases expects additional headers, check those too 2336 for h, v := range tc.ExpectedHeaders { 2337 test.AssertEquals(t, headers.Get(h), v) 2338 } 2339 2340 if tc.ExpectedLink != "" { 2341 found := false 2342 links := headers["Link"] 2343 if slices.Contains(links, tc.ExpectedLink) { 2344 found = true 2345 } 2346 if !found { 2347 t.Errorf("Expected link '%s', but did not find it in (%v)", 2348 tc.ExpectedLink, links) 2349 } 2350 } 2351 2352 if tc.AnyCert { // Certificate is randomly generated, don't match it 2353 return 2354 } 2355 2356 if len(tc.ExpectedCert) > 0 { 2357 // If the expectation was to return a certificate, check that it was the one expected 2358 bodyBytes := responseWriter.Body.Bytes() 2359 test.Assert(t, bytes.Equal(bodyBytes, tc.ExpectedCert), "Certificates don't match") 2360 2361 // Successful requests should be logged as such 2362 reqlogs := mockLog.GetAllMatching(`INFO: [^ ]+ [^ ]+ [^ ]+ 200 .*`) 2363 if len(reqlogs) != 1 { 2364 t.Errorf("Didn't find info logs with code 200. Instead got:\n%s\n", 2365 strings.Join(mockLog.GetAllMatching(`.*`), "\n")) 2366 } 2367 } else { 2368 // Otherwise if the expectation wasn't a certificate, check that the body matches the expected 2369 body := responseWriter.Body.String() 2370 test.AssertUnmarshaledEquals(t, body, tc.ExpectedBody) 2371 2372 // Unsuccessful requests should be logged as such 2373 reqlogs := mockLog.GetAllMatching(fmt.Sprintf(`INFO: [^ ]+ [^ ]+ [^ ]+ %d .*`, tc.ExpectedStatus)) 2374 if len(reqlogs) != 1 { 2375 t.Errorf("Didn't find info logs with code %d. Instead got:\n%s\n", 2376 tc.ExpectedStatus, strings.Join(mockLog.GetAllMatching(`.*`), "\n")) 2377 } 2378 } 2379 }) 2380 } 2381 } 2382 2383 type mockSAWithNewCert struct { 2384 sapb.StorageAuthorityReadOnlyClient 2385 clk clock.Clock 2386 } 2387 2388 func (sa *mockSAWithNewCert) GetCertificate(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { 2389 issuer, err := core.LoadCert("../test/hierarchy/int-e1.cert.pem") 2390 if err != nil { 2391 return nil, fmt.Errorf("failed to load test issuer cert: %w", err) 2392 } 2393 2394 issuerKeyPem, err := os.ReadFile("../test/hierarchy/int-e1.key.pem") 2395 if err != nil { 2396 return nil, fmt.Errorf("failed to load test issuer key: %w", err) 2397 } 2398 issuerKey := loadKey(&testing.T{}, issuerKeyPem) 2399 2400 newKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 2401 if err != nil { 2402 return nil, fmt.Errorf("failed to create test key: %w", err) 2403 } 2404 2405 sn, err := core.StringToSerial(req.Serial) 2406 if err != nil { 2407 return nil, fmt.Errorf("failed to parse test serial: %w", err) 2408 } 2409 2410 template := &x509.Certificate{ 2411 SerialNumber: sn, 2412 DNSNames: []string{"new.ee.boulder.test"}, 2413 } 2414 2415 certDER, err := x509.CreateCertificate(rand.Reader, template, issuer, &newKey.PublicKey, issuerKey) 2416 if err != nil { 2417 return nil, fmt.Errorf("failed to issue test cert: %w", err) 2418 } 2419 2420 cert, err := x509.ParseCertificate(certDER) 2421 if err != nil { 2422 return nil, fmt.Errorf("failed to parse test cert: %w", err) 2423 } 2424 2425 return &corepb.Certificate{ 2426 RegistrationID: 1, 2427 Serial: core.SerialToString(cert.SerialNumber), 2428 Issued: timestamppb.New(sa.clk.Now().Add(-1 * time.Second)), 2429 Der: cert.Raw, 2430 }, nil 2431 } 2432 2433 // TestGetCertificateNew tests for the case when the certificate is new (by 2434 // dynamically generating it at test time), and therefore isn't served by the 2435 // GET api. 2436 func TestGetCertificateNew(t *testing.T) { 2437 wfe, fc, signer := setupWFE(t) 2438 wfe.sa = &mockSAWithNewCert{wfe.sa, fc} 2439 mux := wfe.Handler(metrics.NoopRegisterer) 2440 2441 makeGet := func(path string) *http.Request { 2442 return &http.Request{URL: &url.URL{Path: path}, Method: "GET"} 2443 } 2444 2445 makePost := func(keyID int64, key any, path, body string) *http.Request { 2446 _, _, jwsBody := signer.byKeyID(keyID, key, fmt.Sprintf("http://localhost%s", path), body) 2447 return makePostRequestWithPath(path, jwsBody) 2448 } 2449 2450 altKey := loadKey(t, []byte(test2KeyPrivatePEM)) 2451 _, ok := altKey.(*rsa.PrivateKey) 2452 test.Assert(t, ok, "Couldn't load RSA key") 2453 2454 pkixContent := "application/pem-certificate-chain" 2455 noCache := "public, max-age=0, no-cache" 2456 2457 testCases := []struct { 2458 Name string 2459 Request *http.Request 2460 ExpectedStatus int 2461 ExpectedHeaders map[string]string 2462 ExpectedBody string 2463 }{ 2464 { 2465 Name: "Get", 2466 Request: makeGet("/get/cert/000000000000000000000000000000000001"), 2467 ExpectedStatus: http.StatusForbidden, 2468 ExpectedBody: `{ 2469 "type": "` + probs.ErrorNS + `unauthorized", 2470 "detail": "Certificate is too new for GET API. You should only use this non-standard API to access resources created more than 10s ago", 2471 "status": 403 2472 }`, 2473 }, 2474 { 2475 Name: "ACME Get", 2476 Request: makeGet("/acme/cert/000000000000000000000000000000000002"), 2477 ExpectedStatus: http.StatusOK, 2478 ExpectedHeaders: map[string]string{ 2479 "Content-Type": pkixContent, 2480 }, 2481 }, 2482 { 2483 Name: "ACME POST-as-GET", 2484 Request: makePost(1, nil, "/acme/cert/000000000000000000000000000000000003", ""), 2485 ExpectedStatus: http.StatusOK, 2486 ExpectedHeaders: map[string]string{ 2487 "Content-Type": pkixContent, 2488 }, 2489 }, 2490 } 2491 2492 for _, tc := range testCases { 2493 t.Run(tc.Name, func(t *testing.T) { 2494 responseWriter := httptest.NewRecorder() 2495 mockLog := wfe.log.(*blog.Mock) 2496 mockLog.Clear() 2497 2498 // Mux a request for a certificate 2499 mux.ServeHTTP(responseWriter, tc.Request) 2500 headers := responseWriter.Header() 2501 2502 // Assert that the status code written is as expected 2503 test.AssertEquals(t, responseWriter.Code, tc.ExpectedStatus) 2504 2505 // All of the responses should have the correct cache control header 2506 test.AssertEquals(t, headers.Get("Cache-Control"), noCache) 2507 2508 // If the test cases expects additional headers, check those too 2509 for h, v := range tc.ExpectedHeaders { 2510 test.AssertEquals(t, headers.Get(h), v) 2511 } 2512 2513 // If we're expecting a particular body (because of an error), check that. 2514 if tc.ExpectedBody != "" { 2515 body := responseWriter.Body.String() 2516 test.AssertUnmarshaledEquals(t, body, tc.ExpectedBody) 2517 2518 // Unsuccessful requests should be logged as such 2519 reqlogs := mockLog.GetAllMatching(fmt.Sprintf(`INFO: [^ ]+ [^ ]+ [^ ]+ %d .*`, tc.ExpectedStatus)) 2520 if len(reqlogs) != 1 { 2521 t.Errorf("Didn't find info logs with code %d. Instead got:\n%s\n", 2522 tc.ExpectedStatus, strings.Join(mockLog.GetAllMatching(`.*`), "\n")) 2523 } 2524 } 2525 }) 2526 } 2527 } 2528 2529 // This uses httptest.NewServer because ServeMux.ServeHTTP won't prevent the 2530 // body from being sent like the net/http Server's actually do. 2531 func TestGetCertificateHEADHasCorrectBodyLength(t *testing.T) { 2532 wfe, _, _ := setupWFE(t) 2533 wfe.sa = newMockSAWithCert(t, wfe.sa) 2534 2535 certPemBytes, _ := os.ReadFile("../test/hierarchy/ee-r3.cert.pem") 2536 cert, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem") 2537 test.AssertNotError(t, err, "failed to load test certificate") 2538 2539 chainPemBytes, err := os.ReadFile("../test/hierarchy/int-r3.cert.pem") 2540 test.AssertNotError(t, err, "Error reading ../test/hierarchy/int-r3.cert.pem") 2541 chain := fmt.Sprintf("%s\n%s", string(certPemBytes), string(chainPemBytes)) 2542 chainLen := strconv.Itoa(len(chain)) 2543 2544 mockLog := wfe.log.(*blog.Mock) 2545 mockLog.Clear() 2546 2547 mux := wfe.Handler(metrics.NoopRegisterer) 2548 s := httptest.NewServer(mux) 2549 defer s.Close() 2550 req, _ := http.NewRequest( 2551 "HEAD", fmt.Sprintf("%s/acme/cert/%s", s.URL, core.SerialToString(cert.SerialNumber)), nil) 2552 resp, err := http.DefaultClient.Do(req) 2553 if err != nil { 2554 test.AssertNotError(t, err, "do error") 2555 } 2556 body, err := io.ReadAll(resp.Body) 2557 if err != nil { 2558 test.AssertNotEquals(t, err, "readall error") 2559 } 2560 err = resp.Body.Close() 2561 if err != nil { 2562 test.AssertNotEquals(t, err, "readall error") 2563 } 2564 test.AssertEquals(t, resp.StatusCode, 200) 2565 test.AssertEquals(t, chainLen, resp.Header.Get("Content-Length")) 2566 test.AssertEquals(t, 0, len(body)) 2567 } 2568 2569 type mockSAWithError struct { 2570 sapb.StorageAuthorityReadOnlyClient 2571 } 2572 2573 func (sa *mockSAWithError) GetCertificate(_ context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { 2574 return nil, errors.New("Oops") 2575 } 2576 2577 func TestGetCertificateServerError(t *testing.T) { 2578 // TODO: add tests for failure to parse the retrieved cert, a cert whose 2579 // IssuerNameID is unknown, and a cert whose signature can't be verified. 2580 wfe, _, _ := setupWFE(t) 2581 wfe.sa = &mockSAWithError{wfe.sa} 2582 mux := wfe.Handler(metrics.NoopRegisterer) 2583 2584 cert, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem") 2585 test.AssertNotError(t, err, "failed to load test certificate") 2586 2587 reqPath := fmt.Sprintf("/acme/cert/%s", core.SerialToString(cert.SerialNumber)) 2588 req := &http.Request{URL: &url.URL{Path: reqPath}, Method: "GET"} 2589 2590 // Mux a request for a certificate 2591 responseWriter := httptest.NewRecorder() 2592 mux.ServeHTTP(responseWriter, req) 2593 2594 test.AssertEquals(t, responseWriter.Code, http.StatusInternalServerError) 2595 2596 noCache := "public, max-age=0, no-cache" 2597 test.AssertEquals(t, responseWriter.Header().Get("Cache-Control"), noCache) 2598 2599 body := `{ 2600 "type": "urn:ietf:params:acme:error:serverInternal", 2601 "status": 500, 2602 "detail": "Failed to retrieve certificate" 2603 }` 2604 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), body) 2605 } 2606 2607 func newRequestEvent() *web.RequestEvent { 2608 return &web.RequestEvent{Extra: make(map[string]any)} 2609 } 2610 2611 func TestHeaderBoulderRequester(t *testing.T) { 2612 wfe, _, signer := setupWFE(t) 2613 mux := wfe.Handler(metrics.NoopRegisterer) 2614 responseWriter := httptest.NewRecorder() 2615 2616 key := loadKey(t, []byte(test1KeyPrivatePEM)) 2617 _, ok := key.(*rsa.PrivateKey) 2618 test.Assert(t, ok, "Failed to load test 1 RSA key") 2619 2620 payload := `{}` 2621 path := fmt.Sprintf("%s%d", acctPath, 1) 2622 signedURL := fmt.Sprintf("http://localhost%s", path) 2623 _, _, body := signer.byKeyID(1, nil, signedURL, payload) 2624 request := makePostRequestWithPath(path, body) 2625 2626 mux.ServeHTTP(responseWriter, request) 2627 test.AssertEquals(t, responseWriter.Header().Get("Boulder-Requester"), "1") 2628 2629 // requests that do call sendError() also should have the requester header 2630 payload = `{"agreement":"https://letsencrypt.org/im-bad"}` 2631 _, _, body = signer.byKeyID(1, nil, signedURL, payload) 2632 request = makePostRequestWithPath(path, body) 2633 mux.ServeHTTP(responseWriter, request) 2634 test.AssertEquals(t, responseWriter.Header().Get("Boulder-Requester"), "1") 2635 } 2636 2637 func TestDeactivateAuthorizationHandler(t *testing.T) { 2638 wfe, _, signer := setupWFE(t) 2639 responseWriter := httptest.NewRecorder() 2640 2641 responseWriter.Body.Reset() 2642 2643 payload := `{"status":""}` 2644 _, _, body := signer.byKeyID(1, nil, "http://localhost/1/1", payload) 2645 request := makePostRequestWithPath("1/1", body) 2646 2647 wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, request) 2648 test.AssertUnmarshaledEquals(t, 2649 responseWriter.Body.String(), 2650 `{"type": "`+probs.ErrorNS+`malformed","detail": "Invalid status value","status": 400}`) 2651 2652 responseWriter.Body.Reset() 2653 payload = `{"status":"deactivated"}` 2654 _, _, body = signer.byKeyID(1, nil, "http://localhost/1/1", payload) 2655 request = makePostRequestWithPath("1/1", body) 2656 2657 wfe.AuthorizationHandler(ctx, newRequestEvent(), responseWriter, request) 2658 test.AssertUnmarshaledEquals(t, 2659 responseWriter.Body.String(), 2660 `{ 2661 "identifier": { 2662 "type": "dns", 2663 "value": "not-an-example.com" 2664 }, 2665 "status": "deactivated", 2666 "expires": "2070-01-01T00:00:00Z", 2667 "challenges": [ 2668 { 2669 "status": "valid", 2670 "type": "http-01", 2671 "token": "token", 2672 "url": "http://localhost/acme/chall/1/1/7TyhFQ" 2673 } 2674 ] 2675 }`) 2676 } 2677 2678 func TestDeactivateAccount(t *testing.T) { 2679 responseWriter := httptest.NewRecorder() 2680 wfe, _, signer := setupWFE(t) 2681 2682 responseWriter.Body.Reset() 2683 payload := `{"status":"asd"}` 2684 signedURL := "http://localhost/1" 2685 path := "1" 2686 _, _, body := signer.byKeyID(1, nil, signedURL, payload) 2687 request := makePostRequestWithPath(path, body) 2688 2689 wfe.Account(ctx, newRequestEvent(), responseWriter, request) 2690 test.AssertUnmarshaledEquals(t, 2691 responseWriter.Body.String(), 2692 `{"type": "`+probs.ErrorNS+`malformed","detail": "Unable to update account :: invalid status \"asd\" for account update request, must be \"valid\" or \"deactivated\"","status": 400}`) 2693 2694 responseWriter.Body.Reset() 2695 payload = `{"status":"deactivated"}` 2696 _, _, body = signer.byKeyID(1, nil, signedURL, payload) 2697 request = makePostRequestWithPath(path, body) 2698 2699 wfe.Account(ctx, newRequestEvent(), responseWriter, request) 2700 test.AssertUnmarshaledEquals(t, 2701 responseWriter.Body.String(), 2702 `{ 2703 "key": { 2704 "kty": "RSA", 2705 "n": "yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ", 2706 "e": "AQAB" 2707 }, 2708 "status": "deactivated" 2709 }`) 2710 2711 responseWriter.Body.Reset() 2712 payload = `{"status":"deactivated", "contact":[]}` 2713 _, _, body = signer.byKeyID(1, nil, signedURL, payload) 2714 request = makePostRequestWithPath(path, body) 2715 wfe.Account(ctx, newRequestEvent(), responseWriter, request) 2716 test.AssertUnmarshaledEquals(t, 2717 responseWriter.Body.String(), 2718 `{ 2719 "key": { 2720 "kty": "RSA", 2721 "n": "yNWVhtYEKJR21y9xsHV-PD_bYwbXSeNuFal46xYxVfRL5mqha7vttvjB_vc7Xg2RvgCxHPCqoxgMPTzHrZT75LjCwIW2K_klBYN8oYvTwwmeSkAz6ut7ZxPv-nZaT5TJhGk0NT2kh_zSpdriEJ_3vW-mqxYbbBmpvHqsa1_zx9fSuHYctAZJWzxzUZXykbWMWQZpEiE0J4ajj51fInEzVn7VxV-mzfMyboQjujPh7aNJxAWSq4oQEJJDgWwSh9leyoJoPpONHxh5nEE5AjE01FkGICSxjpZsF-w8hOTI3XXohUdu29Se26k2B0PolDSuj0GIQU6-W9TdLXSjBb2SpQ", 2722 "e": "AQAB" 2723 }, 2724 "status": "deactivated" 2725 }`) 2726 2727 responseWriter.Body.Reset() 2728 key := loadKey(t, []byte(test3KeyPrivatePEM)) 2729 _, ok := key.(*rsa.PrivateKey) 2730 test.Assert(t, ok, "Couldn't load test3 RSA key") 2731 2732 payload = `{"status":"deactivated"}` 2733 path = "3" 2734 signedURL = "http://localhost/3" 2735 _, _, body = signer.byKeyID(3, key, signedURL, payload) 2736 request = makePostRequestWithPath(path, body) 2737 2738 wfe.Account(ctx, newRequestEvent(), responseWriter, request) 2739 2740 test.AssertUnmarshaledEquals(t, 2741 responseWriter.Body.String(), 2742 `{ 2743 "type": "`+probs.ErrorNS+`unauthorized", 2744 "detail": "Unable to validate JWS :: Account is not valid, has status \"deactivated\"", 2745 "status": 403 2746 }`) 2747 } 2748 2749 func TestNewOrder(t *testing.T) { 2750 wfe, _, signer := setupWFE(t) 2751 responseWriter := httptest.NewRecorder() 2752 2753 targetHost := "localhost" 2754 targetPath := "new-order" 2755 signedURL := fmt.Sprintf("http://%s/%s", targetHost, targetPath) 2756 2757 invalidIdentifierBody := ` 2758 { 2759 "Identifiers": [ 2760 {"type": "dns", "value": "not-example.com"}, 2761 {"type": "dns", "value": "www.not-example.com"}, 2762 {"type": "fakeID", "value": "www.i-am-21.com"} 2763 ] 2764 } 2765 ` 2766 2767 validOrderBody := ` 2768 { 2769 "Identifiers": [ 2770 {"type": "dns", "value": "not-example.com"}, 2771 {"type": "dns", "value": "www.not-example.com"}, 2772 {"type": "ip", "value": "9.9.9.9"} 2773 ] 2774 }` 2775 2776 validOrderBodyWithMixedCaseIdentifiers := ` 2777 { 2778 "Identifiers": [ 2779 {"type": "dns", "value": "Not-Example.com"}, 2780 {"type": "dns", "value": "WWW.Not-example.com"}, 2781 {"type": "ip", "value": "9.9.9.9"} 2782 ] 2783 }` 2784 2785 // Body with a SAN that is longer than 64 bytes. This one is 65 bytes. 2786 tooLongCNBody := ` 2787 { 2788 "Identifiers": [ 2789 { 2790 "type": "dns", 2791 "value": "thisreallylongexampledomainisabytelongerthanthemaxcnbytelimit.com" 2792 } 2793 ] 2794 }` 2795 2796 oneLongOneShortCNBody := ` 2797 { 2798 "Identifiers": [ 2799 { 2800 "type": "dns", 2801 "value": "thisreallylongexampledomainisabytelongerthanthemaxcnbytelimit.com" 2802 }, 2803 { 2804 "type": "dns", 2805 "value": "not-example.com" 2806 } 2807 ] 2808 }` 2809 2810 testCases := []struct { 2811 Name string 2812 Request *http.Request 2813 ExpectedBody string 2814 ExpectedHeaders map[string]string 2815 }{ 2816 { 2817 Name: "POST, but no body", 2818 Request: &http.Request{ 2819 Method: "POST", 2820 Header: map[string][]string{ 2821 "Content-Length": {"0"}, 2822 "Content-Type": {expectedJWSContentType}, 2823 }, 2824 }, 2825 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: No body on POST","status":400}`, 2826 }, 2827 { 2828 Name: "POST, with an invalid JWS body", 2829 Request: makePostRequestWithPath("hi", "hi"), 2830 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Parse error reading JWS","status":400}`, 2831 }, 2832 { 2833 Name: "POST, properly signed JWS, payload isn't valid", 2834 Request: signAndPost(signer, targetPath, signedURL, "foo"), 2835 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Request payload did not parse as JSON","status":400}`, 2836 }, 2837 { 2838 Name: "POST, empty DNS identifier", 2839 Request: signAndPost(signer, targetPath, signedURL, `{"identifiers":[{"type":"dns","value":""}]}`), 2840 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"NewOrder request included empty identifier","status":400}`, 2841 }, 2842 { 2843 Name: "POST, empty IP identifier", 2844 Request: signAndPost(signer, targetPath, signedURL, `{"identifiers":[{"type":"ip","value":""}]}`), 2845 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"NewOrder request included empty identifier","status":400}`, 2846 }, 2847 { 2848 Name: "POST, invalid DNS identifier", 2849 Request: signAndPost(signer, targetPath, signedURL, `{"identifiers":[{"type":"dns","value":"example.invalid"}]}`), 2850 ExpectedBody: `{"type":"` + probs.ErrorNS + `rejectedIdentifier","detail":"Invalid identifiers requested :: Cannot issue for \"example.invalid\": Domain name does not end with a valid public suffix (TLD)","status":400}`, 2851 }, 2852 { 2853 Name: "POST, invalid IP identifier", 2854 Request: signAndPost(signer, targetPath, signedURL, `{"identifiers":[{"type":"ip","value":"127.0.0.0.0.0.0.1"}]}`), 2855 ExpectedBody: `{"type":"` + probs.ErrorNS + `rejectedIdentifier","detail":"Invalid identifiers requested :: Cannot issue for \"127.0.0.0.0.0.0.1\": IP address is invalid","status":400}`, 2856 }, 2857 { 2858 Name: "POST, no identifiers in payload", 2859 Request: signAndPost(signer, targetPath, signedURL, "{}"), 2860 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"NewOrder request did not specify any identifiers","status":400}`, 2861 }, 2862 { 2863 Name: "POST, invalid identifier type in payload", 2864 Request: signAndPost(signer, targetPath, signedURL, invalidIdentifierBody), 2865 ExpectedBody: `{"type":"` + probs.ErrorNS + `unsupportedIdentifier","detail":"NewOrder request included unsupported identifier: type \"fakeID\", value \"www.i-am-21.com\"","status":400}`, 2866 }, 2867 { 2868 Name: "POST, notAfter and notBefore in payload", 2869 Request: signAndPost(signer, targetPath, signedURL, `{"identifiers":[{"type": "dns", "value": "not-example.com"}], "notBefore":"now", "notAfter": "later"}`), 2870 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"NotBefore and NotAfter are not supported","status":400}`, 2871 }, 2872 { 2873 Name: "POST, good payload, all names too long to fit in CN", 2874 Request: signAndPost(signer, targetPath, signedURL, tooLongCNBody), 2875 ExpectedBody: ` 2876 { 2877 "status": "pending", 2878 "expires": "2021-02-01T01:01:01Z", 2879 "identifiers": [ 2880 { "type": "dns", "value": "thisreallylongexampledomainisabytelongerthanthemaxcnbytelimit.com"} 2881 ], 2882 "authorizations": [ 2883 "http://localhost/acme/authz/1/1" 2884 ], 2885 "finalize": "http://localhost/acme/finalize/1/1" 2886 }`, 2887 }, 2888 { 2889 Name: "POST, good payload, one potential CNs less than 64 bytes and one longer", 2890 Request: signAndPost(signer, targetPath, signedURL, oneLongOneShortCNBody), 2891 ExpectedBody: ` 2892 { 2893 "status": "pending", 2894 "expires": "2021-02-01T01:01:01Z", 2895 "identifiers": [ 2896 { "type": "dns", "value": "not-example.com"}, 2897 { "type": "dns", "value": "thisreallylongexampledomainisabytelongerthanthemaxcnbytelimit.com"} 2898 ], 2899 "authorizations": [ 2900 "http://localhost/acme/authz/1/1" 2901 ], 2902 "finalize": "http://localhost/acme/finalize/1/1" 2903 }`, 2904 }, 2905 { 2906 Name: "POST, good payload", 2907 Request: signAndPost(signer, targetPath, signedURL, validOrderBody), 2908 ExpectedBody: ` 2909 { 2910 "status": "pending", 2911 "expires": "2021-02-01T01:01:01Z", 2912 "identifiers": [ 2913 { "type": "dns", "value": "not-example.com"}, 2914 { "type": "dns", "value": "www.not-example.com"}, 2915 { "type": "ip", "value": "9.9.9.9"} 2916 ], 2917 "authorizations": [ 2918 "http://localhost/acme/authz/1/1" 2919 ], 2920 "finalize": "http://localhost/acme/finalize/1/1" 2921 }`, 2922 }, 2923 { 2924 Name: "POST, good payload, but when the input had mixed case", 2925 Request: signAndPost(signer, targetPath, signedURL, validOrderBodyWithMixedCaseIdentifiers), 2926 ExpectedBody: ` 2927 { 2928 "status": "pending", 2929 "expires": "2021-02-01T01:01:01Z", 2930 "identifiers": [ 2931 { "type": "dns", "value": "not-example.com"}, 2932 { "type": "dns", "value": "www.not-example.com"}, 2933 { "type": "ip", "value": "9.9.9.9"} 2934 ], 2935 "authorizations": [ 2936 "http://localhost/acme/authz/1/1" 2937 ], 2938 "finalize": "http://localhost/acme/finalize/1/1" 2939 }`, 2940 }, 2941 } 2942 2943 for _, tc := range testCases { 2944 t.Run(tc.Name, func(t *testing.T) { 2945 responseWriter.Body.Reset() 2946 2947 wfe.NewOrder(ctx, newRequestEvent(), responseWriter, tc.Request) 2948 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.ExpectedBody) 2949 2950 headers := responseWriter.Header() 2951 for k, v := range tc.ExpectedHeaders { 2952 test.AssertEquals(t, headers.Get(k), v) 2953 } 2954 }) 2955 } 2956 2957 // Test that we log the "Created" field. 2958 responseWriter.Body.Reset() 2959 request := signAndPost(signer, targetPath, signedURL, validOrderBody) 2960 requestEvent := newRequestEvent() 2961 wfe.NewOrder(ctx, requestEvent, responseWriter, request) 2962 2963 if requestEvent.Created != "1" { 2964 t.Errorf("Expected to log Created field when creating Order: %#v", requestEvent) 2965 } 2966 } 2967 2968 func TestFinalizeOrder(t *testing.T) { 2969 wfe, _, signer := setupWFE(t) 2970 responseWriter := httptest.NewRecorder() 2971 2972 targetHost := "localhost" 2973 targetPath := "1/1" 2974 signedURL := fmt.Sprintf("http://%s/%s", targetHost, targetPath) 2975 2976 // This example is a well-formed CSR for the name "example.com". 2977 goodCertCSRPayload := `{ 2978 "csr": "MIHRMHgCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ2hlvArQl5k0L1eF1vF5dwr7ASm2iKqibmauund-z3QJpuudnNEjlyOXi-IY1rxyhehRrtbm_bbcNCtZLgbkPvoAAwCgYIKoZIzj0EAwIDSQAwRgIhAJ8z2EDll2BvoNRotAknEfrqeP6K5CN1NeVMB4QOu0G1AiEAqAVpiGwNyV7SEZ67vV5vyuGsKPAGnqrisZh5Vg5JKHE=" 2979 }` 2980 2981 egUrl := mustParseURL("1/1") 2982 2983 testCases := []struct { 2984 Name string 2985 Request *http.Request 2986 ExpectedHeaders map[string]string 2987 ExpectedBody string 2988 }{ 2989 { 2990 Name: "POST, but no body", 2991 Request: &http.Request{ 2992 URL: egUrl, 2993 RequestURI: targetPath, 2994 Method: "POST", 2995 Header: map[string][]string{ 2996 "Content-Length": {"0"}, 2997 "Content-Type": {expectedJWSContentType}, 2998 }, 2999 }, 3000 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: No body on POST","status":400}`, 3001 }, 3002 { 3003 Name: "POST, with an invalid JWS body", 3004 Request: makePostRequestWithPath(targetPath, "hi"), 3005 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Parse error reading JWS","status":400}`, 3006 }, 3007 { 3008 Name: "POST, properly signed JWS, payload isn't valid", 3009 Request: signAndPost(signer, targetPath, signedURL, "foo"), 3010 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: Request payload did not parse as JSON","status":400}`, 3011 }, 3012 { 3013 Name: "Invalid path", 3014 Request: signAndPost(signer, "1", "http://localhost/1", "{}"), 3015 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Invalid request path","status":404}`, 3016 }, 3017 { 3018 Name: "Bad acct ID in path", 3019 Request: signAndPost(signer, "a/1", "http://localhost/a/1", "{}"), 3020 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Invalid account ID","status":400}`, 3021 }, 3022 { 3023 Name: "Mismatched acct ID in path/JWS", 3024 // Note(@cpu): We use "http://localhost/2/1" here not 3025 // "http://localhost/order/2/1" because we are calling the Order 3026 // handler directly and it normally has the initial path component 3027 // stripped by the global WFE2 handler. We need the JWS URL to match the request 3028 // URL so we fudge both such that the finalize-order prefix has been removed. 3029 Request: signAndPost(signer, "2/1", "http://localhost/2/1", "{}"), 3030 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Mismatched account ID","status":400}`, 3031 }, 3032 { 3033 Name: "Order ID is invalid", 3034 Request: signAndPost(signer, "1/okwhatever/finalize-order", "http://localhost/1/okwhatever/finalize-order", "{}"), 3035 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Invalid order ID","status":400}`, 3036 }, 3037 { 3038 Name: "Order doesn't exist", 3039 // mocks/mocks.go's StorageAuthority's GetOrder mock treats ID 2 as missing 3040 Request: signAndPost(signer, "1/2", "http://localhost/1/2", "{}"), 3041 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"No order for ID 2","status":404}`, 3042 }, 3043 { 3044 Name: "Order is already finalized", 3045 // mocks/mocks.go's StorageAuthority's GetOrder mock treats ID 1 as an Order with a Serial 3046 Request: signAndPost(signer, "1/1", "http://localhost/1/1", goodCertCSRPayload), 3047 ExpectedBody: `{"type":"` + probs.ErrorNS + `orderNotReady","detail":"Order's status (\"valid\") is not acceptable for finalization","status":403}`, 3048 }, 3049 { 3050 Name: "Order is expired", 3051 // mocks/mocks.go's StorageAuthority's GetOrder mock treats ID 7 as an Order that has already expired 3052 Request: signAndPost(signer, "1/7", "http://localhost/1/7", goodCertCSRPayload), 3053 ExpectedBody: `{"type":"` + probs.ErrorNS + `malformed","detail":"Order 7 is expired","status":404}`, 3054 }, 3055 { 3056 Name: "Good CSR, Pending Order", 3057 Request: signAndPost(signer, "1/4", "http://localhost/1/4", goodCertCSRPayload), 3058 ExpectedBody: `{"type":"` + probs.ErrorNS + `orderNotReady","detail":"Order's status (\"pending\") is not acceptable for finalization","status":403}`, 3059 }, 3060 { 3061 Name: "Good CSR, Ready Order", 3062 Request: signAndPost(signer, "1/8", "http://localhost/1/8", goodCertCSRPayload), 3063 ExpectedHeaders: map[string]string{ 3064 "Location": "http://localhost/acme/order/1/8", 3065 "Retry-After": "3", 3066 }, 3067 ExpectedBody: ` 3068 { 3069 "status": "processing", 3070 "expires": "2000-01-01T00:00:00Z", 3071 "identifiers": [ 3072 {"type":"dns","value":"example.com"} 3073 ], 3074 "profile": "default", 3075 "authorizations": [ 3076 "http://localhost/acme/authz/1/1" 3077 ], 3078 "finalize": "http://localhost/acme/finalize/1/8" 3079 }`, 3080 }, 3081 } 3082 3083 for _, tc := range testCases { 3084 t.Run(tc.Name, func(t *testing.T) { 3085 responseWriter.Body.Reset() 3086 wfe.FinalizeOrder(ctx, newRequestEvent(), responseWriter, tc.Request) 3087 for k, v := range tc.ExpectedHeaders { 3088 got := responseWriter.Header().Get(k) 3089 if v != got { 3090 t.Errorf("Header %q: Expected %q, got %q", k, v, got) 3091 } 3092 } 3093 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.ExpectedBody) 3094 }) 3095 } 3096 3097 // Check a bad CSR request separately from the above testcases. We don't want 3098 // to match the whole response body because the "detail" of a bad CSR problem 3099 // contains a verbose Go error message that can change between versions (e.g. 3100 // Go 1.10.4 to 1.11 changed the expected format) 3101 badCSRReq := signAndPost(signer, "1/8", "http://localhost/1/8", `{"CSR": "ABCD"}`) 3102 responseWriter.Body.Reset() 3103 wfe.FinalizeOrder(ctx, newRequestEvent(), responseWriter, badCSRReq) 3104 responseBody := responseWriter.Body.String() 3105 test.AssertContains(t, responseBody, "Error parsing certificate request") 3106 } 3107 3108 func TestKeyRollover(t *testing.T) { 3109 responseWriter := httptest.NewRecorder() 3110 wfe, _, signer := setupWFE(t) 3111 3112 existingKey, err := rsa.GenerateKey(rand.Reader, 2048) 3113 test.AssertNotError(t, err, "Error creating random 2048 RSA key") 3114 3115 newKeyBytes, err := os.ReadFile("../test/test-key-5.der") 3116 test.AssertNotError(t, err, "Failed to read ../test/test-key-5.der") 3117 newKeyPriv, err := x509.ParsePKCS1PrivateKey(newKeyBytes) 3118 test.AssertNotError(t, err, "Failed parsing private key") 3119 newJWKJSON, err := jose.JSONWebKey{Key: newKeyPriv.Public()}.MarshalJSON() 3120 test.AssertNotError(t, err, "Failed to marshal JWK JSON") 3121 3122 wfe.KeyRollover(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("", "{}")) 3123 test.AssertUnmarshaledEquals(t, 3124 responseWriter.Body.String(), 3125 `{ 3126 "type": "`+probs.ErrorNS+`malformed", 3127 "detail": "Unable to validate JWS :: Parse error reading JWS", 3128 "status": 400 3129 }`) 3130 3131 testCases := []struct { 3132 Name string 3133 Payload string 3134 ExpectedResponse string 3135 NewKey crypto.Signer 3136 ErrorStatType string 3137 }{ 3138 { 3139 Name: "Missing account URL", 3140 Payload: `{"oldKey":` + test1KeyPublicJSON + `}`, 3141 ExpectedResponse: `{ 3142 "type": "` + probs.ErrorNS + `malformed", 3143 "detail": "Inner key rollover request specified Account \"\", but outer JWS has Key ID \"http://localhost/acme/acct/1\"", 3144 "status": 400 3145 }`, 3146 NewKey: newKeyPriv, 3147 ErrorStatType: "KeyRolloverMismatchedAccount", 3148 }, 3149 { 3150 Name: "incorrect old key", 3151 Payload: `{"oldKey":` + string(newJWKJSON) + `,"account":"http://localhost/acme/acct/1"}`, 3152 ExpectedResponse: `{ 3153 "type": "` + probs.ErrorNS + `malformed", 3154 "detail": "Unable to validate JWS :: Inner JWS does not contain old key field matching current account key", 3155 "status": 400 3156 }`, 3157 NewKey: newKeyPriv, 3158 ErrorStatType: "KeyRolloverWrongOldKey", 3159 }, 3160 { 3161 Name: "Valid key rollover request, key exists", 3162 Payload: `{"oldKey":` + test1KeyPublicJSON + `,"account":"http://localhost/acme/acct/1"}`, 3163 ExpectedResponse: `{ 3164 "type": "urn:ietf:params:acme:error:conflict", 3165 "detail": "New key is already in use for a different account", 3166 "status": 409 3167 }`, 3168 NewKey: existingKey, 3169 }, 3170 { 3171 Name: "Valid key rollover request", 3172 Payload: `{"oldKey":` + test1KeyPublicJSON + `,"account":"http://localhost/acme/acct/1"}`, 3173 ExpectedResponse: `{ 3174 "key": ` + string(newJWKJSON) + `, 3175 "status": "valid" 3176 }`, 3177 NewKey: newKeyPriv, 3178 }, 3179 } 3180 3181 for _, tc := range testCases { 3182 t.Run(tc.Name, func(t *testing.T) { 3183 wfe.stats.joseErrorCount.Reset() 3184 responseWriter.Body.Reset() 3185 _, _, inner := signer.embeddedJWK(tc.NewKey, "http://localhost/key-change", tc.Payload) 3186 _, _, outer := signer.byKeyID(1, nil, "http://localhost/key-change", inner) 3187 wfe.KeyRollover(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("key-change", outer)) 3188 t.Log(responseWriter.Body.String()) 3189 t.Log(tc.ExpectedResponse) 3190 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.ExpectedResponse) 3191 if tc.ErrorStatType != "" { 3192 test.AssertMetricWithLabelsEquals( 3193 t, wfe.stats.joseErrorCount, prometheus.Labels{"type": tc.ErrorStatType}, 1) 3194 } 3195 }) 3196 } 3197 } 3198 3199 func TestKeyRolloverMismatchedJWSURLs(t *testing.T) { 3200 responseWriter := httptest.NewRecorder() 3201 wfe, _, signer := setupWFE(t) 3202 3203 newKeyBytes, err := os.ReadFile("../test/test-key-5.der") 3204 test.AssertNotError(t, err, "Failed to read ../test/test-key-5.der") 3205 newKeyPriv, err := x509.ParsePKCS1PrivateKey(newKeyBytes) 3206 test.AssertNotError(t, err, "Failed parsing private key") 3207 3208 _, _, inner := signer.embeddedJWK(newKeyPriv, "http://localhost/wrong-url", "{}") 3209 _, _, outer := signer.byKeyID(1, nil, "http://localhost/key-change", inner) 3210 wfe.KeyRollover(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("key-change", outer)) 3211 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), ` 3212 { 3213 "type": "urn:ietf:params:acme:error:malformed", 3214 "detail": "Unable to validate JWS :: Outer JWS 'url' value \"http://localhost/key-change\" does not match inner JWS 'url' value \"http://localhost/wrong-url\"", 3215 "status": 400 3216 }`) 3217 } 3218 3219 func TestGetOrder(t *testing.T) { 3220 wfe, _, signer := setupWFE(t) 3221 3222 makeGet := func(path string) *http.Request { 3223 return &http.Request{URL: &url.URL{Path: path}, Method: "GET"} 3224 } 3225 3226 makePost := func(keyID int64, path, body string) *http.Request { 3227 _, _, jwsBody := signer.byKeyID(keyID, nil, fmt.Sprintf("http://localhost/%s", path), body) 3228 return makePostRequestWithPath(path, jwsBody) 3229 } 3230 3231 testCases := []struct { 3232 Name string 3233 Request *http.Request 3234 Response string 3235 Headers map[string]string 3236 }{ 3237 { 3238 Name: "Good request", 3239 Request: makeGet("1/1"), 3240 Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/1","certificate":"http://localhost/acme/cert/serial"}`, 3241 }, 3242 { 3243 Name: "404 request", 3244 Request: makeGet("1/2"), 3245 Response: `{"type":"` + probs.ErrorNS + `malformed","detail":"No order for ID 2", "status":404}`, 3246 }, 3247 { 3248 Name: "Invalid request path", 3249 Request: makeGet("asd"), 3250 Response: `{"type":"` + probs.ErrorNS + `malformed","detail":"Invalid request path","status":404}`, 3251 }, 3252 { 3253 Name: "Invalid account ID", 3254 Request: makeGet("asd/asd"), 3255 Response: `{"type":"` + probs.ErrorNS + `malformed","detail":"Invalid account ID","status":400}`, 3256 }, 3257 { 3258 Name: "Invalid order ID", 3259 Request: makeGet("1/asd"), 3260 Response: `{"type":"` + probs.ErrorNS + `malformed","detail":"Invalid order ID","status":400}`, 3261 }, 3262 { 3263 Name: "Real request, wrong account", 3264 Request: makeGet("2/1"), 3265 Response: `{"type":"` + probs.ErrorNS + `malformed","detail":"No order found for account ID 2", "status":404}`, 3266 }, 3267 { 3268 Name: "Internal error request", 3269 Request: makeGet("1/3"), 3270 Response: `{"type":"` + probs.ErrorNS + `serverInternal","detail":"Failed to retrieve order for ID 3","status":500}`, 3271 }, 3272 { 3273 Name: "Invalid POST-as-GET", 3274 Request: makePost(1, "1/1", "{}"), 3275 Response: `{"type":"` + probs.ErrorNS + `malformed","detail":"Unable to validate JWS :: POST-as-GET requests must have an empty payload", "status":400}`, 3276 }, 3277 { 3278 Name: "Valid POST-as-GET, wrong account", 3279 Request: makePost(1, "2/1", ""), 3280 Response: `{"type":"` + probs.ErrorNS + `malformed","detail":"No order found for account ID 2", "status":404}`, 3281 }, 3282 { 3283 Name: "Valid POST-as-GET", 3284 Request: makePost(1, "1/1", ""), 3285 Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/1","certificate":"http://localhost/acme/cert/serial"}`, 3286 }, 3287 { 3288 Name: "GET new order from old endpoint", 3289 Request: makeGet("1/9"), 3290 Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/9","certificate":"http://localhost/acme/cert/serial"}`, 3291 }, 3292 { 3293 Name: "POST-as-GET new order", 3294 Request: makePost(1, "1/9", ""), 3295 Response: `{"status": "valid","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/9","certificate":"http://localhost/acme/cert/serial"}`, 3296 }, 3297 { 3298 Name: "POST-as-GET processing order", 3299 Request: makePost(1, "1/10", ""), 3300 Response: `{"status": "processing","expires": "2000-01-01T00:00:00Z","identifiers":[{"type":"dns", "value":"example.com"}], "profile": "default", "authorizations":["http://localhost/acme/authz/1/1"],"finalize":"http://localhost/acme/finalize/1/10"}`, 3301 Headers: map[string]string{"Retry-After": "3"}, 3302 }, 3303 } 3304 3305 for _, tc := range testCases { 3306 t.Run(tc.Name, func(t *testing.T) { 3307 responseWriter := httptest.NewRecorder() 3308 wfe.GetOrder(ctx, newRequestEvent(), responseWriter, tc.Request) 3309 t.Log(tc.Name) 3310 t.Log("actual:", responseWriter.Body.String()) 3311 t.Log("expect:", tc.Response) 3312 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.Response) 3313 for k, v := range tc.Headers { 3314 test.AssertEquals(t, responseWriter.Header().Get(k), v) 3315 } 3316 }) 3317 } 3318 } 3319 3320 func makeRevokeRequestJSON(reason *revocation.Reason) ([]byte, error) { 3321 certPemBytes, err := os.ReadFile("../test/hierarchy/ee-r3.cert.pem") 3322 if err != nil { 3323 return nil, err 3324 } 3325 certBlock, _ := pem.Decode(certPemBytes) 3326 return makeRevokeRequestJSONForCert(certBlock.Bytes, reason) 3327 } 3328 3329 func makeRevokeRequestJSONForCert(der []byte, reason *revocation.Reason) ([]byte, error) { 3330 revokeRequest := struct { 3331 CertificateDER core.JSONBuffer `json:"certificate"` 3332 Reason *revocation.Reason `json:"reason"` 3333 }{ 3334 CertificateDER: der, 3335 Reason: reason, 3336 } 3337 revokeRequestJSON, err := json.Marshal(revokeRequest) 3338 if err != nil { 3339 return nil, err 3340 } 3341 return revokeRequestJSON, nil 3342 } 3343 3344 // Valid revocation request for existing, non-revoked cert, signed using the 3345 // issuing account key. 3346 func TestRevokeCertificateByApplicantValid(t *testing.T) { 3347 wfe, _, signer := setupWFE(t) 3348 wfe.sa = newMockSAWithCert(t, wfe.sa) 3349 3350 mockLog := wfe.log.(*blog.Mock) 3351 mockLog.Clear() 3352 3353 revokeRequestJSON, err := makeRevokeRequestJSON(nil) 3354 test.AssertNotError(t, err, "Failed to make revokeRequestJSON") 3355 _, _, jwsBody := signer.byKeyID(1, nil, "http://localhost/revoke-cert", string(revokeRequestJSON)) 3356 3357 responseWriter := httptest.NewRecorder() 3358 wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter, 3359 makePostRequestWithPath("revoke-cert", jwsBody)) 3360 3361 test.AssertEquals(t, responseWriter.Code, 200) 3362 test.AssertEquals(t, responseWriter.Body.String(), "") 3363 test.AssertDeepEquals(t, mockLog.GetAllMatching("Authenticated revocation"), []string{ 3364 `INFO: [AUDIT] Authenticated revocation JSON={"Serial":"000000000000000000001d72443db5189821","Reason":0,"RegID":1,"Method":"applicant"}`, 3365 }) 3366 } 3367 3368 // Valid revocation request for existing, non-revoked cert, signed using the 3369 // certificate private key. 3370 func TestRevokeCertificateByKeyValid(t *testing.T) { 3371 wfe, _, signer := setupWFE(t) 3372 wfe.sa = newMockSAWithCert(t, wfe.sa) 3373 3374 mockLog := wfe.log.(*blog.Mock) 3375 mockLog.Clear() 3376 3377 keyPemBytes, err := os.ReadFile("../test/hierarchy/ee-r3.key.pem") 3378 test.AssertNotError(t, err, "Failed to load key") 3379 key := loadKey(t, keyPemBytes) 3380 3381 revocationReason := revocation.KeyCompromise 3382 revokeRequestJSON, err := makeRevokeRequestJSON(&revocationReason) 3383 test.AssertNotError(t, err, "Failed to make revokeRequestJSON") 3384 _, _, jwsBody := signer.embeddedJWK(key, "http://localhost/revoke-cert", string(revokeRequestJSON)) 3385 3386 responseWriter := httptest.NewRecorder() 3387 wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter, 3388 makePostRequestWithPath("revoke-cert", jwsBody)) 3389 3390 test.AssertEquals(t, responseWriter.Code, 200) 3391 test.AssertEquals(t, responseWriter.Body.String(), "") 3392 test.AssertDeepEquals(t, mockLog.GetAllMatching("Authenticated revocation"), []string{ 3393 `INFO: [AUDIT] Authenticated revocation JSON={"Serial":"000000000000000000001d72443db5189821","Reason":1,"RegID":0,"Method":"privkey"}`, 3394 }) 3395 } 3396 3397 // Invalid revocation request: although signed with the cert key, the cert 3398 // wasn't issued by any issuer the Boulder is aware of. 3399 func TestRevokeCertificateNotIssued(t *testing.T) { 3400 wfe, _, signer := setupWFE(t) 3401 wfe.sa = newMockSAWithCert(t, wfe.sa) 3402 3403 // Make a self-signed junk certificate 3404 k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 3405 test.AssertNotError(t, err, "unexpected error making random private key") 3406 // Use a known serial from the mockSAWithValidCert mock. 3407 // This ensures that any failures here are due to the certificate's issuer 3408 // not matching up with issuers known by the mock, rather than due to the 3409 // certificate's serial not matching up with serials known by the mock. 3410 knownCert, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem") 3411 test.AssertNotError(t, err, "Unexpected error loading test cert") 3412 template := &x509.Certificate{ 3413 SerialNumber: knownCert.SerialNumber, 3414 } 3415 certDER, err := x509.CreateCertificate(rand.Reader, template, template, k.Public(), k) 3416 test.AssertNotError(t, err, "Unexpected error creating self-signed junk cert") 3417 3418 keyPemBytes, err := os.ReadFile("../test/hierarchy/ee-r3.key.pem") 3419 test.AssertNotError(t, err, "Failed to load key") 3420 key := loadKey(t, keyPemBytes) 3421 3422 revokeRequestJSON, err := makeRevokeRequestJSONForCert(certDER, nil) 3423 test.AssertNotError(t, err, "Failed to make revokeRequestJSON for certDER") 3424 _, _, jwsBody := signer.embeddedJWK(key, "http://localhost/revoke-cert", string(revokeRequestJSON)) 3425 3426 responseWriter := httptest.NewRecorder() 3427 wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter, 3428 makePostRequestWithPath("revoke-cert", jwsBody)) 3429 // It should result in a 404 response with a problem body 3430 test.AssertEquals(t, responseWriter.Code, 404) 3431 test.AssertEquals(t, responseWriter.Body.String(), "{\n \"type\": \"urn:ietf:params:acme:error:malformed\",\n \"detail\": \"Unable to revoke :: Certificate from unrecognized issuer\",\n \"status\": 404\n}") 3432 } 3433 3434 func TestRevokeCertificateExpired(t *testing.T) { 3435 wfe, fc, signer := setupWFE(t) 3436 wfe.sa = newMockSAWithCert(t, wfe.sa) 3437 3438 keyPemBytes, err := os.ReadFile("../test/hierarchy/ee-r3.key.pem") 3439 test.AssertNotError(t, err, "Failed to load key") 3440 key := loadKey(t, keyPemBytes) 3441 3442 revokeRequestJSON, err := makeRevokeRequestJSON(nil) 3443 test.AssertNotError(t, err, "Failed to make revokeRequestJSON") 3444 3445 _, _, jwsBody := signer.embeddedJWK(key, "http://localhost/revoke-cert", string(revokeRequestJSON)) 3446 3447 cert, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem") 3448 test.AssertNotError(t, err, "Failed to load test certificate") 3449 3450 fc.Set(cert.NotAfter.Add(time.Hour)) 3451 3452 responseWriter := httptest.NewRecorder() 3453 wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter, 3454 makePostRequestWithPath("revoke-cert", jwsBody)) 3455 test.AssertEquals(t, responseWriter.Code, 403) 3456 test.AssertEquals(t, responseWriter.Body.String(), "{\n \"type\": \"urn:ietf:params:acme:error:unauthorized\",\n \"detail\": \"Unable to revoke :: Certificate is expired\",\n \"status\": 403\n}") 3457 } 3458 3459 func TestRevokeCertificateReasons(t *testing.T) { 3460 wfe, _, signer := setupWFE(t) 3461 wfe.sa = newMockSAWithCert(t, wfe.sa) 3462 ra := wfe.ra.(*MockRegistrationAuthority) 3463 3464 reason0 := revocation.Unspecified 3465 reason1 := revocation.KeyCompromise 3466 reason2 := revocation.CACompromise 3467 reason100 := revocation.Reason(100) 3468 3469 testCases := []struct { 3470 Name string 3471 Reason *revocation.Reason 3472 ExpectedHTTPCode int 3473 ExpectedBody string 3474 ExpectedReason *revocation.Reason 3475 }{ 3476 { 3477 Name: "Valid reason", 3478 Reason: &reason1, 3479 ExpectedHTTPCode: http.StatusOK, 3480 ExpectedReason: &reason1, 3481 }, 3482 { 3483 Name: "No reason", 3484 ExpectedHTTPCode: http.StatusOK, 3485 ExpectedReason: &reason0, 3486 }, 3487 { 3488 Name: "Unsupported reason", 3489 Reason: &reason2, 3490 ExpectedHTTPCode: http.StatusBadRequest, 3491 ExpectedBody: `{"type":"` + probs.ErrorNS + `badRevocationReason","detail":"Unable to revoke :: disallowed revocation reason: 2","status":400}`, 3492 }, 3493 { 3494 Name: "Non-existent reason", 3495 Reason: &reason100, 3496 ExpectedHTTPCode: http.StatusBadRequest, 3497 ExpectedBody: `{"type":"` + probs.ErrorNS + `badRevocationReason","detail":"Unable to revoke :: disallowed revocation reason: 100","status":400}`, 3498 }, 3499 } 3500 3501 for _, tc := range testCases { 3502 t.Run(tc.Name, func(t *testing.T) { 3503 revokeRequestJSON, err := makeRevokeRequestJSON(tc.Reason) 3504 test.AssertNotError(t, err, "Failed to make revokeRequestJSON") 3505 _, _, jwsBody := signer.byKeyID(1, nil, "http://localhost/revoke-cert", string(revokeRequestJSON)) 3506 3507 responseWriter := httptest.NewRecorder() 3508 wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter, 3509 makePostRequestWithPath("revoke-cert", jwsBody)) 3510 3511 test.AssertEquals(t, responseWriter.Code, tc.ExpectedHTTPCode) 3512 if tc.ExpectedBody != "" { 3513 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), tc.ExpectedBody) 3514 } else { 3515 test.AssertEquals(t, responseWriter.Body.String(), tc.ExpectedBody) 3516 } 3517 if tc.ExpectedReason != nil { 3518 test.AssertEquals(t, ra.lastRevocationReason, *tc.ExpectedReason) 3519 } 3520 }) 3521 } 3522 } 3523 3524 // A revocation request signed by an incorrect certificate private key. 3525 func TestRevokeCertificateWrongCertificateKey(t *testing.T) { 3526 wfe, _, signer := setupWFE(t) 3527 wfe.sa = newMockSAWithCert(t, wfe.sa) 3528 3529 keyPemBytes, err := os.ReadFile("../test/hierarchy/ee-e1.key.pem") 3530 test.AssertNotError(t, err, "Failed to load key") 3531 key := loadKey(t, keyPemBytes) 3532 3533 revocationReason := revocation.KeyCompromise 3534 revokeRequestJSON, err := makeRevokeRequestJSON(&revocationReason) 3535 test.AssertNotError(t, err, "Failed to make revokeRequestJSON") 3536 _, _, jwsBody := signer.embeddedJWK(key, "http://localhost/revoke-cert", string(revokeRequestJSON)) 3537 3538 responseWriter := httptest.NewRecorder() 3539 wfe.RevokeCertificate(ctx, newRequestEvent(), responseWriter, 3540 makePostRequestWithPath("revoke-cert", jwsBody)) 3541 test.AssertEquals(t, responseWriter.Code, 403) 3542 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), 3543 `{"type":"`+probs.ErrorNS+`unauthorized","detail":"Unable to revoke :: JWK embedded in revocation request must be the same public key as the cert to be revoked","status":403}`) 3544 } 3545 3546 type mockSAGetRegByKeyFails struct { 3547 sapb.StorageAuthorityReadOnlyClient 3548 } 3549 3550 func (sa *mockSAGetRegByKeyFails) GetRegistrationByKey(_ context.Context, req *sapb.JSONWebKey, _ ...grpc.CallOption) (*corepb.Registration, error) { 3551 return nil, fmt.Errorf("whoops") 3552 } 3553 3554 // When SA.GetRegistrationByKey errors (e.g. gRPC timeout), NewAccount should 3555 // return internal server errors. 3556 func TestNewAccountWhenGetRegByKeyFails(t *testing.T) { 3557 wfe, _, signer := setupWFE(t) 3558 wfe.sa = &mockSAGetRegByKeyFails{wfe.sa} 3559 key := loadKey(t, []byte(testE2KeyPrivatePEM)) 3560 _, ok := key.(*ecdsa.PrivateKey) 3561 test.Assert(t, ok, "Couldn't load ECDSA key") 3562 payload := `{"contact":["mailto:person@mail.com"],"agreement":"` + agreementURL + `"}` 3563 responseWriter := httptest.NewRecorder() 3564 _, _, body := signer.embeddedJWK(key, "http://localhost/new-account", payload) 3565 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("/new-account", body)) 3566 if responseWriter.Code != 500 { 3567 t.Fatalf("Wrong response code %d for NewAccount with failing GetRegByKey (wanted 500)", responseWriter.Code) 3568 } 3569 var prob probs.ProblemDetails 3570 err := json.Unmarshal(responseWriter.Body.Bytes(), &prob) 3571 test.AssertNotError(t, err, "unmarshalling response") 3572 if prob.Type != probs.ErrorNS+probs.ServerInternalProblem { 3573 t.Errorf("Wrong type for returned problem: %#v", prob.Type) 3574 } 3575 } 3576 3577 type mockSAGetRegByKeyNotFound struct { 3578 sapb.StorageAuthorityReadOnlyClient 3579 } 3580 3581 func (sa *mockSAGetRegByKeyNotFound) GetRegistrationByKey(_ context.Context, req *sapb.JSONWebKey, _ ...grpc.CallOption) (*corepb.Registration, error) { 3582 return nil, berrors.NotFoundError("not found") 3583 } 3584 3585 func TestNewAccountWhenGetRegByKeyNotFound(t *testing.T) { 3586 wfe, _, signer := setupWFE(t) 3587 wfe.sa = &mockSAGetRegByKeyNotFound{wfe.sa} 3588 key := loadKey(t, []byte(testE2KeyPrivatePEM)) 3589 _, ok := key.(*ecdsa.PrivateKey) 3590 test.Assert(t, ok, "Couldn't load ECDSA key") 3591 // When SA.GetRegistrationByKey returns NotFound, and no onlyReturnExisting 3592 // field is sent, NewAccount should succeed. 3593 payload := `{"contact":["mailto:person@mail.com"],"termsOfServiceAgreed":true}` 3594 signedURL := "http://localhost/new-account" 3595 responseWriter := httptest.NewRecorder() 3596 _, _, body := signer.embeddedJWK(key, signedURL, payload) 3597 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("/new-account", body)) 3598 if responseWriter.Code != http.StatusCreated { 3599 t.Errorf("Bad response to NewRegistration: %d, %s", responseWriter.Code, responseWriter.Body) 3600 } 3601 3602 // When SA.GetRegistrationByKey returns NotFound, and onlyReturnExisting 3603 // field **is** sent, NewAccount should fail with the expected error. 3604 payload = `{"contact":["mailto:person@mail.com"],"termsOfServiceAgreed":true,"onlyReturnExisting":true}` 3605 responseWriter = httptest.NewRecorder() 3606 _, _, body = signer.embeddedJWK(key, signedURL, payload) 3607 // Process the new account request 3608 wfe.NewAccount(ctx, newRequestEvent(), responseWriter, makePostRequestWithPath("/new-account", body)) 3609 test.AssertEquals(t, responseWriter.Code, http.StatusBadRequest) 3610 test.AssertUnmarshaledEquals(t, responseWriter.Body.String(), ` 3611 { 3612 "type": "urn:ietf:params:acme:error:accountDoesNotExist", 3613 "detail": "No account exists with the provided key", 3614 "status": 400 3615 }`) 3616 } 3617 3618 func TestPrepAuthzForDisplay(t *testing.T) { 3619 t.Parallel() 3620 wfe, _, _ := setupWFE(t) 3621 3622 authz := &core.Authorization{ 3623 ID: "12345", 3624 Status: core.StatusPending, 3625 RegistrationID: 1, 3626 Identifier: identifier.NewDNS("example.com"), 3627 Challenges: []core.Challenge{ 3628 {Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"}, 3629 {Type: core.ChallengeTypeHTTP01, Status: core.StatusPending, Token: "token"}, 3630 {Type: core.ChallengeTypeTLSALPN01, Status: core.StatusPending, Token: "token"}, 3631 }, 3632 } 3633 3634 // This modifies the authz in-place. 3635 wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz) 3636 3637 // Ensure ID and RegID are omitted. 3638 authzJSON, err := json.Marshal(authz) 3639 test.AssertNotError(t, err, "Failed to marshal authz") 3640 test.AssertNotContains(t, string(authzJSON), "\"id\":\"12345\"") 3641 test.AssertNotContains(t, string(authzJSON), "\"registrationID\":\"1\"") 3642 } 3643 3644 func TestPrepRevokedAuthzForDisplay(t *testing.T) { 3645 t.Parallel() 3646 wfe, _, _ := setupWFE(t) 3647 3648 authz := &core.Authorization{ 3649 ID: "12345", 3650 Status: core.StatusInvalid, 3651 RegistrationID: 1, 3652 Identifier: identifier.NewDNS("example.com"), 3653 Challenges: []core.Challenge{ 3654 {Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"}, 3655 {Type: core.ChallengeTypeHTTP01, Status: core.StatusPending, Token: "token"}, 3656 {Type: core.ChallengeTypeTLSALPN01, Status: core.StatusPending, Token: "token"}, 3657 }, 3658 } 3659 3660 // This modifies the authz in-place. 3661 wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz) 3662 3663 // All of the challenges should be revoked as well. 3664 for _, chall := range authz.Challenges { 3665 test.AssertEquals(t, chall.Status, core.StatusInvalid) 3666 } 3667 } 3668 3669 func TestPrepWildcardAuthzForDisplay(t *testing.T) { 3670 t.Parallel() 3671 wfe, _, _ := setupWFE(t) 3672 3673 authz := &core.Authorization{ 3674 ID: "12345", 3675 Status: core.StatusPending, 3676 RegistrationID: 1, 3677 Identifier: identifier.NewDNS("*.example.com"), 3678 Challenges: []core.Challenge{ 3679 {Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"}, 3680 }, 3681 } 3682 3683 // This modifies the authz in-place. 3684 wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz) 3685 3686 // The identifier should not start with a star, but the authz should be marked 3687 // as a wildcard. 3688 test.AssertEquals(t, strings.HasPrefix(authz.Identifier.Value, "*."), false) 3689 test.AssertEquals(t, authz.Wildcard, true) 3690 } 3691 3692 func TestPrepAuthzForDisplayShuffle(t *testing.T) { 3693 t.Parallel() 3694 wfe, _, _ := setupWFE(t) 3695 3696 authz := &core.Authorization{ 3697 ID: "12345", 3698 Status: core.StatusPending, 3699 RegistrationID: 1, 3700 Identifier: identifier.NewDNS("example.com"), 3701 Challenges: []core.Challenge{ 3702 {Type: core.ChallengeTypeDNS01, Status: core.StatusPending, Token: "token"}, 3703 {Type: core.ChallengeTypeHTTP01, Status: core.StatusPending, Token: "token"}, 3704 {Type: core.ChallengeTypeTLSALPN01, Status: core.StatusPending, Token: "token"}, 3705 }, 3706 } 3707 3708 // The challenges should be presented in an unpredictable order. 3709 3710 // Create a structure to count how many times each challenge type ends up in 3711 // each position in the output authz.Challenges list. 3712 counts := make(map[core.AcmeChallenge]map[int]int) 3713 counts[core.ChallengeTypeDNS01] = map[int]int{0: 0, 1: 0, 2: 0} 3714 counts[core.ChallengeTypeHTTP01] = map[int]int{0: 0, 1: 0, 2: 0} 3715 counts[core.ChallengeTypeTLSALPN01] = map[int]int{0: 0, 1: 0, 2: 0} 3716 3717 // Prep the authz 100 times, and count where each challenge ended up each time. 3718 for range 100 { 3719 // This modifies the authz in place 3720 wfe.prepAuthorizationForDisplay(&http.Request{Host: "localhost"}, authz) 3721 for i, chall := range authz.Challenges { 3722 counts[chall.Type][i] += 1 3723 } 3724 } 3725 3726 // Ensure that at least some amount of randomization is happening. 3727 for challType, indices := range counts { 3728 for index, count := range indices { 3729 test.Assert(t, count > 10, fmt.Sprintf("challenge type %s did not appear in position %d as often as expected", challType, index)) 3730 } 3731 } 3732 } 3733 3734 // noSCTMockRA is a mock RA that always returns a `berrors.MissingSCTsError` from `FinalizeOrder` 3735 type noSCTMockRA struct { 3736 MockRegistrationAuthority 3737 } 3738 3739 func (ra *noSCTMockRA) FinalizeOrder(context.Context, *rapb.FinalizeOrderRequest, ...grpc.CallOption) (*corepb.Order, error) { 3740 return nil, berrors.MissingSCTsError("noSCTMockRA missing scts error") 3741 } 3742 3743 func TestFinalizeSCTError(t *testing.T) { 3744 wfe, _, signer := setupWFE(t) 3745 3746 // Set up an RA mock that always returns a berrors.MissingSCTsError from 3747 // `FinalizeOrder` 3748 wfe.ra = &noSCTMockRA{} 3749 3750 // Create a response writer to capture the WFE response 3751 responseWriter := httptest.NewRecorder() 3752 3753 // This example is a well-formed CSR for the name "example.com". 3754 goodCertCSRPayload := `{ 3755 "csr": "MIHRMHgCAQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ2hlvArQl5k0L1eF1vF5dwr7ASm2iKqibmauund-z3QJpuudnNEjlyOXi-IY1rxyhehRrtbm_bbcNCtZLgbkPvoAAwCgYIKoZIzj0EAwIDSQAwRgIhAJ8z2EDll2BvoNRotAknEfrqeP6K5CN1NeVMB4QOu0G1AiEAqAVpiGwNyV7SEZ67vV5vyuGsKPAGnqrisZh5Vg5JKHE=" 3756 }` 3757 3758 // Create a finalization request with the above payload 3759 request := signAndPost(signer, "1/8", "http://localhost/1/8", goodCertCSRPayload) 3760 3761 // POST the finalize order request. 3762 wfe.FinalizeOrder(ctx, newRequestEvent(), responseWriter, request) 3763 3764 // We expect the berrors.MissingSCTsError error to have been converted into 3765 // a serverInternal error with the right message. 3766 test.AssertUnmarshaledEquals(t, 3767 responseWriter.Body.String(), 3768 `{"type":"`+probs.ErrorNS+`serverInternal","detail":"Error finalizing order :: Unable to meet CA SCT embedding requirements","status":500}`) 3769 } 3770 3771 func TestOrderToOrderJSONV2Authorizations(t *testing.T) { 3772 wfe, fc, _ := setupWFE(t) 3773 expires := fc.Now() 3774 orderJSON := wfe.orderToOrderJSON(&http.Request{}, &corepb.Order{ 3775 Id: 1, 3776 RegistrationID: 1, 3777 Identifiers: []*corepb.Identifier{identifier.NewDNS("a").ToProto()}, 3778 Status: string(core.StatusPending), 3779 Expires: timestamppb.New(expires), 3780 V2Authorizations: []int64{1, 2}, 3781 }) 3782 test.AssertDeepEquals(t, orderJSON.Authorizations, []string{ 3783 "http://localhost/acme/authz/1/1", 3784 "http://localhost/acme/authz/1/2", 3785 }) 3786 } 3787 3788 func TestAccountMarshaling(t *testing.T) { 3789 acct := &core.Registration{ 3790 ID: 1987, 3791 Agreement: "disagreement", 3792 Status: core.StatusValid, 3793 } 3794 3795 marshaled, err := json.Marshal(acct) 3796 if err != nil { 3797 t.Fatalf("marshalling account object: %s", err) 3798 } 3799 3800 var got core.Registration 3801 err = json.Unmarshal(marshaled, &got) 3802 if err != nil { 3803 t.Fatalf("unmarshaling account object: %s", err) 3804 } 3805 3806 // The Agreement should always be cleared. 3807 test.AssertEquals(t, got.Agreement, "") 3808 // The ID field should be zeroed. 3809 test.AssertEquals(t, got.ID, int64(0)) 3810 // The Status field should be preserved. 3811 test.AssertEquals(t, got.Status, core.StatusValid) 3812 } 3813 3814 // TestGet404 tests that a 404 is served and that the expected endpoint of 3815 // "/" is logged when an unknown path is requested. This will test the 3816 // codepath to the wfe.Index() handler which handles "/" and all non-api 3817 // endpoint requests to make sure the endpoint is set properly in the logs. 3818 func TestIndexGet404(t *testing.T) { 3819 // Setup 3820 wfe, _, _ := setupWFE(t) 3821 path := "/nopathhere/nope/nofilehere" 3822 req := &http.Request{URL: &url.URL{Path: path}, Method: "GET"} 3823 logEvent := &web.RequestEvent{} 3824 responseWriter := httptest.NewRecorder() 3825 3826 // Send a request to wfe.Index() 3827 wfe.Index(context.Background(), logEvent, responseWriter, req) 3828 3829 // Test that a 404 is received as expected 3830 test.AssertEquals(t, responseWriter.Code, http.StatusNotFound) 3831 // Test that we logged the "/" endpoint 3832 test.AssertEquals(t, logEvent.Endpoint, "/") 3833 // Test that the rest of the path is logged as the slug 3834 test.AssertEquals(t, logEvent.Slug, path[1:]) 3835 } 3836 3837 // TestARI tests that requests for real certs result in renewal info, while 3838 // requests for certs that don't exist result in errors. 3839 func TestARI(t *testing.T) { 3840 wfe, _, _ := setupWFE(t) 3841 msa := newMockSAWithCert(t, wfe.sa) 3842 wfe.sa = msa 3843 3844 features.Set(features.Config{ServeRenewalInfo: true}) 3845 defer features.Reset() 3846 3847 makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) { 3848 return &http.Request{URL: &url.URL{Path: path}, Method: "GET"}, 3849 &web.RequestEvent{Endpoint: endpoint, Extra: map[string]any{}} 3850 } 3851 3852 // Load the leaf certificate. 3853 cert, err := core.LoadCert("../test/hierarchy/ee-r3.cert.pem") 3854 test.AssertNotError(t, err, "failed to load test certificate") 3855 3856 // Ensure that a correct draft-ietf-acme-ari03 query results in a 200. 3857 certID := fmt.Sprintf("%s.%s", 3858 base64.RawURLEncoding.EncodeToString(cert.AuthorityKeyId), 3859 base64.RawURLEncoding.EncodeToString(cert.SerialNumber.Bytes()), 3860 ) 3861 req, event := makeGet(certID, renewalInfoPath) 3862 resp := httptest.NewRecorder() 3863 wfe.RenewalInfo(context.Background(), event, resp, req) 3864 test.AssertEquals(t, resp.Code, http.StatusOK) 3865 test.AssertEquals(t, resp.Header().Get("Retry-After"), "21600") 3866 var ri core.RenewalInfo 3867 err = json.Unmarshal(resp.Body.Bytes(), &ri) 3868 test.AssertNotError(t, err, "unmarshalling renewal info") 3869 test.Assert(t, ri.SuggestedWindow.Start.After(cert.NotBefore), "suggested window begins before cert issuance") 3870 test.Assert(t, ri.SuggestedWindow.End.Before(cert.NotAfter), "suggested window ends after cert expiry") 3871 3872 // Ensure that a correct draft-ietf-acme-ari03 query for a revoked cert 3873 // results in a renewal window in the past. 3874 msa.status = core.OCSPStatusRevoked 3875 req, event = makeGet(certID, renewalInfoPath) 3876 resp = httptest.NewRecorder() 3877 wfe.RenewalInfo(context.Background(), event, resp, req) 3878 test.AssertEquals(t, resp.Code, http.StatusOK) 3879 test.AssertEquals(t, resp.Header().Get("Retry-After"), "21600") 3880 err = json.Unmarshal(resp.Body.Bytes(), &ri) 3881 test.AssertNotError(t, err, "unmarshalling renewal info") 3882 test.Assert(t, ri.SuggestedWindow.End.Before(wfe.clk.Now()), "suggested window should end in the past") 3883 test.Assert(t, ri.SuggestedWindow.Start.Before(ri.SuggestedWindow.End), "suggested window should start before it ends") 3884 3885 // Ensure that a draft-ietf-acme-ari03 query for a non-existent serial 3886 // results in a 404. 3887 certID = fmt.Sprintf("%s.%s", 3888 base64.RawURLEncoding.EncodeToString(cert.AuthorityKeyId), 3889 base64.RawURLEncoding.EncodeToString( 3890 big.NewInt(0).Add(cert.SerialNumber, big.NewInt(1)).Bytes(), 3891 ), 3892 ) 3893 req, event = makeGet(certID, renewalInfoPath) 3894 resp = httptest.NewRecorder() 3895 wfe.RenewalInfo(context.Background(), event, resp, req) 3896 test.AssertEquals(t, resp.Code, http.StatusNotFound) 3897 test.AssertEquals(t, resp.Header().Get("Retry-After"), "") 3898 3899 // Ensure that a query with a non-CertID path fails. 3900 req, event = makeGet("lolwutsup", renewalInfoPath) 3901 resp = httptest.NewRecorder() 3902 wfe.RenewalInfo(context.Background(), event, resp, req) 3903 test.AssertEquals(t, resp.Code, http.StatusBadRequest) 3904 test.AssertContains(t, resp.Body.String(), "Invalid path") 3905 3906 // Ensure that a query with no path slug at all bails out early. 3907 req, event = makeGet("", renewalInfoPath) 3908 resp = httptest.NewRecorder() 3909 wfe.RenewalInfo(context.Background(), event, resp, req) 3910 test.AssertEquals(t, resp.Code, http.StatusNotFound) 3911 test.AssertContains(t, resp.Body.String(), "Must specify a request path") 3912 } 3913 3914 // TestIncidentARI tests that requests certs impacted by an ongoing revocation 3915 // incident result in a 200 with a retry-after header and a suggested retry 3916 // window in the past. 3917 func TestIncidentARI(t *testing.T) { 3918 wfe, _, _ := setupWFE(t) 3919 expectSerial := big.NewInt(12345) 3920 expectSerialString := core.SerialToString(big.NewInt(12345)) 3921 wfe.sa = newMockSAWithIncident(wfe.sa, []string{expectSerialString}) 3922 3923 features.Set(features.Config{ServeRenewalInfo: true}) 3924 defer features.Reset() 3925 3926 makeGet := func(path, endpoint string) (*http.Request, *web.RequestEvent) { 3927 return &http.Request{URL: &url.URL{Path: path}, Method: "GET"}, 3928 &web.RequestEvent{Endpoint: endpoint, Extra: map[string]any{}} 3929 } 3930 3931 var issuer issuance.NameID 3932 for k := range wfe.issuerCertificates { 3933 // Grab the first known issuer. 3934 issuer = k 3935 break 3936 } 3937 certID := fmt.Sprintf("%s.%s", 3938 base64.RawURLEncoding.EncodeToString(wfe.issuerCertificates[issuer].SubjectKeyId), 3939 base64.RawURLEncoding.EncodeToString(expectSerial.Bytes()), 3940 ) 3941 req, event := makeGet(certID, renewalInfoPath) 3942 resp := httptest.NewRecorder() 3943 wfe.RenewalInfo(context.Background(), event, resp, req) 3944 test.AssertEquals(t, resp.Code, 200) 3945 test.AssertEquals(t, resp.Header().Get("Retry-After"), "21600") 3946 var ri core.RenewalInfo 3947 err := json.Unmarshal(resp.Body.Bytes(), &ri) 3948 test.AssertNotError(t, err, "unmarshalling renewal info") 3949 // The start of the window should be in the past. 3950 test.AssertEquals(t, ri.SuggestedWindow.Start.Before(wfe.clk.Now()), true) 3951 // The end of the window should be after the start. 3952 test.AssertEquals(t, ri.SuggestedWindow.End.After(ri.SuggestedWindow.Start), true) 3953 // The end of the window should also be in the past. 3954 test.AssertEquals(t, ri.SuggestedWindow.End.Before(wfe.clk.Now()), true) 3955 // The explanationURL should be set. 3956 test.AssertEquals(t, ri.ExplanationURL, "http://big.bad/incident") 3957 } 3958 3959 func Test_sendError(t *testing.T) { 3960 features.Reset() 3961 wfe, _, _ := setupWFE(t) 3962 testResponse := httptest.NewRecorder() 3963 3964 testErr := berrors.RateLimitError(0, "test") 3965 wfe.sendError(testResponse, &web.RequestEvent{Endpoint: "test"}, probs.RateLimited("test"), testErr) 3966 // Ensure a 0 value RetryAfter results in no Retry-After header. 3967 test.AssertEquals(t, testResponse.Header().Get("Retry-After"), "") 3968 // Ensure the Link header isn't populatsed. 3969 test.AssertEquals(t, testResponse.Header().Get("Link"), "") 3970 3971 testErr = berrors.RateLimitError(time.Millisecond*500, "test") 3972 wfe.sendError(testResponse, &web.RequestEvent{Endpoint: "test"}, probs.RateLimited("test"), testErr) 3973 // Ensure a 500ms RetryAfter is rounded up to a 1s Retry-After header. 3974 test.AssertEquals(t, testResponse.Header().Get("Retry-After"), "1") 3975 // Ensure the Link header is populated. 3976 test.AssertEquals(t, testResponse.Header().Get("Link"), "<https://letsencrypt.org/docs/rate-limits>;rel=\"help\"") 3977 3978 // Clear headers for the next test. 3979 testResponse.Header().Del("Retry-After") 3980 testResponse.Header().Del("Link") 3981 3982 testErr = berrors.RateLimitError(time.Millisecond*499, "test") 3983 wfe.sendError(testResponse, &web.RequestEvent{Endpoint: "test"}, probs.RateLimited("test"), testErr) 3984 // Ensure a 499ms RetryAfter results in no Retry-After header. 3985 test.AssertEquals(t, testResponse.Header().Get("Retry-After"), "") 3986 // Ensure the Link header isn't populatsed. 3987 test.AssertEquals(t, testResponse.Header().Get("Link"), "") 3988 } 3989 3990 func Test_sendErrorInternalServerError(t *testing.T) { 3991 features.Reset() 3992 wfe, _, _ := setupWFE(t) 3993 testResponse := httptest.NewRecorder() 3994 3995 wfe.sendError(testResponse, &web.RequestEvent{}, probs.ServerInternal("oh no"), nil) 3996 test.AssertEquals(t, testResponse.Header().Get("Retry-After"), "60") 3997 } 3998 3999 // mockSAForARI provides a mock SA with the methods required for an issuance and 4000 // a renewal with the ARI `Replaces` field. 4001 // 4002 // Note that FQDNSetTimestampsForWindow always return an empty list, which allows us to act 4003 // as if a certificate is not getting the renewal exemption, even when we are repeatedly 4004 // issuing for the same names. 4005 type mockSAForARI struct { 4006 sapb.StorageAuthorityReadOnlyClient 4007 cert *corepb.Certificate 4008 } 4009 4010 func (sa *mockSAForARI) FQDNSetTimestampsForWindow(ctx context.Context, in *sapb.CountFQDNSetsRequest, opts ...grpc.CallOption) (*sapb.Timestamps, error) { 4011 return &sapb.Timestamps{Timestamps: nil}, nil 4012 } 4013 4014 // GetCertificate returns the inner certificate if it matches the given serial. 4015 func (sa *mockSAForARI) GetCertificate(ctx context.Context, req *sapb.Serial, _ ...grpc.CallOption) (*corepb.Certificate, error) { 4016 if req.Serial == sa.cert.Serial { 4017 return sa.cert, nil 4018 } 4019 return nil, berrors.NotFoundError("certificate with serial %q not found", req.Serial) 4020 } 4021 4022 func (sa *mockSAForARI) ReplacementOrderExists(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Exists, error) { 4023 if in.Serial == sa.cert.Serial { 4024 return &sapb.Exists{Exists: false}, nil 4025 4026 } 4027 return &sapb.Exists{Exists: true}, nil 4028 } 4029 4030 func (sa *mockSAForARI) IncidentsForSerial(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*sapb.Incidents, error) { 4031 return &sapb.Incidents{}, nil 4032 } 4033 4034 func (sa *mockSAForARI) GetCertificateStatus(ctx context.Context, in *sapb.Serial, opts ...grpc.CallOption) (*corepb.CertificateStatus, error) { 4035 return &corepb.CertificateStatus{Serial: in.Serial, Status: string(core.OCSPStatusGood)}, nil 4036 } 4037 4038 func TestOrderMatchesReplacement(t *testing.T) { 4039 wfe, _, _ := setupWFE(t) 4040 4041 expectExpiry := time.Now().AddDate(0, 0, 1) 4042 expectSerial := big.NewInt(1337) 4043 testKey, _ := rsa.GenerateKey(rand.Reader, 1024) 4044 rawCert := x509.Certificate{ 4045 NotAfter: expectExpiry, 4046 DNSNames: []string{"example.com", "example-a.com"}, 4047 SerialNumber: expectSerial, 4048 } 4049 mockDer, err := x509.CreateCertificate(rand.Reader, &rawCert, &rawCert, &testKey.PublicKey, testKey) 4050 test.AssertNotError(t, err, "failed to create test certificate") 4051 4052 wfe.sa = &mockSAForARI{ 4053 cert: &corepb.Certificate{ 4054 RegistrationID: 1, 4055 Serial: expectSerial.String(), 4056 Der: mockDer, 4057 }, 4058 } 4059 4060 // Working with a single matching identifier. 4061 err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, identifier.ACMEIdentifiers{identifier.NewDNS("example.com")}, expectSerial.String()) 4062 test.AssertNotError(t, err, "failed to check order is replacement") 4063 4064 // Working with a different matching identifier. 4065 err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, identifier.ACMEIdentifiers{identifier.NewDNS("example-a.com")}, expectSerial.String()) 4066 test.AssertNotError(t, err, "failed to check order is replacement") 4067 4068 // No matching identifiers. 4069 err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, identifier.ACMEIdentifiers{identifier.NewDNS("example-b.com")}, expectSerial.String()) 4070 test.AssertErrorIs(t, err, berrors.Malformed) 4071 4072 // RegID for predecessor order does not match. 4073 err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 2}, identifier.ACMEIdentifiers{identifier.NewDNS("example.com")}, expectSerial.String()) 4074 test.AssertErrorIs(t, err, berrors.Unauthorized) 4075 4076 // Predecessor certificate not found. 4077 err = wfe.orderMatchesReplacement(context.Background(), &core.Registration{ID: 1}, identifier.ACMEIdentifiers{identifier.NewDNS("example.com")}, "1") 4078 test.AssertErrorIs(t, err, berrors.NotFound) 4079 } 4080 4081 type mockRA struct { 4082 rapb.RegistrationAuthorityClient 4083 expectProfileName string 4084 } 4085 4086 // NewOrder returns an error if the "" 4087 func (sa *mockRA) NewOrder(ctx context.Context, in *rapb.NewOrderRequest, opts ...grpc.CallOption) (*corepb.Order, error) { 4088 if in.CertificateProfileName != sa.expectProfileName { 4089 return nil, errors.New("not expected profile name") 4090 } 4091 now := time.Now().UTC() 4092 created := now.AddDate(-30, 0, 0) 4093 exp := now.AddDate(30, 0, 0) 4094 return &corepb.Order{ 4095 Id: 123456789, 4096 RegistrationID: 987654321, 4097 Created: timestamppb.New(created), 4098 Expires: timestamppb.New(exp), 4099 Identifiers: []*corepb.Identifier{identifier.NewDNS("example.com").ToProto()}, 4100 Status: string(core.StatusValid), 4101 V2Authorizations: []int64{1}, 4102 CertificateSerial: "serial", 4103 Error: nil, 4104 CertificateProfileName: in.CertificateProfileName, 4105 }, nil 4106 } 4107 4108 func TestNewOrderWithProfile(t *testing.T) { 4109 wfe, _, signer := setupWFE(t) 4110 expectProfileName := "test-profile" 4111 wfe.ra = &mockRA{expectProfileName: expectProfileName} 4112 mux := wfe.Handler(metrics.NoopRegisterer) 4113 wfe.certProfiles = map[string]string{expectProfileName: "description"} 4114 4115 // Test that the newOrder endpoint returns the proper error if an invalid 4116 // profile is specified. 4117 invalidOrderBody := ` 4118 { 4119 "Identifiers": [ 4120 {"type": "dns", "value": "example.com"} 4121 ], 4122 "Profile": "bad-profile" 4123 }` 4124 4125 responseWriter := httptest.NewRecorder() 4126 r := signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, invalidOrderBody) 4127 mux.ServeHTTP(responseWriter, r) 4128 test.AssertEquals(t, responseWriter.Code, http.StatusBadRequest) 4129 var errorResp map[string]any 4130 err := json.Unmarshal(responseWriter.Body.Bytes(), &errorResp) 4131 test.AssertNotError(t, err, "Failed to unmarshal error response") 4132 test.AssertEquals(t, errorResp["type"], "urn:ietf:params:acme:error:invalidProfile") 4133 test.AssertEquals(t, errorResp["detail"], "profile name \"bad-profile\" not recognized") 4134 4135 // Test that the newOrder endpoint returns no error if the valid profile is specified. 4136 validOrderBody := ` 4137 { 4138 "Identifiers": [ 4139 {"type": "dns", "value": "example.com"} 4140 ], 4141 "Profile": "test-profile" 4142 }` 4143 responseWriter = httptest.NewRecorder() 4144 r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, validOrderBody) 4145 mux.ServeHTTP(responseWriter, r) 4146 test.AssertEquals(t, responseWriter.Code, http.StatusCreated) 4147 var errorResp1 map[string]any 4148 err = json.Unmarshal(responseWriter.Body.Bytes(), &errorResp1) 4149 test.AssertNotError(t, err, "Failed to unmarshal order response") 4150 test.AssertEquals(t, errorResp1["status"], "valid") 4151 4152 // Set the acceptable profiles to the empty set, the WFE should no longer accept any profiles. 4153 wfe.certProfiles = map[string]string{} 4154 responseWriter = httptest.NewRecorder() 4155 r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, validOrderBody) 4156 mux.ServeHTTP(responseWriter, r) 4157 test.AssertEquals(t, responseWriter.Code, http.StatusBadRequest) 4158 var errorResp2 map[string]any 4159 err = json.Unmarshal(responseWriter.Body.Bytes(), &errorResp2) 4160 test.AssertNotError(t, err, "Failed to unmarshal error response") 4161 test.AssertEquals(t, errorResp2["type"], "urn:ietf:params:acme:error:invalidProfile") 4162 test.AssertEquals(t, errorResp2["detail"], "profile name \"test-profile\" not recognized") 4163 } 4164 4165 func makeARICertID(leaf *x509.Certificate) (string, error) { 4166 if leaf == nil { 4167 return "", errors.New("leaf certificate is nil") 4168 } 4169 4170 // Marshal the Serial Number into DER. 4171 der, err := asn1.Marshal(leaf.SerialNumber) 4172 if err != nil { 4173 return "", err 4174 } 4175 4176 // Check if the DER encoded bytes are sufficient (at least 3 bytes: tag, 4177 // length, and value). 4178 if len(der) < 3 { 4179 return "", errors.New("invalid DER encoding of serial number") 4180 } 4181 4182 // Extract only the integer bytes from the DER encoded Serial Number 4183 // Skipping the first 2 bytes (tag and length). The result is base64url 4184 // encoded without padding. 4185 serial := base64.RawURLEncoding.EncodeToString(der[2:]) 4186 4187 // Convert the Authority Key Identifier to base64url encoding without 4188 // padding. 4189 aki := base64.RawURLEncoding.EncodeToString(leaf.AuthorityKeyId) 4190 4191 // Construct the final identifier by concatenating AKI and Serial Number. 4192 return fmt.Sprintf("%s.%s", aki, serial), nil 4193 } 4194 4195 func TestCountNewOrderWithReplaces(t *testing.T) { 4196 wfe, fc, signer := setupWFE(t) 4197 4198 // Pick a random issuer to "issue" expectCert. 4199 var issuer *issuance.Certificate 4200 for _, v := range wfe.issuerCertificates { 4201 issuer = v 4202 break 4203 } 4204 testKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 4205 expectSerial := big.NewInt(1337) 4206 expectCert := &x509.Certificate{ 4207 NotBefore: fc.Now(), 4208 NotAfter: fc.Now().AddDate(0, 0, 90), 4209 DNSNames: []string{"example.com"}, 4210 SerialNumber: expectSerial, 4211 AuthorityKeyId: issuer.SubjectKeyId, 4212 } 4213 expectCertId, err := makeARICertID(expectCert) 4214 test.AssertNotError(t, err, "failed to create test cert id") 4215 expectDer, err := x509.CreateCertificate(rand.Reader, expectCert, expectCert, &testKey.PublicKey, testKey) 4216 test.AssertNotError(t, err, "failed to create test certificate") 4217 4218 // MockSA that returns the certificate with the expected serial. 4219 wfe.sa = &mockSAForARI{ 4220 cert: &corepb.Certificate{ 4221 RegistrationID: 1, 4222 Serial: core.SerialToString(expectSerial), 4223 Der: expectDer, 4224 Issued: timestamppb.New(expectCert.NotBefore), 4225 Expires: timestamppb.New(expectCert.NotAfter), 4226 }, 4227 } 4228 mux := wfe.Handler(metrics.NoopRegisterer) 4229 responseWriter := httptest.NewRecorder() 4230 4231 // Set the fake clock forward to 1s past the suggested renewal window start 4232 // time. 4233 renewalWindowStart := core.RenewalInfoSimple(expectCert.NotBefore, expectCert.NotAfter).SuggestedWindow.Start 4234 fc.Set(renewalWindowStart.Add(time.Second)) 4235 4236 body := fmt.Sprintf(` 4237 { 4238 "Identifiers": [ 4239 {"type": "dns", "value": "example.com"} 4240 ], 4241 "Replaces": %q 4242 }`, expectCertId) 4243 4244 r := signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, body) 4245 mux.ServeHTTP(responseWriter, r) 4246 test.AssertEquals(t, responseWriter.Code, http.StatusCreated) 4247 test.AssertMetricWithLabelsEquals(t, wfe.stats.ariReplacementOrders, prometheus.Labels{"isReplacement": "true", "limitsExempt": "true"}, 1) 4248 } 4249 4250 func TestNewOrderRateLimits(t *testing.T) { 4251 wfe, fc, signer := setupWFE(t) 4252 4253 // Set the default ratelimits to only allow one new order per account per 24 4254 // hours. 4255 txnBuilder, err := ratelimits.NewTransactionBuilder(ratelimits.LimitConfigs{ 4256 ratelimits.NewOrdersPerAccount.String(): &ratelimits.LimitConfig{ 4257 Burst: 1, 4258 Count: 1, 4259 Period: config.Duration{Duration: time.Hour * 24}}, 4260 }, nil, metrics.NoopRegisterer, blog.NewMock()) 4261 test.AssertNotError(t, err, "making transaction composer") 4262 wfe.txnBuilder = txnBuilder 4263 4264 // Pick a random issuer to "issue" extantCert. 4265 var issuer *issuance.Certificate 4266 for _, v := range wfe.issuerCertificates { 4267 issuer = v 4268 break 4269 } 4270 testKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 4271 test.AssertNotError(t, err, "failed to create test key") 4272 extantCert := &x509.Certificate{ 4273 NotBefore: fc.Now(), 4274 NotAfter: fc.Now().AddDate(0, 0, 90), 4275 DNSNames: []string{"example.com"}, 4276 SerialNumber: big.NewInt(1337), 4277 AuthorityKeyId: issuer.SubjectKeyId, 4278 } 4279 extantCertId, err := makeARICertID(extantCert) 4280 test.AssertNotError(t, err, "failed to create test cert id") 4281 extantDer, err := x509.CreateCertificate(rand.Reader, extantCert, extantCert, &testKey.PublicKey, testKey) 4282 test.AssertNotError(t, err, "failed to create test certificate") 4283 4284 // Mock SA that returns the certificate with the expected serial. 4285 wfe.sa = &mockSAForARI{ 4286 cert: &corepb.Certificate{ 4287 RegistrationID: 1, 4288 Serial: core.SerialToString(extantCert.SerialNumber), 4289 Der: extantDer, 4290 Issued: timestamppb.New(extantCert.NotBefore), 4291 Expires: timestamppb.New(extantCert.NotAfter), 4292 }, 4293 } 4294 4295 // Set the fake clock forward to 1s past the suggested renewal window start 4296 // time. 4297 renewalWindowStart := core.RenewalInfoSimple(extantCert.NotBefore, extantCert.NotAfter).SuggestedWindow.Start 4298 fc.Set(renewalWindowStart.Add(time.Second)) 4299 4300 mux := wfe.Handler(metrics.NoopRegisterer) 4301 4302 // Request the certificate for the first time. Because we mocked together 4303 // the certificate, it will have been issued 60 days ago. 4304 r := signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, 4305 `{"Identifiers": [{"type": "dns", "value": "example.com"}]}`) 4306 responseWriter := httptest.NewRecorder() 4307 mux.ServeHTTP(responseWriter, r) 4308 test.AssertEquals(t, responseWriter.Code, http.StatusCreated) 4309 4310 // Request another, identical certificate. This should fail for violating 4311 // the NewOrdersPerAccount rate limit. 4312 r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, 4313 `{"Identifiers": [{"type": "dns", "value": "example.com"}]}`) 4314 responseWriter = httptest.NewRecorder() 4315 mux.ServeHTTP(responseWriter, r) 4316 features.Set(features.Config{ 4317 UseKvLimitsForNewOrder: true, 4318 }) 4319 test.AssertEquals(t, responseWriter.Code, http.StatusTooManyRequests) 4320 4321 // Make a request with the "Replaces" field, which should satisfy ARI checks 4322 // and therefore bypass the rate limit. 4323 r = signAndPost(signer, newOrderPath, "http://localhost"+newOrderPath, 4324 fmt.Sprintf(`{"Identifiers": [{"type": "dns", "value": "example.com"}], "Replaces": %q}`, extantCertId)) 4325 responseWriter = httptest.NewRecorder() 4326 mux.ServeHTTP(responseWriter, r) 4327 test.AssertEquals(t, responseWriter.Code, http.StatusCreated) 4328 } 4329 4330 func TestNewAccountCreatesContacts(t *testing.T) { 4331 t.Parallel() 4332 4333 key := loadKey(t, []byte(test2KeyPrivatePEM)) 4334 _, ok := key.(*rsa.PrivateKey) 4335 test.Assert(t, ok, "Couldn't load test2 key") 4336 4337 path := newAcctPath 4338 signedURL := fmt.Sprintf("http://localhost%s", path) 4339 4340 testCases := []struct { 4341 name string 4342 contacts []string 4343 expected []string 4344 }{ 4345 { 4346 name: "No email", 4347 contacts: []string{}, 4348 expected: []string{}, 4349 }, 4350 { 4351 name: "One email", 4352 contacts: []string{"mailto:person@mail.com"}, 4353 expected: []string{"person@mail.com"}, 4354 }, 4355 { 4356 name: "Two emails", 4357 contacts: []string{"mailto:person1@mail.com", "mailto:person2@mail.com"}, 4358 expected: []string{"person1@mail.com", "person2@mail.com"}, 4359 }, 4360 { 4361 name: "Invalid email", 4362 contacts: []string{"mailto:lol@%mail.com"}, 4363 expected: []string{}, 4364 }, 4365 { 4366 name: "One valid email, one invalid email", 4367 contacts: []string{"mailto:person@mail.com", "mailto:lol@%mail.com"}, 4368 expected: []string{}, 4369 }, 4370 { 4371 name: "Valid email with non-email prefix", 4372 contacts: []string{"heliograph:person@mail.com"}, 4373 expected: []string{}, 4374 }, 4375 { 4376 name: "Non-email prefix with correct field signal instructions", 4377 contacts: []string{`heliograph:STATION OF RECEPTION: High Ridge above Black Hollow, near Lone Pine. 4378 AZIMUTH TO SIGNAL STATION: Due West, bearing Twin Peaks. 4379 WATCH PERIOD: Third hour post-zenith; observation maintained for 30 minutes. 4380 SIGNAL CODE: Standard Morse, three-flash attention signal. 4381 ALTERNATE SITE: If no reply, move to Observation Point B at Broken Cairn.`}, 4382 expected: []string{}, 4383 }, 4384 } 4385 4386 for _, tc := range testCases { 4387 t.Run(tc.name, func(t *testing.T) { 4388 t.Parallel() 4389 4390 wfe, _, signer := setupWFE(t) 4391 4392 mockImpl := mocks.NewMockSalesforceClientImpl() 4393 wfe.ee = mocks.NewMockExporterImpl(mockImpl) 4394 4395 contactsJSON, err := json.Marshal(tc.contacts) 4396 test.AssertNotError(t, err, "Failed to marshal contacts") 4397 4398 payload := fmt.Sprintf(`{"contact":%s,"termsOfServiceAgreed":true}`, contactsJSON) 4399 _, _, body := signer.embeddedJWK(key, signedURL, payload) 4400 request := makePostRequestWithPath(path, body) 4401 4402 responseWriter := httptest.NewRecorder() 4403 wfe.NewAccount(context.Background(), newRequestEvent(), responseWriter, request) 4404 4405 for _, email := range tc.expected { 4406 test.AssertSliceContains(t, mockImpl.GetCreatedContacts(), email) 4407 } 4408 }) 4409 } 4410 }