Import code from internal repository to git

This commit is contained in:
Justine Tunney 2016-03-01 17:18:14 -05:00
commit 0ef0c933d2
2490 changed files with 281594 additions and 0 deletions

View file

@ -0,0 +1,38 @@
package(
default_visibility = ["//java/com/google/domain/registry:registry_project"],
)
java_library(
name = "tmch",
srcs = glob(["*.java"]),
resources = glob([
"*.crl",
"*.crt",
"*.asc",
]),
deps = [
"//java/com/google/common/annotations",
"//java/com/google/common/base",
"//java/com/google/common/collect",
"//java/com/google/common/io",
"//java/com/google/common/net",
"//java/com/google/common/util/concurrent",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/cron",
"//java/com/google/domain/registry/keyring/api",
"//java/com/google/domain/registry/model",
"//java/com/google/domain/registry/request",
"//java/com/google/domain/registry/util",
"//java/com/google/domain/registry/xml",
"//third_party/java/appengine:appengine-api",
"//third_party/java/bouncycastle",
"//third_party/java/bouncycastle_bcpg",
"//third_party/java/dagger",
"//third_party/java/joda_time",
"//third_party/java/jsr305_annotations",
"//third_party/java/jsr330_inject",
"//third_party/java/objectify:objectify-v4_1",
"//third_party/java/servlet/servlet_api",
],
)

View file

@ -0,0 +1,80 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.domain.registry.model.tmch.ClaimsListShard;
import org.joda.time.DateTime;
import java.util.List;
/**
* Claims List (MarksDB DNL CSV) Parser.
*
* <p>This is a quick and dirty CSV parser made specifically for the DNL CSV format defined in the
* TMCH specification. It doesn't support any fancy CSV features like quotes.
*
* @see "http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.1"
*/
public class ClaimsListParser {
/**
* Converts the lines from the DNL CSV file into a {@link ClaimsListShard} object.
*
* <p>Please note that this does <b>not</b> insert the object into the datastore.
*/
public static ClaimsListShard parse(List<String> lines) {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
// First line: <version>,<DNL List creation datetime>
List<String> firstLine = Splitter.on(',').splitToList(lines.get(0));
checkArgument(firstLine.size() == 2, String.format(
"Line 1: Expected 2 elements, found %d", firstLine.size()));
Integer version = Integer.valueOf(firstLine.get(0));
DateTime creationTime = DateTime.parse(firstLine.get(1));
checkArgument(version == 1, String.format(
"Line 1: Expected version 1, found %d", version));
// Second line contains headers: DNL,lookup-key,insertion-datetime
List<String> secondLine = Splitter.on(',').splitToList(lines.get(1));
checkArgument(secondLine.size() == 3, String.format(
"Line 2: Expected 3 elements, found %d", secondLine.size()));
checkArgument("DNL".equals(secondLine.get(0)), String.format(
"Line 2: Expected header \"DNL\", found \"%s\"", secondLine.get(0)));
checkArgument("lookup-key".equals(secondLine.get(1)), String.format(
"Line 2: Expected header \"lookup-key\", found \"%s\"", secondLine.get(1)));
checkArgument("insertion-datetime".equals(secondLine.get(2)), String.format(
"Line 2: Expected header \"insertion-datetime\", found \"%s\"", secondLine.get(2)));
// Subsequent lines: <DNL>,<lookup key>,<DNL insertion datetime>
for (int i = 2; i < lines.size(); i++) {
List<String> currentLine = Splitter.on(',').splitToList(lines.get(i));
checkArgument(currentLine.size() == 3, String.format(
"Line %d: Expected 3 elements, found %d", i + 1, currentLine.size()));
String label = currentLine.get(0);
String lookupKey = currentLine.get(1);
DateTime.parse(currentLine.get(2)); // This is the insertion time, currently unused.
builder.put(label, lookupKey);
}
return ClaimsListShard.create(creationTime, builder.build());
}
}

View file

@ -0,0 +1,257 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import org.joda.time.DateTime;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* Parser of LORDN log responses from the MarksDB server during the NORDN process.
*
* @see "http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3.1"
*/
@Immutable
public final class LordnLog implements Iterable<Entry<String, LordnLog.Result>> {
/** Indicates whether or not the LORDN upload succeeded. */
public enum Status { ACCEPTED, REJECTED }
/** Result code for individual DN lines. */
@Immutable
public static final class Result {
/** Outcome categories for individual DN lines. */
public enum Outcome { OK, WARNING, ERROR }
private final int code;
private final String description;
private final Outcome outcome;
private Result(int code, String description) {
this.code = code;
this.description = description;
if (2000 <= code && code <= 2099) {
this.outcome = Outcome.OK;
} else if (3500 <= code && code <= 3699) {
this.outcome = Outcome.WARNING;
} else if (4500 <= code && code <= 4699) {
this.outcome = Outcome.ERROR;
} else {
throw new IllegalArgumentException("Invalid DN result code: " + code);
}
}
public int getCode() {
return code;
}
public String getDescription() {
return description;
}
public Outcome getOutcome() {
return outcome;
}
@Override
public String toString() {
return toStringHelper(this)
.add("code", code)
.add("outcome", outcome)
.add("description", description)
.toString();
}
}
private static final Map<Integer, Result> RESULTS = ImmutableMap.<Integer, Result>builder()
.put(2000, new Result(2000, "OK"))
.put(2001, new Result(2001, "OK but not processed"))
.put(3601, new Result(3601, "TCN Acceptance Date after Registration Date"))
.put(3602, new Result(3602, "Duplicate DN Line"))
.put(3603, new Result(3603, "DNROID Notified Earlier"))
.put(3604, new Result(3604, "TCN Checksum invalid"))
.put(3605, new Result(3605, "TCN Expired"))
.put(3606, new Result(3606, "Wrong TCNID used"))
.put(3609, new Result(3609, "Invalid SMD used"))
.put(3610, new Result(3610, "DN reported outside of the time window"))
.put(3611, new Result(3611, "DN does not match the labels in SMD"))
.put(3612, new Result(3612, "SMDID does not exist"))
.put(3613, new Result(3613, "SMD was revoked when used"))
.put(3614, new Result(3614, "TCNID does not exist"))
.put(3615, new Result(3615, "Recent-dnl-insertion outside of the time window"))
.put(3616, new Result(3616, "Registration Date of DN in claims before the end of Sunrise"))
.put(3617, new Result(3617, "Registrar has not been approved by the TMDB"))
.put(4501, new Result(4501, "Syntax Error in DN Line"))
.put(4601, new Result(4601, "Invalid TLD used"))
.put(4602, new Result(4602, "Registrar ID Invalid"))
.put(4603, new Result(4603, "Registration Date in the future"))
.put(4606, new Result(4606, "TLD not in Sunrise or Claims"))
.put(4607, new Result(4607, "Application Date in the future"))
.put(4608, new Result(4608, "Application Date is later than Registration Date"))
.put(4609, new Result(4609, "TCNID wrong syntax"))
.put(4610, new Result(4610, "TCN Acceptance Date is in the future"))
.put(4611, new Result(4611, "Label has never existed in the TMDB"))
.build();
/** Base64 matcher between one and sixty characters. */
private static final Pattern LOG_ID_PATTERN = Pattern.compile("[-A-Za-z0-9+/=]{1,60}");
private final String logId;
private final Status status;
private final DateTime logCreation;
private final DateTime lordnCreation;
private final boolean hasWarnings;
private final ImmutableMap<String, Result> results;
private LordnLog(
String logId,
Status status,
DateTime logCreation,
DateTime lordnCreation,
boolean hasWarnings,
ImmutableMap<String, Result> results) {
this.logId = logId;
this.status = status;
this.logCreation = logCreation;
this.lordnCreation = lordnCreation;
this.hasWarnings = hasWarnings;
this.results = results;
}
public String getLogId() {
return logId;
}
public Status getStatus() {
return status;
}
public DateTime getLogCreation() {
return logCreation;
}
public DateTime getLordnCreation() {
return lordnCreation;
}
public boolean hasWarnings() {
return hasWarnings;
}
@Nullable
public Result getResult(String roid) {
return results.get(roid);
}
@Override
public Iterator<Entry<String, Result>> iterator() {
return results.entrySet().iterator();
}
@Override
public String toString() {
return toStringHelper(this)
.add("logId", logId)
.add("status", status)
.add("logCreation", logCreation)
.add("lordnCreation", lordnCreation)
.add("hasWarnings", hasWarnings)
.add("results", results)
.toString();
}
/** Turns lines of NORDN log returned by MarksDB into a data structure. */
public static LordnLog parse(List<String> lines) {
// First line: <version>,<LORDN Log creation datetime>,<LORDN file creation datetime>,
// <LORDN Log Identifier>,<Status flag>,<Warning flag>,<Number of DN Lines>
List<String> firstLine = Splitter.on(',').splitToList(lines.get(0));
checkArgument(firstLine.size() == 7, String.format(
"Line 1: Expected 7 elements, found %d", firstLine.size()));
// + <version>, version of the file, this field MUST be 1.
int version = Integer.parseInt(firstLine.get(0));
checkArgument(version == 1, String.format(
"Line 1: Expected version 1, found %d", version));
// + <LORDN Log creation datetime>, date and time in UTC that the
// LORDN Log was created.
DateTime logCreation = DateTime.parse(firstLine.get(1));
// + <LORDN file creation datetime>, date and time in UTC of
// creation for the LORDN file that this log file is referring
// to.
DateTime lordnCreation = DateTime.parse(firstLine.get(2));
// + <LORDN Log Identifier>, unique identifier of the LORDN Log
// provided by the TMDB. This identifier could be used by the
// Registry Operator to unequivocally identify the LORDN Log.
// The identified will be a string of a maximum LENGTH of 60
// characters from the Base 64 alphabet.
String logId = firstLine.get(3);
checkArgument(LOG_ID_PATTERN.matcher(logId).matches(),
"Line 1: Log ID does not match base64 pattern: %s", logId);
// + <Status flag>, whether the LORDN file has been accepted for
// processing by the TMDB. Possible values are "accepted" or
// "rejected".
Status status = Status.valueOf(firstLine.get(4).toUpperCase());
// + <Warning flag>, whether the LORDN Log has any warning result
// codes. Possible values are "no-warnings" or "warnings-
// present".
boolean hasWarnings = !"no-warnings".equals(firstLine.get(5));
// + <Number of DN Lines>, number of DNs effective allocations
// processed in the LORDN file.
int dnLines = Integer.parseInt(firstLine.get(6));
int actual = lines.size() - 2;
checkArgument(dnLines == actual,
"Line 1: Number of entries (%d) differs from declaration (%d)", actual, dnLines);
// Second line contains headers: roid,result-code
checkArgument(lines.get(1).equals("roid,result-code"),
"Line 2: Unexpected header list: %s", lines.get(1));
// Subsequent lines: <roid>,<result code>
ImmutableMap.Builder<String, Result> builder = new ImmutableMap.Builder<>();
for (int i = 2; i < lines.size(); i++) {
List<String> currentLine = Splitter.on(',').splitToList(lines.get(i));
checkArgument(currentLine.size() == 2, String.format(
"Line %d: Expected 2 elements, found %d", i + 1, currentLine.size()));
String roid = currentLine.get(0);
int code = Integer.parseInt(currentLine.get(1));
Result result = checkNotNull(RESULTS.get(code),
"Line %d: Unknown result code: %d", i, code);
builder.put(roid, result);
}
return new LordnLog(logId, status, logCreation, lordnCreation, hasWarnings, builder.build());
}
}

