Skip to content

Commit 47f067e

Browse files
committed
sign the uploaded user key
1 parent 4aaae25 commit 47f067e

File tree

8 files changed

+131
-22
lines changed

8 files changed

+131
-22
lines changed

src/main/frontend/views/@index.tsx

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,20 @@ export default function MainView() {
3333
<>
3434

3535
<Card theme="elevated">
36-
<div slot="title">Lapland</div>
37-
<div slot="subtitle">The Exotic North</div>
38-
<div>Lapland is the northern-most region of Finland and an active outdoor destination.</div>
36+
<div slot="title">Upload user key</div>
37+
<div slot="subtitle">Only upload the public key</div>
38+
<Upload
39+
target="/rest/key/userSign"
40+
accept=".pub"
41+
maxFiles={1}
42+
maxFileSize={maxFileSizeInBytes}
43+
onFileReject={(event) => {
44+
Notification.show(event.detail.error);
45+
}}
46+
headers={csrfHeaders}
47+
/>
48+
<Button theme="primary">Submit</Button>
3949
</Card>
40-
41-
<Upload
42-
target="/rest/key/sign"
43-
accept=".pub"
44-
maxFiles={1}
45-
maxFileSize={maxFileSizeInBytes}
46-
onFileReject={(event) => {
47-
Notification.show(event.detail.error);
48-
}}
49-
headers={csrfHeaders}
50-
/>
51-
<Button theme="primary">Submit</Button>
5250
</>
5351
);
5452
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.binarycodes.homelab.sshkeysigner.config;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
@ConfigurationProperties(prefix = "app")
6+
public record ApplicationProperties(
7+
String caUserPath,
8+
String caHostPath
9+
) {
10+
}

src/main/java/io/binarycodes/homelab/sshkeysigner/endpoint/HelloEndpoint.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ public String sayHello(String name) {
1414
return "Hello " + name;
1515
}
1616
}
17+
1718
}

src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyController.java

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package io.binarycodes.homelab.sshkeysigner.keymanagement;
22

33
import lombok.extern.log4j.Log4j2;
4+
import org.springframework.core.io.ByteArrayResource;
5+
import org.springframework.http.HttpHeaders;
46
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.MediaType;
58
import org.springframework.http.ResponseEntity;
69
import org.springframework.web.bind.annotation.PostMapping;
710
import org.springframework.web.bind.annotation.RequestMapping;
@@ -10,8 +13,6 @@
1013
import org.springframework.web.multipart.MultipartFile;
1114
import org.springframework.web.server.ResponseStatusException;
1215

13-
import java.io.File;
14-
1516
@Log4j2
1617
@RestController
1718
@RequestMapping("/rest/key")
@@ -29,12 +30,46 @@ public KeyInfo generateKey(@RequestParam String comment, @RequestParam String pa
2930
return key.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Key generation failed"));
3031
}
3132

