Skip to content

Commit

Permalink
cleanups (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
tsaarni committed Jun 18, 2022
1 parent 9646edb commit 65c3079
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 111 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/generate-javadoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
uses: gradle/gradle-build-action@v2

- name: Generate javadoc
run: ./gradlew generateJavadoc
run: ./gradlew javadoc

- name: Deploy 🚀
uses: JamesIves/[email protected]
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.gradle
**/build/
**/bin/
!src/**/build/

# Ignore Gradle GUI config
Expand Down
107 changes: 98 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,44 @@

## Description

Certy gives you a simple Java API to create X509 certificates for your unit tests.
Certy gives you a simple Java API for creating X509 certificates inside your unit tests.
No more committing test certificates and keys into the repository!

Certy is Java version of similar tool for command line and Golang use: [certyaml](https://github.com/tsaarni/certyaml).
Certy is Java version of similar tool for command line and Golang: [certyaml](https://github.com/tsaarni/certyaml).

## Documentation

You can read the latest documentation [here](https://tsaarni.github.io/certy/).
Read the latest documentation [here](https://tsaarni.github.io/certy/).

## Example

Two credentials are created: `ca` and `server`.
The `ca` certificate will be created with defaults parameters: it will be self-signed certificate with 256 bits EC key type.
The `server` certificate is set to be signed by the `ca` and its subject alternative name is set to `www.example.com`.
Only minimal set of fields needs to be defined since defaults work for most use cases.
For example `ca` certificate will be self-signed root CA since issuer is not set.
The `server` certificate is set to be signed by `ca` and its subject alternative name is set to `app.127.0.0.1.nip.io` to allow its use as server certificate for given domain.
Key usage for end-entity certificates defaults to allow their use as both server and client certificates.
When the defaults are not correct for particular use, they can be overwritten by calling the respective builder methods.

```java
Credential ca = new Credential().subject("CN=ca");
Credential server = new Credential().subject("CN=server")
.issuer(ca)
.subjectAltName("DNS:www.example.com");
.subjectAltName("DNS:app.127.0.0.1.nip.io");
```

Next, we can write CA certificate, server certificate and key to disk in PEM format:
The `ca` certificate, `server` certificate and associated private key are written as PEM files:

```java
ca.writeCertificateAsPem(Paths.get("ca.pem"));
server.writeCertificateAsPem(Paths.get("server.pem"))
.writePrivateKeyAsPem(Paths.get("server-key.pem"));
```

Or alternatively, we can create truststore and keystore in PKCS12 format:
They can be stored in PKCS12 (or JKS) truststore and keystore:

```java
KeyStore truststore = KeyStore.getInstance("PKCS12");
truststore.load(null, null);
truststore.load(null, null); // Required to initialize the keystore.
truststore.setCertificateEntry("ca", ca.getCertificate());
truststore.store(Files.newOutputStream(Paths.get("trusted.p12")), "secret".toCharArray());

Expand All @@ -47,3 +50,89 @@ keystore.load(null, null);
keystore.setKeyEntry("server", server.getPrivateKey(), null, server.getCertificates());
keystore.store(Files.newOutputStream(Paths.get("server.p12")), "secret".toCharArray());
```

Following certificates were created:

```console
$ openssl x509 -in ca.pem -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1655536454193 (0x18175a98a31)
Signature Algorithm: ecdsa-with-SHA256
Issuer: CN = ca
Validity
Not Before: Jun 18 07:14:14 2022 GMT
Not After : Jun 18 07:14:14 2023 GMT
Subject: CN = ca
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
...
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
8F:14:88:5A:27:5D:F5:B8:8D:16:AB:F1:51:21:29:F8:52:5A:65:0B
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
Signature Algorithm: ecdsa-with-SHA256
...

$ openssl x509 -in server.pem -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 1655536454415 (0x18175a98b0f)
Signature Algorithm: ecdsa-with-SHA256
Issuer: CN = ca
Validity
Not Before: Jun 18 07:14:14 2022 GMT
Not After : Jun 18 07:14:14 2023 GMT
Subject: CN = server
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
...
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
B0:AC:25:D9:8D:5D:17:02:22:DA:71:C0:52:04:D3:8E:B4:A0:AC:D9
X509v3 Key Usage: critical
Digital Signature, Key Encipherment, Key Agreement
X509v3 Subject Alternative Name:
DNS:app.127.0.0.1.nip.io
Signature Algorithm: ecdsa-with-SHA256
...
```

And the content of keystores:

```console
$ keytool -list -keystore trusted.p12 -storepass secret
Keystore type: PKCS12
Keystore provider: SUN

Your keystore contains 1 entry

ca, Jun 18, 2022, trustedCertEntry,
Certificate fingerprint (SHA-256): 3F:54:0D:F3:CE:A8:0A:E9:72:D1:55:96:2B:A2:4E:11:5E:96...

$ keytool -list -keystore server.p12 -storepass secret
Keystore type: PKCS12
Keystore provider: SUN

Your keystore contains 1 entry

server, Jun 18, 2022, PrivateKeyEntry,
Certificate fingerprint (SHA-256): 4E:6A:7C:57:B7:21:31:E2:58:6E:35:95:5F:26:4F:8F:F9:F4...
```

Check out the [unit tests](lib/src/test/java/fi/protonode/certy/TestCredential.java) for more code examples.
48 changes: 25 additions & 23 deletions lib/build.gradle
Original file line number Diff line number Diff line change
@@ -1,45 +1,47 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java library project to get you started.
* For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle
* User Manual available at https://docs.gradle.org/7.4.2/userguide/building_java_projects.html
*/

plugins {
id 'java-library'
id 'maven-publish'
id 'jacoco' // Adds jacocoTestReport task for coverage.
}

repositories {
mavenCentral()
}

dependencies {
// Bouncy Castle
implementation 'org.bouncycastle:bcprov-jdk18on:1.71'
implementation 'org.bouncycastle:bcprov-ext-jdk18on:1.71'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.71'

// Logging
implementation 'org.slf4j:slf4j-api:1.7.36'

testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
testImplementation 'org.mockito:mockito-inline:4.6.1'
testImplementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.2'
}

tasks.named('test') {
useJUnitPlatform()
}

test {
testLogging {
showStackTraces = true
exceptionFormat = 'full'
}
}

task generateJavadoc(type: Javadoc) {
source = sourceSets.main.allJava
classpath = project.sourceSets.main.compileClasspath
java {
withSourcesJar()
}

publishing {
publications {
maven(MavenPublication) {
groupId = 'fi.protonode.certy'
artifactId = 'certy'
version = '0.1'

from components.java

pom {
licenses {
license {
name = 'The Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
49 changes: 26 additions & 23 deletions lib/src/main/java/fi/protonode/certy/Credential.java
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ public Credential() {
}

/**
* Defines the distinguished name for the certificate.<p>
* Defines the distinguished name for the certificate (mandatory).<p>
* Example: {@code "CN=Joe"}.
*
* @param val Subject name.
Expand Down Expand Up @@ -209,9 +209,9 @@ public Credential keySize(int val) {
}

/**
* Sets certificate's {@code NotAfter} field by given duration from the current time.
* Defines {@link #notAfter} by duration from current time.
* {@link #notAfter} takes precedence over expires.
* The default value is 1 year if not defined
* The default value is 1 year if {@code expires} is not set.
*
* @param val Time until expiration.
* @return The Credential itself.
Expand All @@ -222,10 +222,10 @@ public Credential expires(Duration val) {
}

/**
* Defines certificate not to be valid before this time.
* The default value is current time if not defined.
* Defines certificate not to be valid before given time.
* The default value is current time if {@code notBefore} is not set.
*
* @param val Date when certificate becomes valid.
* @param val Time when certificate becomes valid.
* @return The Credential itself.
*/
public Credential notBefore(Date val) {
Expand All @@ -234,10 +234,10 @@ public Credential notBefore(Date val) {
}

/**
* Defines certificate not to be valid after this time.
* Default value is current time + expires if {@code notAfter} is not defined.
* Defines certificate not to be valid after given time.
* Default value is current time + expires if {@code notAfter} is not set.
*
* @param val Date when certificate expires.
* @param val Time when certificate expires.
* @return The Credential itself.
*/
public Credential notAfter(Date val) {
Expand All @@ -248,7 +248,7 @@ public Credential notAfter(Date val) {
/**
* Defines a sequence of values for x509 key usage extension.<p>
*
* If {@code keyUsage} is undefined following defaults are used:<p>
* Following defaults are used if {@code keyUsages} is not set:<p>
* CertSign and CRLSign are set for CA certificates.
* KeyEncipherment and DigitalSignature are set for end-entity certificates with RSA key.
* KeyEncipherment, DigitalSignature and KeyAgreement are set for end-entity certificates with EC key.
Expand All @@ -262,8 +262,7 @@ public Credential keyUsages(List<KeyUsage> val) {
}

/**
* Defines a sequence of x509 extended key usages.
* Not defined by default.
* Defines an optional list of x509 extended key usages.
*
* @param val List of extended key usages.
* @return The Credential itself.
Expand All @@ -286,10 +285,11 @@ public Credential issuer(Credential val) {
}

/**
* Defines certificate's basic constraints isCA attribute.
* If IsCA is not defined, self-signed certificates are set as CA certificates, everything else is not set.
* Defines basic constraints CA attribute.
* Self-signed certificates are automatically set {@code CA:true} unless {@code isCa} is explicitly set.
* Otherwise {@code CA:false}.
*
* @param val Value for isCA attribute of basic constraints.
* @param val Value for CA attribute of basic constraints.
* @return The Credential itself.
*/
public Credential isCa(Boolean val) {
Expand All @@ -298,7 +298,7 @@ public Credential isCa(Boolean val) {
}

/**
* (Re)generate certificate and private key with currently defined attributes.
* (Re)generate certificate and private key with currently set values.
*
* @return The Credential itself.
*/
Expand Down Expand Up @@ -331,6 +331,7 @@ public Credential generate()
effectiveNotAfter = Date.from(effectiveNotBefore.toInstant().plus(expires));
}

// In theory subject could be empty but did not find a way to allow empty X500Name in Bouncy Castle.
if (subject == null) {
throw new IllegalArgumentException("subject name must be set");
}
Expand Down Expand Up @@ -365,6 +366,8 @@ public Credential generate()
keyUsages.stream().collect(Collectors.summingInt(KeyUsage::getValue))));

if (subjectAltNames != null) {
// If subject could be null, subjectAltName would be set critical.
// But did not find a way to set empty subject in Bouncy Castle, so subject == null is never true.
builder.addExtension(Extension.subjectAlternativeName, subject == null, subjectAltNames);
}

Expand All @@ -383,7 +386,7 @@ public Credential generate()
}

/**
* Returns PEM block containing the X509 certificate.
* Returns PEM block containing X509 certificate.
*
* @return String containing the certificate.
*/
Expand All @@ -400,7 +403,7 @@ public String getCertificateAsPem() throws CertificateException, NoSuchAlgorithm
}

/**
* Returns PEM block containing the private key in PKCS8 format.
* Returns PEM block containing private key in PKCS8 format.
*
* @return String containing the private key.
*/
Expand All @@ -418,7 +421,7 @@ public String getPrivateKeyAsPem()
}

/**
* Writes X509 certificate to a file as PEM.
* Writes X509 certificate to a file as PEM block.
*
* @param out Path to write the PEM file to.
* @return The Credential itself.
Expand All @@ -438,7 +441,7 @@ public Credential writeCertificateAsPem(Path out)
}

/**
* Writes private key in PKCS8 format to a file as PEM.
* Writes private key in PKCS8 format to a file as PEM block.
*
* @param out Path to write the PEM file to.
* @return The Credential itself.
Expand Down Expand Up @@ -468,10 +471,10 @@ public Certificate getCertificate() throws CertificateException, NoSuchAlgorithm
}

/**
* Returns certificate as an array.
* Returns certificate.
* This is convenience method for use cases where array is required.
*
* @return Array of certificates. Holds always just single certificate.
* @return Array of certificates. Always holds just single certificate.
*/
public Certificate[] getCertificates()
throws CertificateException, NoSuchAlgorithmException {
Expand All @@ -483,7 +486,7 @@ public Certificate[] getCertificates()
/**
* Returns certificate.
*
* @return Certificate in {@code X509Certificate} format.
* @return Certificate as {@code X509Certificate}.
*/
public X509Certificate getX509Certificate() throws CertificateException, NoSuchAlgorithmException {
ensureGenerated();
Expand Down
Loading

0 comments on commit 65c3079

Please sign in to comment.