View file

@ -0,0 +1,48 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.common.base.Verify.verifyNotNull;
import static com.google.domain.registry.util.UrlFetchUtils.setAuthorizationHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.common.base.Optional;
import com.google.domain.registry.keyring.api.KeyModule.Key;
import com.google.domain.registry.model.registry.Registry;
import javax.inject.Inject;
/** Helper class for setting the authorization header on a MarksDB LORDN request. */
final class LordnRequestInitializer {
@Inject @Key("marksdbLordnPassword") Optional<String> marksdbLordnPassword;
@Inject LordnRequestInitializer() {}
/** Initializes a URL fetch request for talking to the MarksDB server. */
void initialize(HTTPRequest request, String tld) {
setAuthorizationHeader(request, getMarksDbLordnCredentials(tld));
}
/** Returns the username and password for the current TLD to login to the MarksDB server. */
private Optional<String> getMarksDbLordnCredentials(String tld) {
if (marksdbLordnPassword.isPresent()) {
String lordnUsername = verifyNotNull(Registry.get(tld).getLordnUsername(),
"lordnUsername is not set for %s.", Registry.get(tld).getTld());
return Optional.of(String.format("%s:%s", lordnUsername, marksdbLordnPassword.get()));
} else {
return Optional.absent();
}
}
}

View file

@ -0,0 +1,162 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.appengine.api.taskqueue.LeaseOptions;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.TaskOptions.Method;
import com.google.appengine.api.taskqueue.TransientFailureException;
import com.google.apphosting.api.DeadlineExceededException;
import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.Uninterruptibles;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.util.NonFinalForTesting;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Helper methods for creating tasks containing CSV line data in the lordn-sunrise and lordn-claims
* queues based on DomainResource changes.
*/
public class LordnTask {
public static final String QUEUE_SUNRISE = "lordn-sunrise";
public static final String QUEUE_CLAIMS = "lordn-claims";
public static final String COLUMNS_CLAIMS = "roid,domain-name,notice-id,registrar-id,"
+ "registration-datetime,ack-datetime,application-datetime";
public static final String COLUMNS_SUNRISE = "roid,domain-name,SMD-id,registrar-id,"
+ "registration-datetime,application-datetime";
private static final Duration LEASE_PERIOD = Duration.standardHours(1);
/** This is the max allowable batch size. */
private static final long BATCH_SIZE = 1000;
@NonFinalForTesting
private static Long backOffMillis = 2000L;
/**
* Converts a list of queue tasks, each containing a row of CSV data, into a single newline-
* delimited String.
*/
public static String convertTasksToCsv(List<TaskHandle> tasks, DateTime now, String columns) {
String header = String.format("1,%s,%d\n%s\n", now, tasks.size(), columns);
StringBuilder csv = new StringBuilder(header);
for (TaskHandle task : checkNotNull(tasks)) {
String payload = new String(task.getPayload());
if (!Strings.isNullOrEmpty(payload)) {
csv.append(payload).append("\n");
}
}
return csv.toString();
}
/** Leases and returns all tasks from the queue with the specified tag tld, in batches. */
public static List<TaskHandle> loadAllTasks(Queue queue, String tld) {
ImmutableList.Builder<TaskHandle> allTasks = new ImmutableList.Builder<>();
int numErrors = 0;
long backOff = backOffMillis;
while (true) {
try {
List<TaskHandle> tasks = queue.leaseTasks(LeaseOptions.Builder
.withTag(tld)
.leasePeriod(LEASE_PERIOD.getMillis(), TimeUnit.MILLISECONDS)
.countLimit(BATCH_SIZE));
allTasks.addAll(tasks);
if (tasks.isEmpty()) {
return allTasks.build();
}
} catch (TransientFailureException | DeadlineExceededException e) {
if (++numErrors >= 3) {
throw new RuntimeException("Error leasing tasks", e);
}
Uninterruptibles.sleepUninterruptibly(backOff, TimeUnit.MILLISECONDS);
backOff *= 2;
}
}
}
/**
* Enqueues a task in the LORDN queue representing a line of CSV for LORDN export.
*/
public static void enqueueDomainResourceTask(DomainResource domain) {
ofy().assertInTransaction();
// This method needs to use ofy transactionTime as the DomainResource's creationTime because
// CreationTime isn't yet populated when this method is called during the resource flow.
String tld = domain.getTld();
if (domain.getLaunchNotice() == null) {
getQueue(QUEUE_SUNRISE).add(TaskOptions.Builder
.withTag(tld)
.method(Method.PULL)
.payload(getCsvLineForSunriseDomain(domain, ofy().getTransactionTime())));
} else {
getQueue(QUEUE_CLAIMS).add(TaskOptions.Builder
.withTag(tld)
.method(Method.PULL)
.payload(getCsvLineForClaimsDomain(domain, ofy().getTransactionTime())));
}
}
/** Returns the corresponding CSV LORDN line for a sunrise domain. */
public static String getCsvLineForSunriseDomain(DomainResource domain, DateTime transactionTime) {
// Only skip nulls in the outer join because only application time is allowed to be null.
Joiner joiner = Joiner.on(',');
return joiner.skipNulls().join(
joiner.join(
domain.getRepoId(),
domain.getFullyQualifiedDomainName(),
domain.getSmdId(),
getIanaIdentifier(domain.getCreationClientId()),
transactionTime), // Used as creation time.
domain.getApplicationTime()); // This may be null for sunrise QLP domains.
}
/** Returns the corresponding CSV LORDN line for a claims domain. */
public static String getCsvLineForClaimsDomain(DomainResource domain, DateTime transactionTime) {
// Only skip nulls in the outer join because only application time is allowed to be null.
Joiner joiner = Joiner.on(',');
return joiner.skipNulls().join(
joiner.join(
domain.getRepoId(),
domain.getFullyQualifiedDomainName(),
domain.getLaunchNotice().getNoticeId().getTcnId(),
getIanaIdentifier(domain.getCreationClientId()),
transactionTime, // Used as creation time.
domain.getLaunchNotice().getAcceptedTime()),
domain.getApplicationTime()); // This may be null if this wasn't from landrush.
}
/** Retrieves the IANA identifier for a registrar based on the client id. */
private static String getIanaIdentifier(String clientId) {
Registrar registrar = checkNotNull(
Registrar.loadByClientId(clientId),
"No registrar found for client id: %s", clientId);
// Return the string "null" for null identifiers, since some Registrar.Types such as OTE will
// have null iana ids.
return String.valueOf(registrar.getIanaIdentifier());
}
}

