Skip to content

Commit

Permalink
Merge pull request #138 from RIPE-NCC/feature/generic-parser-access-c…
Browse files Browse the repository at this point in the history
…ertificate

Access certificate with generic signed object parser
  • Loading branch information
ties authored Nov 1, 2023
2 parents ea842ac + bbf1bcd commit 0ffb290
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
# test against latest update of each major Java version, as well as specific updates of LTS versions:
java: [ 11, 17 ]
java: [ 11, 17, 21 ]
name: Java ${{ matrix.java }}
steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ 17 ]
java: [ 21 ]

steps:
- name: Setup java
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ next (snapshot) release, e.g. `1.1-SNAPSHOT` after releasing `1.0`.

## Changelog

## 2023-10-31 1.36
* Access the certificate for the generic signed object parser.

## 2023-10-03 1.35
* Build targets JDK 11
* Prefixes in ROAs are sorted by (prefix, maxlength - missing first)
Expand Down
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@

<net.ripe.ipresource.version>1.52</net.ripe.ipresource.version>
<bouncycastle.version>1.74</bouncycastle.version>
<guava.version>32.0.0-jre</guava.version>
<guava.version>32.1.2-jre</guava.version>
<joda-time.version>2.10.13</joda-time.version>
<xstream.version>1.4.20</xstream.version>
<commons-io.version>2.11.0</commons-io.version>
<lombok.version>1.18.22</lombok.version>
<lombok.version>1.18.30</lombok.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -138,7 +138,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.2.0</version>
<version>5.6.0</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -353,7 +353,7 @@
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>6.5.0</version>
<version>8.4.2</version>
<executions>
<execution>
<goals>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import net.ripe.rpki.commons.crypto.cms.ghostbuster.GhostbustersCms;
import net.ripe.rpki.commons.crypto.cms.manifest.ManifestCms;
import net.ripe.rpki.commons.crypto.cms.roa.RoaCms;
import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificate;
import net.ripe.rpki.commons.util.RepositoryObjectType;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.joda.time.DateTime;

import java.security.cert.Certificate;
import java.util.Optional;

import static net.ripe.rpki.commons.util.RepositoryObjectType.*;
Expand All @@ -17,6 +19,15 @@ public DateTime getSigningTime() {
return super.getSigningTime();
}

/**
* Extend visibility of the certificate to make it public.
* @return the certificate.
*/
@Override
public X509ResourceCertificate getCertificate() {
return super.getCertificate();
}

