summaryrefslogtreecommitdiffstats
path: root/athenz-identity-provider-service/src/main/java/com/yahoo/vespa/hosted/ca/Certificates.java
blob: df904bf8010a0a7f9e020767dfe89dede01fd6e9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.ca;

import com.yahoo.security.Pkcs10Csr;
import com.yahoo.security.SubjectAlternativeName;
import com.yahoo.security.X509CertificateBuilder;
import com.yahoo.security.X509CertificateUtils;
import com.yahoo.vespa.athenz.identityprovider.api.VespaUniqueInstanceId;

import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.time.Clock;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA;
import static com.yahoo.security.SubjectAlternativeName.Type.DNS;

/**
 * Helper class for creating {@link X509Certificate}s.
 *
 * @author mpolden
 */
public class Certificates {

    private static final Duration CERTIFICATE_TTL = Duration.ofDays(30);
    private static final String INSTANCE_ID_DELIMITER = ".instanceid.athenz.";

    private final Clock clock;

    public Certificates(Clock clock) {
        this.clock = Objects.requireNonNull(clock, "clock must be non-null");
    }

    /** Create a new certificate from csr signed by the given CA private key */
    public X509Certificate create(Pkcs10Csr csr, X509Certificate caCertificate, PrivateKey caPrivateKey) {
        var x500principal = caCertificate.getSubjectX500Principal();
        var now = clock.instant();
        var notBefore = now.minus(Duration.ofHours(1));
        var notAfter = now.plus(CERTIFICATE_TTL);
        var builder = X509CertificateBuilder.fromCsr(csr,
                                              x500principal,
                                              notBefore,
                                              notAfter,
                                              caPrivateKey,
                                              SHA256_WITH_ECDSA,
                                              X509CertificateBuilder.generateRandomSerialNumber());
        for (var san : csr.getSubjectAlternativeNames()) {
            builder = builder.addSubjectAlternativeName(san.decode());
        }
        return builder.build();
    }

    /** Returns instance ID parsed from the Subject Alternative Names in given csr */
    public static String instanceIdFrom(Pkcs10Csr csr) {
        return getInstanceIdFromSAN(csr.getSubjectAlternativeNames())
                .orElseThrow(() -> new IllegalArgumentException("No instance ID found in CSR"));
    }

    public static Optional<String> instanceIdFrom(X509Certificate certificate) {
        return getInstanceIdFromSAN(X509CertificateUtils.getSubjectAlternativeNames(certificate));
    }

    private static Optional<String> getInstanceIdFromSAN(List<SubjectAlternativeName> subjectAlternativeNames) {
        return subjectAlternativeNames.stream()
                .filter(san -> san.getType() == DNS)
                .map(SubjectAlternativeName::getValue)
                .map(Certificates::parseInstanceId)
                .flatMap(Optional::stream)
                .map(VespaUniqueInstanceId::asDottedString)
                .findFirst();
    }

    private static Optional<VespaUniqueInstanceId> parseInstanceId(String dnsName) {
        var delimiterStart = dnsName.indexOf(INSTANCE_ID_DELIMITER);
        if (delimiterStart == -1) return Optional.empty();
        dnsName = dnsName.substring(0, delimiterStart);
        try {
            return Optional.of(VespaUniqueInstanceId.fromDottedString(dnsName));
        } catch (IllegalArgumentException e) {
            return Optional.empty();
        }
    }

    public static String getSubjectAlternativeNames(Pkcs10Csr csr, SubjectAlternativeName.Type sanType) {
        return csr.getSubjectAlternativeNames().stream()
                .map(SubjectAlternativeName::decode)
                .filter(san -> san.getType() == sanType)
                .map(SubjectAlternativeName::getValue)
                .collect(Collectors.joining(","));
    }
}