View file

@ -0,0 +1,129 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate;
import static com.google.appengine.api.urlfetch.HTTPMethod.GET;
import static com.google.domain.registry.util.HexDumper.dumpHex;
import static com.google.domain.registry.util.UrlFetchUtils.setAuthorizationHeader;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.base.Optional;
import com.google.common.io.ByteSource;
import com.google.domain.registry.config.ConfigModule.Config;
import com.google.domain.registry.keyring.api.KeyModule.Key;
import com.google.domain.registry.util.UrlFetchException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URL;
import java.security.Security;
import java.security.SignatureException;
import java.util.List;
import javax.annotation.Tainted;
import javax.inject.Inject;
/** Shared code for tasks that download stuff from MarksDB. */
public final class Marksdb {
@Inject URLFetchService fetchService;
@Inject @Config("tmchMarksdbUrl") String tmchMarksdbUrl;
@Inject @Key("marksdbPublicKey") PGPPublicKey marksdbPublicKey;
@Inject Marksdb() {}
/**
* Extracts a {@link PGPSignature} object from a blob of {@code .sig} data.
*
* @throws SignatureException if a signature object couldn't be extracted for any reason.
*/
private static PGPSignature pgpExtractSignature(@Tainted byte[] signature)
throws SignatureException {
try {
ByteArrayInputStream input = new ByteArrayInputStream(signature);
PGPObjectFactory decoder = new BcPGPObjectFactory(PGPUtil.getDecoderStream(input));
Object object = decoder.nextObject();
if (object == null) {
throw new SignatureException(String.format(
"No OpenPGP packets found in signature.\n%s",
dumpHex(signature)));
}
if (!(object instanceof PGPSignatureList)) {
throw new SignatureException(String.format(
"Expected PGPSignatureList packet but got %s\n%s",
object.getClass().getSimpleName(),
dumpHex(signature)));
}
PGPSignatureList sigs = (PGPSignatureList) object;
if (sigs.isEmpty()) {
throw new SignatureException(String.format(
"PGPSignatureList doesn't have a PGPSignature.\n%s",
dumpHex(signature)));
}
return sigs.get(0);
} catch (IOException e) {
throw new SignatureException(String.format(
"Failed to extract PGPSignature object from .sig blob.\n%s",
dumpHex(signature)), e);
}
}
@SuppressWarnings("deprecation")
private static void pgpVerifySignature(byte[] data, byte[] signature, PGPPublicKey publicKey)
throws PGPException, SignatureException {
Security.addProvider(new BouncyCastleProvider());
PGPSignature sig = pgpExtractSignature(signature);
sig.init(new BcPGPContentVerifierBuilderProvider(), publicKey);
sig.update(data);
if (!sig.verify()) {
throw new SignatureException(String.format(
"MarksDB PGP signature verification failed.\n%s",
dumpHex(signature)));
}
}
byte[] fetch(URL url, Optional<String> login) throws IOException {
HTTPRequest req = new HTTPRequest(url, GET, validateCertificate().setDeadline(60d));
setAuthorizationHeader(req, login);
HTTPResponse rsp = fetchService.fetch(req);
if (rsp.getResponseCode() != SC_OK) {
throw new UrlFetchException("Failed to fetch from MarksDB", req, rsp);
}
return rsp.getContent();
}
List<String> fetchSignedCsv(
Optional<String> login, String csvPath, String sigPath)
throws IOException, SignatureException, PGPException {
byte[] csv = fetch(new URL(tmchMarksdbUrl + csvPath), login);
byte[] sig = fetch(new URL(tmchMarksdbUrl + sigPath), login);
pgpVerifySignature(csv, sig, marksdbPublicKey);
return ByteSource.wrap(csv).asCharSource(US_ASCII).readLines();
}
}

View file