public Optional<RepositoryObjectType> getRepositoryObjectType() {
final ASN1ObjectIdentifier contentType = getContentType();
if (AspaCms.CONTENT_TYPE.equals(contentType)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package net.ripe.rpki.commons.crypto.util;

import lombok.Getter;
import lombok.experimental.UtilityClass;
import net.ripe.rpki.commons.crypto.cms.GenericRpkiSignedObjectParser;
import net.ripe.rpki.commons.crypto.crl.X509Crl;
import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificateParser;
import net.ripe.rpki.commons.util.RepositoryObjectType;
import net.ripe.rpki.commons.validation.ValidationResult;
import org.joda.time.Instant;

import java.net.URI;

@UtilityClass
public class SignedObjectUtil {
/**
* Extract the creation time from an object. This does <emph>not yet</emph> follow the method described in
* https://datatracker.ietf.org/doc/draft-timbru-sidrops-publication-server-bcp/00/. It differs in that it uses
* the signing time for RPKI signed objects. This is a trade-off:
* * signing-time is more correct when multi-use EE certificates are present.
* * signing-time likely does not match the modification time of the CRL.
*
* This needs to be revisited in 2024.
*
* @param uri URL of the object
* @param decoded object bytes
* @return the file creation time of the object
* @throws NoTimeParsedException if creation time could not be extracted.
*/
public static Instant getFileCreationTime(URI uri, byte[] decoded) throws NoTimeParsedException {

final RepositoryObjectType objectType = RepositoryObjectType.parse(uri.toString());
try {
switch (objectType) {
case Manifest:
case Aspa:
case Roa:
case Gbr:
var signedObjectParser = new GenericRpkiSignedObjectParser();

signedObjectParser.parse(ValidationResult.withLocation(uri), decoded);
var signingTime = signedObjectParser.getSigningTime();

if (signingTime == null) {
return signedObjectParser.getCertificate().getValidityPeriod().getNotValidBefore().toInstant();
}
return signingTime.toInstant();
case Certificate:
X509ResourceCertificateParser x509CertificateParser = new X509ResourceCertificateParser();
x509CertificateParser.parse(ValidationResult.withLocation(uri), decoded);
final var cert = x509CertificateParser.getCertificate().getCertificate();
return Instant.ofEpochMilli(cert.getNotBefore().getTime());
case Crl:
var x509Crl = X509Crl.parseDerEncoded(decoded, ValidationResult.withLocation(uri));
var crl = x509Crl.getCrl();
return Instant.ofEpochMilli(crl.getThisUpdate().getTime());
case Unknown:
default:
throw new NoTimeParsedException(decoded, uri, "Could not determine file type");
}
} catch (Exception e) {
if (e instanceof NoTimeParsedException) {
throw e;
}
throw new NoTimeParsedException(decoded, uri, "Could not parse object", e);
}
}

@Getter
public static class NoTimeParsedException extends Exception {
private static final long serialVersionUID = 1L;

private byte[] decoded;
private URI uri;
public NoTimeParsedException(byte[] decoded, URI uri, String message) {
super(uri.toString() + ": " + message);
this.decoded = decoded;
this.uri = uri;
}

public NoTimeParsedException(byte[] decoded, URI uri, String message, Throwable cause) {
super(uri.toString() + ": " + message, cause);
this.decoded = decoded;
this.uri = uri;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ void should_parse_roa() throws IOException {
assertThat(parser.getSigningTime()).isEqualTo(DateTime.parse("2011-11-11T01:55:18+00:00"));
}

/**
* Parse an invalid object, but still extract validity period and signing time.
*/
@Test
void should_parse_generic() throws IOException {
GenericRpkiSignedObjectParser parser = parse("interop/aspa/BAD-profile-13-AS211321-profile-13.asa");

assertThat(parser.getSigningTime()).isEqualTo(DateTime.parse("2021-11-11T11:19:00Z"));

assertThat(parser.getCertificate().getValidityPeriod().getNotValidBefore()).isEqualTo(DateTime.parse("2021-11-11T11:14:00Z"));
}


private GenericRpkiSignedObjectParser parse(String path) throws IOException {
byte[] bytes = Resources.toByteArray(Resources.getResource(path));
Expand Down
46 changes: 46 additions & 0 deletions src/test/java/net/ripe/rpki/commons/util/SignedObjectUtilTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package net.ripe.rpki.commons.util;

import com.google.common.io.Resources;
import net.ripe.rpki.commons.crypto.util.SignedObjectUtil;
import org.joda.time.DateTime;
import org.joda.time.Instant;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import java.io.IOException;
import java.net.URI;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class SignedObjectUtilTest {
@DisplayName("Should parse the file creation time from RPKI objects")
@ParameterizedTest(name = "{index} => {0} filename={1} expected-creation-time={3} path={2}")
@CsvSource({
"ASPA, sample.asa, interop/aspa/GOOD-profile-15-draft-ietf-sidrops-profile-15-sample.asa, 2023-06-07T09:08:41Z",
// GBR parser has issues
// "GBR, sample.gbr, conformance/root/goodRealGbrNothingIsWrong.gbr, 2023-06-07T09:01:01Z",
// router certificate case is missing due to lack of samples.
"Manifest, sample.mft, conformance/root/root.mft, 2013-10-28T21:24:39Z",
"ROA, sample.roa, interop/rpkid-objects/nI2bsx18I5mlex8lBpY0WSJUYio.roa, 2011-11-11T01:55:18Z",
"'Generic signed object (that does not match object profile)', generic-signed-object.gbr, interop/aspa/BAD-profile-13-AS211321-profile-13.asa, 2021-11-11T11:19:00Z",
})
void shouldParseObject(String description, String fileName, String path, String modified) throws IOException, SignedObjectUtil.NoTimeParsedException {
Instant creationTime = SignedObjectUtil.getFileCreationTime(URI.create(fileName), Resources.toByteArray(Resources.getResource(path)));

assertThat(creationTime).isEqualTo(DateTime.parse(modified));
}

@Test
void shouldThrowOnUnknown_payload() {
assertThatThrownBy(() -> SignedObjectUtil.getFileCreationTime(URI.create("foo.cer"), new byte[] {(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF}))
.isInstanceOf(SignedObjectUtil.NoTimeParsedException.class);
}
@Test
void shouldThrowOnUnknown_extension() {
assertThatThrownBy(() -> SignedObjectUtil.getFileCreationTime(URI.create("foo.xxx"), Resources.toByteArray(Resources.getResource("interop/aspa/BAD-profile-13-AS211321-profile-13.asa"))))
.isInstanceOf(SignedObjectUtil.NoTimeParsedException.class);
}
}

0 comments on commit 0ffb290

Please sign in to comment.