Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access certificate with generic signed object parser #138

Merged
merged 6 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
Fixed Show fixed Hide fixed
ties marked this conversation as resolved.
Show resolved Hide resolved
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 {
Dismissed Show dismissed Hide dismissed
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);
}
}
Loading