32-
@PostMapping("/sign")
33-
public ResponseEntity<Boolean> upload(@RequestParam("file") MultipartFile multipartFile) {
33+
@PostMapping("/userSign")
34+
public ResponseEntity<ByteArrayResource> signUserKey(@RequestParam("file") MultipartFile multipartFile) {
3435
log.debug("Uploading file '" + multipartFile.getOriginalFilename() + "'");
3536
try {
36-
//File file = keyService.save(multipartFile);
37-
return ResponseEntity.ok().body(true);
37+
var signed = keyService.signUserKey(multipartFile.getOriginalFilename(), multipartFile.getBytes());
38+
39+
return signed.map(signedPublicKeyDownload -> {
40+
var httpHeaders = new HttpHeaders();
41+
httpHeaders.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + signedPublicKeyDownload.filename());
42+
httpHeaders.add("Cache-Control", "no-cache, no-store, must-revalidate");
43+
httpHeaders.add("Pragma", "no-cache");
44+
httpHeaders.add("Expires", "0");
45+
46+
var resource = new ByteArrayResource(signedPublicKeyDownload.signedKey());
47+
48+
return ResponseEntity.ok().headers(httpHeaders).contentLength(signedPublicKeyDownload.signedKey().length).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
49+
}).orElseGet(() -> ResponseEntity.badRequest().build());
50+
} catch (Exception e) {
51+
log.error("Error uploading file.", e);
52+
return ResponseEntity.internalServerError().build();
53+
}
54+
}
55+
56+
@PostMapping("/hostSign")
57+
public ResponseEntity<ByteArrayResource> signHostKey(@RequestParam("file") MultipartFile multipartFile) {
58+
log.debug("Uploading file '" + multipartFile.getOriginalFilename() + "'");
59+
try {
60+
var signed = keyService.signUserKey(multipartFile.getOriginalFilename(), multipartFile.getBytes());
61+
62+
return signed.map(signedPublicKeyDownload -> {
63+
var httpHeaders = new HttpHeaders();
64+
httpHeaders.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + signedPublicKeyDownload.filename());
65+
httpHeaders.add("Cache-Control", "no-cache, no-store, must-revalidate");
66+
httpHeaders.add("Pragma", "no-cache");
67+
httpHeaders.add("Expires", "0");
68+
69+
var resource = new ByteArrayResource(signedPublicKeyDownload.signedKey());
70+
71+
return ResponseEntity.ok().headers(httpHeaders).contentLength(signedPublicKeyDownload.signedKey().length).contentType(MediaType.APPLICATION_OCTET_STREAM).body(resource);
72+
}).orElseGet(() -> ResponseEntity.badRequest().build());
3873
} catch (Exception e) {
3974
log.error("Error uploading file.", e);
4075
return ResponseEntity.internalServerError().build();

src/main/java/io/binarycodes/homelab/sshkeysigner/keymanagement/KeyService.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,32 @@
44
import com.sshtools.common.ssh.SshException;
55
import com.sshtools.common.ssh.components.SshCertificate;
66
import com.sshtools.common.ssh.components.SshKeyPair;
7+
import io.binarycodes.homelab.sshkeysigner.config.ApplicationProperties;
78
import lombok.extern.log4j.Log4j2;
9+
import org.apache.commons.io.FilenameUtils;
810
import org.springframework.stereotype.Service;
911

1012
import java.io.IOException;
1113
import java.nio.charset.StandardCharsets;
14+
import java.nio.file.Path;
1215
import java.util.Optional;
1316

1417
@Log4j2
1518
@Service
1619
public class KeyService {
1720
/* https://jadaptive.com/app/manpage/en/article/2895616 */
1821

22+
private static final String CERTIFICATE_FILE_NAME_SUFFIX = "cert";
23+
private final ApplicationProperties applicationProperties;
24+
25+
private enum SIGN_TYPE {
26+
USER, HOST;
27+
}
28+
29+
public KeyService(ApplicationProperties applicationProperties) {
30+
this.applicationProperties = applicationProperties;
31+
}
32+
1933
public Optional<KeyInfo> generateKey(String comment, String passphrase) {
2034
try {
2135
var pair = SshKeyPairGenerator.generateKeyPair(SshKeyPairGenerator.ED25519);
@@ -83,4 +97,43 @@ private SshKeyPair keyInfoToKeyPair(KeyInfo keyInfo, String passphrase) throws I
8397
return SshPrivateKeyFileFactory.parse(keyInfo.privateKey().getBytes(StandardCharsets.UTF_8)).toKeyPair(passphrase);
8498
}
8599

100+
public Optional<SignedPublicKeyDownload> signUserKey(String filename, byte[] bytes) {
101+
return signKey(SIGN_TYPE.USER, filename, bytes, "binarycodes");
102+
}
103+
104+
public Optional<SignedPublicKeyDownload> signHostKey(String filename, byte[] bytes) {
105+
return signKey(SIGN_TYPE.HOST, filename, bytes, "hostname");
106+
}
107+
108+
private Optional<SignedPublicKeyDownload> signKey(SIGN_TYPE signType, String filename, byte[] bytes, String typeData) {
109+
try {
110+
var publicKeyFileToSign = SshPublicKeyFileFactory.parse(bytes);
111+
var keyPairToSign = SshKeyPair.getKeyPair(null, publicKeyFileToSign.toPublicKey());
112+
113+
var signed = switch (signType) {
114+
case USER ->
115+
SshCertificateAuthority.generateUserCertificate(keyPairToSign, 0L, typeData, 1, readUserCAKeys());
116+
case HOST ->
117+
SshCertificateAuthority.generateHostCertificate(keyPairToSign, 0L, typeData, 1, readHostCAKeys());
118+
};
119+
120+
var signedKey = SshPublicKeyFileFactory.create(signed.getCertificate(), publicKeyFileToSign.getComment(), SshPublicKeyFileFactory.OPENSSH_FORMAT);
121+
var downloadFilename = "%s-%s.%s".formatted(FilenameUtils.getBaseName(filename), CERTIFICATE_FILE_NAME_SUFFIX, FilenameUtils.getExtension(filename));
122+
123+
return Optional.of(new SignedPublicKeyDownload(downloadFilename, signedKey.getFormattedKey()));
124+
} catch (IOException e) {
125+
log.error(e.getMessage(), e);
126+
} catch (InvalidPassphraseException | SshException e) {
127+
throw new RuntimeException(e);
128+
}
129+
return Optional.empty();
130+
}
131+
132+
private SshKeyPair readUserCAKeys() throws IOException, InvalidPassphraseException {
133+
return SshPrivateKeyFileFactory.parse(Path.of(applicationProperties.caUserPath())).toKeyPair("");
134+
}
135+
136+
private SshKeyPair readHostCAKeys() throws IOException, InvalidPassphraseException {
137+
return SshPrivateKeyFileFactory.parse(Path.of(applicationProperties.caHostPath())).toKeyPair("");
138+
}
86139
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package io.binarycodes.homelab.sshkeysigner.keymanagement;
2+
3+
public record SignedPublicKeyDownload(String filename, byte[] signedKey) {
4+
}

src/main/resources/application-dev.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ logging:
66
org:
77
atmosphere: warn
88
springframework: warn
9+
io:
10+
binarycodes: trace
911

1012
keycloak:
1113
client: my-test-client
12-
client_secret: NOVQKaevwzliFfqwzTRnHZmrEssd1s9f
14+
client_secret: NGMFDNlSqjxVWgqwOyZQ4NS2fY18BG0x
1315
url: http://localhost:8090
1416
realm: my-test-realm
1517

src/main/resources/application.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
server:
22
port: ${PORT:8080}
33

4+
5+
app:
6+
ca_user_path: "./user_ca_key"
7+
ca_host_path: "./host_ca_key"
8+
9+
410
spring:
511
profiles:
612
active: @spring.profiles.active@

0 commit comments

Comments
 (0)