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

Add new APIs to read EC public and private keys from files #535

Merged
merged 7 commits into from
Dec 12, 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/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
# See: https://help.github.com/articles/about-codeowners/

# These owners will be the default owners for everything in the repo.
* @shafreenAnfar @Bhashinee
* @shafreenAnfar @Bhashinee @MohamedSabthar
6 changes: 3 additions & 3 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
org = "ballerina"
name = "crypto"
version = "2.6.0"
version = "2.6.1"
authors = ["Ballerina"]
keywords = ["security", "hash", "hmac", "sign", "encrypt", "decrypt", "private key", "public key"]
repository = "https://github.com/ballerina-platform/module-ballerina-crypto"
Expand All @@ -15,8 +15,8 @@ graalvmCompatible = true
[[platform.java17.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "crypto-native"
version = "2.6.0"
path = "../native/build/libs/crypto-native-2.6.0.jar"
version = "2.6.1"
path = "../native/build/libs/crypto-native-2.6.1-SNAPSHOT.jar"

[[platform.java17.dependency]]
groupId = "org.bouncycastle"
Expand Down
2 changes: 1 addition & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ distribution-version = "2201.8.0"
[[package]]
org = "ballerina"
name = "crypto"
version = "2.6.0"
version = "2.6.1"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "test"},
Expand Down
26 changes: 26 additions & 0 deletions ballerina/private_public_key.bal
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,20 @@ public isolated function decodeRsaPrivateKeyFromKeyFile(string keyFile, string?
'class: "io.ballerina.stdlib.crypto.nativeimpl.Decode"
} external;

# Decodes the EC private key from the given private key and private key password.
# ```ballerina
# string keyFile = "/path/to/private.key";
# crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyFile(keyFile, "keyPassword");
# ```
#
# + keyFile - Path to the key file
# + keyPassword - Password of the key file if it is encrypted
# + return - Reference to the private key or else a `crypto:Error` if the private key was unreadable
public isolated function decodeEcPrivateKeyFromKeyFile(string keyFile, string? keyPassword = ())
returns PrivateKey|Error = @java:Method {
'class: "io.ballerina.stdlib.crypto.nativeimpl.Decode"
} external;

# Decodes the RSA public key from the given PKCS#12 archive file.
# ```ballerina
# crypto:TrustStore trustStore = {
Expand Down Expand Up @@ -174,6 +188,18 @@ public isolated function decodeRsaPublicKeyFromCertFile(string certFile) returns
'class: "io.ballerina.stdlib.crypto.nativeimpl.Decode"
} external;

# Decodes the EC public key from the given public certificate file.
# ```ballerina
# string certFile = "/path/to/public.cert";
# crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromCertFile(certFile);
# ```
#
# + certFile - Path to the certificate file
# + return - Reference to the public key or else a `crypto:Error` if the public key was unreadable
public isolated function decodeEcPublicKeyFromCertFile(string certFile) returns PublicKey|Error = @java:Method {
'class: "io.ballerina.stdlib.crypto.nativeimpl.Decode"
} external;

# Builds the RSA public key from the given modulus and exponent parameters.
# ```ballerina
# string modulus = "luZFdW1ynitztkWLC6xKegbRWxky...";
Expand Down
76 changes: 59 additions & 17 deletions ballerina/tests/private_public_key_test.bal
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ isolated function testParseEncryptedPrivateKeyFromP12() returns Error? {
password: "ballerina"
};
PrivateKey result = check decodeRsaPrivateKeyFromKeyStore(keyStore, "ballerina", "ballerina");
test:assertEquals(result["algorithm"], "RSA");
test:assertEquals(result.algorithm, "RSA");
}

