///|
/// x509dump prints the contents of an X.509 certificate, a PKCS#10
/// certification request or a certificate revocation list.
///
/// Usage:
///
/// moon run cmd/x509dump --
///
/// The file may hold PEM or raw DER.
///|
/// Formats bytes as colon separated lowercase hex, the way certificate
/// fingerprints are usually written.
fn format_hex_colons(bytes : Bytes) -> String {
let encoded = @hex.encode(bytes[:])
let buffer = StringBuilder()
let mut index = 0
while index < encoded.length() {
if index > 0 {
buffer.write_string(":")
}
buffer.write_substring(encoded, index, 2)
index = index + 2
}
buffer.to_string()
}
///|
/// Computes a SHA-256 digest.
fn sha256(bytes : Bytes) -> Bytes {
Bytes::from_array(@crypto.sha256(bytes[:])[:])
}
///|
/// Computes a SHA-1 digest.
fn sha1(bytes : Bytes) -> Bytes {
Bytes::from_array(@crypto.sha1(bytes[:])[:])
}
///|
/// What the tool found in the input.
pub(all) enum Dumpable {
Certificate(@x509.Certificate)
Chain(Array[@x509.Certificate])
Request(@x509.CertificationRequest)
RevocationList(@x509.CertificateList)
}
///|
/// Tries to read the DER bytes as a certificate.
fn try_certificate(der : Bytes) -> Dumpable? {
Some(Certificate(@x509.parse_certificate(der[:]))) catch {
_ => None
}
}
///|
/// Tries to read the DER bytes as a certification request.
fn try_request(der : Bytes) -> Dumpable? {
Some(Request(@x509.parse_certification_request(der[:]))) catch {
_ => None
}
}
///|
/// Tries to read the DER bytes as a certificate revocation list.
fn try_certificate_list(der : Bytes) -> Dumpable? {
Some(RevocationList(@x509.parse_certificate_list(der[:]))) catch {
_ => None
}
}
///|
/// Returns the longest chain that can be built from a bundle.
fn longest_chain(
certificates : Array[@x509.Certificate],
) -> Array[@x509.Certificate] {
let mut best : Array[@x509.Certificate] = []
for certificate in certificates {
let chain = @x509.build_chain(certificate, certificates)
if chain.length() > best.length() {
best = chain
}
}
best
}
///|
/// Parses the input, accepting PEM or raw DER.
///
/// A certificate is attempted first because the two structures share their
/// outer shape; a request and a revocation list only parse once the earlier
/// attempts fail.
fn parse_input(bytes : Bytes) -> Dumpable? {
let text = @utf8.decode_lossy(bytes[:])
if @pem.is_pem(text) {
let certificates = @x509.parse_certificate_bundle(text) catch { _ => [] }
if certificates.length() > 1 {
return Some(Chain(longest_chain(certificates)))
}
let block = @pem.decode_pem_single(text) catch { _ => return None }
return parse_der(block.data)
}
parse_der(bytes)
}
///|
/// Tries the DER payload as a certificate, a request and finally a CRL.
fn parse_der(der : Bytes) -> Dumpable? {
match try_certificate(der) {
Some(value) => Some(value)
None =>
match try_request(der) {
Some(value) => Some(value)
None => try_certificate_list(der)
}
}
}
///|
/// Appends the fingerprints of an encoding.
fn write_fingerprints(buffer : StringBuilder, der : Bytes) -> Unit {
buffer.write_string(" SHA-256 fingerprint: ")
buffer.write_string(format_hex_colons(sha256(der)))
buffer.write_string("\n")
buffer.write_string(" SHA-1 fingerprint: ")
buffer.write_string(format_hex_colons(sha1(der)))
buffer.write_string("\n")
}
///|
/// Renders a certificate, its fingerprints and the extension details that the
/// summary does not cover.
fn render_certificate(certificate : @x509.Certificate) -> String {
let buffer = StringBuilder()
buffer.write_string(certificate.to_string())
write_fingerprints(buffer, certificate.raw)
match certificate.extended_key_usage() {
Some(usages) => {
buffer.write_string(" Extended key usage (")
buffer.write_string(usages.length().to_string())
buffer.write_string(")\n")
for usage in usages {
buffer.write_string(" ")
buffer.write_string(@x509.extended_key_usage_display_name(usage))
buffer.write_string(" (")
buffer.write_string(usage.to_string())
buffer.write_string(")\n")
}
}
None => ()
}
buffer.to_string()
}
///|
/// Renders a certification request with its fingerprints.
fn render_request(request : @x509.CertificationRequest) -> String {
let buffer = StringBuilder()
buffer.write_string(request.to_string())
write_fingerprints(buffer, request.raw)
buffer.to_string()
}
///|
/// Renders a certificate revocation list with its fingerprints.
fn render_revocation_list(crl : @x509.CertificateList) -> String {
let buffer = StringBuilder()
buffer.write_string(crl.to_string())
write_fingerprints(buffer, crl.raw)
buffer.to_string()
}
///|
/// Renders a certificate chain, leaf first.
fn render_chain(chain : Array[@x509.Certificate]) -> String {
let buffer = StringBuilder()
buffer.write_string("Certificate chain (")
buffer.write_string(chain.length().to_string())
buffer.write_string(")\n")
buffer.write_string(@x509.describe_chain(chain))
for i, certificate in chain {
buffer.write_string(" ")
buffer.write_string(i.to_string())
buffer.write_string(" SHA-256 fingerprint: ")
buffer.write_string(format_hex_colons(sha256(certificate.raw)))
buffer.write_string("\n")
}
buffer.to_string()
}
///|
/// Renders whatever was found in the input.
fn render(dumpable : Dumpable) -> String {
match dumpable {
Certificate(certificate) => render_certificate(certificate)
Chain(chain) => render_chain(chain)
Request(request) => render_request(request)
RevocationList(crl) => render_revocation_list(crl)
}
}
///|
/// Prints usage information.
fn print_usage() -> Unit {
println("x509dump - print a certificate, a request or a revocation list")
println("")
println("Usage: x509dump ")
println("")
println("The file may hold PEM or raw DER.")
println("")
println("Options:")
println(" -h, --help print this message")
}
///|
/// Prints an error and terminates with a non-zero status.
fn fail(message : String) -> Unit {
println("x509dump: " + message)
@sys.exit(1)
}
///|
fn main {
let arguments = @env.args()
let mut path : String? = None
for i in 1.. {
print_usage()
return
}
unknown => {
if unknown.has_prefix("-") {
fail("unknown option " + unknown)
return
}
path = Some(unknown)
}
}
}
let path = match path {
Some(path) => path
None => {
print_usage()
@sys.exit(2)
return
}
}
if !@fs.path_exists(path) {
fail("no such file: " + path)
return
}
let bytes = @fs.read_file_to_bytes(path) catch {
_ => {
fail("could not read " + path)
return
}
}
let dumpable = match parse_input(bytes) {
Some(dumpable) => dumpable
None => {
fail("no certificate, request or revocation list found in " + path)
return
}
}
println(render(dumpable))
}