@ -0,0 +1,165 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate;
import static com.google.appengine.api.urlfetch.HTTPMethod.POST;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.net.HttpHeaders.LOCATION;
import static com.google.common.net.MediaType.CSV_UTF_8;
import static com.google.domain.registry.tmch.LordnTask.COLUMNS_CLAIMS;
import static com.google.domain.registry.tmch.LordnTask.COLUMNS_SUNRISE;
import static com.google.domain.registry.tmch.LordnTask.convertTasksToCsv;
import static com.google.domain.registry.util.UrlFetchUtils.getHeaderFirst;
import static com.google.domain.registry.util.UrlFetchUtils.setPayloadMultipart;
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.base.Optional;
import com.google.domain.registry.config.ConfigModule.Config;
import com.google.domain.registry.request.Action;
import com.google.domain.registry.request.Parameter;
import com.google.domain.registry.request.RequestParameters;
import com.google.domain.registry.util.Clock;
import com.google.domain.registry.util.FormattingLogger;
import com.google.domain.registry.util.UrlFetchException;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.Random;
import javax.inject.Inject;
/**
* Action that reads the NORDN pull queues, uploads claims and sunrise marks data to TMCH, and
* enqueues subsequent upload verification tasks. A unique actionLogId is generated and passed
* along to the verify action so that connected verify tasks can be identified by looking at logs.
*
* @see NordnVerifyAction
*/
@Action(path = NordnUploadAction.PATH, method = Action.Method.POST, automaticallyPrintOk = true)
public final class NordnUploadAction implements Runnable {
static final String PATH = "/_dr/task/nordnUpload";
static final String LORDN_PHASE_PARAM = "lordn-phase";
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
/**
* A unique (enough) id that is outputted in log lines to make it clear which log lines are
* associated with a given invocation of the NordnUploadAction in the event that multiple
* instances execute simultaneously.
*/
private final String actionLogId = String.valueOf(1000000000 + new Random().nextInt(1000000000));
@Inject Clock clock;
@Inject LordnRequestInitializer lordnRequestInitializer;
@Inject URLFetchService fetchService;
@Inject @Config("tmchMarksdbUrl") String tmchMarksdbUrl;
@Inject @Parameter(LORDN_PHASE_PARAM) String phase;
@Inject @Parameter(RequestParameters.PARAM_TLD) String tld;
@Inject NordnUploadAction() {}
/**
* These LORDN parameter names correspond to the relative paths in LORDN URLs and cannot be
* changed on our end.
*/
private static final String PARAM_LORDN_PHASE_SUNRISE = "sunrise";
private static final String PARAM_LORDN_PHASE_CLAIMS = "claims";
/** How long to wait before attempting to verify an upload by fetching the log. */
private static final Duration VERIFY_DELAY = Duration.standardMinutes(30);
@Override
public void run() {
try {
processLordnTasks();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void processLordnTasks() throws IOException {
checkArgument(phase.equals(PARAM_LORDN_PHASE_SUNRISE)
|| phase.equals(PARAM_LORDN_PHASE_CLAIMS),
"Invalid phase specified to Nordn servlet: %s.", phase);
DateTime now = clock.nowUtc();
Queue queue = getQueue(
phase.equals(PARAM_LORDN_PHASE_SUNRISE) ? LordnTask.QUEUE_SUNRISE : LordnTask.QUEUE_CLAIMS);
String columns = phase.equals(PARAM_LORDN_PHASE_SUNRISE) ? COLUMNS_SUNRISE : COLUMNS_CLAIMS;
List<TaskHandle> tasks = LordnTask.loadAllTasks(queue, tld);
if (!tasks.isEmpty()) {
String csvData = convertTasksToCsv(tasks, now, columns);
uploadCsvToLordn(String.format("/LORDN/%s/%s", tld, phase), csvData);
queue.deleteTask(tasks);
}
}
/**
* Upload LORDN file to MarksDB.
*
* <p>Idempotency: If the exact same LORDN report is uploaded twice, the MarksDB server will
* return the same confirmation number.
*
* @see "http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3"
*/
private void uploadCsvToLordn(String urlPath, String csvData) throws IOException {
String url = tmchMarksdbUrl + urlPath;
logger.infofmt("LORDN upload task %s: Sending to URL: %s ; data: %s",
actionLogId, url, csvData);
HTTPRequest req = new HTTPRequest(new URL(url), POST, validateCertificate().setDeadline(60d));
lordnRequestInitializer.initialize(req, tld);
setPayloadMultipart(req, "file", "claims.csv", CSV_UTF_8, csvData);
HTTPResponse rsp = fetchService.fetch(req);
logger.infofmt("LORDN upload task %s response: HTTP response code %d, response data: %s",
actionLogId, rsp.getResponseCode(), rsp.getContent());
if (rsp.getResponseCode() != SC_ACCEPTED) {
throw new UrlFetchException(
String.format("LORDN upload task %s error: Failed to upload LORDN claims to MarksDB",
actionLogId),
req, rsp);
}
Optional<String> location = getHeaderFirst(rsp, LOCATION);
if (!location.isPresent()) {
throw new UrlFetchException(
String.format("LORDN upload task %s error: MarksDB failed to provide a Location header",
actionLogId),
req, rsp);
}
getQueue(NordnVerifyAction.QUEUE).add(makeVerifyTask(new URL(location.get()), csvData));
}
private TaskOptions makeVerifyTask(URL url, String csvData) {
// This task doesn't technically need csvData. The only reason it's passed along is in case the
// upload is rejected, in which case csvData will be logged so that it may be uploaded manually.
return withUrl(NordnVerifyAction.PATH)
.header(NordnVerifyAction.URL_HEADER, url.toString())
.header(NordnVerifyAction.HEADER_ACTION_LOG_ID, actionLogId)
.param(RequestParameters.PARAM_TLD, tld)
.param(NordnVerifyAction.PARAM_CSV_DATA, csvData)
.countdownMillis(VERIFY_DELAY.getMillis());
}
}

View file

@ -0,0 +1,141 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate;
import static com.google.appengine.api.urlfetch.HTTPMethod.GET;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteSource;
import com.google.domain.registry.request.Action;
import com.google.domain.registry.request.Header;
import com.google.domain.registry.request.HttpException.BadRequestException;
import com.google.domain.registry.request.HttpException.ConflictException;
import com.google.domain.registry.request.Parameter;
import com.google.domain.registry.request.RequestParameters;
import com.google.domain.registry.request.Response;
import com.google.domain.registry.util.FormattingLogger;
import com.google.domain.registry.util.UrlFetchException;
import java.io.IOException;
import java.net.URL;
import java.util.Map.Entry;
import javax.inject.Inject;
/**
* NORDN CSV uploading system, verify operation.
*
* <p>Every three hours (max twenty-six hours) we generate CSV files for each TLD which we need
* to upload to MarksDB. The upload is a two-phase process. We send the CSV data as a POST request
* and get back a 202 Accepted. This response will give us a URL in the Location header, where
* we'll check back later for the actual result.
*
* @see NordnUploadAction
* @see <a href="http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-5.2.3.3">
* http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-5.2.3.3</a>
*/
@Action(path = NordnVerifyAction.PATH, method = Action.Method.POST, automaticallyPrintOk = true)
public final class NordnVerifyAction implements Runnable {
public static final String PARAM_CSV_DATA = "csvData";
static final String PATH = "/_dr/task/nordnVerify";
static final String QUEUE = "marksdb";
static final String URL_HEADER = "X-DomainRegistry-Nordn-Url";
static final String HEADER_ACTION_LOG_ID = "X-DomainRegistry-ActionLogId";
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
@Inject LordnRequestInitializer lordnRequestInitializer;
@Inject Response response;
@Inject URLFetchService fetchService;
@Inject @Header(URL_HEADER) URL url;
@Inject @Header(HEADER_ACTION_LOG_ID) String actionLogId;
@Inject @Parameter(RequestParameters.PARAM_TLD) String tld;
@Inject @Parameter(PARAM_CSV_DATA) String csvData;
@Inject NordnVerifyAction() {}
@Override
public void run() {
try {
verify();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Fetch LORDN log file from MarksDB to confirm successful upload.
*
* <p>Idempotency: The confirmation URL will always return the same result once it becomes
* available.
*
* @throws ConflictException if MarksDB has not yet finished processing the LORDN upload
* @see "http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3.1"
*/
@VisibleForTesting
LordnLog verify() throws IOException {
if (csvData.isEmpty()) {
throw new BadRequestException(
String.format("LORDN verify task %s: Missing CSV payload.", actionLogId));
}
logger.infofmt("LORDN verify task %s: Sending request to URL %s.", actionLogId, url);
HTTPRequest req = new HTTPRequest(url, GET, validateCertificate().setDeadline(60d));
lordnRequestInitializer.initialize(req, tld);
HTTPResponse rsp = fetchService.fetch(req);
logger.infofmt("LORDN verify task %s response: HTTP response code %d, response data: %s",
actionLogId, rsp.getResponseCode(), rsp.getContent());
if (rsp.getResponseCode() == SC_NO_CONTENT) {
// Send a 400+ status code so App Engine will retry the task.
throw new ConflictException("Not ready");
}
if (rsp.getResponseCode() != SC_OK) {
throw new UrlFetchException(
String.format("LORDN verify task %s: Failed to verify LORDN upload to MarksDB.",
actionLogId),
req, rsp);
}
LordnLog log =
LordnLog.parse(ByteSource.wrap(rsp.getContent()).asCharSource(UTF_8).readLines());
if (log.getStatus() == LordnLog.Status.ACCEPTED) {
logger.infofmt("LORDN verify task %s: Upload accepted", actionLogId);
} else {
logger.severefmt("LORDN verify task %s: Upload rejected with reason: %s", actionLogId, log);
}
for (Entry<String, LordnLog.Result> result : log) {
switch (result.getValue().getOutcome()) {
case OK:
break;
case WARNING:
// fall through
case ERROR:
logger.warning(result.toString());
break;
default:
logger.warningfmt("LORDN verify task %s: Unexpected outcome: %s",
actionLogId, result.toString());
break;
}
}
return log;
}
}

View file

@ -0,0 +1,72 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.common.base.Preconditions.checkArgument;
import static org.joda.time.DateTimeZone.UTC;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.domain.registry.model.smd.SignedMarkRevocationList;
import org.joda.time.DateTime;
import java.util.List;
/**
* Signed Mark Data Revocation List (SMDRL) CSV Parser
*
* <p>This is a quick and dirty CSV parser made specifically for the SMDRL CSV format defined in
* the TMCH specification. It doesn't support any fancy CSV features like quotes.
*
* @see "http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.2"
*/
public final class SmdrlCsvParser {
/** Converts the lines from the DNL CSV file into a data structure. */
public static SignedMarkRevocationList parse(List<String> lines) {
ImmutableMap.Builder<String, DateTime> revokes = new ImmutableMap.Builder<>();
// First line: <version>,<SMD Revocation List creation datetime>
List<String> firstLine = Splitter.on(',').splitToList(lines.get(0));
checkArgument(firstLine.size() == 2, String.format(
"Line 1: Expected 2 elements, found %d", firstLine.size()));
Integer version = Integer.valueOf(firstLine.get(0));
checkArgument(version == 1, String.format(
"Line 1: Expected version 1, found %d", version));
DateTime creationTime = DateTime.parse(firstLine.get(1)).withZone(UTC);
// Second line contains headers: smd-id,insertion-datetime
List<String> secondLine = Splitter.on(',').splitToList(lines.get(1));
checkArgument(secondLine.size() == 2, String.format(
"Line 2: Expected 2 elements, found %d", secondLine.size()));
checkArgument("smd-id".equals(secondLine.get(0)), String.format(
"Line 2: Expected header \"smd-id\", found \"%s\"", secondLine.get(0)));
checkArgument("insertion-datetime".equals(secondLine.get(1)), String.format(
"Line 2: Expected header \"insertion-datetime\", found \"%s\"", secondLine.get(1)));
// Subsequent lines: <smd-id>,<revoked SMD datetime>
for (int i = 2; i < lines.size(); i++) {
List<String> currentLine = Splitter.on(',').splitToList(lines.get(i));
checkArgument(currentLine.size() == 2, String.format(
"Line %d: Expected 2 elements, found %d", i + 1, currentLine.size()));
String smdId = currentLine.get(0);
DateTime revokedTime = DateTime.parse(currentLine.get(1));
revokes.put(smdId, revokedTime);
}
return SignedMarkRevocationList.create(creationTime, revokes.build());
}
}

View file

@ -0,0 +1,143 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.common.base.Throwables.propagateIfInstanceOf;
import static com.google.domain.registry.util.CacheUtils.memoizeWithLongExpiration;
import static com.google.domain.registry.util.CacheUtils.memoizeWithShortExpiration;
import static com.google.domain.registry.util.ResourceUtils.readResourceUtf8;
import static com.google.domain.registry.util.X509Utils.loadCrl;
import com.google.common.base.Supplier;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.model.tmch.TmchCrl;
import com.google.domain.registry.util.Clock;
import com.google.domain.registry.util.NonFinalForTesting;
import com.google.domain.registry.util.SystemClock;
import com.google.domain.registry.util.X509Utils;
import java.security.GeneralSecurityException;
import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.ThreadSafe;
/** Datastore singleton for ICANN's TMCH root certificate and revocation list. */
@Immutable
@ThreadSafe
public final class TmchCertificateAuthority {
private static final RegistryEnvironment ENVIRONMENT = RegistryEnvironment.get();
private static final String ROOT_CRT_FILE = "icann-tmch.crt";
private static final String TEST_ROOT_CRT_FILE = "icann-tmch-test.crt";
private static final String CRL_FILE = "icann-tmch.crl";
private static final String TEST_CRL_FILE = "icann-tmch-test.crl";
/**
* A cached supplier that loads the crl from datastore or chooses a default value.
* <p>
* We keep the cache here rather than caching TmchCrl in the model, because loading the crl string
* into an X509CRL instance is expensive and should itself be cached.
*/
private static final Supplier<X509CRL> CRL_CACHE =
memoizeWithShortExpiration(new Supplier<X509CRL>() {
@Override
public X509CRL get() {
TmchCrl storedCrl = TmchCrl.get();
try {
X509CRL crl = loadCrl((storedCrl == null)
? readResourceUtf8(
TmchCertificateAuthority.class,
ENVIRONMENT.config().getTmchCaTestingMode() ? TEST_CRL_FILE : CRL_FILE)
: storedCrl.getCrl());
crl.verify(getRoot().getPublicKey());
return crl;
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}});
/** A cached function that loads the crt from a jar resource. */
private static final Supplier<X509Certificate> ROOT_CACHE =
memoizeWithLongExpiration(new Supplier<X509Certificate>() {
@Override
public X509Certificate get() {
try {
X509Certificate root = X509Utils.loadCertificate(readResourceUtf8(
TmchCertificateAuthority.class,
ENVIRONMENT.config().getTmchCaTestingMode() ? TEST_ROOT_CRT_FILE : ROOT_CRT_FILE));
root.checkValidity(clock.nowUtc().toDate());
return root;
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}});
@NonFinalForTesting
private static Clock clock = new SystemClock();
/**
* Check that {@code cert} is signed by the ICANN TMCH CA root and not revoked.
*
* <p>Support for certificate chains has not been implemented.
*
* @throws GeneralSecurityException for unsupported protocols, certs not signed by the TMCH,
* incorrect keys, and for invalid, old, not-yet-valid or revoked certificates.
* @see X509Utils#verifyCertificate
*/
public static void verify(X509Certificate cert) throws GeneralSecurityException {
synchronized (TmchCertificateAuthority.class) {
X509Utils.verifyCertificate(getRoot(), getCrl(), cert, clock.nowUtc().toDate());
}
}
/**
* Update to the latest TMCH X.509 certificate revocation list and save to the datastore.
*
* <p>Your ASCII-armored CRL must be signed by the current ICANN root certificate.
*
* <p>This will not take effect (either on this instance or on others) until the CRL_CACHE next
* refreshes itself.
*
* @throws GeneralSecurityException for unsupported protocols, certs not signed by the TMCH,
* incorrect keys, and for invalid, old, not-yet-valid or revoked certificates.
* @see X509Utils#verifyCrl
*/
public static void updateCrl(String asciiCrl) throws GeneralSecurityException {
X509CRL crl = X509Utils.loadCrl(asciiCrl);
X509Utils.verifyCrl(getRoot(), getCrl(), crl, clock.nowUtc().toDate());
TmchCrl.set(asciiCrl);
}
public static X509Certificate getRoot() throws GeneralSecurityException {
try {
return ROOT_CACHE.get();
} catch (RuntimeException e) {
propagateIfInstanceOf(e.getCause(), GeneralSecurityException.class);
throw e;
}
}
public static X509CRL getCrl() throws GeneralSecurityException {
try {
return CRL_CACHE.get();
} catch (RuntimeException e) {
propagateIfInstanceOf(e.getCause(), GeneralSecurityException.class);
throw e;
}
}
}

View file

@ -0,0 +1,48 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.domain.registry.request.Action.Method.POST;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Optional;
import com.google.domain.registry.config.ConfigModule.Config;
import com.google.domain.registry.request.Action;
import java.io.IOException;
import java.net.URL;
import java.security.GeneralSecurityException;
import javax.inject.Inject;
/** Task to download the latest ICANN TMCH CRL from MarksDB. */
@Action(path = "/_dr/task/tmchCrl", method = POST, automaticallyPrintOk = true)
public final class TmchCrlTask implements Runnable {
@Inject Marksdb marksdb;
@Inject @Config("tmchCrlUrl") URL tmchCrlUrl;
@Inject TmchCrlTask() {}
/** Synchronously fetches latest ICANN TMCH CRL and saves it to datastore. */
@Override
public void run() {
try {
TmchCertificateAuthority
.updateCrl(new String(marksdb.fetch(tmchCrlUrl, Optional.<String>absent()), UTF_8));
} catch (IOException | GeneralSecurityException e) {
throw new RuntimeException("Failed to update ICANN TMCH CRL.", e);
}
}
}

View file

@ -0,0 +1,54 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.common.base.CharMatcher.whitespace;
import com.google.common.io.ByteSource;
import com.google.domain.registry.model.smd.EncodedSignedMark;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRing;
import java.io.IOException;
import java.io.InputStream;
/** Helper class for common data loaded from the jar and datastore at runtime. */
public final class TmchData {
private static final String BEGIN_ENCODED_SMD = "-----BEGIN ENCODED SMD-----";
private static final String END_ENCODED_SMD = "-----END ENCODED SMD-----";
@SuppressWarnings("deprecation")
static PGPPublicKey loadPublicKey(ByteSource pgpPublicKeyFile) {
try (InputStream input = pgpPublicKeyFile.openStream();
InputStream decoder = PGPUtil.getDecoderStream(input)) {
return new BcPGPPublicKeyRing(decoder).getPublicKey();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/** Extracts encoded SMD from an ASCII-armored string. */
public static EncodedSignedMark readEncodedSignedMark(String data) {
int beginTagIndex = data.indexOf(BEGIN_ENCODED_SMD);
int endTagIndex = data.indexOf(END_ENCODED_SMD);
if (beginTagIndex >= 0 && endTagIndex >= 0) {
data = data.substring(beginTagIndex + BEGIN_ENCODED_SMD.length(), endTagIndex);
}
return EncodedSignedMark.create("base64", whitespace().removeFrom(data));
}
}

View file

@ -0,0 +1,59 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.domain.registry.request.Action.Method.POST;
import com.google.common.base.Optional;
import com.google.domain.registry.keyring.api.KeyModule.Key;
import com.google.domain.registry.model.tmch.ClaimsListShard;
import com.google.domain.registry.request.Action;
import com.google.domain.registry.util.FormattingLogger;
import org.bouncycastle.openpgp.PGPException;
import java.io.IOException;
import java.security.SignatureException;
import java.util.List;
import javax.inject.Inject;
/** Task to download the latest domain name list (aka claims list) from MarksDB. */
@Action(path = "/_dr/task/tmchDnl", method = POST, automaticallyPrintOk = true)
public final class TmchDnlTask implements Runnable {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
private static final String DNL_CSV_PATH = "/dnl/dnl-latest.csv";
private static final String DNL_SIG_PATH = "/dnl/dnl-latest.sig";
@Inject Marksdb marksdb;
@Inject @Key("marksdbDnlLogin") Optional<String> marksdbDnlLogin;
@Inject TmchDnlTask() {}
/** Synchronously fetches latest domain name list and saves it to datastore. */
@Override
public void run() {
List<String> lines;
try {
lines = marksdb.fetchSignedCsv(marksdbDnlLogin, DNL_CSV_PATH, DNL_SIG_PATH);
} catch (SignatureException | IOException | PGPException e) {
throw new RuntimeException(e);
}
ClaimsListShard claims = ClaimsListParser.parse(lines);
claims.save();
logger.infofmt("Inserted %,d claims into datastore, created at %s",
claims.size(), claims.getCreationTime());
}
}

View file

@ -0,0 +1,77 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.common.io.Resources.asByteSource;
import static com.google.common.io.Resources.getResource;
import static com.google.domain.registry.request.RequestParameters.extractRequiredHeader;
import static com.google.domain.registry.request.RequestParameters.extractRequiredParameter;
import com.google.domain.registry.keyring.api.KeyModule.Key;
import com.google.domain.registry.request.Header;
import com.google.domain.registry.request.HttpException.BadRequestException;
import com.google.domain.registry.request.Parameter;
import dagger.Module;
import dagger.Provides;
import org.bouncycastle.openpgp.PGPPublicKey;
import java.net.MalformedURLException;
import java.net.URL;
import javax.servlet.http.HttpServletRequest;
/** Dagger module for TMCH package. */
@Module
public final class TmchModule {
private static final PGPPublicKey MARKSDB_PUBLIC_KEY = TmchData
.loadPublicKey(asByteSource(getResource(TmchModule.class, "marksdb-public-key.asc")));
@Provides
@Key("marksdbPublicKey")
static PGPPublicKey getMarksdbPublicKey() {
return MARKSDB_PUBLIC_KEY;
}
@Provides
@Parameter(NordnUploadAction.LORDN_PHASE_PARAM)
static String provideLordnPhase(HttpServletRequest req) {
return extractRequiredParameter(req, NordnUploadAction.LORDN_PHASE_PARAM);
}
@Provides
@Header(NordnVerifyAction.URL_HEADER)
static URL provideUrl(HttpServletRequest req) {
try {
return new URL(extractRequiredHeader(req, NordnVerifyAction.URL_HEADER));
} catch (MalformedURLException e) {
throw new BadRequestException("Bad URL: " + NordnVerifyAction.URL_HEADER);
}
}
@Provides
@Header(NordnVerifyAction.HEADER_ACTION_LOG_ID)
static String provideActionLogId(HttpServletRequest req) {
return extractRequiredHeader(req, NordnVerifyAction.HEADER_ACTION_LOG_ID);
}
@Provides
@Parameter(NordnVerifyAction.PARAM_CSV_DATA)
static String provideCsvData(HttpServletRequest req) {
return extractRequiredParameter(req, NordnVerifyAction.PARAM_CSV_DATA);
}
}

View file

@ -0,0 +1,59 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.domain.registry.request.Action.Method.POST;
import com.google.common.base.Optional;
import com.google.domain.registry.keyring.api.KeyModule.Key;
import com.google.domain.registry.model.smd.SignedMarkRevocationList;
import com.google.domain.registry.request.Action;
import com.google.domain.registry.util.FormattingLogger;
import org.bouncycastle.openpgp.PGPException;
import java.io.IOException;
import java.security.SignatureException;
import java.util.List;
import javax.inject.Inject;
/** Task to download the latest signed mark revocation list from MarksDB. */
@Action(path = "/_dr/task/tmchSmdrl", method = POST, automaticallyPrintOk = true)
public final class TmchSmdrlTask implements Runnable {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
private static final String SMDRL_CSV_PATH = "/smdrl/smdrl-latest.csv";
private static final String SMDRL_SIG_PATH = "/smdrl/smdrl-latest.sig";
@Inject Marksdb marksdb;
@Inject @Key("marksdbSmdrlLogin") Optional<String> marksdbSmdrlLogin;
@Inject TmchSmdrlTask() {}
/** Synchronously fetches latest signed mark revocation list and saves it to datastore. */
@Override
public void run() {
List<String> lines;
try {
lines = marksdb.fetchSignedCsv(marksdbSmdrlLogin, SMDRL_CSV_PATH, SMDRL_SIG_PATH);
} catch (SignatureException | IOException | PGPException e) {
throw new RuntimeException(e);
}
SignedMarkRevocationList smdrl = SmdrlCsvParser.parse(lines);
smdrl.save();
logger.infofmt("Inserted %,d smd revocations into datastore, created at %s",
smdrl.size(), smdrl.getCreationTime());
}
}

View file

@ -0,0 +1,192 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.tmch;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.getRootCause;
import static com.google.common.base.Throwables.propagateIfInstanceOf;
import static com.google.domain.registry.xml.XmlTransformer.loadXmlSchemas;
import com.google.common.collect.ImmutableList;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import javax.xml.crypto.AlgorithmMethod;
import javax.xml.crypto.KeySelector;
import javax.xml.crypto.KeySelectorException;
import javax.xml.crypto.KeySelectorResult;
import javax.xml.crypto.MarshalException;
import javax.xml.crypto.XMLCryptoContext;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.validation.Schema;
/** Helper class for verifying TMCH certificates and XML signatures. */
@ThreadSafe
public final class TmchXmlSignature {
private static final Schema SCHEMA =
loadXmlSchemas(ImmutableList.of("mark.xsd", "dsig.xsd", "smd.xsd"));
/**
* Verifies that signed mark data contains a valid signature.
*
* <p>This method DOES NOT check if the SMD ID is revoked. It's only concerned with the
* cryptographic stuff.
*
* @throws GeneralSecurityException for unsupported protocols, certs not signed by the TMCH,
* incorrect keys, and for invalid, old, not-yet-valid or revoked certificates.
* @throws IOException
* @throws MarshalException
* @throws ParserConfigurationException
* @throws SAXException
*/
public static void verify(byte[] smdXml)
throws GeneralSecurityException,
IOException,
MarshalException,
ParserConfigurationException,
SAXException,
XMLSignatureException {
checkArgument(smdXml.length > 0);
Document doc = parseSmdDocument(new ByteArrayInputStream(smdXml));
NodeList signatureNodes = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
if (signatureNodes.getLength() != 1) {
throw new XMLSignatureException("Expected exactly one <ds:Signature> element.");
}
XMLSignatureFactory factory = XMLSignatureFactory.getInstance("DOM");
KeyValueKeySelector selector = new KeyValueKeySelector();
DOMValidateContext context = new DOMValidateContext(selector, signatureNodes.item(0));
XMLSignature signature = factory.unmarshalXMLSignature(context);
boolean isValid;
try {
isValid = signature.validate(context);
} catch (XMLSignatureException e) {
Throwable cause = getRootCause(e);
propagateIfInstanceOf(cause, GeneralSecurityException.class);
throw e;
}
if (!isValid) {
throw new XMLSignatureException(explainValidationProblem(context, signature));
}
}
private static Document parseSmdDocument(InputStream input)
throws SAXException, IOException, ParserConfigurationException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setSchema(SCHEMA);
dbf.setAttribute("http://apache.org/xml/features/validation/schema/normalized-value", false);
dbf.setNamespaceAware(true);
return dbf.newDocumentBuilder().parse(input);
}
private static String explainValidationProblem(
DOMValidateContext context, XMLSignature signature)
throws XMLSignatureException {
@SuppressWarnings("unchecked") // Safe by specification.
List<Reference> references = signature.getSignedInfo().getReferences();
StringBuilder builder = new StringBuilder();
builder.append("Signature failed core validation\n");
boolean sv = signature.getSignatureValue().validate(context);
builder.append("Signature validation status: " + sv + "\n");
for (Reference ref : references) {
builder.append("references[");
builder.append(ref.getURI());
builder.append("] validity status: ");
builder.append(ref.validate(context));
builder.append("\n");
}
return builder.toString();
}
/** Callback class for DOM validator checks validity of {@code <ds:KeyInfo>} elements. */
private static final class KeyValueKeySelector extends KeySelector {
@Nullable
@Override
public KeySelectorResult select(
@Nullable KeyInfo keyInfo,
KeySelector.Purpose purpose,
AlgorithmMethod method,
XMLCryptoContext context) throws KeySelectorException {
if (keyInfo == null) {
return null;
}
for (Object keyInfoChild : keyInfo.getContent()) {
if (keyInfoChild instanceof X509Data) {
X509Data x509Data = (X509Data) keyInfoChild;
for (Object x509DataChild : x509Data.getContent()) {
if (x509DataChild instanceof X509Certificate) {
X509Certificate cert = (X509Certificate) x509DataChild;
try {
TmchCertificateAuthority.verify(cert);
} catch (SignatureException e) {
throw new KeySelectorException(new CertificateSignatureException(e.getMessage()));
} catch (GeneralSecurityException e) {
throw new KeySelectorException(e);
}
return new SimpleKeySelectorResult(cert.getPublicKey());
}
}
}
}
throw new KeySelectorException("No public key found.");
}
}
/** @see TmchXmlSignature.KeyValueKeySelector */
private static class SimpleKeySelectorResult implements KeySelectorResult {
private PublicKey publicKey;
SimpleKeySelectorResult(PublicKey publicKey) {
this.publicKey = checkNotNull(publicKey, "publicKey");
}
@Override
public java.security.Key getKey() {
return publicKey;
}
}
/** CertificateException wrapper. */
public static class CertificateSignatureException extends CertificateException {
public CertificateSignatureException(String message) {
super(message);
}
}
}

View file

@ -0,0 +1,15 @@
-----BEGIN X509 CRL-----
MIICVDCCATwCAQEwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxPDA6BgNV
BAoTM0ludGVybmV0IENvcnBvcmF0aW9uIGZvciBBc3NpZ25lZCBOYW1lcyBhbmQg
TnVtYmVyczEvMC0GA1UEAxMmSUNBTk4gVHJhZGVtYXJrIENsZWFyaW5naG91c2Ug
UGlsb3QgQ0EXDTEzMDcwOTAwMDAwMFoXDTE4MDYyNTIzNTk1OVowWzBZAiAusBt6
+hp7nbLd/oLa0HRKfentcmbRPTYWiDEoJ82FexcNMTMwNzA5MjIwMzIwWjAmMAoG
A1UdFQQDCgEBMBgGA1UdGAQRGA8yMDEzMDcwOTIxMjcwMFqgLzAtMB8GA1UdIwQY
MBaAFMOtPqbWEQBFgFw6V0qKbdwxDZ5xMAoGA1UdFAQDAgECMA0GCSqGSIb3DQEB
CwUAA4IBAQCa3ZHr/qihqZ/M6Eo9SQ2G2dkvT6cs1L71YkiGmCpZdWvYm99sJ3yj
iLe00vEyLWCPcq/qmgmhM0+Ou8ZHZ2nw2f4mzkjRwpzUn8oNMd5FHwlhpcRXHyjH
DhPbX1a5xITPZj9UUq0Lhk+bciCtC+G/jtEIuaIWL5bW1KER+FxI3Tt3888xx17W
0QoQiXEHltjl9zTj09YiVb4usGOQysLzAbhnyG5IBDBn11gWwx/g7rcXFO/z0KIp
h3rKk5noar/kpp/qAzNDyByMfD2uJtHnxHLrafsK3HY6CDLCp6GqLNSA5zxSjVOq
3sEaZYxI0Fg5DqBuN7efSCbM9bwFTYMy
-----END X509 CRL-----

View file

@ -0,0 +1,26 @@
-----BEGIN CERTIFICATE-----
MIIEVjCCAz6gAwIBAgIgLrAbevoae52y3f6C2tB0Sn3p7XJm0T02FogxKCfNhXkw
DQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxPDA6BgNVBAoTM0ludGVybmV0
IENvcnBvcmF0aW9uIGZvciBBc3NpZ25lZCBOYW1lcyBhbmQgTnVtYmVyczEvMC0G
A1UEAxMmSUNBTk4gVHJhZGVtYXJrIENsZWFyaW5naG91c2UgUGlsb3QgQ0EwHhcN
MTMwNjI2MDAwMDAwWhcNMjMwNjI1MjM1OTU5WjB8MQswCQYDVQQGEwJVUzE8MDoG
A1UEChMzSW50ZXJuZXQgQ29ycG9yYXRpb24gZm9yIEFzc2lnbmVkIE5hbWVzIGFu
ZCBOdW1iZXJzMS8wLQYDVQQDEyZJQ0FOTiBUcmFkZW1hcmsgQ2xlYXJpbmdob3Vz
ZSBQaWxvdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMJiRqFg
iCoDF8zMJMKHPMEuSpjbEl9ZWII+1WawDyt+jw841HsTT+6MwZsqExbQvukgvnuS
lA3Rg3xTFxodMaVZWsVQJy2PXGHVFRLnCp05DYZsMGZabuN9mIekYwtjePo89Lz0
JtU3ibL3squGG3gg6TLtPjks7Txm18BYPOYLznui32GUz+1aIZuk2p5A/rSldsh3
bke68IX5WZhKuIxT0+BjS8yfLWI0HCUs71WVxzvlJ1v22/eMK0WEA6+ZhCbOKIav
VtGNJrwIYwhZmxqfiR1HzHTLvrV0SLlJ2bwNk/yzKm8IJfuFezQ5BBtQ2RS9opFX
X8ft3v+uQQQvi+MCAwEAAaOBwzCBwDASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud
DgQWBBTDrT6m1hEARYBcOldKim3cMQ2ecTAOBgNVHQ8BAf8EBAMCAQYwNAYDVR0f
BC0wKzApoCegJYYjaHR0cDovL2NybC5pY2Fubi5vcmcvdG1jaF9waWxvdC5jcmww
RQYDVR0gBD4wPDA6BgMqAwQwMzAxBggrBgEFBQcCARYlaHR0cDovL3d3dy5pY2Fu
bi5vcmcvcGlsb3RfcmVwb3NpdG9yeTANBgkqhkiG9w0BAQsFAAOCAQEAKUfEJ5X6
QAttajjRVseJFQxRXGHTgCaDk8C/1nj1ielZAuZtgdUpWDUr0NnGCi+LHSsgdTYR
+vMrxir7EVYQevrBobELkxeTEfjF9FVqjBHInyPFLOFkz15zGG2IwPJps+vhAd/7
gT0ph1k2FEkJFGL5LwRf1ms4IX0vDkxTIX8Qxy1jczCiSsoV8pwlhh2NHAkpGQWN
/pTS0Uqi7uU5Bm/IoGvPBzUp5n5SjUMnTZx/+1zAuerSabt483sXBcWsjgl7MqFt
fONiAtNeMNfh60lTMu4zgVwLZTO4TQM5Q2uylPPmZtwnA88QvM2IL85cIYJHd0z9
jpUQMBGHXF2WQA==
-----END CERTIFICATE-----

View file

@ -0,0 +1,13 @@
-----BEGIN X509 CRL-----
MIIB8DCB2QIBATANBgkqhkiG9w0BAQsFADB2MQswCQYDVQQGEwJVUzE8MDoGA1UE
ChMzSW50ZXJuZXQgQ29ycG9yYXRpb24gZm9yIEFzc2lnbmVkIE5hbWVzIGFuZCBO
dW1iZXJzMSkwJwYDVQQDEyBJQ0FOTiBUcmFkZW1hcmsgQ2xlYXJpbmdob3VzZSBD
QRcNMTMwNzI0MDAwMDAwWhcNMTUwNzIzMjM1OTU5WqAvMC0wHwYDVR0jBBgwFoAU
XMDxlizKTFsp8UB00xs2PkfUbgQwCgYDVR0UBAMCAQEwDQYJKoZIhvcNAQELBQAD
ggEBAGmBog7fIBEvnDCV1Pkaiosibnxpp8d3E9NQRJRBlyLSNJKTB7mwcRlyVl/q
nAJrPZy6d01v58KpvrLLYRHkbsHC+qWom61Tu8ssVfGTrN6Re/CY8WtPQiiGjtDF
ZQBLgmjRGsUZ8r5L9DjBQNHEQmEMXYMiV7mId2+08GBHT1QDgUw74RXMqsD8BzeF
BqSyNJwxUzuCeU7xK4xRErnursXZo1fqgEc/zfqm9AjJx2frFmc/aXYvvw13z+ZP
PPsPAeMDUO/LBCGZJAf28LUKn5NmVcUXhYkxprjLuLu7eMveRn7LAiK6LDKL4SXL
AD6ouYPCx82fLnJ8o+2r6tHUHSk=
-----END X509 CRL-----

View file

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEQjCCAyqgAwIBAgIhAJNCMqhNjz3cXVJPj7yvcZvro1FKQR+dTC6tXazem5g+
MA0GCSqGSIb3DQEBCwUAMHYxCzAJBgNVBAYTAlVTMTwwOgYDVQQKEzNJbnRlcm5l
dCBDb3Jwb3JhdGlvbiBmb3IgQXNzaWduZWQgTmFtZXMgYW5kIE51bWJlcnMxKTAn
BgNVBAMTIElDQU5OIFRyYWRlbWFyayBDbGVhcmluZ2hvdXNlIENBMB4XDTEzMDcy
NDAwMDAwMFoXDTIzMDcyMzIzNTk1OVowdjELMAkGA1UEBhMCVVMxPDA6BgNVBAoT
M0ludGVybmV0IENvcnBvcmF0aW9uIGZvciBBc3NpZ25lZCBOYW1lcyBhbmQgTnVt
YmVyczEpMCcGA1UEAxMgSUNBTk4gVHJhZGVtYXJrIENsZWFyaW5naG91c2UgQ0Ew
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5MX6qpRnqFzEXa9w3G0b8
LTEVZzpOpcSq2BXJO16+iuZ964mpay2hm2BdZk89hSmZhUy2ePBR6PdS0GMmzzXL
NiyTHJlDIPxxXTR39Iqs8QChJ8wle4pYUu8JUk2vJ0r7PhFweeCCQZ5gvHdCwopS
bXeolj4NCqsvzU8iROsLRHSZbE83i2pkL+qBoyzjny9MO2rvMNPo5WrDNrno6hvC
hlf8Pv77HTNCazI2MeW0ArfLin4pSe6nLnDsQA11SF9bbgwDgVMQFvmB8nEvUbZW
Atnp3auaWqaylC+G0p3frFvMCUJMPrghiPwBABl3bk1GLjXXVl7D8SubKd2Xwv63
AgMBAAGjgbowgbcwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUXMDxlizK
TFsp8UB00xs2PkfUbgQwDgYDVR0PAQH/BAQDAgEGMC4GA1UdHwQnMCUwI6AhoB+G
HWh0dHA6Ly9jcmwuaWNhbm4ub3JnL3RtY2guY3JsMEIGA1UdIAQ7MDkwBgYEVR0g
ADAvBggrBgEFBQcCATAjMCEGCCsGAQUFBwIBFhVodHRwczovL2NhLmljYW5uLm9y
Zy8wDQYJKoZIhvcNAQELBQADggEBAAM29FBdwQSAx8dD4ZYtCYjXxTonNCP2qveG
wrpMJcq/I3Jp/N4etsnj+K5ej5sSlDuo8sTMF7lgMkgjrc6zgJl0+Gct2RhbRNzN
5ittE9JwJZ3Us4vwiy6gqMO5Ie9YaKMZy2MYP2iFp6AhBKIc2Iz+8aFfnFzdSEx2
b3xc+t1A09dzpnzU6zvHWUUkTYq9fTg1er1npni9ZErvp0jEyHVWi5GXvWap68XH
pVF6TBmPW2UBEEnFgd3SxbMXLhaD/wzV12tlSYjxaNed+H5qbVVbSVwN9yBeWU29
/pkZu79TqFFxTd4CJTWOBq0+yLO1Ts2ZZ1l+GgU6e3hI1XERSNc=
-----END CERTIFICATE-----

View file

@ -0,0 +1,31 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2.0.14 (GNU/Linux)
mQENBFHaxrYBCADHDPUdSYCs9wuBUhbGYfi39LKlDLhlmia7Tlv1rSJb8RPcU7DL
eXSU9zKRDpW31KQtHnjARinobjQNc6YXQOGtjDVr3LGTojGT1y+obVmgWytpHNnb
kSu1pktQRDPpYPBMBOD/MVpz8nZxgjYWyYTIkfTgkA/l1YKRyA30ieHVR8qKhJZY
D6u/dViQcdZCLsvs0XYYhPDjNi7M2wzVAdRjh9txnJgDymgqHF59onzkZauyFnc9
EUEUURITzbfMifvGSZ/h+zdCUT/tSH3wSuVRxXqAh+cApT2c+gI88/TlGci2YOZV
POU24M7xuYPb9rIK4b9tR12ZQF+g/4n9eDprABEBAAG0LHN1cHBvcnQgYXQgbWFy
a3NkYi5vcmcgPHN1cHBvcnRAbWFya3NkYi5vcmc+iQE4BBMBAgAiBQJR2sa2AhsD
BgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRC4xOmbTP03TGTCB/9J1xKyNkJe
wsSHhBga4MnBiS3gr3waZSfYaiWvx7br5FxI3Zv2jPAouqkpf8XbJgc4RvqZjFmi
5YSwpLgChSa5DkA6kPgReNCEfPvtBX7iRrZTv7RSSzZUuGiVED9s4Qk0+ppc8RgW
HRLYetvNvMIKL+6bvUlvztr+KM914JpzDEXgkBok2RxVq33FKUMIiy4vinbS2/7N
LpGlCMG7vgdBHIjBItUq9ipCtiDvvmPHiwHErgMgkx67pmDGxViMHDIMpzb8YTHw
lCxRwnLPILmm371hqOuVsu/pUVC16TEzYRDwZGbn4uKzWT0ZvYsPNcFGKC35ujEH
3fACoP6CMt/AuQENBFHaxrYBCADuDjCyxpDMakGSmk5zXIW/r0F9wqpIaPqm7xoR
15ajbvBUguM3gVH7bC4qihaSVFq2S8soWpyu88JyeWFiK+nMiIUALbfwXTA3DdIm
zmA/aKBNCeMyWXH1iy5zDCO0S343HH3/QWYTTy0aSUoMiW4AovKrnbZWti0WOoC4
a8MUY1Ib1DhG/CJBzx4l52m5jOnmKB4foa7VmaWAjlG/s4ZDoEUPW8p6d2fmF05q
/ImGuzSwXegCuTY7rmkWskf4SshTYryyel76v1SteOFhXFaxcewbQgxIn1uGczd5
P3/RBznF+Zm9OQK3840sqnhn1y/A3HadT7OI9Ot31TVXpRK5ABEBAAGJAR8EGAEC
AAkFAlHaxrYCGwwACgkQuMTpm0z9N0yzzwgApO1IzPwd5OwFymQNIizKOl3VTBTR
s5xtGBARg3fQsPmQen/SWZXu7VqJzr9QngQRJUT0nqdQPsGqOU72f8UkZGg6UD/o
mqfSfzLnOGnXLpC5g4bR+Z1PHlOV33r0HnWA8GPZsnYOXw414NYLmNW+tlC9t1F0
WNBevhzXQujBfj0p+Znf3Gap8xvtZ9EAbeYUEpUQXEKh5UcBHBQCzmvDgukISK7w
wBpVWDwLHIYghE9OElat0y+ttD+RhSkUf+ufIw+L6BTHVD5E+W+BFoMFmNU3Kk/y
BoGsCR/i63T3egBVn1IzFezNy8YUVTtqOLyYlIe2R35zafxXs2anwvqA6Q==
=2ED8
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -0,0 +1,16 @@
// Copyright 2016 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
@javax.annotation.ParametersAreNonnullByDefault
package com.google.domain.registry.tmch;