@test:Config {}
Expand Down Expand Up @@ -85,13 +85,13 @@ isolated function testReadPrivateKeyFromP12WithInvalidKeyPassword() {
@test:Config {}
isolated function testParsePrivateKeyFromKeyFile() returns Error? {
PrivateKey result = check decodeRsaPrivateKeyFromKeyFile(PRIVATE_KEY_PATH);
test:assertEquals(result["algorithm"], "RSA");
test:assertEquals(result.algorithm, "RSA");
}

@test:Config {}
isolated function testParseEncryptedPrivateKeyFromKeyFile() returns Error? {
PrivateKey result = check decodeRsaPrivateKeyFromKeyFile(ENCRYPTED_PRIVATE_KEY_PATH, "ballerina");
test:assertEquals(result["algorithm"], "RSA");
test:assertEquals(result.algorithm, "RSA");
}

@test:Config {}
Expand All @@ -117,7 +117,7 @@ isolated function testParseEncryptedPrivateKeyFromKeyFileWithNoPassword() {
@test:Config {}
isolated function testParseEncryptedPrivateKeyFromKeyPairFile() returns Error? {
PrivateKey result = check decodeRsaPrivateKeyFromKeyFile(ENCRYPTED_KEY_PAIR_PATH, "ballerina");
test:assertEquals(result["algorithm"], "RSA");
test:assertEquals(result.algorithm, "RSA");
}

@test:Config {}
Expand All @@ -143,7 +143,33 @@ isolated function testParseEncryptedPrivateKeyFromKeyPairFileWithNoPassword() {
@test:Config {}
isolated function testParsePrivateKeyFromKeyPairFile() returns Error? {
PrivateKey result = check decodeRsaPrivateKeyFromKeyFile(KEY_PAIR_PATH);
test:assertEquals(result["algorithm"], "RSA");
test:assertEquals(result.algorithm, "RSA");
}

@test:Config {}
isolated function testParseEcPrivateKeyFromKeyFile() returns Error? {
PrivateKey result = check decodeEcPrivateKeyFromKeyFile(EC_PRIVATE_KEY_PATH);
test:assertEquals(result.algorithm, "ECDSA");
}

@test:Config {}
isolated function testParseErrorEcPrivateKeyFromKeyFile() returns Error? {
PrivateKey|Error result = decodeEcPrivateKeyFromKeyFile(PRIVATE_KEY_PATH);
if result is Error {
test:assertEquals(result.message(), "Not a valid EC key");
} else {
test:assertFail("Expected error not found");
}
}

@test:Config {}
isolated function testParseErrorEcPublicKeyFromKeyFile() returns Error? {
PublicKey|Error result = decodeEcPublicKeyFromCertFile(PRIVATE_KEY_PATH);
if result is Error {
test:assertEquals(result.message(), "Unable to do public key operations: signed fields invalid");
} else {
test:assertFail("Expected error not found");
}
}

@test:Config {}
Expand All @@ -163,13 +189,13 @@ isolated function testParsePublicKeyFromP12() returns Error? {
password: "ballerina"
};
PublicKey publicKey = check decodeRsaPublicKeyFromTrustStore(trustStore, "ballerina");
test:assertEquals(publicKey["algorithm"], "RSA");
Certificate certificate = <Certificate>publicKey["certificate"];
test:assertEquals(publicKey.algorithm, "RSA");
Certificate certificate = <Certificate>publicKey.certificate;

string serial = (<int>certificate["serial"]).toString();
string issuer = <string>certificate["issuer"];
string subject = <string>certificate["subject"];
string signingAlgorithm = <string>certificate["signingAlgorithm"];
string serial = (<int>certificate.serial).toString();
string issuer = <string>certificate.issuer;
string subject = <string>certificate.subject;
string signingAlgorithm = <string>certificate.signingAlgorithm;

test:assertEquals(serial, "2097012467");
test:assertEquals(issuer, "CN=localhost,OU=WSO2,O=WSO2,L=Mountain View,ST=CA,C=US");
Expand Down Expand Up @@ -222,20 +248,36 @@ isolated function testReadPublicKeyFromP12WithInvalidAlias() {
@test:Config {}
isolated function testParsePublicKeyFromX509CertFile() returns Error? {
PublicKey publicKey = check decodeRsaPublicKeyFromCertFile(X509_PUBLIC_CERT_PATH);
test:assertEquals(publicKey["algorithm"], "RSA");
Certificate certificate = <Certificate>publicKey["certificate"];
test:assertEquals(publicKey.algorithm, "RSA");
Certificate certificate = <Certificate>publicKey.certificate;

string serial = (<int>certificate["serial"]).toString();
string issuer = <string>certificate["issuer"];
string subject = <string>certificate["subject"];
string signingAlgorithm = <string>certificate["signingAlgorithm"];
string serial = (<int>certificate.serial).toString();
string issuer = <string>certificate.issuer;
string subject = <string>certificate.subject;
string signingAlgorithm = <string>certificate.signingAlgorithm;

test:assertEquals(serial, "2097012467");
test:assertEquals(issuer, "CN=localhost,OU=WSO2,O=WSO2,L=Mountain View,ST=CA,C=US");
test:assertEquals(subject, "CN=localhost,OU=WSO2,O=WSO2,L=Mountain View,ST=CA,C=US");
test:assertEquals(signingAlgorithm, "SHA256withRSA");
}

@test:Config {}
isolated function testParseEcPublicKeyFromX509CertFile() returns Error? {
PublicKey publicKey = check decodeEcPublicKeyFromCertFile(EC_CERT_PATH);
test:assertEquals(publicKey.algorithm, "EC");
Certificate certificate = <Certificate>publicKey.certificate;

string serial = (<int>certificate.serial).toString();
string issuer = <string>certificate.issuer;
string subject = <string>certificate.subject;
string signingAlgorithm = <string>certificate.signingAlgorithm;

test:assertEquals(serial, "813081972327485475");
test:assertEquals(issuer, "CN=sigstore-intermediate,O=sigstore.dev");
test:assertEquals(signingAlgorithm, "SHA384withECDSA");
}

@test:Config {}
isolated function testReadPublicKeyFromNonExistingCertFile() {
PublicKey|Error result = decodeRsaPublicKeyFromCertFile(INVALID_PUBLIC_CERT_PATH);
Expand Down
18 changes: 18 additions & 0 deletions ballerina/tests/resources/ec-cert.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-----BEGIN CERTIFICATE-----
MIICzzCCAlWgAwIBAgIUJaBYA4gOhyDrjigfC0ilvvXakCMwCgYIKoZIzj0EAwMw
NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl
cm1lZGlhdGUwHhcNMjMxMjA4MDYxODI0WhcNMjMxMjA4MDYyODI0WjAAMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEyjXIb66skPFKbH8Bmjdg1DqZ6eOJV3za17Zs
EYpEgT2p33lzkiC2K9X39cATWrT1vd+PpzkRa6RrDobjrfzggqOCAXQwggFwMA4G
A1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUQik2
yPY3ziieNIhK2bADm1bRpmYwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y
ZD8wJAYDVR0RAQH/BBowGIEWZHdzbnNld3dhbmRpQGdtYWlsLmNvbTApBgorBgEE
AYO/MAEBBBtodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20wKwYKKwYBBAGDvzAB
CAQdDBtodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20wgYoGCisGAQQB1nkCBAIE
fAR6AHgAdgDdPTBqxscRMmMZHhyZZzcCokpeuN48rf+HinKALynujgAAAYxIE8WD
AAAEAwBHMEUCIQCWhoJcbSAxit5NiPMrze0N0JdQC/dmVu/EpKkCiT4Y4gIgBazA
hpv2Oq49TvaYINlfO86ziKcTEAP9uh93JlWXHvowCgYIKoZIzj0EAwMDaAAwZQIw
Nq1+EWmcWn4IcHOYe0QJaikbSXxRn6wC16Kn6M/BMqgH1I8MmnY46MeQozY+FyCO
AjEAz0WV59exLU1hhMhtCOHG7WNyC2/vt+EMeUxM2tkNtQk/rHjEhorkwiqlgzAT
4jel
-----END CERTIFICATE-----
8 changes: 8 additions & 0 deletions ballerina/tests/resources/ec-key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Bag Attributes
friendlyName: ec-keypair
localKeyID: 54 69 6D 65 20 31 37 30 31 30 37 39 30 34 34 33 30 32
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCCbdsjMze7hLWRnyp8P
aBXFBeUojC+lv4HGtvIJAJ8HIA==
-----END PRIVATE KEY-----
2 changes: 2 additions & 0 deletions ballerina/tests/test_utils.bal
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const string KEY_PAIR_PATH = "tests/resources/keyPair.pem";
const string ENCRYPTED_PRIVATE_KEY_PATH = "tests/resources/encryptedPrivate.key";
const string PRIVATE_KEY_PATH = "tests/resources/private.key";
const string X509_PUBLIC_CERT_PATH = "tests/resources/public.crt";
const string EC_CERT_PATH = "tests/resources/ec-cert.crt";
const string EC_PRIVATE_KEY_PATH = "tests/resources/ec-key.pem";

const string INVALID_KEYSTORE_PATH = "tests/resources/cert/keyStore.p12.invalid";
const string INVALID_PRIVATE_KEY_PATH = "tests/resources/cert/private.key.invalid";
Expand Down
5 changes: 5 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added
- [Introduce new APIs to decode private and public keys from files](https://github.com/ballerina-platform/ballerina-library/issues/5871)

## [2.6.0] - 2023-12-08

### Added
- [Introduce new APIs to interact with EC private keys and public keys](https://github.com/ballerina-platform/ballerina-library/issues/5821)

Expand Down
56 changes: 51 additions & 5 deletions docs/spec/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,15 @@ The conforming implementation of the specification is released and included in t
* 3.4. [SHA384](#34-sha384)
* 3.5. [SHA512](#35-sha512)
4. [Decode private/public key](#4-decode-private-public-keys)
* 4.1. [Decode Private key from PKCS12 file](#41-decode-private-key-from-pkcs12-file)
* 4.1. [Decode RSA Private key from PKCS12 file](#41-rsa-decode-private-key-from-pkcs12-file)
* 4.2. [Decode RSA Private key using Private key and Password](#42-decode-rsa-private-key-using-private-key-and-password)
* 4.3. [Decode RSA Public key from PKCS12 file](#43-decode-rsa-public-key-from-pkcs12-file)
* 4.4. [Decode RSA Public key from the certificate file](#44-decode-rsa-public-key-from-the-certificate-file)
* 4.5. [Build RSA Public key from modulus and exponent parameters](#45-build-rsa-public-key-from-modulus-and-exponent-parameters)
* 4.5. [Decode EC Private key from PKCS12 file](#45-decode-ec-private-key-from-pkcs12-file)
* 4.6. [Decode EC Private key using Private key and Password](#46-decode-ec-private-key-using-private-key-and-password)
* 4.7. [Decode EC Public key from PKCS12 file](#47-decode-ec-public-key-from-pkcs12-file)
* 4.8. [Decode EC Public key from the certificate file](#48-decode-ec-public-key-from-the-certificate-file)
* 4.9. [Build RSA Public key from modulus and exponent parameters](#49-build-rsa-public-key-from-modulus-and-exponent-parameters)
MohamedSabthar marked this conversation as resolved.
Show resolved Hide resolved
5. [Encrypt-Decrypt](#5-encrypt-decrypt)
* 5.1. [Encryption](#51-encryption)
* 5.1.1. [RSA](#511-rsa)
Expand Down Expand Up @@ -194,9 +198,9 @@ byte[] hmac = check crypto:hmacSha512(data, key);

The `crypto` library supports decoding the RSA private key from a `.p12` file and a key file in the `PEM` format. Also, it supports decoding a public key from a `.p12` file and a certificate file in the `X509` format. Additionally, this supports building an RSA public key with the modulus and exponent parameters.

### 4.1. [Decode Private key from PKCS12 file](#41-decode-private-key-from-pkcs12-file)
### 4.1. [Decode RSA Private key from PKCS12 file](#41-rsa-decode-private-key-from-pkcs12-file)

This API can be used to decode the private key from the given PKCS#12 file.
This API can be used to decode the RSA private key from the given PKCS#12 file.

```ballerina
crypto:KeyStore keyStore = {
Expand Down Expand Up @@ -236,7 +240,49 @@ string certFile = "/path/to/public.cert";
crypto:PublicKey publicKey = check crypto:decodeRsaPublicKeyFromCertFile(certFile);
```

### 4.5. [Build RSA Public key from modulus and exponent parameters](#45-build-rsa-public-key-from-modulus-and-exponent-parameters)
### 4.5. [Decode EC Private key from PKCS12 file](#45-decode-ec-private-key-from-pkcs12-file)

This API can be used to decode the EC private key from the given PKCS#12 file.

```ballerina
crypto:KeyStore keyStore = {
path: "/path/to/keyStore.p12",
password: "keyStorePassword"
};
crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyStore(keyStore, "keyAlias", "keyPassword");
```

### 4.6. [Decode EC Private key using Private key and Password](#46-decode-ec-private-key-using-private-key-and-password)

This API can be used to decode the EC private key from the given private key and private key password.

```ballerina
string keyFile = "/path/to/private.key";
crypto:PrivateKey privateKey = check crypto:decodeEcPrivateKeyFromKeyFile(keyFile, "keyPassword");
```

### 4.7. [Decode EC Public key from PKCS12 file](#47-decode-ec-public-key-from-pkcs12-file)

This API can be used to decode the RSA public key from the given PKCS#12 archive file.

```ballerina
crypto:TrustStore trustStore = {
path: "/path/tp/truststore.p12",
password: "truststorePassword"
};
crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromTrustStore(trustStore, "keyAlias");
```

### 4.8. [Decode EC Public key from the certificate file](#48-decode-ec-public-key-from-the-certificate-file)

This API can be used to decode the EC public key from the given public certificate file.

```ballerina
string certFile = "/path/to/public.cert";
crypto:PublicKey publicKey = check crypto:decodeEcPublicKeyFromCertFile(certFile);
```

### 4.9. [Build RSA Public key from modulus and exponent parameters](#49-build-rsa-public-key-from-modulus-and-exponent-parameters)

This API can be used to build the RSA public key from the given modulus and exponent parameters.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@
}

public static Object decodeRsaPrivateKeyFromKeyFile(BString keyFilePath, Object keyPassword) {
Object decodedPrivateKey = getPrivateKey(keyFilePath, keyPassword);
if (decodedPrivateKey instanceof PrivateKey privateKey) {
return buildRsPrivateKeyRecord(privateKey);
}
return decodedPrivateKey;
}

public static Object decodeEcPrivateKeyFromKeyFile(BString keyFilePath, Object keyPassword) {
Object decodedPrivateKey = getPrivateKey(keyFilePath, keyPassword);
if (decodedPrivateKey instanceof PrivateKey privateKey) {
return buildEcPrivateKeyRecord(privateKey);
}
return decodedPrivateKey;

Check warning on line 134 in native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decode.java

View check run for this annotation

Codecov / codecov/patch

native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decode.java#L134

Added line #L134 was not covered by tests
}

private static Object getPrivateKey(BString keyFilePath, Object keyPassword) {
Security.addProvider(new BouncyCastleProvider());
File privateKeyFile = new File(keyFilePath.getValue());
try (PEMParser pemParser = new PEMParser(new FileReader(privateKeyFile, StandardCharsets.UTF_8))) {
Expand Down Expand Up @@ -150,8 +166,7 @@
return CryptoUtils.createError("Failed to parse private key information from: " +
keyFilePath.getValue());
}
PrivateKey privateKey = converter.getPrivateKey(privateKeyInfo);
return buildRsPrivateKeyRecord(privateKey);
return converter.getPrivateKey(privateKeyInfo);
} catch (FileNotFoundException e) {
return CryptoUtils.createError("Key file not found at: " + privateKeyFile.getAbsoluteFile());
} catch (PKCSException | IOException e) {
Expand All @@ -177,7 +192,7 @@
}

private static Object buildEcPrivateKeyRecord(PrivateKey privateKey) {
if (privateKey.getAlgorithm().equals(Constants.EC_ALGORITHM)) {
if (privateKey.getAlgorithm().startsWith(Constants.EC_ALGORITHM)) {
return getPrivateKeyRecord(privateKey);
}
return CryptoUtils.createError("Not a valid EC key");
Expand Down Expand Up @@ -235,6 +250,19 @@
}
}

public static Object decodeEcPublicKeyFromCertFile(BString certFilePath) {
File certFile = new File(certFilePath.getValue());
try (FileInputStream fileInputStream = new FileInputStream(certFile)) {
CertificateFactory certificateFactory = CertificateFactory.getInstance(Constants.CERTIFICATE_TYPE_X509);
X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(fileInputStream);
return buildEcPublicKeyRecord(certificate);
} catch (FileNotFoundException e) {
return CryptoUtils.createError("Certificate file not found at: " + certFile.getAbsolutePath());

Check warning on line 260 in native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decode.java

View check run for this annotation

Codecov / codecov/patch

native/src/main/java/io/ballerina/stdlib/crypto/nativeimpl/Decode.java#L259-L260

Added lines #L259 - L260 were not covered by tests
} catch (CertificateException | IOException e) {
return CryptoUtils.createError("Unable to do public key operations: " + e.getMessage());
}
}

private static Object buildRsaPublicKeyRecord(Certificate certificate) {
BMap<BString, Object> certificateBMap = enrichPublicKeyInfo(certificate);
PublicKey publicKey = certificate.getPublicKey();
Expand Down
Loading